I'm wondering how to select a child when the callback of a forEach does not directly refer to the desired element.
const $searchField = document.querySelectorAll('.js-search');
if ($searchField.length > 0) {
$searchField.forEach(function($el) {
// $el = .js-search
$el.addEventListener('click', function(event) {
$el.classList.add('is-active');
});
// Here, i need to target the input, not .js-search
$el.addEventListener('keyup', function(event) {
if ($el.value.length > 0) {
$el.classList.add('is-active');
} else {
$el.classList.remove('is-active')
}
//});
});
}
Here, $el refers to the parent .js-search so obviously the keyup can not work. I would like to make sure to select the input, but I'm not sure how to do it properly.
Demo is available on Codepen!
The goal is to keep the state is-active when the search is completed (has at least 1 character).
Thanks
Just select the child input's .value, instead of the $el's .value:
const $searchField = document.querySelectorAll('.js-search');
$searchField.forEach(function($el) {
// $el = .js-search
$el.addEventListener('click', function() {
$el.classList.add('is-active');
});
const input = $el.querySelector('input');
input.addEventListener('keyup', function() {
if (input.value.length > 0) {
$el.classList.add('is-active');
} else {
$el.classList.remove('is-active')
}
});
});
Note that there's no need for the if check if you don't want - calling forEach on an empty collection won't throw an error, it just won't iterate over anything.
You also might consider including a polyfill for NodeList.prototype.forEach (if you aren't already), since older browsers don't support it. (Alternatively, transform the collection into an array, or use Array.prototype.forEach.call)
A bit opinion-based, but there's no need to prefix variable names with $ - this isn't PHP. Often, a $ prefix is done when indicating that something is a jQuery ($) collection. If you're doing DOM manipulation, probably best not to use a variable name that starts with $, to avoid confusion for future readers of your code.
You can use this.querySelector('input'); where this will the parent element and querySelector will get the first input child element
const $searchField = document.querySelectorAll('.js-search');
if ($searchField.length > 0) {
$searchField.forEach(function($el) {
$el.addEventListener('click', function(event) {
$el.classList.add('is-active');
let $input = this.querySelector('input');
$input.addEventListener('keyup', function(et) {
console.log(et.target.value)
});
});
});
document.addEventListener('click', function(event) {
if (!$(event.target).closest('.js-search').length) {
closeSearchs();
}
$('.js-search.is-active').not($(event.target).closest('.js-search')).removeClass('is-active');
});
}
function closeSearchs() {
$searchField.forEach(function($el) {
$el.classList.toggle('is-active');
});
}
.field-search {
position: relative;
&.is-active {
.search-input {
background-color: rgba(0, 0, 0, .35);
color: white;
}
}
.search-input {
background: none;
box-shadow: none;
border-radius: 4px;
color: black;
font-size: 1.2rem;
font-weight: 400;
height: 100%;
outline: none;
padding-left: 4.5rem;
transition: background-color .2s;
width: 365px;
&::placeholder {
color: black;
font-size: 1.2rem;
font-weight: 400;
}
&.is-active {
background-color: rgba(255, 255, 255, .1);
}
}
}
<div class="field-search js-search">
<input type="search" class="search-input" placeholder="Search...">
</div>
Related
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);
I'd like to reset localstorage daily and remove a lot of the old storage but also keep a few important items for some users (localStorage.clear() not an option).
The keys I'd like removed all have the value true. The important items to keep have different values. Rather than listing every single item (200+) that I want to remove do you think it's possible to remove all at once by targeting the value of true?
As you can see from this JSfiddle example the one button adds three items to local storage and two of them have the true value.
$(".no").on('click', function() {
localStorage.removeItem(true)
});
$(".one").on('click', function() {
localStorage.setItem("one", true)
localStorage.setItem("two", "yes")
localStorage.setItem("three", true)
});
.no {
width: 40px;
padding: 10px 40px;
border: 2px solid black;
}
.one {
width: 40px;
padding: 10px 40px;
border: 2px solid black;
}
<div class="one">one</div>
<br>
<div class="no">clear</div>
Get list of item keys from localStorage:
const localStorageKeys = Object.keys(localStorage);
Check where value is true and remove:
for (let key of localStorageKeys) {
if (localStorage[key] === "true") {
localStorage.removeItem(key);
}
}
You can try with the below code.
const localStorageKeys = Object.keys(localStorage);
for (let key of localStorageKeys) {
/*Also check with True as well if value is added like this */
if (localStorage[key] === "true" || localStorage[key] === "True") {
console.log(key);
}
}
My simple test program adds and subtracts numbers correctly. However I'd like to turn my global variable, counter into a local variable. I can't logically see how to do this and still keep the program running. Is there a way to position counter so that it is only accessible from the addNumber or subtractNumber closures? Thanks so much!
'use strict';
var counter;
counter = 0;
document.getElementsByClassName('box')[0].addEventListener('click', addNumber);
document.getElementsByClassName('box')[1].addEventListener('click', subtractNumber);
document.getElementsByClassName('box_answer')[0].textContent = 'The answer is ' +counter;
function calculateNumber(x) {
counter += x;
displayNumber();
}
function displayNumber() {
document.getElementsByClassName('box_answer')[0].textContent = 'The answer is ' +counter;
}
function addNumber() {
calculateNumber(1);
}
function subtractNumber() {
calculateNumber(-1);
}
body {
margin: 0;
padding: 0;
font-size: 2rem;
font-family: sans-serif;
}
.box {
margin-bottom: 20px;
padding: 10px;
color: white;
background-color: blue;
}
.box_answer {
padding: 10px;
background-color: orange;
}
<div class="box">addNumber</div>
<div class="box">subtractNumber</div>
<div class="box_answer"></div>
The way to do it is to take all of your code and get it out of the global scope, which you should always strive to do anyway. This is generally done by wrapping all of your code in an Immediately Invoked Function Expression.
Also, you don't need a separate function for adding and subtracting. Just have each click event handler call a single function and pass the value to use to that function.
Lastly, unless you have a specific use case, avoid DOM API calls like:
.getElementsByName()
.getElementsByTagName()
.getElementsByClassName()
because they all return "live" node lists, which cause the entire DOM to be re-scanned for matching elements every time the node list is referenced in your code. This can lead to dramatic performance hits. Instead, use the more modern:
.querySelector()
.querySelectorAll()
and, of course:
.getElementById()
These three API's will handle most of your DOM query needs.
(function(){
'use strict';
// .getElementsByClassName returns a "live" node list, which is not recommended
// for most use cases because it requires re-scanning the DOM every time the node
// list is used. querySelector() and .querySelectorAll() are the modern successors
// to that.
var answer = document.querySelector('.box_answer');
// Find box by its unique class (or id) so that you don't scan the entire DOM only to
// throw away all found elements but the one you want:
document.querySelector('.box.add').addEventListener('click', function() { doMath(1); });
document.querySelector('.box.subtract').addEventListener('click', function() { doMath(-1); });
var counter = 0;
function doMath(x) {
counter += x;
displayNumber();
}
function displayNumber() {
answer.textContent = 'The answer is ' + counter;
}
})();
body {
margin: 0;
padding: 0;
font-size: 2rem;
font-family: sans-serif;
}
.box {
margin-bottom: 20px;
padding: 10px;
color: white;
background-color: blue;
cursor:pointer; /* Change pointer to let user know they can click */
/* Prevent text in element from being selected. Helps when lots of clicking happens. */
user-select:none;
}
.box_answer {
padding: 10px;
background-color: orange;
}
<div class="box add">addNumber</div>
<!-- Give each box a different class (or id) to differentiate one from the other. -->
<div class="box subtract">subtractNumber</div>
<div class="box_answer">The answer is:</div>
You could move all of the variables and functions inide of an IIFE (immediately-invoked function expression).
'use strict';
void function () {
function calculateNumber(x) {
counter += x;
displayNumber();
}
function displayNumber() {
document.getElementsByClassName('box_answer')[0].textContent = 'The answer is ' +counter;
}
function addNumber() {
calculateNumber(1);
}
function subtractNumber() {
calculateNumber(-1);
}
var counter = 0;
document.getElementsByClassName('box')[0].addEventListener('click', addNumber);
document.getElementsByClassName('box')[1].addEventListener('click', subtractNumber);
document.getElementsByClassName('box_answer')[0].textContent = 'The answer is ' + counter;
}();
body { margin: 0; padding: 0; font-size: 2rem; font-family: sans-serif; }
.box { margin-bottom: 20px; padding: 10px; color: white; background-color: blue; }
.box_answer { padding: 10px; background-color: orange; }
<div class="box">addNumber</div>
<div class="box">subtractNumber</div>
<div class="box_answer"></div>
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.
I am trying to pass data attribute in custom element as an object but while receiving inside attachedCallback method getting value "[object object]" in a string form.
So can anyone help me to figure out what is the work around to get the attributes as an object inside custom-element(web component).
code sample
<script>
class myElements extends HTMLElement {
createdCallback() {
this.innerHTML = `<h1>Hello</h1>`;
}
attachedCallback() {
console.log(this.getAttribute('data'));
}
}
document.registerElement('my-element', myElements);
</script>
custom element tag
<script>
var object = { key1: 111, key2: 222, key3: function(){return "hi"}, key4:[1,2,3]};
function changeHandler() {
page('/default', function() {
// some logic to decide which route to redirect to
if (admin) {
page.redirect('/admin');
} else {
page.redirect('/guest');
}
});
}
</script>
<my-element data="object" onchange="changeHandler"></my-element>
Note: suppose that <my-element> is a dropdown which gives user option to choose some value.
Solution: Still no native solution in custom-element specs(v0 and v1).
Since Custom Elements doesn't support data binding so we need a sugaring layer for that (e.g., Polymer or SkateJS) as mention by #tony in the comment.
Try by converting object to JSON string,
var object = { key1: 111, key2: 222};
JSON.stringify(object);
Then when you want to get the value, parse it back to object
JSON.parse(this.getAttribute('data'));
Custom Elements does not modify the standard HTML element attribute behaviour which is always of type string.
Because of that, you shoud instead either:
Send a change Event from your custom element that will trigger the onchange handler.
Register you object/function callback via a custom element method.
Modify a state attribute that will be observed (with Mutation Observer) by its container.
If you want to use attribute anyways you can always use eval().
Example with solution 1 with call to changeHandler():
//define custom element
class DropDown extends HTMLElement
{
connectedCallback ()
{
var span = this.querySelector( 'span' )
//define ul list
var ul = this.querySelector( 'ul' )
ul.onclick = ev => {
if ( this.value != ev.target.textContent )
{
this.value = ev.target.textContent
this.setAttribute( 'value', this.value )
span.textContent = this.value
this.dispatchEvent( new CustomEvent( 'change' ) )
}
}
//show or hide
this.onclick = ev => ul.classList.toggle( 'show' )
}
}
customElements.define( 'drop-down', DropDown )
drop-down {
position: relative ;
cursor: pointer ;
display: inline-block ;
}
drop-down > span {
border: 1px solid #aae ;
padding: 2px 5px ;
}
drop-down > ul {
position: absolute ;
top: 4px ; left: 5px ;
list-style: none ;
outline: 1px solid #aae ;
padding: 0 ;
display: inline-block ;
opacity: 0 ;
transition: all 0.3s ease-out ;
background: white ;
visibility: hidden ;
z-index: 10 ;
}
drop-down > ul.show {
opacity: 1 ;
visibility: visible ;
}
drop-down > ul > li {
padding: 2px 5px ;
}
drop-down > ul > li:hover {
background: lightgoldenrodyellow ;
}
<drop-down onchange="changeHandler()">
<span>select value</span>
<ul>
<li>Chrome</li>
<li>Firefox</li>
<li>Opera</li>
<li>Safari</li>
</ul>
</drop-down>
<script>
function changeHandler ()
{
console.log( 'changeHandler()', event.target.value )
}
</script>