How to make this div scrollTop value match that of the textarea? - javascript

I'm using a div to format and display the text from a textarea of equal dimensions and I need them to be permanently in sync. However, I haven't been able to synchronize their respective scrollTops after the input text goes past the bottom of the textarea.
My process has been similar to the one described here, however I can't get his solution to work on my project.
Here's a demo and snippets of the minimum relevant code:
<section>
<div class="input-text__container">
<div id="input-text--mirror" class="input-text"></div>
<textarea
id="input-text--original"
cols="30"
rows="6"
autofocus
class="input-text"
placeholder="Enter your text here"
autocomplete="off"
autocorrect="off"
spellcheck="false"
></textarea>
</div>
<section>
#import url('https://fonts.googleapis.com/css2?family=Inter:wght#400;500&display=swap');
html {
font-size: 62.5%;
box-sizing: border-box;
scroll-behavior: smooth;
}
*,
*::after,
*::before {
margin: 0;
padding: 0;
border: 0;
box-sizing: inherit;
vertical-align: baseline;
}
body {
height: 100vh;
}
section {
display: flex;
flex-direction: column;
min-height: 100%;
padding: 1rem;
}
.input-text__container {
width: 100%;
position: relative;
flex: 1;
}
.input-text {
width: 100%;
height: 100%;
position: absolute;
font-size: 3.2rem;
overflow-wrap: break-word;
font-family: "Inter";
}
#input-text--mirror {
background-color: #e9ecf8;
color: #0a3871;
overflow: hidden;
}
#input-text--original {
background-color: transparent;
-webkit-text-fill-color: transparent;
resize: none;
outline: none;
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
#input-text--original::placeholder {
color: #e9ecf8;
-webkit-text-fill-color: #052051;
}
#input-text--original::selection {
-webkit-text-fill-color: #ffffff;
}
.invalid {
font-weight: 400;
color: #ff0000;
}
#input-text--original::-webkit-scrollbar {
display: none;
}
let invalidInput = false;
const patterns = {
valid: "a-z ",
invalid: "[^a-z ]",
linebreaks: "\r|\r\n|\n",
};
const textIn = document.querySelector("#input-text--original");
const mirror = document.querySelector("#input-text--mirror");
function validateInput(string, className) {
let anyInvalidChar = false;
// Generate regular expressions for validation
const regExpInvalids = new RegExp(patterns.invalid, "g");
const regExpLinebreaks = new RegExp(patterns.linebreaks);
// Generate innerHTML for mirror
const mirrorContent = string.replace(regExpInvalids, (match) => {
if (regExpLinebreaks.test(match)) {
return "<br/>";
} else {
anyInvalidChar = true;
return `<span class=${className}>${match}</span>`;
}
});
// Update mirror
mirror.innerHTML = mirrorContent;
return anyInvalidChar;
}
textIn.addEventListener("input", (e) => {
const plain = textIn.value;
const newInputValidity = validateInput(plain, "invalid");
mirror.scrollTop = textIn.scrollTop;
});
textIn.addEventListener(
"scroll",
() => {
mirror.scrollTop = textIn.scrollTop;
},
{ passive: true }
);
On a desktop screen typing the first 8 natural numbers in a column should be enough to reproduce the issue.
The last thing I checked, but perhaps the most relevant so far was this. It seems to deal with the exact same issue on React, but I'm afraid I don't know how to adapt that solution to Vanilla JavaScript, since I'm just starting to learn React. Please, notice, I'm trying to find a solution that doesn't depend on libraries like jQuery or React.
Besides that, I tried the solution described in the aforementioned blog, by replacing return "<br/>"; with return "<br/> "; in my validateInput function but that didn't work. I also added a conditional to append a space to plain in const plain = textIn.value; in case the last char was a linebreak, but I had no luck.
I also included console.log commands before and after mirror.scrollTop = textIn.scrollTop; in the textIn scroll handler to track the values of each scrollTop and even when they were different, the mirror scrollTop wasn't updated. I read it might be because divs weren't scrollable by default, but adding "overflow: scroll" to its styles didn't fix the problem either.
I read about other properties related to scrollTop, like offsetTop and pageYOffset, but they're either read-only or not defined for divs.
I've reviewed the following posts/sites, too, but I've still haven't been able to fix this problem.
https://codepen.io/Goweb/pen/rgrjWx
https://stackoverflow.com/questions/68092068/making-two-textareas-horizontally-scroll-in-sync
Scrolling 2 different elements in same time
React : setting scrollTop property of div doesn't work
sync scrolling of textarea input and highlighting container
.scrollTop(0) not working for getting a div to scroll to the top
How to attach a scroll event to a text input?
I no longer remember what else I've reviewed, but nothing has worked and I no longer know what else to do. Thank you for your attention and help.

After trying to replicate the solution for a React app that I mentioned in the post, using vanilla JavaScript (demo here), I tried to apply that to my own project and all I had to do was adding a <br> tag to the mirror in the end of my validateInput function. That is: mirror.innerHTML = mirrorContent + "<br>";.
Besides that, updating the mirror's scrollTop every time the input event on the textarea was triggered was not needed. Neither was it to pass the { passive: true } argument to the scroll event.
The modified code is here:
function validateInput(string, className) {
let anyInvalidChar = false;
// Generate regular expressions for validation
const regExpInvalids = new RegExp(patterns.invalid, "g");
const regExpLinebreaks = new RegExp(patterns.linebreaks);
// Generate innerHTML for mirror
const mirrorContent = string.replace(regExpInvalids, (match) => {
if (regExpLinebreaks.test(match)) {
return "<br/>";
} else {
anyInvalidChar = true;
return `<span class=${className}>${match}</span>`;
}
});
// Update mirror
mirror.innerHTML = mirrorContent + "<br>";
return anyInvalidChar;
}
textIn.addEventListener("input", (e) => {
const plain = textIn.value;
const newInputValidity = validateInput(plain, "invalid");
});
textIn.addEventListener("scroll", () => mirror.scrollTop = textIn.scrollTop);

Related

JavaScript onclick change image and increase the counter

I try to make a simple like/dislike function to my page. The image changing is working but the counter not and I do not know why. Any idea how to make it workable?
By the way I have read a bunch of questions about like/dislike system with JS but its not really works for me.
const imageChange = document.querySelector('.likeClassQ')
var countL = 0;
var buttonLike = document.getElementById("likeButton");
var displayLike = document.getElementById("likes");
buttonLike.addEventListener('click', () => {
imageChange.classList.toggle('likeClassFilled')
})
buttonLike.onclick = function() {
if (buttonLike.classList == 'likeClass') {
countL++;
buttonLike.classList.add = "likeClassFilled";
} else if (buttonLike.classList == "likeClassFilled") {
countL--;
buttonLike.classList.add = "likeClass";
}
displayLike.innerHTML = countL;
}
.likeClass {
background-color: red;
background-repeat: no-repeat;
background-size: contain;
padding: 10px;
border: none;
}
.likeClassFilled {
background-color: green;
}
<span><span id="likes">0</span> Likes</span><br>
<button id="likeButton" class="likeClass likeClassQ">Like</button>
There is no need to assign a function to onclick and use addEventListener. Just use one and stick to it.
Your CSS classes are all over the place. Use one for the general styling and another one for your state. Or better yet, use the data attribute if the element or maybe even a stylized checkbox for that. Mixing CSS classes and business logic is a slippery slope.
classList has methods like toggle, add and includes, but you have to use those fields as methods and not as simple fields. Also, you can not use the comparison operator (==) with objects. You would only use that on simple values like strings or numbers. You execute functions/methods by writing brackets after the method/function name and passing any parameters in those. When you use the assignment operator (=), you are not calling anything.
Your logic about saving the state and deriving the current state is flawed. I changed it to toggle a class on each click. Hence you will not find any classes being added or removed within the condition.
const imageChange = document.querySelector('.likeClassQ')
var countL = 0;
var buttonLike = document.getElementById("likeButton");
var displayLike = document.getElementById("likes");
buttonLike.onclick = function() {
if (buttonLike.classList.contains('likeClassFilled')) {
countL--;
} else {
countL++;
}
buttonLike.classList.toggle("likeClassFilled");
displayLike.innerHTML = countL;
}
.likeClass {
background-color: red;
background-repeat: no-repeat;
background-size: contain;
padding: 10px;
border: none;
}
.likeClassFilled {
background-color: green;
}
<span><span id="likes">0</span> Likes</span><br>
<button id="likeButton" class="likeClass likeClassQ">Like</button>

How do you get the style of focussed HTML element using JavaScript?

I would like to get the computed style of an element when it is focussed. I am using puppeteer and I would like to compare the CSS before focus to the CSS after focus. I can successfully get focus on an element using element.focus() and can confirm it's focussed by checking the active element (document.activeElement). However, the computed style (getComputedStyle()) for this element is the same as before focus, which is not what I expected.
Is there a way to get the CSS of the element after focus?
To be clear, I am writing a JavaScript tool which can scrape any website to check if the outline or border of an element under focus has sufficient contrast with the background on the page (such that it conforms with accessibility guidelines https://www.w3.org/WAI/WCAG21/Understanding/non-text-contrast.html). To do this I need to be able to get the colour of the outline/border when the element is under focus. Normally a focus outline would be defined in the CSS under :focus { ... } so this is what I wish to retrieve for any element.
you need to pay attention to the order of code in the script. if you change the style before the getComputedStyle() you'll get the change.
exmple:
document.querySelector("input").addEventListener("focus", (e) => {
console.log(window.getComputedStyle(e.target, null).color);
e.target.style.color = "red";
console.log(window.getComputedStyle(e.target, null).color);
e.target.style.color = "blue";
})
<input type="text" style="color: blue;">
As getComputedStyle returns a live CSSStyleDeclaration, you have to record the CSS styles by yourself manually to keep the CSS before focus.
Here's a working example:
let allInputs = document.querySelectorAll('input')
let beforeFocusStyles = Array.from(allInputs).reduce(function(final, elem) {
final[elem.dataset.identifier] = (function() {
let liveStyle = window.getComputedStyle(elem)
let value = {}
for (let key in liveStyle) {
value[key] = liveStyle[key]
}
return value
})()
return final
}, {})
Array.from(allInputs).forEach(elem => {
elem.onfocus = function() {
let afterFocusStyle = window.getComputedStyle(elem)
let differenceInStyle = (function() {
let beforeFocusStyle = beforeFocusStyles[elem.dataset.identifier]
let differences = []
for (let key in afterFocusStyle) {
if (beforeFocusStyle[key] !== afterFocusStyle[key]) {
differences.push([key, beforeFocusStyle[key], afterFocusStyle[key]])
}
}
return differences
})()
differenceInStyle.forEach(difference => {
console.log(difference[0], difference[1], difference[2])
})
}
})
.type1 {
outline: 1px solid black;
border: none;
}
.type1:focus {
outline: 2px solid red;
border: none;
}
.type2 {
outline: 1px solid blue;
border: none;
}
.type2:focus {
outline: 2px solid green;
border: none;
}
<input type="text" class="type1" data-identifier="1">
<input type="text" class="type2" data-identifier="2">

Optimizing jquery hide() and show() on Safari

I have a site with a large outline, and I'm trying to let our users filter it down so they can see just the stuff they want. Each line of the outline has a set of classes that say what category it's in, and I'm hide/showing them via jQuery when the users select a particular category.
Here's the current location so you can see it in action:
https://courses.edx.org/courses/course-v1:HarvardX+CHEM160+1T2017/76695c0ad7604bb897570ecb906db6e3/
And here's the javascript and css for this page:
$(document).ready(function() {
console.log('working');
// Keeping track of all the currently visible items.
var currentlyShown = [];
var index;
var showAllButton = $('#showAll');
// If any of the object's classes match any of the selected options, show it.
function showRightClasses() {
console.log('showing: ' + currentlyShown);
if (currentlyShown.length == 0) {
showAllButton.click();
}
$('.hiddenpage').each(function(i) {
if (_.intersection(this.className.split(' '), currentlyShown).length > 0) {
$(this).show('slow');
} else {
$(this).hide('slow');
}
});
}
if (showAllButton.prop('checked')) {
currentlyShown.push('hiddenpage');
showRightClasses();
}
showAllButton.change(function() {
if (!this.checked) {
index = currentlyShown.indexOf('hiddenpage');
if (index !== -1) {
currentlyShown.splice(index, 1);
}
} else {
currentlyShown.push('hiddenpage');
}
showRightClasses();
});
$('.pageselector').change(function() {
subject = $(this).attr('name');
if (!this.checked) {
index = currentlyShown.indexOf(subject);
if (index !== -1) {
currentlyShown.splice(index, 1);
}
} else {
currentlyShown.push(subject);
}
if (showAllButton.prop('checked')) {
showAllButton.click();
}
showRightClasses();
});
});
.hiddenpage {
display: none;
}
.checkboxes {
float: right;
padding: 8px;
border: 4px outset #aaa;
border-radius: 8px;
background-color: #eee;
}
.checkboxes label {
display: inline;
}
.nav-section {
font-size: 120%;
font-weight: bold;
margin-top: 1em;
}
.nav-sub {
font-weight: bold;
margin-left: 1em;
}
.nav-unit {
font-weight: normal;
margin-left: 2em;
}
This works, but on Safari it's dreadfully slow, and it's not particularly fast on Firefox either. Is there a more efficient way to hide/show the rows in this outline without losing the animation? Am I accidentally doing something foolish like having every row run code that hides every other row?
I should note that I have no ability to control the rest of the environment. I can't change the version of jQuery that the site uses, or remove Underscore, for example. I can only control the code you see above, and the HTML for the list.
First of all, if you care about speed, ditch the 'slow' param in .show('slow') and .hide('slow'). This triggers a very performance-heavy jQuery animation.
With all the frames you're loosing right now, this will not work nice anyway. If you need animation there, maybe you could try something with opacity instead, since (css-based) opacity animation is very cheap.
EDIT: just checked this on the site you linked and it works nice and snappy with just .show() and .hide(). The 'slow' param is definitely your bottleneck, so either just remove it or look for a different way to animate, if you absolutely need to.

How do I simplify this code to reduce the amount of lines necessary? (Javascript)

https://plnkr.co/V14X7icWCrmUw6IrCRVV
That's the plunker for the code. I've never linked to plunker so if it doesn't work, let me know.
What it is supposed to do is when a user hovers over some of the text, that same text should appear in the yellow box.
I thought I should have been able to do it with just a few lines, and substituting the index number with a variable, and looping through them with a while loop. I couldn't quite figure it out and had to just make like 20 different functions. I got it to do what I wanted it to do, but I can't help but think there should be a simpler way to do it.
Here is the Javascript: (The plunker link has the CSS and HTML)
var gamesArray = ['Metal Gear Solid 1', 'The Last of Us', 'Uncharted', 'Snake Eater', 'Need for Speed', 'Forza', 'Halo', 'Conker\'s Bad Fur Day', 'WWF No Mercy', 'WWF Wrestlemania 2000', 'Spelunky', 'The Last of Us Part 2', 'The Walking Dead Season 1', 'The Phantom Pain', 'Ys Memories of Celceta', 'Ys Seven', 'Dragon Ball Z Tenkaichi Tag Team', 'Naruto: Ultimate Ninja Heroes', 'Mortal Kombat'];
var itemList = document.getElementsByClassName('myClass');
var box2 = document.getElementsByClassName('answerBox');
box2[0].style.borderColor = 'black';
box2[0].style.color = 'red';
//var num = 0;
//var i = itemList[num];
//var j = gamesArray[num];
function choice000(){
box2[0].textContent = gamesArray[0];
}
function choice001(){
box2[0].textContent = gamesArray[1];
}
function choice002(){
box2[0].textContent = gamesArray[2];
}
function choice003(){
box2[0].textContent = gamesArray[3];
}
function choice004(){
box2[0].textContent = gamesArray[4];
}
function choice005(){
box2[0].textContent = gamesArray[5];
}
function choice006(){
box2[0].textContent = gamesArray[6];
}
function choice007(){
box2[0].textContent = gamesArray[7];
}
function choice008(){
box2[0].textContent = gamesArray[8];
}
function choice009(){
box2[0].textContent = gamesArray[9];
}
function choice010(){
box2[0].textContent = gamesArray[10];
}
function choice011(){
box2[0].textContent = gamesArray[11];
}
function choice012(){
box2[0].textContent = gamesArray[12];
}
function choice013(){
box2[0].textContent = gamesArray[13];
}
function choice014(){
box2[0].textContent = gamesArray[14];
}
function choice015(){
box2[0].textContent = gamesArray[15];
}
function choice016(){
box2[0].textContent = gamesArray[16];
}
function choice017(){
box2[0].textContent = gamesArray[17];
}
function choice018(){
box2[0].textContent = gamesArray[18];
}
function choice019(){
box2[0].textContent = gamesArray[19];
}
Just use one function:
function choice(game){
box2[0].textContent = gamesArray[game];
}
function choice(number) {
box2[0].textContent = gamesArray[number];
}
Use this function and then replace any of the other function calls. Example choice003() is replaced with choice(3)
I like that you doubt your solution, because it is indeed overwhelming! It's a very important rule: if you see certain pattern, repetition of some kind, you can always make it shorter.
First of all, you need to decide if your items are already created in HTML or do you want to create them using JavaScript. In other words: you should have only one source of data. In your example you'd need to maintain two data sources at once — array in JavaScript and list in HTML.
HTML-driven data
It's extremely important to separate HTML and JavaScript as much as possible. Below you'll find a working example without even a smallest amount of any JS functions or events.
If you design your code that way it's easier to keep track of everything, as it stays simple. JavaScript code below has only around 6 real-lines and it can be simplified even more!
If you need to provide any additional data in the box, that are not visible to the user by default, you can use data attributes.
I've used jQuery since I'm used to it, but you can easily achieve the same effect with roughly the same amount of lines with pure JavaScript.
/*
We define every event and action only in JavaScript.
We're keeping HTML *pure* and *simple*.
*/
$(function(){
var $games = $('.gameChoiceList li');
$games.on('mouseover', function() { // After moving mouse on any [.gameChoiceList li] element
var $game = $(this);
var $result = $('.answerBox');
$result.text($game.text()); // Display current [.gameChoiceList li] text in [.answerBox]
});
});
/* Styles go here */
body {
background-color: skyblue;
font-family: Arial, sans-serif; /*
We provide feedback if user doesn't have Arial font installed:
sans-serif is a group of serif fonts, so possible replacement wont be far from Arial */
}
.answerContainer{ /*
We don't need width: auto; and height: auto; as those are the default values */
border-style: solid;
margin-top: 30px;
}
.testAnswer{
border-style: solid;
padding: 10px;
}
.answerBox{
border-style: solid;
text-align: center;
padding: 20px;
height: 200px;
width: 50%;
background-color: yellow;
}
/*/ Extra: /*/
.gameChoiceList {
float: left;
padding: 0;
width: 40%;
}
.gameChoiceList li {
list-style-type: none;
margin: 2px;
}
.gameChoiceList li a {
display: block;
border: solid 1px;
background: #fff;
text-align: center;
padding: 5px;
}
.answerBox {
float: right;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<ul class="gameChoiceList">
<!--
We don't need the same class for every <li>, as it is easily accesible by:
.gameChoiceList li OR .gameChoiceList > li
We don't need the same class for every <a>, as it is easily accesible by:
.gameChoiceList li a OR .gameChoiceList a
-->
<li>Metal Gear Solid 1</li>
<li>The Last of Us</li>
<li>Uncharted</li>
<li>Snake Eater</li>
<li>Need for Speed</li>
<li>Forza</li>
<li>Halo</li>
<li>Conker's Bad Fur Day</li>
<li>WWF No Mercy</li>
<li>WWF Wrestlemania 2000</li>
<li>Spelunky</li>
<li>The Last of Us Part 2</li>
<li>The Walking Dead Season 1</li>
<li>Metal Gear Solid V: The Phantom Pain</li>
<li>Ys Memories of Celceta</li>
<li>Ys Seven</li>
<li>Dragon Ball Z Tenkaichi Tag Team</li>
<li>Naruto Ultimate Ninja Heroes</li>
<li>Mortal Kombat</li>
</ul>
<p class="answerBox">Mouseover on any of the games on the side to display its name here.</p>
Extra thought
I noticed that in your JavaScript code you're changing borderColor and color of .answerBox. You should never do that.
As it's part of visual styles, you should define adequate styles beforehand in your CSS file and then, toggle certain classes as needed. For example, below code is much easier to maintain:
/*/
Of course all the code below can be simplified to just 1 line:
document.querySelector('p').className = 'important';
I wanted to show something universal that helps separate the logic even more:
You can pass any element, class and content to the make() function.
/*/
var make = function(element, className, content) {
element.className = className;
element.textContent = content;
};
var paragraph = document.querySelector('p');
setTimeout(function() {
make(paragraph, 'important', 'Important paragraph');
}, 1000); // Make important after 1 second
setTimeout(function() {
make(paragraph, 'irrelevant', 'Irrelevant paragraph. do dont read it!');
}, 2000); // Make important after 2 seconds
/*/ Since we defined every class beforehand,
it's easier to adjust styles for certain actions in the future /*/
p {
color: green;
}
p.important {
color: red;
border: solid 1px red;
padding: 10px;
}
p.irrelevant {
color: gray;
font-size: .8em;
}
<p>Very nice paragraph</p>
Stay pure!
~Wiktor

Why is the scrollWidth of a valid text area undefined?

I have found many answers suggesting I use scrollWidth and scrollHeight to get the content width and height, respectively, of a text area. However, my code returns the scrollWidth as undefined. Any idea why this could be the case?
function changewidth(o) {
if ($(o).val().length>8)
{
var current = $(o).css("width");
console.log($(".name").scrollWidth+'px');
$(o).css("width",$(".name").scrollWidth+'px');
}
}
And this javascript function is being called by this text area:
<textarea rows="1" onkeyup="changewidth(this)" class="name" type="textarea" placeholder="name"></textarea>
The console prints undefinedpx. Based on research, I have tried to alter the CSS and currently have the following:
.name {
width: 100px;
margin: 0;
padding: 0;
border: none;
font-size: 20px;
color: #00B45E;
resize: none;
overflow: hidden;
}
You have to try this, No need to wrap currently passed object into $() unless you need it.
function changewidth(o) {
if ($(o).val().length > 8) {
var current = $(o).css("width");
console.log(o.scrollWidth + 'px'); // o.scrollWidth
$(o).css("width", o.scrollWidth + 'px'); // use object as o direclty
}
}
With pure Javascript
function changewidth(o) {
if (o.value.length>8)
{
var current = o.style.width;
console.log(o.scrollWidth+'px');
o.style.width = o.scrollWidth+'px';
}
}
Fiddle
With pure javascript
Demo
scrollWidth
scrollWidth only applies to plain Javascript, it seems you are using it on jQuery.
instead of $(".name").scrollWidth try changing it to document.getElementsByClassName("name").scrollWidth

Categories