Add swipe card tinder on ionic 4 - ionic-framework

I would like to ask about tinder swipeable card with ionic 4. I don't find the link see below: https://github.com/ionic-team/ionic-ion-swipe-cards
'

I think I implemented it and I hope you can leverage this code and provide feedback.
The component template consists of 3 parts:
indicator for user's choices, it gets visibility (opacity) whenever
the user drags their choice into yes/no direction
actual stack of cards
buttons that user can use to make their choice as an alternative to
dragging
The template code + scss code is as follows:
// HTML:
<div class="tinder" [hidden]="!cards.length">
<div class="tinder--status">
<div [style.opacity]="crossVisible? '1':'0'">
<svg width="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<svg:path
d="M405 136.798L375.202 107 256 226.202 136.798 107 107 136.798 226.202 256 107 375.202 136.798 405 256 285.798 375.202 405 405 375.202 285.798 256z"
fill="#CDD6DD" />
</svg>
</div>
<div [style.opacity]="heartVisible? '1':'0'">
<svg width="200px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<svg:path
d="M349.6 64c-36.4 0-70.7 16.7-93.6 43.9C233.1 80.7 198.8 64 162.4 64 97.9 64 48 114.2 48 179.1c0 79.5 70.7 143.3 177.8 241.7L256 448l30.2-27.2C393.3 322.4 464 258.6 464 179.1 464 114.2 414.1 64 349.6 64zm-80.8 329.3l-4.2 3.9-8.6 7.8-8.6-7.8-4.2-3.9c-50.4-46.3-94-86.3-122.7-122-28-34.7-40.4-63.1-40.4-92.2 0-22.9 8.4-43.9 23.7-59.3 15.2-15.4 36-23.8 58.6-23.8 26.1 0 52 12.2 69.1 32.5l24.5 29.1 24.5-29.1c17.1-20.4 43-32.5 69.1-32.5 22.6 0 43.4 8.4 58.7 23.8 15.3 15.4 23.7 36.5 23.7 59.3 0 29-12.5 57.5-40.4 92.2-28.8 35.7-72.3 75.7-122.8 122z"
fill="#FFACE4" />
</svg>
</div>
</div>
<div class="tinder--cards" (pan)="handlePan($event)" (panend)="handlePanEnd($event)">
<div #tinderCard class="tinder--card" (transitionend)="handleShift()" *ngFor="let card of cards; let i = index"
[ngStyle]="{ zIndex: cards.length - i, transform: 'scale(' + (20 - i) / 20 + ') translateY(-' + 20 * i + 'px)' }">
<img #tinderCardImage [src]="card.img" (load)="tinderCardImage.style.opacity = 1">
<h3>{{ card.title }}</h3>
<p>{{ card.description }}</p>
</div>
</div>
<div class="tinder--buttons">
<button (click)="userClickedButton($event, false)">
<svg width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<svg:path
d="M405 136.798L375.202 107 256 226.202 136.798 107 107 136.798 226.202 256 107 375.202 136.798 405 256 285.798 375.202 405 405 375.202 285.798 256z"
fill="#CDD6DD" />
</svg>
</button>
<button (click)="userClickedButton($event, true)">
<svg width="30px" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<svg:path
d="M349.6 64c-36.4 0-70.7 16.7-93.6 43.9C233.1 80.7 198.8 64 162.4 64 97.9 64 48 114.2 48 179.1c0 79.5 70.7 143.3 177.8 241.7L256 448l30.2-27.2C393.3 322.4 464 258.6 464 179.1 464 114.2 414.1 64 349.6 64zm-80.8 329.3l-4.2 3.9-8.6 7.8-8.6-7.8-4.2-3.9c-50.4-46.3-94-86.3-122.7-122-28-34.7-40.4-63.1-40.4-92.2 0-22.9 8.4-43.9 23.7-59.3 15.2-15.4 36-23.8 58.6-23.8 26.1 0 52 12.2 69.1 32.5l24.5 29.1 24.5-29.1c17.1-20.4 43-32.5 69.1-32.5 22.6 0 43.4 8.4 58.7 23.8 15.3 15.4 23.7 36.5 23.7 59.3 0 29-12.5 57.5-40.4 92.2-28.8 35.7-72.3 75.7-122.8 122z"
fill="#FFACE4" />
</svg>
</button>
</div>
</div>
// SCSS:
.tinder {
width: 100%;
height: 100%;
overflow: hidden;
background-color: rgba(0,0,0,0.4);
position: absolute;
left: 0;
top: 0;
}
.tinder--status {
position: absolute;
top: 50%;
margin-top: -30px;
z-index: 2;
width: 100%;
text-align: center;
pointer-events: none;
}
.tinder--status > div {
transition: all 0.3s ease-in-out;
}
.tinder--status svg {
transition: all 0.3s ease-in-out;
position: absolute;
width: 100px;
margin-left: -50px;
}
.tinder--cards {
text-align: center;
display: flex;
flex-direction: column;
position: fixed;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
overflow: hidden;
}
.tinder--card {
display: inline-block;
width: 260px;
height: 70%;
background: #FFFFFF;
padding-bottom: 40px;
border-radius: 8px;
overflow: hidden;
position: absolute;
will-change: transform;
transition: all 0.3s ease-in-out;
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.moving.tinder--card {
transition: none;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
.tinder--card img {
max-width: 100%;
max-height: 75%;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s ease-in-out;
}
.tinder--card h3 {
margin-top: 16px;
font-size: 24px;
padding: 0 16px;
pointer-events: none;
}
.tinder--card p {
margin-top: 24px;
font-size: 16px;
padding: 0 16px;
pointer-events: none;
}
.tinder--buttons {
position: absolute;
flex: 0 0 100px;
text-align: center;
bottom: 20px;
left: 0;
right: 0;
}
.tinder--buttons button {
border-radius: 50%;
line-height: 50px;
width: 50px;
height: 50px;
border: 0;
background: #FFFFFF;
display: inline-block;
padding-top: 10px;
margin: 0 12px;
}
.tinder--buttons button:focus {
outline: 0;
}
Some notes:
the template features *ngFor that replicates each card and positions
it in the stack
we leverage hammer.js' pan and pan-end gesture events to deal with dragging
we are listening for transition-end events to actually remove cards from the stack
// TS:
import { Component, Input, ViewChildren, QueryList, ElementRef, EventEmitter, Output, Renderer2 } from '#angular/core';
#Component({
selector: 'tinder-ui',
templateUrl: 'tinder-ui.component.html',
styleUrls: ['tinder-ui.component.scss'],
})
export class TinderUIComponent {
#Input('cards') cards: Array<{
img: string,
title: string,
description: string
}>;
#ViewChildren('tinderCard') tinderCards: QueryList<ElementRef>;
tinderCardsArray: Array<ElementRef>;
#Output() choiceMade = new EventEmitter();
moveOutWidth: number;
shiftRequired: boolean;
transitionInProgress: boolean;
heartVisible: boolean;
crossVisible: boolean;
constructor(private renderer: Renderer2) {
}
userClickedButton(event, heart) {
event.preventDefault();
if (!this.cards.length) return false;
if (heart) {
this.tinderCardsArray[0].nativeElement.style.transform = 'translate(' + this.moveOutWidth + 'px, -100px) rotate(-30deg)';
this.toggleChoiceIndicator(false,true);
this.emitChoice(heart, this.cards[0]);
} else {
this.tinderCardsArray[0].nativeElement.style.transform = 'translate(-' + this.moveOutWidth + 'px, -100px) rotate(30deg)';
this.toggleChoiceIndicator(true,false);
this.emitChoice(heart, this.cards[0]);
};
this.shiftRequired = true;
this.transitionInProgress = true;
};
handlePan(event) {
if (event.deltaX === 0 || (event.center.x === 0 && event.center.y === 0) || !this.cards.length) return;
if (this.transitionInProgress) {
this.handleShift();
}
this.renderer.addClass(this.tinderCardsArray[0].nativeElement, 'moving');
if (event.deltaX > 0) { this.toggleChoiceIndicator(false,true) }
if (event.deltaX < 0) { this.toggleChoiceIndicator(true,false) }
let xMulti = event.deltaX * 0.03;
let yMulti = event.deltaY / 80;
let rotate = xMulti * yMulti;
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', 'translate(' + event.deltaX + 'px, ' + event.deltaY + 'px) rotate(' + rotate + 'deg)');
this.shiftRequired = true;
};
handlePanEnd(event) {
this.toggleChoiceIndicator(false,false);
if (!this.cards.length) return;
this.renderer.removeClass(this.tinderCardsArray[0].nativeElement, 'moving');
let keep = Math.abs(event.deltaX) < 80 || Math.abs(event.velocityX) < 0.5;
if (keep) {
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', '');
this.shiftRequired = false;
} else {
let endX = Math.max(Math.abs(event.velocityX) * this.moveOutWidth, this.moveOutWidth);
let toX = event.deltaX > 0 ? endX : -endX;
let endY = Math.abs(event.velocityY) * this.moveOutWidth;
let toY = event.deltaY > 0 ? endY : -endY;
let xMulti = event.deltaX * 0.03;
let yMulti = event.deltaY / 80;
let rotate = xMulti * yMulti;
this.renderer.setStyle(this.tinderCardsArray[0].nativeElement, 'transform', 'translate(' + toX + 'px, ' + (toY + event.deltaY) + 'px) rotate(' + rotate + 'deg)');
this.shiftRequired = true;
this.emitChoice(!!(event.deltaX > 0), this.cards[0]);
}
this.transitionInProgress = true;
};
toggleChoiceIndicator(cross, heart) {
this.crossVisible = cross;
this.heartVisible = heart;
};
handleShift() {
this.transitionInProgress = false;
this.toggleChoiceIndicator(false,false)
if (this.shiftRequired) {
this.shiftRequired = false;
this.cards.shift();
};
};
emitChoice(heart, card) {
this.choiceMade.emit({
choice: heart,
payload: card
})
};
ngAfterViewInit() {
this.moveOutWidth = document.documentElement.clientWidth * 1.5;
this.tinderCardsArray = this.tinderCards.toArray();
this.tinderCards.changes.subscribe(()=>{
this.tinderCardsArray = this.tinderCards.toArray();
})
};
}
Note re ts part:
#Input is used to get the list of cards
we leverage #Output to emit choices by the user
we use #ViewChildren to keep track of the stack of cards
card is only actually removed (array.shift()) when the transition is finished (transition end event)
the component itself is hidden in case it has no cards in the stack, idea is that we show this component from a page component for example by referencing an array that can contain cards
Hopefully, this is a good example of how such component can be implemented
Editor URL: https://stackblitz.com/edit/ionic-4-template-bks4dd
Demo: https://ionic-4-template-bks4dd.stackblitz.io
Article: medium post

Related

streamlabs streamboss Name change

Let me start by saying thank you in advance!
I'm on this site called StreamLabs for twitch streamers and they've introduced a way to use custom HTML, CSS, & JS to customize your look.
They have a thing called "Stream boss" that reacts to users following, sub, and/or donating.
I don't know how to change the name of the streamboss can someone help me with that
/*
bar(asset) width is 315
math for doing the stuff
x = {asset_length} - ( {asset_length} * ( {current_health}/{max_health} ) );
*/
// Events will be sent when the boss is damaged or killed.
// Please use event listeners to run functions.
document.addEventListener('bossLoad', function(obj) {
// obj.detail will contain information about the current boss
// this will fire only once when the widget loads
console.log(obj.detail);
//$('#user_pic').attr('src', obj.detail.boss_img);
$('#current_health').text(obj.detail.current_health);
$('#total_health').text(obj.detail.total_health);
$('#username').text(obj.detail.boss_name);
});
document.addEventListener('bossDamaged', function(obj) {
// obj.detail will contain information about the boss and a
// custom message
var x_coordinate = 0;
console.log(obj.detail);
// set the text
$('#current_health').text(obj.detail.boss.current_health);
// calculate where the x is
x_coordinate = 315 - (315 * (Number(obj.detail.boss.current_health) / Number(obj.detail.boss.total_health)));
// round the output
x_coordinate = -(~~x_coordinate);
// we animate the `background-position-x`
$('#vbi-health-bar').animate({
'background-position-x': x_coordinate + 'px'
}, 1000);
});
// Similarly for for when a boss is killed
document.addEventListener('bossKilled', function(obj) {
var x_coordinate = 0;
console.log(obj.detail);
$('#username').text(obj.detail.boss.boss_name);
//$('#user_pic').attr('src', obj.detail.boss.boss_img);
$('#current_health').text(obj.detail.boss.current_health);
$('#total_health').text(obj.detail.boss.total_health);
// calculate where the x is
x_coordinate = 315 - (315 * (Number(obj.detail.boss.current_health) / Number(obj.detail.boss.total_health)));
// round the output
x_coordinate = -(~~x_coordinate);
// we animate the `background-position-x`
$('#vbi-health-bar').animate({
'background-position-x': x_coordinate + 'px'
}, 1000);
});
#import url('https://fonts.googleapis.com/css?family=Teko:700');
/* All html objects will be wrapped in the #wrap div */
#username,
#user_hp_cont {
height: 33px;
line-height: 50px;
font-family: 'Teko', sans-serif;
font-size: 1.2em;
font-weight: 700;
text-shadow: 0px 0px 5.64px rgba(0, 0, 0, 0.004);
background-image: -webkit-linear-gradient(-180deg, #c0ff00 65%, #00ff0c);
/* For Chrome and Safari */
background-image: -moz-linear-gradient(-180deg, #c0ff00 65%, #00ff0c);
/* For old Fx (3.6 to 15) */
background-image: -ms-linear-gradient(-180deg, #c0ff00 65%, #00ff0c);
/* For pre-releases of IE 10*/
background-image: -o-linear-gradient(-180deg, #c0ff00 65%, #00ff0c);
/* For old Opera (11.1 to 12.0) */
background-image: linear-gradient(-180deg, #c0ff00 65%, #00ff0c);
/* Standard syntax; must be last #c0ff00, #00ff0c*/
color: transparent;
-webkit-background-clip: text;
background-clip: text;
text-transform: uppercase;
}
.boss_cont {
height: 120px;
color: white;
background-color: transparent;
background-image: url(https://s3.us-east-2.amazonaws.com/streamlabs-designers/indigo/1530912534steam_boss_frame.png);
background-size: auto 100%;
background-repeat: no-repeat;
background-position: center;
}
.vbi-aligner {
width: 370px;
height: 35px;
margin: 0 auto;
}
.vbi-aligner>#username {
float: left;
}
.vbi-aligner>#user_hp_cont {
float: right;
}
.vbi-health-frame {
position: absolute;
width: 315px;
height: 47px;
left: calc(50% - 157px);
background-image: url(https://s3.us-east-2.amazonaws.com/streamlabs-designers/indigo/1530912558health_bar_frame.png);
background-repeat: no-repeat;
background-size: cover;
background-position: center;
-ms-transform: translateY(15px);
-webkit-transform: translateY(15px);
transform: translateY(15px);
overflow: hidden;
z-index: 1;
}
.vbi-health-magic {
background: transparent;
z-index: 0;
}
.vbi-health-bar {
position: absolute;
top: 1px;
width: 315px;
height: 45px;
background-image: url(https://s3.us-east-2.amazonaws.com/streamlabs-designers/indigo/1530912589health_bar.png);
background-repeat: no-repeat;
background-size: cover;
background-position: 0 0;
-ms-transform: translateX(0);
-webkit-transform: translateX(0);
transform: translateX(0);
-ms-transition: transform .5s ease-in-out;
-webkit-transition: transform .5s ease-in-out;
transition: transform .5s ease-in-out;
-webkit-clip-path: polygon(0 50%, 25px 0, 100% 0, 100% 100%, 25px 100%);
clip-path: polygon(0 50%, 25px 0, 100% 0, 100% 100%, 25px 100%);
}
<!-- All html objects will be wrapped in the #wrap div -->
<div class='boss_cont'>
<div class="vbi-aligner">
<div id='ussername'></div>
<div id='user_hp_cont'>
HP <span id='current_health'>0</span>/<span id='total_health'>0</span>
</div>
</div>
<div class="vbi-health-frame vbi-health-magic">
<div class="vbi-health-bar" id="vbi-health-bar"></div>
</div>
<div class="vbi-health-frame"></div>
<div style="display: none;" class='user_pic_cont'>
<img id='user_pic' src='' \>
</div>
<div id='message'></div>
</div>
It's right there in the custom JS section. Wherever you see the line:
$('#username').text(obj.detail.boss_name);
Change '#username' to whatever you want your boss name to be. It's in there twice.

Easy-pie-chart just css no jquery

I would like to make this image just with css. (No need jquery and actions) How to do it?
https://www.google.hu/search?q=easy+pie+chart&rlz=1C1MSIM_enHU614HU614&espv=2&biw=1920&bih=979&source=lnms&tbm=isch&sa=X&ved=0CAYQ_AUoAWoVChMIzI-jvdSExgIVRrkUCh0jBQBX#imgrc=DSIkq4T-4XdNAM%253A%3BMH0ADANO8vzAwM%3Bhttp%253A%252F%252Fi1-scripts.softpedia-static.com%252Fscreenshots%252FEasy-pie-chart_1.png%3Bhttp%253A%252F%252Fwebscripts.softpedia.com%252Fscript%252FGraphs-and-Charts%252FEasy-pie-chart-75237.html%3B518%3B171
This is very complex.
See this demo: http://jsfiddle.net/d7vKd/871/
You are almost required to use javascript/jquery to have any functionality.
<div class="radial-progress" data-progress="0">
<div class="circle">
<div class="mask full">
<div class="fill"></div>
</div>
<div class="mask half">
<div class="fill"></div>
<div class="fill fix"></div>
</div>
<div class="shadow"></div>
</div>
<div class="inset">
<div class="percentage"></div>
</div>
Javascript
$('head style[type="text/css"]').attr('type', 'text/less');
less.refreshStyles();
window.randomize = function() {
$('.radial-progress').attr('data-progress', Math.floor(Math.random() * 100));
}
setTimeout(window.randomize, 200);
$('.radial-progress').click(window.randomize);
// Read more here: https://medium.com/#andsens/radial-progress-indicator-using-css-a917b80c43f9
LESS
#import url(http://fonts.googleapis.com/css?family=Lato:100,300,400,700,900,100italic,300italic,400italic,700italic,900italic);
.radial-progress {
#circle-size: 120px;
#circle-background: #d6dadc;
#circle-color: #97a71d;
#inset-size: 90px;
#inset-color: #fbfbfb;
#transition-length: 1s;
#shadow: 6px 6px 10px rgba(0,0,0,0.2);
#percentage-color: #97a71d;
#percentage-font-size: 22px;
#percentage-text-width: 57px;
margin: 50px;
width: #circle-size;
height: #circle-size;
background-color: #circle-background;
border-radius: 50%;
.circle {
.mask, .fill, .shadow {
width: #circle-size;
height: #circle-size;
position: absolute;
border-radius: 50%;
}
.shadow {
box-shadow: #shadow inset;
}
.mask, .fill {
-webkit-backface-visibility: hidden;
transition: -webkit-transform #transition-length;
transition: -ms-transform #transition-length;
transition: transform #transition-length;
border-radius: 50%;
}
.mask {
clip: rect(0px, #circle-size, #circle-size, #circle-size/2);
.fill {
clip: rect(0px, #circle-size/2, #circle-size, 0px);
background-color: #circle-color;
}
}
}
.inset {
width: #inset-size;
height: #inset-size;
position: absolute;
margin-left: (#circle-size - #inset-size)/2;
margin-top: (#circle-size - #inset-size)/2;
background-color: #inset-color;
border-radius: 50%;
box-shadow: #shadow;
.percentage {
height: #percentage-font-size;
width: #percentage-text-width;
overflow: hidden;
position: absolute;
top: (#inset-size - #percentage-font-size) / 2;
left: (#inset-size - #percentage-text-width) / 2;
line-height: 1;
.numbers {
margin-top: -#percentage-font-size;
transition: width #transition-length;
span {
width: #percentage-text-width;
display: inline-block;
vertical-align: top;
text-align: center;
font-weight: 800;
font-size: #percentage-font-size;
font-family: "Lato", "Helvetica Neue", Helvetica, Arial, sans-serif;
color: #percentage-color;
}
}
}
}
#i: 0;
#increment: 180deg / 100;
.loop (#i) when (#i <= 100) {
&[data-progress="#{i}"] {
.circle {
.mask.full, .fill {
-webkit-transform: rotate(#increment * #i);
-ms-transform: rotate(#increment * #i);
transform: rotate(#increment * #i);
}
.fill.fix {
-webkit-transform: rotate(#increment * #i * 2);
-ms-transform: rotate(#increment * #i * 2);
transform: rotate(#increment * #i * 2);
}
}
.inset .percentage .numbers {
width: #i * #percentage-text-width + #percentage-text-width;
}
}
.loop(#i + 1);
}
.loop(#i);
}

CartoDB multiple layer toggle

I'm trying to make one map where you can toggle between three different layers, and keep the same legend visible for all. I'm currently following this documentation:
http://docs.cartodb.com/tutorials/toggle_map_view.html
My map in CartoDB has three separate layers (w/ three datasets for the years 2013, 2014 and 2015).
I'm trying to make a toggle map like the one in the documentation. Here's what I go so far:
<html>
<head>
<link rel="stylesheet" href="http://libs.cartocdn.com/cartodb.js/v3/3.11/themes/css/cartodb.css" />
<script src="http://libs.cartocdn.com/cartodb.js/v3/3.11/cartodb.js"></script>
<style>
html, body {width:100%; height:100%; padding: 0; margin: 0;}
#map { width: 100%; height:100%; background: black;}
#menu { position: absolute; top: 5px; right: 10px; width: 400px; height:60px; background: transparent; z-index:10;}
#menu a {
margin: 15px 10px 0 0;
float: right;
vertical-align: baseline;
width: 70px;
padding: 10px;
text-align: center;
font: bold 11px "Helvetica",Arial;
line-height: normal;
color: #555;
border-radius: 4px;
border: 1px solid #777777;
background: #ffffff;
text-decoration: none;
cursor: pointer;
}
#menu a.selected,
#menu a:hover {
color: #F84F40;
}
</style>
<script>
var map;
function init(){
// initiate leaflet map
map = new L.Map('map', {
center: [20,-20],
zoom: 3
})
L.tileLayer('https://dnv9my2eseobd.cloudfront.net/v3/cartodb.map-4xtxp73f/{z}/{x}/{y}.png', {
attribution: 'Mapbox Terms & Feedback'
}).addTo(map);
var layerUrl = 'http://heathermartino.cartodb.com/api/v2/viz/415f8ed2-d493-11e4-b129-0e018d66dc29/viz.json';
var sublayers = [];
cartodb.createLayer(map, layerUrl)
.addTo(map)
.on('done', function(layer) {
// change the query for the first layer
var subLayerOptions = {
sql: "SELECT * FROM gdp_2014",
cartocss: "#gdp_2014{marker-fill: #F84F40; marker-width: 8; marker-line-color: white; marker-line-width: 2; marker-clip: false; marker-gdp_2015ow-overlap: true;}"
}
var sublayer = layer.getSubLayer(0);
sublayer.set(subLayerOptions);
sublayers.push(sublayer);
}).on('error', function() {
//log the error
});
//we define the queries that will be performed when we click on the buttons, by modifying the SQL of our layer
var LayerActions = {
GDP_2015: function(){
sublayers[0].setSQL("SELECT * FROM gdp_2015");
return true;
},
GDP_2014: function(){
sublayers[0].setSQL("SELECT * FROM gdp_2014");
return true;
},
GDP_2013: function() {
sublayers[0].set({
sql: "SELECT * FROM gdp_2013 WHERE cartodb_georef_status = true",
//as it is said, you can also add some CartoCSS code to make your points look like you want for the different queries
// cartocss: "#ne_10m_populated_places_simple{ marker-fill: black; }"
});
return true;
}
}
$('.button').click(function() {
$('.button').removeClass('selected');
$(this).addClass('selected');
//this gets the id of the different buttons and cgdp_2015s to LayerActions which responds according to the selected id
LayerActions[$(this).attr('id')]();
});
L.tileLayer('https://dnv9my2eseobd.cloudfront.net/v3/cartodb.map-4xtxp73f/{z}/{x}/{y}.png', {
attribution: 'Mapbox Terms & Feedback'
}).addTo(map);
}
</script>
</head>
<body onload="init()">
<div id='map'></div>
<div id='menu'>
2013
2014
2015
</div>
</body>
</html>
Right now when you click on the different buttons for 2013, 2014 and 2015, nothing happens. For reference, my map in carto is http://cdb.io/1Bzm2tD. Any ideas? Thanks in advance!
You have the layers. No need to run SQL again. This should work.
<html>
<head>
<link rel="stylesheet" href="http://libs.cartocdn.com/cartodb.js/v3/3.11/themes/css/cartodb.css" />
<script src="http://libs.cartocdn.com/cartodb.js/v3/3.11/cartodb.js"></script>
<style>
html,
body {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
}
#map {
width: 100%;
height: 100%;
background: black;
}
#menu {
position: absolute;
top: 5px;
right: 10px;
width: 400px;
height: 60px;
background: transparent;
z-index: 10;
}
#menu a {
margin: 15px 10px 0 0;
float: right;
vertical-align: baseline;
width: 70px;
padding: 10px;
text-align: center;
font: bold 11px "Helvetica", Arial;
line-height: normal;
color: #555;
border-radius: 4px;
border: 1px solid #777777;
background: #ffffff;
text-decoration: none;
cursor: pointer;
}
#menu a.selected,
#menu a:hover {
color: #F84F40;
}
.cartodb-layer-selector-box,
.cartodb-searchbox,
.cartodb-share {
display: none !important;
}
</style>
<script>
var layer;
function init() {
var url = 'http://heathermartino.cartodb.com/api/v2/viz/415f8ed2-d493-11e4-b129-0e018d66dc29/viz.json';
var visualizacion = cartodb.createVis("map", url)
.done(function(vis, layers) {
layer = layers[1];
});
}
function showLayer(layerToShow) {
//turn off all layers
layer.getSubLayers().forEach(function(i) {
i.hide()
});
switch (layerToShow.id) {
case "gdp_2013":
layer.getSubLayer(0).show();
break;
case "gdp_2014":
layer.getSubLayer(1).show();
break;
case "gdp_2015":
layer.getSubLayer(2).show();
break;
}
return true;
}
</script>
</head>
<body onload="init()">
<div id='map'></div>
<div id='menu'>
2013
2014
2015
</div>
</body>
</html>
I have created something similar - see if this helps. The trick for me was getting the sublayers separated with a for loop, then creating the buttons to act on each.
function loadPosition(position) {
lati = position.coords.latitude;
longi = position.coords.longitude;
map = L.map('map', {zoomControl: false}).setView([lati, longi], 15);
L.tileLayer('http://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', {maxZoom: 19,}).addTo(map);
L.control.scale({position: 'bottomright'}).addTo(map);
/*CARTODB LAYERS*/
var layerSource = {
user_name: 'YOUR USER NAME',
type: 'cartodb',
cartodb_logo: false,
sublayers: [{
sql: "SELECT * FROM winston_survey_tool WHERE point_class LIKE 'Orientation point'",
cartocss: '#winston_survey_tool{marker-fill:#D94C38;marker-opacity:1;line-color:#FFF;line-width:1;line-opacity:1;marker-allow-overlap:true; [zoom >= 15] {marker-width: 15} [zoom >= 19] {marker-width: 20}}'
},
{
sql: "SELECT * FROM winston_survey_tool WHERE point_class LIKE 'Survey point'",
cartocss: '#winston_survey_tool{marker-fill:#E0D03D;marker-opacity:1;line-color:#FFF;line-width:1;line-opacity:1;marker-allow-overlap:true; [zoom >= 15] {marker-width: 15} [zoom >= 19] {marker-width: 20}}'
}]
};
// STORE SUBLAYERS
var sublayers = [];
// ADD LAYER TO MAP
cartodb.createLayer(map,layerSource)
.addTo(map)
.done(function(layer) {
// SEPARATE THE SUBLAYERS
for (i = 0; i < layer.getSubLayerCount(); i++) {
sublayers[i] = layer.getSubLayer(i);
sublayers[i].hide();
};
// BUTTONS
$('#orientationCheck').click(function () {
orientationValue = $("#orientationCheck").val();
var query = "SELECT * FROM winston_survey_tool WHERE date LIKE'%";
yearSelectVal = $("#yearSelect").val();
query = query + yearSelectVal + "' AND point_class LIKE 'Orientation point'";
sublayers[0] = sublayers[0].setSQL(query);
if(orientationValue=="ON"){
sublayers[0].hide();
$('#orientationCheck').val("OFF");
$("#orientationCheck").addClass("off");
}
else{
sublayers[0].show();
$('#orientationCheck').val("ON");
$("#orientationCheck").removeClass("off");
};
});
$('#surveyCheck').click(function () {
surveyValue = $("#surveyCheck").val();
var query = "SELECT * FROM winston_survey_tool WHERE date LIKE'%";
yearSelectVal = $("#yearSelect").val();
query = query + yearSelectVal + "' AND point_class LIKE 'Survey point'";
sublayers[1] = sublayers[1].setSQL(query);
if(surveyValue=="ON"){
sublayers[1].hide();
$('#surveyCheck').val("OFF");
$("#surveyCheck").addClass("off");
}
else{
sublayers[1].show();
$('#surveyCheck').val("ON");
$("#surveyCheck").removeClass("off");
};
});
});

jQuery Slide Inner div from right to left

Here's what I've got so far:
JSFiddle
CSS:
#button-top { width: 100px; position: absolute; left: 75%; top: 40px; padding-left: 100px;overflow: hidden;}
#button-top:hover, #button-bottom:hover {cursor: pointer;}
.slide { position: relative; height: 100px; overflow: hidden; width: 350px; }
.slide img {position: relative; z-index: 100;}
.slide p {width: 80px; padding:8px 16px; color: #fff; margin: 0; }
.innerTop, .innerBottom { position: absolute; left: 0; top: 10px; width: 200px; height: 90px; padding: 6px; background: url(http://i39.tinypic.com/nz4ldw.png) 0 0 no-repeat; z-index: 50; display: none; }
#button-bottom { width: 100px; position: absolute; left: 75%; top: 240px; padding-left: 100px;overflow: hidden;}
SCRIPT:
$('#button-top').click(function() {
$('.innerTop').toggle('slide');
});
$('#button-bottom').click(function() {
$('.innerBottom').toggle('slide');
});
HTML:
<div class="hide-mobile" id="button-top">
[IMG]
<div class="slide innerTop"><p>HIDDEN SLIDING CONTENT</p></div>
</div>
<div class="hide-mobile" id="button-bottom">
[IMG]
<div class="slide innerBottom"><p>HIDDEN SLIDING CONTENT</p></div>
</div>
I'd like the blue inner div to slide from RIGHT to LEFT.
It can be easily done using animate function. Here is the solution:
$('#button-top').toggle(function() {
$('.innerTop').animate({'left':'3px'});
}, function() {
$('.innerTop').animate({'left':'103px'});
});
$('#button-bottom').toggle(function() {
$('.innerBottom').animate({'left':'3px'});
}, function() {
$('.innerBottom').animate({'left':'103px'});
});
http://jsfiddle.net/nAaMV/6/
You can do this:
// Set the options for the effect type chosen
var options = { direction: 'right' };
// Set the duration (default: 400 milliseconds)
var duration = 700;
$('#button-top').click(function() {
$('.innerTop').toggle('slide', options, duration);
});
$('#button-bottom').click(function() {
$('.innerBottom').toggle('slide', options, duration);
});
Please make sure to include the jQuery UI file in your code, like I have included in the fiddle ( See the checkbox for the jQuery UI 1.9.2 checked in right side ).
FIDDLE DEMO #1 or FIDDLE DEMO #2

HTML slider with two inputs possible?

Is it possible to make a HTML5 slider with two input values, for example to select a price range? If so, how can it be done?
I've been looking for a lightweight, dependency free dual slider for some time (it seemed crazy to import jQuery just for this) and there don't seem to be many out there. I ended up modifying #Wildhoney's code a bit and really like it.
function getVals(){
// Get slider values
var parent = this.parentNode;
var slides = parent.getElementsByTagName("input");
var slide1 = parseFloat( slides[0].value );
var slide2 = parseFloat( slides[1].value );
// Neither slider will clip the other, so make sure we determine which is larger
if( slide1 > slide2 ){ var tmp = slide2; slide2 = slide1; slide1 = tmp; }
var displayElement = parent.getElementsByClassName("rangeValues")[0];
displayElement.innerHTML = slide1 + " - " + slide2;
}
window.onload = function(){
// Initialize Sliders
var sliderSections = document.getElementsByClassName("range-slider");
for( var x = 0; x < sliderSections.length; x++ ){
var sliders = sliderSections[x].getElementsByTagName("input");
for( var y = 0; y < sliders.length; y++ ){
if( sliders[y].type ==="range" ){
sliders[y].oninput = getVals;
// Manually trigger event first time to display values
sliders[y].oninput();
}
}
}
}
section.range-slider {
position: relative;
width: 200px;
height: 35px;
text-align: center;
}
section.range-slider input {
pointer-events: none;
position: absolute;
overflow: hidden;
left: 0;
top: 15px;
width: 200px;
outline: none;
height: 18px;
margin: 0;
padding: 0;
}
section.range-slider input::-webkit-slider-thumb {
pointer-events: all;
position: relative;
z-index: 1;
outline: 0;
}
section.range-slider input::-moz-range-thumb {
pointer-events: all;
position: relative;
z-index: 10;
-moz-appearance: none;
width: 9px;
}
section.range-slider input::-moz-range-track {
position: relative;
z-index: -1;
background-color: rgba(0, 0, 0, 1);
border: 0;
}
section.range-slider input:last-of-type::-moz-range-track {
-moz-appearance: none;
background: none transparent;
border: 0;
}
section.range-slider input[type=range]::-moz-focus-outer {
border: 0;
}
<!-- This block can be reused as many times as needed -->
<section class="range-slider">
<span class="rangeValues"></span>
<input value="5" min="0" max="15" step="0.5" type="range">
<input value="10" min="0" max="15" step="0.5" type="range">
</section>
No, the HTML5 range input only accepts one input. I would recommend you to use something like the jQuery UI range slider for that task.
Coming late, but noUiSlider avoids having a jQuery-ui dependency, which the accepted answer does not. Its only "caveat" is IE support is for IE9 and newer, if legacy IE is a deal breaker for you.
It's also free, open source and can be used in commercial projects without restrictions.
Installation: Download noUiSlider, extract the CSS and JS file somewhere in your site file system, and then link to the CSS from head and to JS from body:
<!-- In <head> -->
<link href="nouislider.min.css" rel="stylesheet">
<!-- In <body> -->
<script src="nouislider.min.js"></script>
Example usage: Creates a slider which goes from 0 to 100, and starts set to 20-80.
HTML:
<div id="slider">
</div>
JS:
var slider = document.getElementById('slider');
noUiSlider.create(slider, {
start: [20, 80],
connect: true,
range: {
'min': 0,
'max': 100
}
});
Sure you can simply use two sliders overlaying each other and add a bit of javascript (actually not more than 5 lines) that the selectors are not exceeding the min/max values (like in #Garys) solution.
Attached you'll find a short snippet adapted from a current project including some CSS3 styling to show what you can do (webkit only). I also added some labels to display the selected values.
It uses JQuery but a vanillajs version is no magic though.
#Update: The code below was just a proof of concept. Due to many requests I've added a possible solution for Mozilla Firefox (without changing the original code). You may want to refractor the code below before using it.
(function() {
function addSeparator(nStr) {
nStr += '';
var x = nStr.split('.');
var x1 = x[0];
var x2 = x.length > 1 ? '.' + x[1] : '';
var rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + '.' + '$2');
}
return x1 + x2;
}
function rangeInputChangeEventHandler(e){
var rangeGroup = $(this).attr('name'),
minBtn = $(this).parent().children('.min'),
maxBtn = $(this).parent().children('.max'),
range_min = $(this).parent().children('.range_min'),
range_max = $(this).parent().children('.range_max'),
minVal = parseInt($(minBtn).val()),
maxVal = parseInt($(maxBtn).val()),
origin = $(this).context.className;
if(origin === 'min' && minVal > maxVal-5){
$(minBtn).val(maxVal-5);
}
var minVal = parseInt($(minBtn).val());
$(range_min).html(addSeparator(minVal*1000) + ' €');
if(origin === 'max' && maxVal-5 < minVal){
$(maxBtn).val(5+ minVal);
}
var maxVal = parseInt($(maxBtn).val());
$(range_max).html(addSeparator(maxVal*1000) + ' €');
}
$('input[type="range"]').on( 'input', rangeInputChangeEventHandler);
})();
body{
font-family: sans-serif;
font-size:14px;
}
input[type='range'] {
width: 210px;
height: 30px;
overflow: hidden;
cursor: pointer;
outline: none;
}
input[type='range'],
input[type='range']::-webkit-slider-runnable-track,
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
background: none;
}
input[type='range']::-webkit-slider-runnable-track {
width: 200px;
height: 1px;
background: #003D7C;
}
input[type='range']:nth-child(2)::-webkit-slider-runnable-track{
background: none;
}
input[type='range']::-webkit-slider-thumb {
position: relative;
height: 15px;
width: 15px;
margin-top: -7px;
background: #fff;
border: 1px solid #003D7C;
border-radius: 25px;
z-index: 1;
}
input[type='range']:nth-child(1)::-webkit-slider-thumb{
z-index: 2;
}
.rangeslider{
position: relative;
height: 60px;
width: 210px;
display: inline-block;
margin-top: -5px;
margin-left: 20px;
}
.rangeslider input{
position: absolute;
}
.rangeslider{
position: absolute;
}
.rangeslider span{
position: absolute;
margin-top: 30px;
left: 0;
}
.rangeslider .right{
position: relative;
float: right;
margin-right: -5px;
}
/* Proof of concept for Firefox */
#-moz-document url-prefix() {
.rangeslider::before{
content:'';
width:100%;
height:2px;
background: #003D7C;
display:block;
position: relative;
top:16px;
}
input[type='range']:nth-child(1){
position:absolute;
top:35px !important;
overflow:visible !important;
height:0;
}
input[type='range']:nth-child(2){
position:absolute;
top:35px !important;
overflow:visible !important;
height:0;
}
input[type='range']::-moz-range-thumb {
position: relative;
height: 15px;
width: 15px;
margin-top: -7px;
background: #fff;
border: 1px solid #003D7C;
border-radius: 25px;
z-index: 1;
}
input[type='range']:nth-child(1)::-moz-range-thumb {
transform: translateY(-20px);
}
input[type='range']:nth-child(2)::-moz-range-thumb {
transform: translateY(-20px);
}
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.8.3/jquery.min.js"></script>
<div class="rangeslider">
<input class="min" name="range_1" type="range" min="1" max="100" value="10" />
<input class="max" name="range_1" type="range" min="1" max="100" value="90" />
<span class="range_min light left">10.000 €</span>
<span class="range_max light right">90.000 €</span>
</div>
Actually I used my script in html directly. But in javascript when you add oninput event listener for this event it gives the data automatically.You just need to assign the value as per your requirement.
[slider] {
width: 300px;
position: relative;
height: 5px;
margin: 45px 0 10px 0;
}
[slider] > div {
position: absolute;
left: 13px;
right: 15px;
height: 5px;
}
[slider] > div > [inverse-left] {
position: absolute;
left: 0;
height: 5px;
border-radius: 10px;
background-color: #CCC;
margin: 0 7px;
}
[slider] > div > [inverse-right] {
position: absolute;
right: 0;
height: 5px;
border-radius: 10px;
background-color: #CCC;
margin: 0 7px;
}
[slider] > div > [range] {
position: absolute;
left: 0;
height: 5px;
border-radius: 14px;
background-color: #d02128;
}
[slider] > div > [thumb] {
position: absolute;
top: -7px;
z-index: 2;
height: 20px;
width: 20px;
text-align: left;
margin-left: -11px;
cursor: pointer;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4);
background-color: #FFF;
border-radius: 50%;
outline: none;
}
[slider] > input[type=range] {
position: absolute;
pointer-events: none;
-webkit-appearance: none;
z-index: 3;
height: 14px;
top: -2px;
width: 100%;
opacity: 0;
}
div[slider] > input[type=range]:focus::-webkit-slider-runnable-track {
background: transparent;
border: transparent;
}
div[slider] > input[type=range]:focus {
outline: none;
}
div[slider] > input[type=range]::-webkit-slider-thumb {
pointer-events: all;
width: 28px;
height: 28px;
border-radius: 0px;
border: 0 none;
background: red;
-webkit-appearance: none;
}
div[slider] > input[type=range]::-ms-fill-lower {
background: transparent;
border: 0 none;
}
div[slider] > input[type=range]::-ms-fill-upper {
background: transparent;
border: 0 none;
}
div[slider] > input[type=range]::-ms-tooltip {
display: none;
}
[slider] > div > [sign] {
opacity: 0;
position: absolute;
margin-left: -11px;
top: -39px;
z-index:3;
background-color: #d02128;
color: #fff;
width: 28px;
height: 28px;
border-radius: 28px;
-webkit-border-radius: 28px;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
text-align: center;
}
[slider] > div > [sign]:after {
position: absolute;
content: '';
left: 0;
border-radius: 16px;
top: 19px;
border-left: 14px solid transparent;
border-right: 14px solid transparent;
border-top-width: 16px;
border-top-style: solid;
border-top-color: #d02128;
}
[slider] > div > [sign] > span {
font-size: 12px;
font-weight: 700;
line-height: 28px;
}
[slider]:hover > div > [sign] {
opacity: 1;
}
<div slider id="slider-distance">
<div>
<div inverse-left style="width:70%;"></div>
<div inverse-right style="width:70%;"></div>
<div range style="left:0%;right:0%;"></div>
<span thumb style="left:0%;"></span>
<span thumb style="left:100%;"></span>
<div sign style="left:0%;">
<span id="value">0</span>
</div>
<div sign style="left:100%;">
<span id="value">100</span>
</div>
</div>
<input type="range" value="0" max="100" min="0" step="1" oninput="
this.value=Math.min(this.value,this.parentNode.childNodes[5].value-1);
let value = (this.value/parseInt(this.max))*100
var children = this.parentNode.childNodes[1].childNodes;
children[1].style.width=value+'%';
children[5].style.left=value+'%';
children[7].style.left=value+'%';children[11].style.left=value+'%';
children[11].childNodes[1].innerHTML=this.value;" />
<input type="range" value="100" max="100" min="0" step="1" oninput="
this.value=Math.max(this.value,this.parentNode.childNodes[3].value-(-1));
let value = (this.value/parseInt(this.max))*100
var children = this.parentNode.childNodes[1].childNodes;
children[3].style.width=(100-value)+'%';
children[5].style.right=(100-value)+'%';
children[9].style.left=value+'%';children[13].style.left=value+'%';
children[13].childNodes[1].innerHTML=this.value;" />
</div>
The question was: "Is it possible to make a HTML5 slider with two input values, for example to select a price range? If so, how can it be done?"
In 2020 it is possible to create a fully accessible, native, non-jquery HTML5 slider with two thumbs for price ranges. If found this posted after I already created this solution and I thought that it would be nice to share my implementation here.
This implementation has been tested on mobile Chrome and Firefox (Android) and Chrome and Firefox (Linux). I am not sure about other platforms, but it should be quite good. I would love to get your feedback and improve this solution.
This solution allows multiple instances on one page and it consists of just two inputs (each) with descriptive labels for screen readers. You can set the thumb size in the amount of grid labels. Also, you can use touch, keyboard and mouse to interact with the slider. The value is updated during adjustment, due to the 'on input' event listener.
My first approach was to overlay the sliders and clip them. However, that resulted in complex code with a lot of browser dependencies. Then I recreated the solution with two sliders that were 'inline'. This is the solution you will find below.
var thumbsize = 14;
function draw(slider,splitvalue) {
/* set function vars */
var min = slider.querySelector('.min');
var max = slider.querySelector('.max');
var lower = slider.querySelector('.lower');
var upper = slider.querySelector('.upper');
var legend = slider.querySelector('.legend');
var thumbsize = parseInt(slider.getAttribute('data-thumbsize'));
var rangewidth = parseInt(slider.getAttribute('data-rangewidth'));
var rangemin = parseInt(slider.getAttribute('data-rangemin'));
var rangemax = parseInt(slider.getAttribute('data-rangemax'));
/* set min and max attributes */
min.setAttribute('max',splitvalue);
max.setAttribute('min',splitvalue);
/* set css */
min.style.width = parseInt(thumbsize + ((splitvalue - rangemin)/(rangemax - rangemin))*(rangewidth - (2*thumbsize)))+'px';
max.style.width = parseInt(thumbsize + ((rangemax - splitvalue)/(rangemax - rangemin))*(rangewidth - (2*thumbsize)))+'px';
min.style.left = '0px';
max.style.left = parseInt(min.style.width)+'px';
min.style.top = lower.offsetHeight+'px';
max.style.top = lower.offsetHeight+'px';
legend.style.marginTop = min.offsetHeight+'px';
slider.style.height = (lower.offsetHeight + min.offsetHeight + legend.offsetHeight)+'px';
/* correct for 1 off at the end */
if(max.value>(rangemax - 1)) max.setAttribute('data-value',rangemax);
/* write value and labels */
max.value = max.getAttribute('data-value');
min.value = min.getAttribute('data-value');
lower.innerHTML = min.getAttribute('data-value');
upper.innerHTML = max.getAttribute('data-value');
}
function init(slider) {
/* set function vars */
var min = slider.querySelector('.min');
var max = slider.querySelector('.max');
var rangemin = parseInt(min.getAttribute('min'));
var rangemax = parseInt(max.getAttribute('max'));
var avgvalue = (rangemin + rangemax)/2;
var legendnum = slider.getAttribute('data-legendnum');
/* set data-values */
min.setAttribute('data-value',rangemin);
max.setAttribute('data-value',rangemax);
/* set data vars */
slider.setAttribute('data-rangemin',rangemin);
slider.setAttribute('data-rangemax',rangemax);
slider.setAttribute('data-thumbsize',thumbsize);
slider.setAttribute('data-rangewidth',slider.offsetWidth);
/* write labels */
var lower = document.createElement('span');
var upper = document.createElement('span');
lower.classList.add('lower','value');
upper.classList.add('upper','value');
lower.appendChild(document.createTextNode(rangemin));
upper.appendChild(document.createTextNode(rangemax));
slider.insertBefore(lower,min.previousElementSibling);
slider.insertBefore(upper,min.previousElementSibling);
/* write legend */
var legend = document.createElement('div');
legend.classList.add('legend');
var legendvalues = [];
for (var i = 0; i < legendnum; i++) {
legendvalues[i] = document.createElement('div');
var val = Math.round(rangemin+(i/(legendnum-1))*(rangemax - rangemin));
legendvalues[i].appendChild(document.createTextNode(val));
legend.appendChild(legendvalues[i]);
}
slider.appendChild(legend);
/* draw */
draw(slider,avgvalue);
/* events */
min.addEventListener("input", function() {update(min);});
max.addEventListener("input", function() {update(max);});
}
function update(el){
/* set function vars */
var slider = el.parentElement;
var min = slider.querySelector('#min');
var max = slider.querySelector('#max');
var minvalue = Math.floor(min.value);
var maxvalue = Math.floor(max.value);
/* set inactive values before draw */
min.setAttribute('data-value',minvalue);
max.setAttribute('data-value',maxvalue);
var avgvalue = (minvalue + maxvalue)/2;
/* draw */
draw(slider,avgvalue);
}
var sliders = document.querySelectorAll('.min-max-slider');
sliders.forEach( function(slider) {
init(slider);
});
* {padding: 0; margin: 0;}
body {padding: 40px;}
.min-max-slider {position: relative; width: 200px; text-align: center; margin-bottom: 50px;}
.min-max-slider > label {display: none;}
span.value {height: 1.7em; font-weight: bold; display: inline-block;}
span.value.lower::before {content: "€"; display: inline-block;}
span.value.upper::before {content: "- €"; display: inline-block; margin-left: 0.4em;}
.min-max-slider > .legend {display: flex; justify-content: space-between;}
.min-max-slider > .legend > * {font-size: small; opacity: 0.25;}
.min-max-slider > input {cursor: pointer; position: absolute;}
/* webkit specific styling */
.min-max-slider > input {
-webkit-appearance: none;
outline: none!important;
background: transparent;
background-image: linear-gradient(to bottom, transparent 0%, transparent 30%, silver 30%, silver 60%, transparent 60%, transparent 100%);
}
.min-max-slider > input::-webkit-slider-thumb {
-webkit-appearance: none; /* Override default look */
appearance: none;
width: 14px; /* Set a specific slider handle width */
height: 14px; /* Slider handle height */
background: #eee; /* Green background */
cursor: pointer; /* Cursor on hover */
border: 1px solid gray;
border-radius: 100%;
}
.min-max-slider > input::-webkit-slider-runnable-track {cursor: pointer;}
<div class="min-max-slider" data-legendnum="2">
<label for="min">Minimum price</label>
<input id="min" class="min" name="min" type="range" step="1" min="0" max="3000" />
<label for="max">Maximum price</label>
<input id="max" class="max" name="max" type="range" step="1" min="0" max="3000" />
</div>
Note that you should keep the step size to 1 to prevent the values to change due to redraws/redraw bugs.
View online at: https://codepen.io/joosts/pen/rNLdxvK
2022 - Accessible solution - 30 second solution to implement
This solution builds off of this answer by #JoostS. Accessibility is something none of the answers have focused on and that is a problem, so I built off of the above answer by making it more accessible & extensible since it had some flaws.
Usage is very simple:
Use the CDN or host the script locally: https://cdn.jsdelivr.net/gh/maxshuty/accessible-web-components/dist/simpleRange.min.js
Add this element to your template or HTML: <range-selector min-range="0" max-range="1000" />
Hook into it by listening for the range-changed event (or whatever event-name-to-emit-on-change you pass in)
That's it. View the full demo here. You can easily customize it by simply applying attributes like inputs-for-labels to use inputs instead of labels, slider-color to adjust the color, and so much more!
Here is a fiddle:
window.addEventListener('range-changed', (e) => {console.log(`Range changed for: ${e.detail.sliderId}. Min/Max range values are available in this object too`)})
<script src="https://cdn.jsdelivr.net/gh/maxshuty/accessible-web-components#latest/dist/simpleRange.min.js"></script>
<div>
<range-selector
id="rangeSelector1"
min-label="Minimum"
max-label="Maximum"
min-range="1000"
max-range="2022"
number-of-legend-items-to-show="6"
/>
</div>
<div>
<range-selector
id="rangeSelector1"
min-label="Minimum"
max-label="Maximum"
min-range="1"
max-range="500"
number-of-legend-items-to-show="3"
inputs-for-labels
/>
</div>
<div>
<range-selector
id="rangeSelector2"
min-label="Minimum"
max-label="Maximum"
min-range="1000"
max-range="2022"
number-of-legend-items-to-show="3"
slider-color="#6b5b95"
/>
</div>
<div>
<range-selector
id="rangeSelector3"
min-label="Minimum"
max-label="Maximum"
min-range="1000"
max-range="2022"
hide-label
hide-legend
/>
</div>
I decided to address the issues of the linked answer like the labels using display: none (bad for a11y), no visual focus on the slider, etc., and improve the code by cleaning up event listeners and making it much more dynamic and extensible.
I created this tiny library with many options to customize colors, event names, easily hook into it, make the accessible labels i18n capable and much more. Here it is in a fiddle if you want to play around.
You can easily customize the number of legend items it shows, hide or show the labels and legend, and customize the colors of everything, including the focus color like this.
Example using several of the props:
<range-selector
min-label="i18n Minimum Range"
max-label="i18n Maximum Range"
min-range="5"
max-range="555"
number-of-legend-items-to-show="6"
event-name-to-emit-on-change="my-custom-range-changed-event"
slider-color="orange"
circle-color="#f7cac9"
circle-border-color="#083535"
circle-focus-border-color="#3ec400"
/>
Then in your script:
window.addEventListener('my-custom-range-changed-event', (e) => { const data = e.detail; });
Finally if you see that this is missing something that you need I made it very easy to customize this library.
Simply copy this file and at the top you can see cssHelpers and constants objects that contain most of the variables you would likely want to further customize.
Since I built this with a Native Web Component I have taken advantage of disconnectedCallback and other hooks to clean up event listeners and set things up.
Here is a reusable double range slider implementation, base on tutorial Double Range Slider by Coding Artist
near native UI, Chrome/Firefox/Safari compatible
API EventTarget based, with change/input events, minGap/maxGap properties
let $ = (s, c = document) => c.querySelector(s);
let $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s));
class DoubleRangeSlider extends EventTarget {
#minGap = 0;
#maxGap = Number.MAX_SAFE_INTEGER;
#inputs;
style = {
trackColor: '#dadae5',
rangeColor: '#3264fe',
};
constructor(container){
super();
let inputs = $$('input[type="range"]', container);
if(inputs.length !== 2){
throw new RangeError('2 range inputs expected');
}
let [input1, input2] = inputs;
if(input1.min >= input1.max || input2.min >= input2.max){
throw new RangeError('range min should be less than max');
}
if(input1.max > input2.max || input1.min > input2.min){
throw new RangeError('input1\'s max/min should not be greater than input2\'s max/min');
}
this.#inputs = inputs;
let sliderTrack = $('.slider-track', container);
let lastValue1 = input1.value;
input1.addEventListener('input', (e) => {
let value1 = +input1.value;
let value2 = +input2.value;
let minGap = this.#minGap;
let maxGap = this.#maxGap;
let gap = value2 - value1;
let newValue1 = value1;
if(gap < minGap){
newValue1 = value2 - minGap;
}else if(gap > maxGap){
newValue1 = value2 - maxGap;
}
input1.value = newValue1;
if(input1.value !== lastValue1){
lastValue1 = input1.value;
passEvent(e);
fillColor();
}
});
let lastValue2 = input2.value;
input2.addEventListener('input', (e) => {
let value1 = +input1.value;
let value2 = +input2.value;
let minGap = this.#minGap;
let maxGap = this.#maxGap;
let gap = value2 - value1;
let newValue2 = value2;
if(gap < minGap){
newValue2 = value1 + minGap;
}else if(gap > maxGap){
newValue2 = value1 + maxGap;
}
input2.value = newValue2;
if(input2.value !== lastValue2){
lastValue2 = input2.value;
passEvent(e);
fillColor();
}
});
let passEvent = (e) => {
this.dispatchEvent(new e.constructor(e.type, e));
};
input1.addEventListener('change', passEvent);
input2.addEventListener('change', passEvent);
let fillColor = () => {
let overallMax = +input2.max;
let overallMin = +input1.min;
let overallRange = overallMax - overallMin;
let left1 = ((input1.value - overallMin) / overallRange * 100) + '%';
let left2 = ((input2.value - overallMin) / overallRange * 100) + '%';
let {trackColor, rangeColor} = this.style;
sliderTrack.style.background = `linear-gradient(to right, ${trackColor} ${left1}, ${rangeColor} ${left1}, ${rangeColor} ${left2}, ${trackColor} ${left2})`;
};
let init = () => {
let overallMax = +input2.max;
let overallMin = +input1.min;
let overallRange = overallMax - overallMin;
let range1 = input1.max - overallMin;
let range2 = overallMax - input2.min;
input1.style.left = '0px';
input1.style.width = (range1 / overallRange * 100) + '%';
input2.style.right = '0px';
input2.style.width = (range2 / overallRange * 100) + '%';
fillColor();
};
init();
}
get minGap(){
return this.#minGap;
}
set minGap(v){
this.#minGap = v;
}
get maxGap(){
return this.#maxGap;
}
set maxGap(v){
this.#maxGap = v;
}
get values(){
return this.#inputs.map((el) => el.value);
}
set values(values){
if(values.length !== 2 || !values.every(isFinite))
throw new RangeError();
let [input1, input2] = this.#inputs;
let [value1, value2] = values;
if(value1 > input1.max || value1 < input1.min)
throw new RangeError('invalid value for input1');
if(value2 > input2.max || value2 < input2.min)
throw new RangeError('invalid value for input2');
input1.value = value1;
input2.value = value2;
}
get inputs(){
return this.#inputs;
}
get overallMin(){
return this.#inputs[0].min;
}
get overallMax(){
return this.#inputs[1].max;
}
}
function main(){
let container = $('.slider-container');
let slider = new DoubleRangeSlider(container);
slider.minGap = 30;
slider.maxGap = 70;
let inputs = $$('input[name="a"]');
let outputs = $$('output[name="a"]');
outputs[0].value = inputs[0].value;
outputs[1].value = inputs[1].value;
slider.addEventListener('input', (e) => {
let values = slider.values;
outputs[0].value = values[0];
outputs[1].value = values[1];
});
slider.addEventListener('change', (e) => {
let values = slider.values;
console.log('change', values);
outputs[0].value = values[0];
outputs[1].value = values[1];
});
}
document.addEventListener('DOMContentLoaded', main);
.slider-container {
display: inline-block;
position: relative;
width: 360px;
height: 28px;
}
.slider-track {
width: 100%;
height: 5px;
position: absolute;
margin: auto;
top: 0;
bottom: 0;
border-radius: 5px;
}
.slider-container>input[type="range"] {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
position: absolute;
margin: auto;
top: 0;
bottom: 0;
width: 100%;
outline: none;
background-color: transparent;
pointer-events: none;
}
.slider-container>input[type="range"]::-webkit-slider-runnable-track {
-webkit-appearance: none;
height: 5px;
}
.slider-container>input[type="range"]::-moz-range-track {
-moz-appearance: none;
height: 5px;
}
.slider-container>input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
margin-top: -9px;
height: 1.7em;
width: 1.7em;
background-color: #3264fe;
cursor: pointer;
pointer-events: auto;
border-radius: 50%;
}
.slider-container>input[type="range"]::-moz-range-thumb {
-moz-appearance: none;
height: 1.7em;
width: 1.7em;
cursor: pointer;
border: none;
border-radius: 50%;
background-color: #3264fe;
pointer-events: auto;
}
.slider-container>input[type="range"]:active::-webkit-slider-thumb {
background-color: #ffffff;
border: 3px solid #3264fe;
}
<h3>Double Range Slider, Reusable Edition</h3>
<div class="slider-container">
<div class="slider-track"></div>
<input type="range" name="a" min="-130" max="-30" step="1" value="-100" autocomplete="off" />
<input type="range" name="a" min="-60" max="0" step="2" value="-30" autocomplete="off" />
</div>
<div>
<output name="a"></output> ~ <output name="a"></output>
</div>
<pre>
Changes:
1. allow different min/max/step for two inputs
2. new property 'maxGap'
3. added events 'input'/'change'
4. dropped IE/OldEdge support
</pre>
For those working with Vue, there is now Veeno available, based on noUiSlider. But it does not seem to be maintained anymore. :-(
This code covers following points
Dual slider using HTML, CSS, JS
I have modified this slider using embedded ruby so we can save previously applied values using params in rails.
<% left_width = params[:min].nil? ? 0 : ((params[:min].to_f/100000) * 100).to_i %>
<% left_value = params[:min].nil? ? '0' : params[:min] %>
<% right_width = params[:max].nil? ? 100 : ((params[:max].to_f/100000) * 100).to_i %>
<% right_value = params[:max].nil? ? '100000' : params[:max] %>
<div class="range-slider-outer">
<div slider id="slider-distance">
<div class="slider-inner">
<div inverse-left style="width:<%= left_width %>%;"></div>
<div inverse-right style="width:<%= 100 - right_width %>%;"></div>
<div range style="left:<%= left_width %>%;right:<%= 100 - right_width %>%;"></div>
<span thumb style="left:<%= left_width %>%;"></span>
<span thumb style="left:<%= right_width %>%;"></span>
<div sign style="">
Rs.<span id="value"><%= left_value.to_i %></span> to
</div>
<div sign style="">
Rs.<span id="value"><%= right_value.to_i %></span>
</div>
</div>
<input type="range" name="min" value=<%= left_value %> max="100000" min="0" step="100" oninput="
this.value=Math.min(this.value,this.parentNode.childNodes[5].value-1);
let value = (this.value/parseInt(this.max))*100
var children = this.parentNode.childNodes[1].childNodes;
children[1].style.width=value+'%';
children[5].style.left=value+'%';
children[7].style.left=value+'%';children[11].style.left=value+'%';
children[11].childNodes[1].innerHTML=this.value;" />
<input type="range" name="max" value=<%= right_value %> max="100000" min="0" step="100" oninput="
this.value=Math.max(this.value,this.parentNode.childNodes[3].value-(-1));
let value = (this.value/parseInt(this.max))*100
var children = this.parentNode.childNodes[1].childNodes;
children[3].style.width=(100-value)+'%';
children[5].style.right=(100-value)+'%';
children[9].style.left=value+'%';children[13].style.left=value+'%';
children[13].childNodes[1].innerHTML=this.value;" />
</div>
<div class="range-label">
<div>0</div>
<div>100000</div>
</div>
</div>
[slider] {
/*width: 300px;*/
position: relative;
height: 5px;
/*margin: 20px auto;*/
/* height: 100%; */
}
[slider] > div {
position: absolute;
left: 13px;
right: 15px;
height: 14px;
top: 5px;
}
[slider] > div > [inverse-left] {
position: absolute;
left: 0;
height: 14px;
border-radius: 3px;
background-color: #CCC;
/*margin: 0 7px;*/
margin: 0 -7px;
}
[slider] > div > [inverse-right] {
position: absolute;
right: 0;
height: 14px;
border-radius: 3px;
background-color: #CCC;
/*margin: 0 7px;*/
margin: 0 -7px;
}
[slider] > div > [range] {
position: absolute;
left: 0;
height: 14px;
border-radius: 14px;
background-color:#8950fc;
}
[slider] > div > [thumb] {
position: absolute;
top: -3px;
z-index: 2;
height: 20px;
width: 20px;
text-align: left;
margin-left: -11px;
cursor: pointer;
/* box-shadow: 0 3px 8px rgba(0, 0, 0, 0.4); */
background-color: #FFF;
/*border-radius: 50%;*/
border-radius:2px;
outline: none;
}
[slider] > input[type=range] {
position: absolute;
pointer-events: none;
-webkit-appearance: none;
z-index: 3;
height: 14px;
top: -2px;
width: 100%;
opacity: 0;
}
div[slider] > input[type=range]:focus::-webkit-slider-runnable-track {
background: transparent;
border: transparent;
}
div[slider] > input[type=range]:focus {
outline: none;
}
div[slider] > input[type=range]::-webkit-slider-thumb {
pointer-events: all;
width: 28px;
height: 28px;
border-radius: 0px;
border: 0 none;
background: red;
-webkit-appearance: none;
}
div[slider] > input[type=range]::-ms-fill-lower {
background: transparent;
border: 0 none;
}
div[slider] > input[type=range]::-ms-fill-upper {
background: transparent;
border: 0 none;
}
div[slider] > input[type=range]::-ms-tooltip {
display: none;
}
[slider] > div > [sign] {
/* opacity: 0;
position: absolute;
margin-left: -11px;
top: -39px;
z-index:3;
background-color:#1a243a;
color: #fff;
width: 28px;
height: 28px;
border-radius: 28px;
-webkit-border-radius: 28px;
align-items: center;
-webkit-justify-content: center;
justify-content: center;
text-align: center;*/
color: #A5B2CB;
border-radius: 28px;
justify-content: center;
text-align: center;
display: inline-block;
margin-top: 12px;
font-size: 14px;
font-weight: bold;
}
.slider-inner{
text-align:center;
}
/*[slider] > div > [sign]:after {
position: absolute;
content: '';
left: 0;
border-radius: 16px;
top: 19px;
border-left: 14px solid transparent;
border-right: 14px solid transparent;
border-top-width: 16px;
border-top-style: solid;
border-top-color:#1a243a;
}*/
[slider] > div > [sign] > span {
font-size: 12px;
font-weight: 700;
line-height: 28px;
}
[slider]:hover > div > [sign] {
opacity: 1;
}
.range-label{
display: flex;
justify-content: space-between;
margin-top: 28px;
padding: 0px 5px;
}
.range-slider-outer{
width:calc(100% - 20px);
margin:auto;
margin-bottom: 10px;
margin-top: 10px;
}