How to make tabs with pure JS? [closed] - javascript

Closed. This question needs to be more focused. It is not currently accepting answers.
Want to improve this question? Update the question so it focuses on one problem only by editing this post.
Closed 3 years ago.
Improve this question
i'am trying to do a "tab" menu with only pure JS CSS and HTML, where basically i have 7 divs with content and 7 "buttons" which should open the matching div. The strategy i want to use is to put all the divs with the "hidden" or the "Display: none" attribute stacked in the same spot and then, when i click on button it turns it´s matching div to visible. The problem i am facing is how to tell the button which div it should open (using arrays instead of doing it manually), and how to set it back to invisible when i click in other div (i was thinking about an if() that just turns on the visibility if the number of the button selected matches the number of the div, but supposedly every button has a div, so i am confused).

I think you want something like this?
document.addEventListener('click', ({ target: { dataset: { id = '' }}}) => {
if (id.length > 0) {
document.querySelectorAll('.tab').forEach(t => t.classList.add('hidden'));
document.querySelector(`#${id}`).classList.remove('hidden');
}
});
.hidden {
display:none;
}
<button data-id="tab1">tab 1</button>
<button data-id="tab2">tab 2</button>
<div id="tab1" class="tab">Tab 1</div>
<div id="tab2" class="tab hidden">Tab 2</div>
But the reality is we probably don't want to actually use button.
So lets change that up.
const tabClick = ({ target }) => {
const { dataset: { id = '' }} = target;
document.querySelectorAll('.tab').forEach(t => t.classList.remove('selected'));
target.classList.add('selected');
document.querySelectorAll('.tab-panel').forEach(t => t.classList.add('hidden'));
document.querySelector(`#${id}`).classList.remove('hidden');
};
const bindTabs = () => {
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', tabClick);
})
};
// Belts and braces, lets ensure our DOM is loaded and only assign click to the `tabs`
document.addEventListener('DOMContentLoaded', () => {
bindTabs();
});
.tab {
display: inline-block;
background-color: grey;
padding: 0.75rem;
color: #fff;
}
.selected {
background-color: black;
}
.tab-panel {
border: 2px solid black;
min-height: 50px;
max-width: 250px;
padding: 1rem;
}
.hidden {
display:none;
}
<div data-id="tab1" class="tab selected">tab 1</div>
<div data-id="tab2" class="tab">tab 2</div>
<div data-id="tab3" class="tab">tab 3</div>
<div data-id="tab4" class="tab">tab 4</div>
<div id="tab1" class="tab-panel">Tab 1</div>
<div id="tab2" class="tab-panel hidden">Tab 2</div>
<div id="tab3" class="tab-panel hidden">Tab 3</div>
<div id="tab4" class="tab-panel hidden">Tab 4</div>
So how can we approach this with basic javascript.
const tabCount = 4; // If we add a new tab, increase.
const tabClick = (event) => {
const tabButtonClicked = event.target;
const id = event.target.dataset.id;
// First remove selected and hide all tabs
for(let i = 1; i <= tabCount; i++) {
let tabButtonID = "#tabButton" + i;
let tabButton = document.querySelector(tabButtonID);
let tabID = "#" + tabButton.dataset.id;
let tab = document.querySelector(tabID);
tabButton.classList.remove("selected");
tab.classList.add("hidden");
}
// Now we set selected and show the selected tab.
document.querySelector("#" + id).classList.remove("hidden");
tabButtonClicked.classList.add("selected");
};
const bindTabs = () => {
// Loop through number of tabs and add a click event.
for(let i = 1; i <= tabCount; i++) {
let tabButtonID = "#tabButton" + i;
let tabButton = document.querySelector(tabButtonID);
tabButton.addEventListener('click', tabClick);
}
};
// Belts and braces, lets ensure our DOM is loaded and only assign click to the `tabs`
document.addEventListener('DOMContentLoaded', () => {
bindTabs();
});
.tab {
display: inline-block;
background-color: grey;
padding: 0.75rem;
color: #fff;
}
.selected {
background-color: black;
}
.tab-panel {
border: 2px solid black;
min-height: 50px;
max-width: 250px;
padding: 1rem;
}
.hidden {
display:none;
}
<div id="tabButton1" data-id="tab1" class="tab selected">tab 1</div>
<div id="tabButton2" data-id="tab2" class="tab">tab 2</div>
<div id="tabButton3" data-id="tab3" class="tab">tab 3</div>
<div id="tabButton4" data-id="tab4" class="tab">tab 4</div>
<div id="tab1" class="tab-panel">Tab 1</div>
<div id="tab2" class="tab-panel hidden">Tab 2</div>
<div id="tab3" class="tab-panel hidden">Tab 3</div>
<div id="tab4" class="tab-panel hidden">Tab 4</div>

Related

Show and hide different divs on clicks on different buttons [duplicate]

This question already has answers here:
Multiple show/hide divs with separate toggle
(5 answers)
Closed 8 months ago.
I am trying to show and hide different div's on click on different buttons. For example, when I click button "1", it shows the block with id="block-1". When I click on button "4", the block with id="block-4" shows and previous block #block-1 hides. I tried using different ID's because I don't know any other solution to show blocks with different content inside. Unfortunately, my current code doesn't work properly: it toggles the right class to show the div, but I can't hide the previous div or change the block once the button with number is clicked. On the default state, when the page is loaded, the first block (#block-1) should always be visible. Here's the link to codepen with the result: https://codepen.io/tomavl/pen/vYRLJVY
<div class="filter">
<button class="filter-btn active" id="1">1</button>
<button class="filter-btn" id="2">2</button>
<button class="filter-btn" id="3">3</button>
<button class="filter-btn" id="4">4</button>
</div>
<div class="block">
<div class="block-1 block-card active" id="block-1">Block 1</div>
<div class="block-2 block-card" id="block-2">Block 2</div>
<div class="block-3 block-card" id="block-3">Block 3</div>
<div class="block-4 block-card" id="block-4">Block 4</div>
</div>
background-color: red;
color: white;
}
.block-card {
display: none;
}
.block-card.active {
display: block;
}
var filterBtn = document.querySelectorAll(".filter-btn");
for (var i = 0; i < filterBtn.length; i++) {
filterBtn[i].onclick = function () {
if (this.classList) {
for (var j = 0; j < filterBtn.length; j++) {
filterBtn[j].classList.remove("active");
}
this.classList.add("active");
} else {
this.active += " " + active;
}
};
}
$("#2").on("click", function (e) {
e.stopImmediatePropagation();
if ($(this).hasClass("active")) {
$(".block-2").addClass("active");
} else {
$(".block-2").removeClass("active");
}
});
$("#3").on("click", function (e) {
e.stopImmediatePropagation();
if ($(this).hasClass("active")) {
$(".block-3").addClass("active");
} else {
$(".block-3").removeClass("active");
}
});
$("#4").on("click", function (e) {
e.stopImmediatePropagation();
if ($(this).hasClass("active")) {
$(".block-4").addClass("active");
} else {
$(".block-4").removeClass("active");
}
});
You can achieve what you need with much less code by using common classes to group content by behaviour. You can use data attributes where required to store custom metadata in an element.
In the following example all buttons use the same event handler. The differences come simply from the data attribute on the button used to change the selector. The code just removes the active class from all relevant elements before applying it to the target.
let $blocks = $('.block-card');
$('.filter-btn').on('click', e => {
let $btn = $(e.target).addClass('active');
$btn.siblings().removeClass('active');
let selector = $btn.data('target');
$blocks.removeClass('active').filter(selector).addClass('active');
});
body {
background-color: red;
color: white;
}
.block-card {
display: none;
}
.block-card.active {
display: block;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<div class="filter">
<button class="filter-btn active" data-target="#block-1">1</button>
<button class="filter-btn" data-target="#block-2">2</button>
<button class="filter-btn" data-target="#block-3">3</button>
<button class="filter-btn" data-target="#block-4">4</button>
</div>
<div class="block">
<div class="block-card active" id="block-1">Block 1</div>
<div class="block-card" id="block-2">Block 2</div>
<div class="block-card" id="block-3">Block 3</div>
<div class="block-card" id="block-4">Block 4</div>
</div>

How do I ensure that DOM operations in a single tick happen at once?

DOM operations are synchronous but their rendering to the screen is often clubbed together. How do I ensure that they are always processed as a single transaction in terms of rendering on the screen?
I tried requestAnimationScreen and inserted an alert() to see the state in-between. It works as a single transaction for Chrome and Safari but not Firefox. Moreover, is there any spec that defines this behaviour or is this completely browser dependent?
FWIW, if I remove alert() it works as a single transaction even on Firefox.
Here is the code snippet:
function f(showAlert) {
const boxes = document.querySelectorAll('.box');
const firstBox = boxes[0];
const secondBox = boxes[1];
firstBox.remove();
if (showAlert) {
alert("Check");
}
const div = document.createElement('div');
div.classList.add('box');
div.classList.add('new');
secondBox.parentElement.insertBefore(div, secondBox);
}
document.querySelector('.btn1').onclick = () => {
requestAnimationFrame(() => {
f(true);
});
};
document.querySelector('.btn2').onclick = () => {
requestAnimationFrame(() => {
f(false);
});
};
.box {
height: 500px;
width: 500px;
background-color: blue;
}
.box.default {
background-color: pink;
}
.box.new {
background-color: green;
}
.btn-bar {
position: fixed;
right: 0;
top:0;
}
<div class="box">Box 1</div>
<div class="box default">Box 2</div>
<div class="box">Box 3</div>
<div class="box default">Box 4</div>
<div class="box">Box 5</div>
<div class="btn-bar">
<button class="btn1">Alert</button>
<button class="btn2"> No alert</button>
</div>

Scroll to bottom when new message added

I am making a chatbot. I want to scroll to the bottom of the chat box when a new input is given by the user or the Data is sent through API.
It doesn't scroll and scroll just stays in the same position but the data is being added in the chat box
I Have tried the code from other chat bot but it didn't work either
var outputArea = $('#chat-output');
$('#user-input-form').on('submit', function (e) {
e.preventDefault();
var message = $('#user-input').val();
outputArea.append(`
<div class='bot-message'>
<div class='message'>
${message}
</div>
</div>
`);
const req = https.request(options, (res) => {
res.setEncoding('utf8');
res.on('data', (d) => {
const myobj = JSON.parse(d);
if ('narrative' in myobj.conversationalResponse.responses[0]) {
const temp = myobj.conversationalResponse.responses[0].narrative.text;
outputArea.append(`
<div class='user-message'>
<div class='message'>
${temp}
</div>
</div>
`);
} else if ('imageUrl' in myobj.conversationalResponse.responses[0]) {
const img = myobj.conversationalResponse.responses[0].imageUrl;
if ('narrative' in myobj.conversationalResponse.responses[1]) {
const text_r = myobj.conversationalResponse.responses[1].narrative.text;
outputArea.append(`
<div class='user-message'>
<div class ="message">
${text_r}
</div>
</div>
`);
} else {
outputArea.append(`
<div class='user-message'>
<div class='message'>
<img src="" width="300" height="200">
</div>
</div>
`);
}
}
});
});
req.on('error', (error) => {
console.error(error);
});
req.write(data);
req.end();
$('#user-input').val('');
.form-container {
width: 400px;
height: 450px;
padding: 10px;
background-color: white;
overflow: scroll;
position: relative;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div class="chat-popup" id="myForm">
<div class="form-container">
<div class="chat-output" id="chat-output">
<div class="user-message">
<div class="message">Hi! I'm Bot, what's up?</div>
</div>
</div>
<div class="chat-input">
<form action="#0" id="user-input-form" autocomplete="off">
<input type="text" id="user-input" class="user-input" placeholder="Talk to the bot.">
</form>
</div>
</br></br>
<button type="button" class="btn cancel" onclick="closeForm()">Close</button>
</div>
</div>
Another interesting method is by using pure CSS, using the flex-direction method, which works by creating a wrapper for the content inside the scrolling element.
I've whipped up a quick demo below (with a button and some JavaScript for adding new items). You can also check out this separate demo-page.
The trick then lies in reversing the content direction using column-reverse in the scroller. Because the items are in another container, they don't get 'flipped' but instead always line up to the bottom. This, in fact, makes the scroller scrolled to the bottom whenever stuff is added.
Added bonus: keeps scroll position
Also, and this is something I really like about the method; whenever the user has started scrolling (up), the scroller will not lose its scroll position when stuff is being added. So, it will only 'stick' tot the bottom if it was already scrolled (by default, or by the user) to the bottom. This makes sure there's no annoying content jumping, offering a better user experience.
Demo
let scrollerContent = document.getElementById('scrollerContent');
document.getElementById('addItems').addEventListener('click', function() {
let newChild = scrollerContent.lastElementChild.cloneNode(true);
newChild.innerHTML = "Item " + (scrollerContent.children.length + 1);
scrollerContent.appendChild(newChild);
});
.scroller {
overflow: auto;
height: 100px;
display: flex;
flex-direction: column-reverse;
}
.scroller .scroller-content .item {
height: 20px;
transform: translateZ(0); /* fixes a bug in Safari iOS where the scroller doesn't update */
}
<div class="scroller">
<div class="scroller-content" id="scrollerContent">
<div class="item">Item 1</div>
<div class="item">Item 2</div>
<div class="item">Item 3</div>
<div class="item">Item 4</div>
<div class="item">Item 5</div>
<div class="item">Item 6</div>
<div class="item">Item 7</div>
<div class="item">Item 8</div>
<div class="item">Item 9</div>
<div class="item">Item 10</div>
</div>
</div>
<br/><br/>
<button id="addItems">Add more items</button>
You can use scrollTop to achieve it.
I simply wrote an example for your reference, you will find that there are two ways to implement scrollTop, one is to use the animation wrapper, the other is to use it directly, you can compare the difference between the two methods.
const sendMessage = (selector, isAnimate = true) => {
const text = $(selector).val();
const $container = $('.form-container');
$container.append(`<p>${text}</p>`);
if (isAnimate) {
$container.animate({
scrollTop: $container.prop('scrollHeight')
}, 1000);
} else {
$container.scrollTop($container.prop('scrollHeight'));
}
$(selector).val('');
};
$('button:eq(0)').on('click', function() {
sendMessage('input[type=text]');
});
$('button:eq(1)').on('click', function() {
sendMessage('input[type=text]', false);
});
.form-container {
width: 400px;
height: 100px;
padding: 10px;
background-color: white;
overflow: scroll;
position: relative;
}
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js'></script>
<div class='form-container'>
<p>This is line 1 of text</p>
<p>This is line 2 of text</p>
</div>
<div>
<input type='text' placeholder="Type a message.">
<button type='button'>Use animation</button>
<button type='button'>Don't use animation</button>
</div>
This worked for me:
outputArea[0].scrollTop = 9e9;

How do I check whether an element is already bound to an event?

Goal
Avoid unnecessary event bindings.
Sample code
Comment box with a reply button for each individual comment
const btns = document.getElementsByClassName('reply-btn');
for (let i = 0; i < btns.length; i++) {
btns[i].addEventListener('click', showCommentContentAsPreview);
}
function showCommentContentAsPreview(e) {
console.log('showCommentContentAsPreview()');
// CHECK IF THIS BUTTON ALREADY BINDED !!!
const previewDiv = document.getElementById('preview');
const commentId = e.target.getAttribute('data-comment-id')
const commentDiv = document.getElementById('comment-' + commentId);
const commentText = commentDiv.querySelector('p').innerText
const closeReplyBtn = previewDiv.querySelector('button');
const previewContent = previewDiv.querySelector('.preview-content');
// set to preview
previewContent.innerText = commentText;
// show reply close button
closeReplyBtn.classList.remove('hidden');
// bind EventListener to "reply close button"
closeReplyBtn.addEventListener('click', closeReply)
function closeReply() {
console.log('bind to btn');
previewContent.innerText = '';
this.removeEventListener('click', closeReply);
closeReplyBtn.classList.add('hidden');
}
}
.hidden {
display: none;
}
.comment {
border-bottom: 1px solid #000;
padding: 5px;
}
.preview {
background-color: #ccc;
padding: 20px;
margin-top: 20px;
}
<div>
<!-- comment list -->
<div id="comment-1" class="comment">
<p>Comment Content 1</p>
<button class="reply-btn" data-comment-id="1">reply</button>
</div>
<div id="comment-2" class="comment">
<p>Comment Content 2</p>
<button class="reply-btn" data-comment-id="2">reply</button>
</div>
</div>
<!-- output -->
<div>
<div id="preview" class="preview">
<div class="preview-content"></div>
<button class="hidden">Close Preview</button>
</div>
</div>
Simulate problem
When you try the example, the following two scenarios occur:
Click reply once and then click "close preview"
Click on reply several times and then on "close preview".
Question
How can I avoid multiple bindings to the same button? I am already thinking about singleton.
Instead of binding a listener to every element in the series, you can bind a single listener once on a common parent of them all, and then use element.matches() to determine if the click target is the one that you want before doing more work. See the following example:
function logTextContent (elm) {
console.log(elm.textContent);
}
function handleClick (ev) {
if (ev.target.matches('.item')) {
logTextContent(ev.target);
}
}
document.querySelector('ul.list').addEventListener('click', handleClick);
<ul class="list">
<li class="item">Item 1</li>
<li class="item">Item 2</li>
<li class="item">Item 3</li>
<li class="item">Item 4</li>
<li class="item">Item 5</li>
</ul>
With the helpful hints from #Zephyr and #jsejcksn I have rewritten the code of the above question. Thus I have achieved my goal of avoiding multiple identical bindings to one element.
const container = document.getElementById('comment-container');
const previewDiv = document.getElementById('preview');
const closeReplyBtn = previewDiv.querySelector('button');
const previewContent = previewDiv.querySelector('.preview-content');
container.addEventListener('click', handleClick);
function handleClick(ev) {
if (ev.target.matches('.reply-btn')) {
if (ev.target.getAttribute('listener') !== 'true') {
removeOtherListenerFlags();
ev.target.setAttribute('listener', 'true');
showCommentContentAsPreview(ev);
}
}
if (ev.target.matches('#preview button')) {
previewContent.innerText = '';
closeReplyBtn.classList.add('hidden');
removeOtherListenerFlags();
}
}
function showCommentContentAsPreview(e) {
console.log('showCommentContentAsPreview()');
const commentId = e.target.getAttribute('data-comment-id')
const commentDiv = document.getElementById('comment-' + commentId);
const commentText = commentDiv.querySelector('p').innerText
// set to preview
previewContent.innerText = commentText;
// show reply close button
closeReplyBtn.classList.remove('hidden');
}
function removeOtherListenerFlags() {
const replyBtns = container.querySelectorAll('.reply-btn')
Object.keys(replyBtns).forEach((el) => {
replyBtns[el].removeAttribute('listener');
})
}
.hidden {
display: none;
}
.comment {
border-bottom: 1px solid #000;
padding: 5px;
}
.preview {
background-color: #ccc;
padding: 20px;
margin-top: 20px;
}
<div id="comment-container">
<div id="comment-listing">
<!-- comment list -->
<div id="comment-1" class="comment">
<p>Comment Content 1</p>
<button class="reply-btn" data-comment-id="1">reply 1</button>
</div>
<div id="comment-2" class="comment">
<p>Comment Content 2</p>
<button class="reply-btn" data-comment-id="2">reply 2</button>
</div>
</div>
<!-- output -->
<div>
<div id="preview" class="preview">
<div class="preview-content"></div>
<button class="hidden">Close Preview</button>
</div>
</div>
</div>
Cool and Thanks!

How to change background color conditionally?

I am trying to change the color of background.
I am changing every odd results to light green(#f0f5f5) so when the result ends in even number,
I get big white space.
I would like to change background color of pagination section to light green when the result ends in even number.
Sear
search results displays only 5 results so it could be 2th and 4th.
search.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox',
}),
instantsearch.widgets.hits({
container: '#Algolia_Result',
transformItems: function (items) {
return items.map(function (item) {
if (item.objectType === 'Startup') {
item._isDescription = isNotNull(item.description);
} else if (item.objectType === 'NEWS') {
item._isSource = isNotNull(item.source);
} else if (item.objectType === 'Comment') {
item._isComment = isNotNull(item.comment);
return item;
});
},
templates: {
empty: '<div id="empty">No results have been found for {{ query }}.</div><br>',
item: `
<a href="{{linkUrl}}" target="_blank">
<div class="algolia_container">
<div class="item1">
<div id="images"><img src="{{logoUrl}}" alt="{{hits-image}}" id="hits-image"></div>
<div id="objTyeps"><span class="objectType {{objectCss}}">{{objectType}}</span></div>
</div>
<div class="item2">
<div id="objectTitle">
<span id="titleForDisplay">{{#helpers.highlight}}{ "attribute": "titleForDisplay" }{{/helpers.highlight}}</span>
</div>
</div>
<div class="item3">
{{#_isLocation}}
<div id="location">{{#helpers.highlight}}{ "attribute": "location" }{{/helpers.highlight}}</div>
{{/_isLocation}}
</div>
</div></a>
`,
},
}),
instantsearch.widgets.pagination({
container: '#pagination',
}),
]);
#Algolia_Result > div > div > ol > li:nth-child(odd){background-color: #f0f5f5;}
.ais-Pagination-item {
display:inline;
padding: 5px;
margin: 0 5px;
border: 1px solid #E8E8E8;
border-radius:5px;
font-size:18px;
}
.ais-Pagination-list {
text-align: center;
height:45px;
padding-top: 10px;
}
.ais-Pagination-item:hover {
background-color: #DCDCDC;
transition: background-color .2s;
}
.ais-Pagination-item--selected{
background-color: #E8E8E8;
}
<div id="searchbox"></div>
<div id="results">
<div id="Algolia_Result"></div>
<div id="pagination"></div>
</div>
This is ok
This need be fixed as if the background color of pagination area is the same as the last result, it must be green
This is what I get in the console.
You can color background of the pagination row by using JavaScript to count the number of results and apply color if the number of results is even.
Check out the example below.
Example 1 is with an odd number of result rows and the CSS works fine, same as your working example.
Example 2 is with an even number of result rows and uses the JS code to style the pagination background.
// Count the rows
let numRows = document.querySelectorAll('#example-2 .row').length
// If the number of rows is even
if (numRows % 2 == 0) {
// Apply the background color to the pagination row
document.querySelector('#example-2 .pagination').style.backgroundColor = '#eee'
}
.container {
border: 1px solid #000;
margin-bottom: 5px;
}
.row:nth-child(odd) {
background-color: #eee;
}
Example 1
<div id="example-1" class="container">
<div>
<div class="row">Row 1</div>
<div class="row">Row 2</div>
<div class="row">Row 3</div>
</div>
<div>
<div class="pageination">Pagination Row</div>
</div>
</div>
Example 2
<div id="example-2" class="container">
<div>
<div class="row">Row 1</div>
<div class="row">Row 2</div>
<div>
<div>
<div class="pagination">Pagination Row</div>
</div>
</div>
EDIT: So in your example, you would add the following JavaScript.
<script>
let numRows = document.querySelectorAll('.ais-Hits-item').length
if (numRows % 2 == 0) {
document.querySelector('.ais-Pagination-list').style.backgroundColor = '#eee'
}
</script>
EDIT 2: Looking at your code sandbox I can see that the issue is that the JS that counts the number of rows is being run before the rows have been rendered by Algolia.
To solve this issue we need to place our row counting JS into an Algolia callback that is ran after the rows have been rendered. We can use the algolia search.on('render', ...) event callback.
Try this:
search.on('render', () => {
let numRows = document.querySelectorAll('.algolia_container').length;
if (numRows % 2 === 0) {
document.querySelector('#pagination').style.backgroundColor = 'red';
} else {
document.querySelector('#pagination').style.backgroundColor = 'transparent';
}
});

Categories

Resources