Scroll to bottom when new message added - javascript

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;

Related

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>

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 make tabs with pure JS? [closed]

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>

How to make simple 3 step checkout section like quiz

Hello and have a nice day!
I want to create simple step by step element with back button.
I have a next markup
<div class="checkout">
<div class="checkout-step">
<div class="content">This is step 1</div>
<div class="button-prev-step">Previous step</div>
<div class="button-next-step">Next step</div>
</div>
<div class="checkout-step">
<div class="content">This is step 2</div>
<div class="button-prev-step">Previous step</div>
<div class="button-next-step">Next step</div>
</div>
<div class="checkout-step">
<div class="content">This is step 3</div>
<div class="button-prev-step">Previous step</div>
<div class="button-next-step">Next step</div>
</div>
</div>
JS (It's not fully complete code, just part of existing code, I don't fully understand how I can do it)
var checkoutStepCounter = 0;
var checkoutStepLength = $('.checkout-step').length;
$('.button-next-step').click(function() {
checkoutStepCounter++;
if( checkoutStepLength < checkoutStepCounter ) {
$(this).find('.checkout-step').hide();
$(this).find('.checkout-step').next('.checkout-step').show();
}
});
$('.button-prev-step').click(function() {
checkoutStepCounter--;
if( checkoutStepLength < checkoutStepCounter ) {
$(this).find('.checkout-step').hide();
$(this).find('.checkout-step').prev('.checkout-step').show();
}
});
CSS:
.button-prev-step, .button-next-step { display: block; width: 100px; height: 20px; background: red; cursor: pointer; }
.checkout-step { display: none; }
https://codepen.io/Frunky/pen/yKNMOM
How to make function nextStep and prevStep? in result I want to slide between the sections, and in total - send filled data to backend. Now I need to add an ability to slide between .checkout-step blocks. May be I need to add simple slider? like slick or owl, but I don't know if it's right way or not
Simple code snippet to show you next and previous button working. You can do some more research and add your desired animation (slide, fade etc.) as per your requirements.
var checkoutSteps = 3;
var currentStep = 1;
$('.button-next-step').click(function() {
if(currentStep < checkoutSteps){
$(".checkout-step-"+currentStep+"").hide();
currentStep++;
$(".checkout-step-"+currentStep+"").show();
}
});
$('.button-prev-step').click(function() {
if(currentStep > 1){
$(".checkout-step-"+currentStep+"").hide();
currentStep--;
$(".checkout-step-"+currentStep+"").show();
}
});
.button-prev-step, .button-next-step { display: inline-block; width: 100px; height: 20px; background: red; cursor: pointer; }
.checkout-step {
width: 300px;
height: 300px;
border: 1px solid #000;
display: none;
}
.checkout-step-1{
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="checkout">
<div class="checkout-step checkout-step-1">
<div class="content">This is step 1</div>
<div class="button-prev-step">Previous step</div>
<div class="button-next-step">Next step</div>
</div>
<div class="checkout-step checkout-step-2">
<div class="content">This is step 2</div>
<div class="button-prev-step">Previous step</div>
<div class="button-next-step">Next step</div>
</div>
<div class="checkout-step checkout-step-3">
<div class="content">This is step 3</div>
<div class="button-prev-step">Previous step</div>
<div class="button-next-step">Next step</div>
</div>
</div>
Hope this helps

Refactoring jQuery repeating pattern

I have painted my self into a corner in order to quickly prototype.
What's the best way to refactor the following jQuery code? Its functionality is to toggle between some sidebar navigation items. I need it to be more dynamic in order to be scalable.
Would you add the IDs inside the if statements, in an array and iterate through them? Use variables? Create a function and call it on the html side onClick? No matter what I think of, it stills leads to a bunch of repeating code.
Thank you!
// TOGGLING LEFT NAVIGATION
$('#settingsClick').click(function() {
if( $('#addContainer, #noteContainer, #logoContainer, #themeContainer').is(':visible') ) {
$('#addContainer').slideUp(350);
$('#noteContainer').slideUp(350);
$('#logoContainer').slideUp(350);
$('#settingsContainer').slideDown(350);
$('#themeContainer').slideUp(350);
} else {
$('#settingsContainer').slideToggle(350);
}
});
$('#addClick').click(function() {
if( $('#settingsContainer, #noteContainer, #logoContainer, #themeContainer').is(':visible') ) {
$('#settingsContainer').slideUp(350);
$('#noteContainer').slideUp(350);
$('#logoContainer').slideUp(350);
$('#addContainer').slideDown(350);
$('#themeContainer').slideUp(350);
} else {
$('#addContainer').slideToggle(350);
}
});
$('#noteClick').click(function() {
if( $('#settingsContainer, #addContainer, #logoContainer, #themeContainer').is(':visible') ) {
$('#settingsContainer').slideUp(350);
$('#addContainer').slideUp(350);
$('#logoContainer').slideUp(350);
$('#noteContainer').slideDown(350);
$('#themeContainer').slideUp(350);
} else {
$('#noteContainer').slideToggle(350);
}
});
$('#logoClick').click(function() {
if( $('#settingsContainer, #addContainer, #noteContainer, #themeContainer').is(':visible') ) {
$('#settingsContainer').slideUp(350);
$('#addContainer').slideUp(350);
$('#noteContainer').slideUp(350);
$('#logoContainer').slideDown(350);
$('#themeContainer').slideUp(350);
} else {
$('#logoContainer').slideToggle(350);
}
});
$('#themeClick').click(function() {
if( $('#settingsContainer, #addContainer, #noteContainer, #logoContainer').is(':visible') ) {
$('#settingsContainer').slideUp(350);
$('#addContainer').slideUp(350);
$('#noteContainer').slideUp(350);
$('#logoContainer').slideUp(350);
$('#themeContainer').slideDown(350);
} else {
$('#themeContainer').slideToggle(350);
}
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<a id="settingsClick">Click Me</a><br>
<div id="settingsContainer">Content...</div>
<br><br>
<a id="addClick">Click Me</a><br>
<div id="addContainer">Content...</div>
<br><br>
<p> Etc... Etc....</p>
You should group using the common CSS class, i.e. header and content. Using the established relationship you can target the others content holder and content associated with the current clicked header element.
$('.container .header').on('click', function() {
//Get the current element
var $this = $(this);
//find the content
var $content = $this.closest('.container').find('.content'); //$this.next()
//get all contents
var content = $('.container .content');
//Slide up others
content.not($content).slideUp(350);
//Slide down
$content.slideToggle(350);
});
.content {
display: none
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="container">
<div class="header" id="settingsClick">Click Me</div>
<div class="content" id="settingsContainer">Content...</div>
</div>
<div class="container">
<div class="header" id="addClick">Click Me</div>
<div class="content" id="addContainer">Content...</div>
</div>
<div class="container">
<div class="header" id="noteClick">Click Me</div>
<div class="content" id="noteContainer">Content...</div>
</div>
the best bet would be to do it like so
$(document).on('click', ".trigger", function() {
var sibling_content = $(this).siblings(".content");
if (!sibling_content.hasClass('active')) {
$(".content").slideUp('slow').removeClass('active');
sibling_content.slideDown('slow').addClass('active');
} else {
sibling_content.slideUp('slow').removeClass('active');
}
})
.trigger {
background-color: red;
color: white;
font-size: 16px;
}
.content {
background-color: blue;
color: white;
font-size: 16px;
padding: 20px 0;
display: none;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class="container">
<div class="trigger">trigger</div>
<div class="content">content</div>
</div>
<div class="container">
<div class="trigger">trigger</div>
<div class="content">content</div>
</div>
<div class="container">
<div class="trigger">trigger</div>
<div class="content">content</div>
</div>
<div class="container">
<div class="trigger">trigger</div>
<div class="content">content</div>
</div>

Categories

Resources