ionic 3 grid view with two col per row - html

I have one screen , which will have the data to display from database. I already tried grid view in ionic 1 its fine. But ionic 3 i don't know how to do the grid view.From database i will get like 10 or 11 or 13 category names , that names i need to display in grid view with background some image.
I know how to display background image.But i need to display 2 col per row.here my code that i will use to fetch data from database.....
another(loading:any) {
this.subcatdata = { CatID: this.categoryid };
this.authService.subcatte(this.subcatdata).then((result) => {
this.data = result;
console.log(this.data);
if (this.data.status == 1) {
this.Catdata = this.data.SubCatgeoryList;
for (let i = 0; i < this.Catdata.length; i++) {
console.log(this.Catdata[i].SubCategoryName);
}
}
else if (this.data.status == 0) {
let alert = this.alertCtrl.create({
title: 'Error',
subTitle: 'Please Enter Valid Username & Password',
buttons: ['OK']
});
alert.present();
}
loading.dismiss();
}, (err) => {
loading.dismiss();
});
}
In my above code i will get the subcatgory name by using below code :
for (let i = 0; i < this.Catdata.length; i++) {
console.log(this.Catdata[i].SubCategoryName);
}
In my html :
<div class="item item-body no-padding" style="border-width: 0px !important;">
<div class="row no-padding" *ngFor="let data of Catdata; let i = index" (click)="openresources(Catdata[i].SubCatID)">
<div class="col col-50 custom-design2" style="background: url(background url) no-repeat center;background-size: cover;">
<div class="custom-design1"><span class="grid-title">{{Catdata[i].SubCategoryName}}</span></div>
</div>
</div>
</div>
My scss :
.gallery {
-webkit-flex-wrap: wrap;
flex-wrap: wrap;
}
div.no-padding, ion-item.no-padding {
padding: 0 !important;
}
div.custom-design2 {
height: 153px;
padding: 1px;
}
.swiper-pagination-bullet-active {
opacity: 1;
background: #FFF !important;
}
.no-scroll .scroll-content{
overflow: hidden;
}
div.custom-design1 {
text-align: center;
padding: 1px;
height: 153px;
vertical-align: middle;
position: relative;
background-color: rgba(0,0,0,0.5);
color: #fff;
width: 100%;
}
div.custom-design1.extended {
height: 153px;
}
span.grid-title {
font-weight: 700;
position: absolute;
top: 50%;
left: 0;
right: 0;
}
.transparent {
background: transparent !important;
}
.full_height {
height: 100% !important;
border: none;
}
Now in my screen its coming like one data per row, with full row background.
But what i need is two col ( 2 data/sub cat name per row).I know we need to use index + 1, but in ionic 3 i dont know how to do.
If any help, that will be helpfull
Thanks.

Related

Fixed table layout and a single row w/ pseudo element leading to shrunk table width

I've a table in my project with a pseudo element to show which row is active. Having changed the table layout to fixed (which is needed), I started getting this strange layout where the active row would expand to take up the entire table, but the other rows would not:
I've replicated a similar problem here (codepen, snippet below) - it's not exactly the same (the active row doesn't extend), but I'm fairly sure any answer to this would help me fix my problem.
If you comment out the top active::after style you'll see the table return to its correct size.
Thanks
// TABLE DATA
const headers = ['Id', 'Name', 'Age', 'Location'];
const datasetOne = [
['1','John Jones','27','Swindon'],
['2', 'Pete Best', '23', 'Glasgow'],
['3', 'Jules West', '22', 'Exeter'],
['4', 'Kate Ford', '33', 'Fife'],
];
const datasetTwo = [
['5','Ruth Thompson','27','Birmingham'],
['6', 'Dominic Lawson', '23', 'Greater London'],
['7', 'Really really long name', '22', 'Manchester'],
['8', 'Nicholas Johnson', '33', 'Liverpool'],
];
const tableWrapper = document.querySelector('.table-wrapper');
const btn = document.querySelector('.btn');
let dataset = 1;
// Listeners
window.addEventListener('load', () => {
const data = formatData(datasetOne);
tableWrapper.insertAdjacentHTML('afterbegin', createTable(headers, data));
});
btn.addEventListener('click', () => {
// Remove the table
const table = document.querySelector('.table')
table.parentElement.removeChild(table);
// Create and insert a new table
let data;
if(dataset === 1) {
data = formatData(datasetTwo);
dataset = 2;
}
else if(dataset === 2) {
data = formatData(datasetOne);
dataset = 1;
}
tableWrapper.insertAdjacentHTML('afterbegin', createTable(headers, data));
})
// Functions to create the table
function formatData(data) {
const rows = data.map(row => {
return createHTMLRow(row);
});
return rows;
}
function createHTMLRow([id, name, age, location]) {
const row = [
`<td class="td--id">${id}</td>`,
`<td class="td--name">${name}</td>`,
`<td class="td--age">${age}</td>`,
`<td class="td--location">${location}</td>`
];
return row;
}
function createTable (theads, rows) {
const markup = `
<table class="table">
<thead class="thead">
<tr>
${theads.map((thead) => {
return `<th class="th--${thead.toLowerCase()}">${thead}</th>`;
}).join('')}
</tr>
</thead>
<tbody class="tbody">
${
rows.map((row, index) => {
return `<tr class="row ${index===0? 'active':''}">${row.map(col => {
return `${col}`
}).join('')}</tr>`
}).join('')
}
</tbody>
</table>
`;
return markup;
};
.active::after {
position: absolute;
content: '';
left: 0;
top: 0;
width: 2px;
height: 100%;
background-color: green;
}
* {
margin: 0;
box-sizing: border-box;
}
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 100%;
height: 100vh;
background-color: firebrick;
}
.table-wrapper {
display: flex;
background-color: white;
width: 30rem;
overflow: hidden;
}
.table {
display: table;
table-layout:fixed;
border-collapse: collapse;
overflow: hidden;
width: 100%;
}
th {
text-align: start;
padding: 1rem;
background-color: lemonchiffon;
border: 1px solid lightgrey;
}
.th--age, .th--id {
width: 4rem;
}
td {
padding: .5rem 1rem;
border: 1px solid lightgrey;
white-space: nowrap;
}
.td--name {
overflow: hidden;
text-overflow: ellipsis;
}
.row {
position: relative;
height: 2rem;
}
.btn {
padding: 1rem .8rem;
width: 7rem;
background-color: white;
margin-top: 2rem;
}
<div class="container">
<div class="table-wrapper"></div>
<div class="btn">Change Data</div>
</div>
**Edit:
#zer00ne's answer of using min-width did fix the row issue, but it's unfortunately caused other problems with text-overflow:ellipsis and column widths
If you click on the codepen, item 7 currently works and overflows as intended, and all the columns remain a fixed width, even if they aren't given a width in the css (extra space seems to be distributed evenly between them).
Adding min-width to the table, while fixing the row issue, unfortunately breaks this behaviour
Was hoping someone had any ideas on how I can keep the columns fixed (as the codepen currently behaves), while being able to add the pseudo element (or some way of achieving the same effect)
Thanks!
**Edit 2:
I guess I could just manually divide up the total table width between each of the columns, but that seems a bit fragile
Update
I believe that the pseudo-element .active::after may still be the culprit. It feels like a code smell, a quick google lead me to nowhere official, but from what little I found coincides with what I was saying before, It's basically an extra cell in a single row. Because it's a pseudo-element, it's not part of the DOM so if you put one in a place where other elements cannot exist (like being a child of a <tr>), you may get unexpected results.
So here's what I did, it looks good. Try to break it and let me know if it actually does:
I changed where .active is generated -- it is now assigned to the first <td> of the first <tr> within the <tbody>. <td> can hold just about anything and it looks exactly like it did as it was on the <tr>.
Figure I
function formatData(data) { // ↙️ Add the index parameter
const rows = data.map((row, idx) => {
return createHTMLRow(idx, row);
}); // ↖️ Pass it as the 1st parameter
//...
Figure II
// ↙️ Pass the index reference as 1st parameter
function createHTMLRow(i, [id, name, age, location]) {
const row = [
`<td class="td--id ${i===0? 'active':''}">${id}</td>`,
//... ↖️ Here's the index reference interpolated
Original Solution
Still good for most cases. The update is for an edge case.
The table is at width: 100% which doesn't guarantee 100% with fixed tables that have undefined <th> widths. Change table width:
table {min-width: 100%}
// TABLE DATA
const headers = ['Id', 'Name', 'Age', 'Location'];
const datasetOne = [
['1', 'John Jones', '27', 'Swindon'],
['2', 'Pete Best', '23', 'Glasgow'],
['3', 'Jules West', '22', 'Exeter'],
['4', 'Kate Ford', '33', 'Fife'],
];
const datasetTwo = [
['5', 'Ruth Thompson', '27', 'Birmingham'],
['6', 'Dominic Lawson', '23', 'Greater London'],
['7', 'XXXXXXXXXX XXXXXXXXXXX XXXXXXXXXXXXXXXX XXXXXXXXXXXX XXXXXXXXXXXXXX XXXXXXXx XXXXXX', '22', 'Manchester'],
['8', 'Nicholas Johnson', '33', 'Liverpool'],
];
const tableWrapper = document.querySelector('.table-wrapper');
const btn = document.querySelector('.btn');
let dataset = 1;
// Listeners
window.addEventListener('load', () => {
const data = formatData(datasetOne);
tableWrapper.insertAdjacentHTML('afterbegin', createTable(headers, data));
});
btn.addEventListener('click', () => {
// Remove the table
const table = document.querySelector('.table')
table.parentElement.removeChild(table);
// Create and insert a new table
let data;
if (dataset === 1) {
data = formatData(datasetTwo);
dataset = 2;
} else if (dataset === 2) {
data = formatData(datasetOne);
dataset = 1;
}
tableWrapper.insertAdjacentHTML('afterbegin', createTable(headers, data));
});
// Functions to create the table
function formatData(data) {
const rows = data.map((row, idx) => {
return createHTMLRow(idx, row);
});
return rows;
}
function createHTMLRow(i, [id, name, age, location]) {
const row = [
`<td class="td--id ${i===0? 'active':''}">${id}</td>`,
`<td class="td--name">${name}</td>`,
`<td class="td--age">${age}</td>`,
`<td class="td--location">${location}</td>`
];
return row;
}
function createTable(theads, rows) {
const markup = `
<table class="table">
<thead class="thead">
<tr>
${theads.map((thead) => {
return `<th class="th--${thead.toLowerCase()}">${thead}</th>`;
}).join('')}
</tr>
</thead>
<tbody class="tbody">
${
rows.map((row, index) => {
return `<tr class="row">${row.map(col => {
return `${col}`
}).join('')}</tr>`
}).join('')
}
</tbody>
</table>
`;
return markup;
};
.active::after {
position: absolute;
content: '';
left: 0;
top: 0;
width: 2px;
height: 100%;
background-color: green;
}
* {
margin: 0;
box-sizing: border-box;
}
.container {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100vh;
background-color: firebrick;
}
.table-wrapper {
display: flex;
background-color: white;
width: 30rem;
overflow: hidden;
}
.table {
display: table;
table-layout: fixed;
border-collapse: collapse;
overflow: hidden;
width: 100%;
}
th {
text-align: start;
padding: 1rem;
background-color: lemonchiffon;
border: 1px solid lightgrey;
}
.th--age,
.th--id {
width: 4rem;
}
td {
padding: .5rem 1rem;
border: 1px solid lightgrey;
white-space: nowrap;
}
.td--name {
overflow: hidden;
text-overflow: ellipsis;
}
.row {
position: relative;
height: 2rem;
}
.btn {
padding: 1rem .8rem;
width: 7rem;
background-color: white;
margin-top: 2rem;
}
<div class="container">
<div class="table-wrapper"></div>
<div class="btn">Change Data</div>
</div>

Memory Game Cards Not Rotating Properly

I have been learning how to create a memory game and thought I was following the instructions carefully but I have run into a snag. In my html, I have a div for the card and two child divs to style the front and back of the card.
<div class="container">
<div id="memory_board">
<div class="card">
<div id="back0" class="cardFace cardFaceBack"></div>
<div id="front0" class="cardFace cardFaceFront" class="card"></div>
</div>
</div>
</div>
The number of cards for the memory game will vary.
When I run the code, they are stacked on top of each other meaning when you click on one card, the back flips over as expected but the front flips over underneath its original position. Here is my codepen. How can I adjust me code so that it looks like the card flips over properly?
=============== Edit ===================
Quick question, when I include a link to codepen, do I still have include all of the code?
The css for this game is:
* {
margin:0;
padding:0;
box-sizing:border-box;
}
div.container {
display: flex;
justify-content: center;
align-items: center;
vertical-align: middle;
}
div#memory_board{
background:#CCC;
border:#999 1px solid;
display: inline-block;
padding: 10px;
perspective: 1000px;
}
.card {
width:100px;
height:133px;
display: inline-block;
margin:0px;
padding:10px;
transition: transform 1s;
transform-style: preserve-3d;
transform-origin: center right;
cursor: pointer;
position: relative;
}
.card.is-flipped {
transform: translateX(-100%) rotateY(-180deg);
}
.cardFace {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.cardFaceFront {
position: inherit;
background: whitesmoke;
color: black;
font-weight: bold;
font-size: 40px;
border:#000 1px solid;
text-align:center;
vertical-align: middle;
transform: rotateY(180deg);
}
.cardFaceBack {
background: url("https://images.cdn2.stockunlimited.net/preview1300/playing-cards-
background_1608080.jpg"); no-repeat;
background-size: cover;
position: relative;
border:#000 1px solid;
}
In addition, the javascript for this program is:
var memory_array = ['A','A','B','B'];
var memory_values = [];
var memory_tile_ids = [];
var tiles_flipped = 0;
const memory_board = document.getElementById('memory_board');
let getRndInteger = (min, max) => Math.floor(Math.random() * (max - min) ) + min
// the following function randomly shuffles elements in an array using the Fisher-Yates (aka Knuth) shuffle
let shuffle = array => {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle...
while (currentIndex != 0) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}
function initiateCard () {
let card = document.querySelectorAll('.card');
card.forEach( card => card.addEventListener ( 'click', () => {
card.classList.toggle('is-flipped');
}));
}
function newBoard() {
let memoryArray = shuffle (memory_array);
for (let i = 0; i <= memoryArray.length - 1; i++) {
document.getElementById("front" + i).innerText = memory_array[i];
}
}
$( document ).ready(function() {
newBoard ();
initiateCard ();
});
When the game starts, the memory_array shuffles propery and each element is distributed to the cards. However, when you turn a card,
I thought I had followed the instructions but I do not understand why, when turned, the front face is below where the back was. What adjustments do I have to make, after turning the card, so the front of the card in the same place as the back of the card, not below it.
an animation or a gif will be of help to as what you want to achieve, from your comment in your code I could see you are trying to replicate this, but the way you structure your code doesn't seem to be as this
https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle
Okay, so a couple of things. I couldn't get your js to work. Getting this error "Uncaught TypeError: Cannot set properties of null (setting 'innerText')".
And with your CSS you need to position the front and back with position: absolute; otherwise, they affect each other. The back was pushing the front down the page.
And if you set transform-origin: center center; you don't need to do the additional transform: translateY(-100%);
var memory_array = ['A', 'A', 'B', 'B'];
var memory_values = [];
var memory_tile_ids = [];
var tiles_flipped = 0;
const memory_board = document.getElementById('memory_board');
let getRndInteger = (min, max) => Math.floor(Math.random() * (max - min)) + min
// the following function randomly shuffles elements in an array using the Fisher-Yates (aka Knuth) shuffle
let shuffle = array => {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle...
while (currentIndex != 0) {
// Pick a remaining element...
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}
function initiateCard() {
let card = document.querySelectorAll('.card');
card.forEach(card => card.addEventListener('click', () => {
card.classList.toggle('is-flipped');
}));
}
function newBoard() {
let memoryArray = shuffle(memory_array);
for (let i = 0; i <= memoryArray.length - 1; i++) {
document.getElementById("front" + i).innerText = memory_array[i];
}
}
$(document).ready(function () {
newBoard();
initiateCard();
});
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
div.container {
display: flex;
justify-content: center;
align-items: center;
vertical-align: middle;
}
div#memory_board {
background: #CCC;
border: #999 1px solid;
display: inline-block;
padding: 10px;
perspective: 1000px;
}
.card {
width: 100px;
height: 133px;
display: inline-block;
margin: 0px;
/* padding: 10px; */
transition: transform 1s;
transform-style: preserve-3d;
cursor: pointer;
position: relative;
transform-origin: center center;
}
.card.is-flipped {
transform: rotateY(180deg);
}
.cardFace {
position: absolute;
width: 100%;
height: 100%;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
.cardFaceFront {
position: absolute;
background: whitesmoke;
color: black;
font-weight: bold;
font-size: 40px;
border: #000 1px solid;
text-align: center;
vertical-align: middle;
transform: rotateY(180deg);
}
.cardFaceBack {
background: url("https://images.cdn2.stockunlimited.net/preview1300/playing-cards-background_1608080.jpg") no-repeat;
background-size: cover;
position: absolute;
border: #000 1px solid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.0/jquery.min.js"></script>
<div class="container">
<div id="memory_board">
<div class="card is-flipped">
<div id="back0" class="cardFace cardFaceBack"></div>
<div id="front0" class="cardFace cardFaceFront" class="card"></div>
</div>
<div class="card">
<div id="back0" class="cardFace cardFaceBack"></div>
<div id="front0" class="cardFace cardFaceFront" class="card"></div>
</div>
<div class="card">
<div id="back0" class="cardFace cardFaceBack"></div>
<div id="front0" class="cardFace cardFaceFront" class="card"></div>
</div>
<div class="card">
<div id="back0" class="cardFace cardFaceBack"></div>
<div id="front0" class="cardFace cardFaceFront" class="card"></div>
</div>
</div>
</div>

the table border-bottom disappear?

I try to implement a table with large size of data. then due to the performance issue, I just want to render the data in the body window.
But the new render element border disappear.
HTML:
<script src="//unpkg.com/vue#2.5.15/dist/vue.js"></script>
<script type="text/x-template" id="list-template">
<div class='table-body' ref="body" #scroll="handleScroll">
<div class="list-view">
<div
class="list-view-phantom"
:style="{
height: contentHeight
}">
</div>
<div class="list-view-colgroup">
<div class="list-view-item-col-g" v-for='count in 5'>
</div>
</div>
<div
ref="content"
class="list-view-content">
<ul
class="list-view-item"
:style="{
height: itemHeight + 'px'
}"
v-for="item in visibleData" :key='item.value'>
<li class="list-view-item-col" v-for='count in 5'>
{{item.value+count}}
</li>
</ul>
</div>
</div>
</div>
</script>
<div id="app">
<template>
<list-view :data="data"></list-view>
</template>
</div>
JS:
const ListView = {
name: 'ListView',
template: '#list-template',
props: {
data: {
type: Array,
required: true
},
itemHeight: {
type: Number,
default: 30
}
},
computed: {
contentHeight() {
return this.data.length * this.itemHeight + 'px';
}
},
mounted() {
this.updateVisibleData();
},
data() {
return {
visibleData: []
};
},
methods: {
updateVisibleData(scrollTop) {
scrollTop = scrollTop || 0;
const visibleCount = Math.ceil(this.$el.clientHeight / this.itemHeight);
const start = Math.floor(scrollTop / this.itemHeight);
const end = start + visibleCount;
this.visibleData = this.data.slice(start, end);
this.$refs.content.style.transform = `translate3d(0, ${ start * this.itemHeight }px, 0)`;
},
handleScroll() {
const scrollTop = this.$refs.body.scrollTop;
this.updateVisibleData(scrollTop);
}
}
};
new Vue({
components: {
ListView
},
data() {
const data = [];
for (let i = 0; i < 1000; i++) {
data.push({ value: i });
}
return {
data
};
}
}).$mount('#app')
code example:
https://jsfiddle.net/441701328/hq1ej6bx/6/
you can see only the data render in the first time can have border.
could anyone help?
thanks all!!!
table-row-group does not work with divs you can change the whole layout and use tables or instead you can do it like this.
.list-view-item {
padding: 5px;
color: #666;
display: table;
line-height: 30px;
box-sizing: border-box;
border-bottom: 1px solid red;
min-width: 100vw;
}
.list-view-item-col {
display: table-cell;
min-width: 50px;
}
jsfiddle for table-row-group
Hope it helps.
Use display :flex for list-view-item class, Try with following code.Hope it will work fine for you.
.list-view-item {
padding: 5px;
color: #666;
display: flex;
flex-basis: 100%;
flex-direction: row;
line-height: 30px;
box-sizing: border-box;
border-bottom: 1px solid red;
}
Try with this CSS. I hope it will works for you.
.list-view-item {
padding: 5px;
color: #666;
display:table;
line-height: 30px;
box-sizing: border-box;
border-bottom: 1px solid green;
}
I try to change the js code :
this.$refs.content.style.transform = `translateY(0, ${ start * this.itemHeight }px, 0)`;
to :
this.$refs.content.style.transform = `translateY(${ start * this.itemHeight }px)`;
and add a css to div class is list-view:
transform:translateY(0)px;
then the border showed.
don't understand why this action work!
.list-view-item {
padding: 5px;
color: #666;
display: flex;
flex-basis: 100%;
flex-direction: row;
line-height: 30px;
box-sizing: border-box;
border-bottom: 1px solid red;
}

Microsoft BotFramework-WebChat scrolling issues

I'm using the microsoft/BotFramework-WebChat, but I'm having problems getting it to scroll properly.
Often when the bot responds the user is forced to manually scroll to the bottom of the chat log.
I can't find any documentation about hooks that would let me call an API to scroll it.
Is there a way to get it so that the chat window scrolls automatically?
HTML:
<div id="bot-button" style="display:none" >
<p id="need-help" class="triangle-isosceles">Hey! Need any help?</p>
<div id="bot-open" token="temptoken">
<span>
<img id="avatar" src="/img/avatar.png"/>
<i id="message-count">2</i>
</span>
</div>
<div id="bot-close"><img src="/img/close.png" height="20px"/>Close</div>
<div id="webchat" role="main"></div>
</div>
<script src="https://cdn.botframework.com/botframework-webchat/master/webchat.js"></script>
<script src="/js/chat.js"></script>
JavaScript:
(async function () {
// In this demo, we are using Direct Line token from MockBot.
// To talk to your bot, you should use the token exchanged using your Direct Line secret.
// You should never put the Direct Line secret in the browser or client app.
// https://learn.microsoft.com/en-us/azure/bot-service/rest-api/bot-framework-rest-direct-line-3-0-authentication
var bearer_token = document.getElementById("bot-open").getAttribute("token");
const res = await fetch('https://directline.botframework.com/v3/directline/tokens/generate', {
method: 'POST',
headers: {
"Authorization": "Bearer " + bearer_token
}
});
const {
token
} = await res.json();
// We are using a customized store to add hooks to connect event
const store = window.WebChat.createStore({}, ({
dispatch
}) => next => action => {
if (action.type === 'DIRECT_LINE/CONNECT_FULFILLED') {
// When we receive DIRECT_LINE/CONNECT_FULFILLED action, we will send an event activity using WEB_CHAT/SEND_EVENT
dispatch({
type: 'WEB_CHAT/SEND_EVENT',
payload: {
name: 'webchat/join',
value: {
language: window.navigator.language
}
}
});
}
return next(action);
});
const styleOptions = {
bubbleBackground: 'rgba(0, 0, 255, .1)',
bubbleFromUserBackground: 'rgba(0, 255, 0, .1)',
hideUploadButton: true,
botAvatarInitials: 'DA',
};
window.WebChat.renderWebChat({
directLine: window.WebChat.createDirectLine({
token
}),
userID: guid(),
store,
styleOptions
}, document.getElementById('webchat'));
sizeBotChat();
document.querySelector('#webchat > *').focus();
})().catch(err => console.error(err));
function sizeBotChat() {
let bot_container = document.getElementById("bot-button");
if (isMobileDevice()) {
bot_container.style.width = "100%";
bot_container.style.bottom = "0px";
bot_container.style.right = "0px";
let max_height = screen.height - 50;
document.getElementById("webchat").style.maxHeight = max_height + "px";
console.log(screen.height);
} else {
bot_container.style.width = "400px";
bot_container.style.right = "50px";
document.getElementById("webchat").style.maxHeight = "400px";
}
}
CSS (loaded by javascript inserting a link into the head element):
.triangle-isosceles {
position: relative;
padding: 15px;
color: black;
background: white;
border-radius: 10px;
}
/* creates triangle */
.triangle-isosceles:after {
content: "";
display: block;
/* reduce the damage in FF3.0 */
position: absolute;
bottom: -15px;
right: 30px;
width: 0;
border-width: 15px 15px 0;
border-style: solid;
border-color: white transparent;
}
#avatar {
height: 50px;
}
#need-help {
display: none;
}
/* based on badge progress-bar-danger from bootstrap */
#message-count {
display: inline-block;
min-width: 10px;
padding: 3px 7px 3px 7px;
font-size: 12px;
font-weight: 700;
line-height: 1;
color: white;
text-align: center;
white-space: nowrap;
vertical-align: middle;
border-radius: 10px;
background-color: #d9534f;
position: relative;
top: -20px;
right: 20px;
}
#bot-button {
position: fixed;
bottom: 50px;
right: 0px;
width: 100%;
}
#bot-open {
height: 50px;
width: 100%;
text-align: right;
}
#bot-close {
background-color: blue;
background-image: url("https://localhost/img/avatar.png");
background-repeat: no-repeat;
color: white;
height: 22px;
display: none;
height: 50px;
padding: 15px 15px 0 0;
text-align: right;
vertical-align: middle;
}
/* hide chat on load */
#webchat {
display: none;
max-height: 400px;
overflow: scroll;
}
#webchat div.row.message {
margin: 0;
}
The developers designed WebChat to scroll the conversation to the bottom if the user hasn't scrolled up. If the user has scrolled up, there should be a 'New Message' button that appears in the bottom right corner of the chat when the bot sends a new message.
You can modify this behavior by using a custom middleware - which it looks like you already are - and scrolling the last element in the conversation into view when the user receives a message from the bot. See the code snippet below.
const store = window.WebChat.createStore(
{},
({ dispatch }) => next => action => {
if (action.type === 'DIRECT_LINE/INCOMING_ACTIVITY') {
document.querySelector('ul[role="list"]').lastChild.scrollIntoView({behavior: 'smooth', block: 'start'});
}
...
return next(action);
}
);
Hope this helps!

How to Create Multiple Cursors in a single Range Slider? [duplicate]

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;
}