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.
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);
Many days now I'm in a project and until now I have scored many mini goals on a daily routine. So far, I have implement a chart with ChartJS. This chart has a lot of datasets(over 100) so I forced to build a custom legend for the labels.
The result of the legend that I have now is this:
The desirable result that I want is this:
In other words, when I click an object i want to fire up the process as it was the official legend and not the custom that I made.
Now, when I click one object, it works and delete the data from this object and when I click it again it shows up in the chart.
The only thing that I want is to put this line when I click and remove it when I click it again.
Here is my code for the custom legend:
legendCallback: function (chart3) {
var legendHTML = [];
legendHTML.push('<ul>');
for (var i = 0; i < chart3.data.datasets.length; i++) {
if (chart3.isDatasetVisible[i]) {
legendHTML.push('<li>');
}
else {
legendHTML.push('<li class="">');
}
legendHTML.push('<span class="chart-color" style="background-color:' + chart3.data.datasets[i].backgroundColor + '"></span>');
legendHTML.push('<li onclick="updateDataset(event, ' + '\'' + chart3.legend.legendItems[i].datasetIndex + '\'' + ')">' + chart3.data.datasets[i].label + '</span>');
legendHTML.push('</li>');
}
legendHTML.push('</ul>');
return legendHTML.join("");
}
the function for the onClick:
updateDataset = function (e, datasetIndex) {
var index = datasetIndex;
var ci = e.view.chart3;
var meta = ci.getDatasetMeta(index);
meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null;
ci.update();
};
$('li span.chart-color').click(function() {
updateDataset($(this).data('index'));
$(this).toggleClass('li.hidden');
});
the CSS code for the legend:
ul {
list-style: none;
}
li {
display: inline-block;
padding: 0 10px;
cursor: pointer;
}
li.hidden {
text-decoration: line-through !important;
}
li span.chart-color {
border-radius: 5px;
display: inline-block;
height: 10px;
margin-right: 5px;
width: 10px;
}
#chart3-legend {
float: left;
overflow: auto;
}
You try to toggle a class you dont have defined in your style sheet. Classes in CSS start with a dot.
You can also check if the class gets set on the element you intend to set it on. You also might run into issues while trying to use dots in your class name because a dot means that the element has both class names.
So .x.y {styling} means the styling will only be applied to elements that have at least this in there class: class="x y"
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>
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
My team is using an expandable box to condense info on our wiki (in Confluence), it's pretty standard using display:none/block. I'd like this to work with the browser's find functionality. I've found that when I switch to hiding the content using max-height the browser at least finds the text correctly, but I'd like to expand the collapsible when a match is found inside it and re-collapse it when find is no longer looking at it. Is there a way to do that?
I've already tried the focus and selectionchange events to no avail. I guess I could track scrolling for jumps or track keystrokes but neither of those really tells me if the collapsible is where the query was found.
tl;dr is there a way to detect browser find?
Update: Here's an idea of what the code looks like:
var expand = document.querySelector('.expand');
expand.querySelector('.head').addEventListener('click', function(e) {
//toggle a class .show on .expand
})
And my CSS:
.expand .body {
opacity: 0;
max-height: 0;
padding: 0 20px 0 40px;
transition: all .4s;
}
.expand.show .body {
max-height: 3000px;
opacity: 1;
padding: 10px 20px 20px 40px;
}
Actually you can detect key press combination such as Ctrl+F like below
var map = {17: false, 70: false};
$(document).keydown(function(e) {
if (e.keyCode in map) {
map[e.keyCode] = true;
if (map[17] && map[70]){
// FIRE EVENT YOU EVENT
}
}
}).keyup(function(e) {
if (e.keyCode in map) {
map[e.keyCode] = false;
}
});