Food Ranking List Challenge (TestDome) - javascript

I get stuck on this challenge so I need help.
A website needs a list where users can rank their favorite foods.
Write the setup function, which should register click handlers on all
Up! and Down! buttons. The Up! button should move the list item one
place up in the list, while Down! button should move the list item one
place down in the list.
For example, consider this code:
document.body.innerHTML = `<ol>
<li><button>Up!</button>Taco<button>Down!</button></li>
<li><button>Up!</button>Pizza<button>Down!</button></li>
<li><button>Up!</button>Eggs<button>Down!</button></li>
</ol>`;
setup();
If the button Up! button in Pizza list item is clicked, Pizza should
be the first item in the list, while Taco should be the second item.
This is my current code:
function setup() {
let list = document.getElementByTagName('li');
let btnList = document.getElementByTagName('button');
let up;
let down;
for (let l = 0; l < list.length; l++){
for (let b = 0; b< btnList.length; b++){
btnList[b].addEventListener("click", (item, index)=>{
if(btnList[b].textContent == "Up!"){
up = list[l - 1]
} else if(btnList[b].textContent == "Down!"){
down = list[l + 1]
} else{
return list
}
})
}
}
return list;
})
}
}
I need to solve it without jQuery.

You don't need two for loops to iterate over the list elements and the buttons. You only need the buttons, the li elements are their parentNodes.
Moving up
Once you add the event listeners to every button, you can check if they are up or down buttons by checking their textContent. If it's an "up" button, you will need to check if it's the first element in the list. If it is, don't do anything, you can't move it any higher. :)
If it's not the first element, you can use insertBefore to swap nodes. Its first parameter is the node you want to insert, in this case the parent element; li. The second parameter is the reference; the node before which the li is inserted. That's the previousElementSibling of the parent.
Moving down
To move an element down is pretty similar. You probably think that you will use insertAfter to swap, but there is no insertAfter method. You can use insertBefore again, but this time the node you insert will be the nextElementSibling and the reference is the parent.
Check out the snippet below:
document.body.innerHTML = `<ol>
<li><button>Up!</button>Taco<button>Down!</button></li>
<li><button>Up!</button>Pizza<button>Down!</button></li>
<li><button>Up!</button>Eggs<button>Down!</button></li>
</ol>`
function setup() {
let btnList = document.querySelectorAll('button')
for (let button of btnList) {
button.addEventListener('click', handleClick)
}
}
function handleClick(e) {
let parent = e.target.parentElement
let isUp = e.target.textContent === 'Up!'
let isDown = e.target.textContent === 'Down!'
let isFirst = !parent.previousElementSibling
let isLast = !parent.nextElementSibling
if (isUp && !isFirst)
parent.parentNode.insertBefore(parent, parent.previousElementSibling)
if (isDown && !isLast)
parent.parentNode.insertBefore(parent.nextElementSibling, parent)
}
setup()
button {
background-color: hsl(206, 100%, 52%);
border: 0;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin: 2px 4px;
padding: 4px 8px;
}
button:hover {
background-color: hsl(206, 100%, 40%);
}
You could also just add a single event listener on the ol element and check if the click target is a button. You can use event.target.tagName for this. If the tagName is BUTTON (always uppercase in HTML), you just return to exit early:
document.body.innerHTML = `<ol>
<li><button>Up!</button>Taco<button>Down!</button></li>
<li><button>Up!</button>Pizza<button>Down!</button></li>
<li><button>Up!</button>Eggs<button>Down!</button></li>
</ol>`
function setup() {
document.querySelector('ol').addEventListener('click', e => {
if (e.target.tagName !== 'BUTTON')
return
let parent = e.target.parentElement
let isUp = e.target.textContent === 'Up!'
let isDown = e.target.textContent === 'Down!'
let isFirst = !parent.previousElementSibling
let isLast = !parent.nextElementSibling
if (isUp && !isFirst)
parent.parentNode.insertBefore(parent, parent.previousElementSibling)
if (isDown && !isLast)
parent.parentNode.insertBefore(parent.nextElementSibling, parent)
})
}
setup()
button {
background-color: hsl(206, 100%, 52%);
border: 0;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin: 2px 4px;
padding: 4px 8px;
}
button:hover {
background-color: hsl(206, 100%, 40%);
}

Use the code below
function setup() {
//Write your code here
$.fn.moveUp = function() {
before = $(this).prev();
$(this).insertBefore(before)
}
$.fn.moveDown = function() {
after=$(this).next();
$(this).insertAfter(after)
}
$("button:contains('Up!')").click(function() {
$(this).parent().moveUp()
})
$("button:contains('Down!')").click(function() {
$(this).parent().moveDown()
})
}

//Following code will give you 100% perfect score.
function setup() {
let buttons = document.querySelectorAll('button');
// console.log(buttons);
buttons.forEach(function(btn){
// console.log(btn);
btn.addEventListener("click", function(){
// console.log(btn.textContent);
if(btn.textContent.includes('Up!')){
// console.log(btn.parentElement.previousElementSibling);
// console.log(btn.parentElement.nextElementSibling);
if(btn.parentElement.previousElementSibling != null){
var store = btn.parentElement.previousElementSibling.innerHTML;
btn.parentElement.previousElementSibling.innerHTML= btn.parentElement.innerHTML;
btn.parentElement.innerHTML= store;
setup();
}else{ console.log("Hello Developer, this element is at start");}
}else{
// taking down.
if(btn.parentElement.nextElementSibling != null){
var stored = btn.parentElement.nextElementSibling.innerHTML;
btn.parentElement.nextElementSibling.innerHTML = btn.parentElement.innerHTML;
btn.parentElement.innerHTML = stored;
setup();
} else {
console.log("Hi developer, element already at last");
}
}
});
});
}
// Example case
document.body.innerHTML = `<ol>
<li><button>Up!</button>Taco<button>Down!</button></li>
<li><button>Up!</button>Pizza<button>Down!</button></li>
<li><button>Up!</button>Eggs<button>Down!</button></li>
</ol>`;
setup();
document.getElementsByTagName('button')[2].click();
console.log(document.body.innerHTML);

Related

Javascript: How to change class and CSS when value is zero

I have 9 boxes in my html.
There is a value, id called 'lifepoint'.
There is a mouse-click function: click once & decrease one life point. This code is completed.
function decrementlife() {
var element = document.getElementById('lifepoint');
var value = element.innerHTML;
--value;
console.log(value);
document.getElementById('lifepoint').innerHTML = value;
if(value <= 0) { alert("Game Over!")};
}
Also, there is a css style, called 'crackbox'.
.crackbox {
position: relative;
background: linear-gradient(0deg, black, rgb(120, 120, 120));
width: 12vh;
height: 12vh;
border-radius: 30%;
margin: 5px;
}
I want to change all box class from 'box' to 'crackbox' if life point is zero. Therefore, all box style can be 'crackbox'.
The below code is fail...
$(document).ready(function () {
$(".box").each(function() {
document.getElementById('lifepoint').innerHTML = value;
if(value <= 0) {
".box".toggleClass('crackbox')
};
})
});
Instead of using document ready, call another function from decrement life if the value turns 0. I am writing the code for your help.
function decrementlife() {
var element = document.getElementById('lifepoint');
var value = element.innerHTML;
--value;
console.log(value);
document.getElementById('lifepoint').innerHTML = value;
if(value <= 0) { changeClass(); alert("Game Over!")};
}
function changeClass(){
$('.box').addClass('crackbox').removeClass('box');
}
Hope, it helps!!
The simplest way would be to use querySelectorAll and loop through the elements:
for(let i = 0, list = document.querySelectorAll(".box"); i < list.length; i++)
{
list[i].classList.toggle('crackbox');
}
Or shorter ES6 version:
[...document.querySelectorAll(".box")].forEach(el => el.classList.toggle('crackbox'))

Is there a way to have an add event listener first execute a mousedown, then mouseover, and finally mouseup?

So right now I have a 20 by 20 grid and I want the user to be able to click and select multiple cells in the grid. There is a method I was able to find online but the problem is that mouseover takes over and highlights the cells right when the mouse is over the cells and this is not what I want. I want the user click on a cells then basically drag their mouse and highlight the cells that they want then execute mouseup once they let go.
These are my files.
let graph = document.getElementById("container");
graph.style.display = "flex";
function createGraph() {
let j = 0;
for (let i = 0; i < 20; i++) {
let row = document.createElement("div");
row.id = "row" + i;
row.style.height = "50px";
row.style.width = "50px";
graph.appendChild(row);
let currentRow = document.getElementById("row" + i);
j++;
for (let j = 0; j < 20; j++) {
let cell = document.createElement("div");
cell.classList.add("cells");
///id's are used later in the project
cell.id = "index" + j + i;
cell.style.border = "1px solid black";
cell.style.height = "50px";
cell.style.width = "50px";
currentRow.appendChild(cell);
}
}
}
createGraph();
function main() {
document.querySelectorAll(".cells").forEach(item => {
["mousedown", "mouseover", "mouseup"].forEach(function(e) {
item.addEventListener(e, function() {
item.style.backgroundColor = "red";
})
})
})
}
main();
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
So in the main function I added an even listener to all the cells and I am trying to change their color to red. The problem is that the mouseover event takes over the mousedown which is what I want to happen first. How can I make it so the user is able to first click down on a cell then drag their mouse and keep highlighting cells and once they let go of the mouse the highlighting stops. Is there away to first execute the mousedown, then mouseover and finally the mouseup?
I refactored your code a little. Here is a simple example how you can use toggle state:
let graph = document.getElementById('container');
function createGraph() {
for (let i = 0; i < 20; i++) {
let row = document.createElement('div');
row.id = 'row' + i;
row.className = 'rows';
for (let j = 0; j < 20; j++) {
let cell = document.createElement('div');
cell.className = 'cells';
cell.id = 'index' + j + i;
row.appendChild(cell);
}
graph.appendChild(row);
}
}
createGraph();
function main() {
let canSelect = false;
document.addEventListener('mousedown', () => canSelect = true);
document.addEventListener('mouseup', () => canSelect = false);
document.querySelectorAll('.cells').forEach(item => {
['mousedown', 'mouseover'].forEach(function(e) {
item.addEventListener(e, () => {
if (!canSelect && e !== 'mousedown') return;
item.style.backgroundColor = 'red';
})
})
})
}
main();
#container {
display: flex;
}
.rows, .cells {
width: 50px;
height: 50px;
}
.cells {
border: 1px solid black;
}
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
</head>
<body>
<div id="container"></div>
</body>
</html>
The trick with things like this is to only add the mouseover event on mousedown to begin with. mouseover is generally an expensive event to have anyways (because it fires a lot), so you only "turn it on" when you want and remove it when you don't.
Also, if you're hooking the same event to multiple elements in the same parent, it is far better to assign the event to the parent and then check the target and act when it is one of the children you want (usually using the .matches() method).
Then, you don't have to worry about mousemove firing first, because it'll always fire second. Just be aware it'll probably fire MANY times per cell, so you need to write your code to handle that.
let targetElements = [];
const parent = document.querySelector('.parent');
const mouseoverHandler = ({ target }) => {
if (!target.matches('.parent span')
|| targetElements.includes(target)) {
return;
}
targetElements.push(target);
};
parent.addEventListener('mousedown', ({ target }) => {
// use whatever selector makes sense for your children
if (!target.matches('.parent span')) return;
// reset the list here in case they mouseup-ed outside of the parent
targetElements = [];
// turn mouseover "on"
parent.addEventListener('mouseover', mouseoverHandler);
targetElements.push(target);
console.log('mouseover on');
});
parent.addEventListener('mouseup', ({ target }) => {
// use whatever selector makes sense for your children
if (!event.target.matches('.parent span')) return;
// turn mouseover "off"
parent.removeEventListener('mouseover', mouseoverHandler);
// do something with them
targetElements.forEach(el => el.classList.toggle('on'));
console.log('mouseover off');
});
.parent {
border: 2px solid #333;
width: 150px;
display: flex;
flex-wrap: wrap;
}
.parent span {
flex: 0 0 50px;
flex-wrap: wrap;
height: 50px;
border: 1px solid #CCC;
margin: -1px;
height: 50px;
display: -block;
}
.parent span:hover {
/* doesn't seem to work in the demo window */
background: #EEC;
cursor: pointer;
}
.parent span.on {
background: #F00;
}
<div class="parent">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>

Javascript click on button to change backgroud color

I want to click one by one within these buttons and change the background colour of them to red. If I select all A, B and C, then every button should change to red. When I clicked one by one to every button, they change colour back to default color.
<button type="button">All</button>
<button type="button">A</button>
<button type="button">B</button>
<button type="button">C</button>
Here is my code
optAll.addEventListener('click', function (e) {
e.target.style.backgroundColor = "red";
optA.style.backgroundColor = "red";
optB.style.backgroundColor = "red";
optC.style.backgroundColor = "red";
optD.style.backgroundColor = "red";
});
You need to begin by splitting up the code where you assign the click handlers.
To avoid unnecessary queries, you can query once and then use the results later multiple times.
const main = document.querySelector('#all');
const ones = document.querySelectorAll('.one');
Then assign the handler to the button that toggles them all. It will toggle them by iterating over the elements you got further up.
I am assuming you also want to toggle the "main" button, if not, remove that line.
main.addEventListener('click', function (e) {
e.target.style.backgroundColor = "red";
ones.forEach( el => {
el.style.backgroundColor = "red";
});
});
Then the "toggle one" buttons, iterate over them and assign a handler that only affects the element clicked.
ones.forEach( el => {
el.addEventListener('click', function (e) {
e.target.style.backgroundColor = "red";
});
});
const main = document.querySelector('#all');
const ones = document.querySelectorAll('.one');
const updatedColor = 'red';
const originalColor = main.style.backgroundColor;
main.addEventListener('click', function (e) {
updateColor(e.target);
ones.forEach( el => {
updateColor(el);
});
});
ones.forEach( el => {
el.addEventListener('click', function (e) {
updateColor(el);
});
});
// Update the element color based on the current color
function updateColor(el) {
if (el.style.backgroundColor === originalColor) {
el.style.backgroundColor = updatedColor;
} else {
el.style.backgroundColor = originalColor;
}
}
<button id="all" type="button">All</button>
<button class="one" type="button">A</button>
<button class="one" type="button">B</button>
<button class="one" type="button">C</button>
UPDATE: to handle going back to the original color on second, or any even number, clicks, we can first store the original color in a constant.
const originalColor = main.style.backgroundColor;
And then use that constant, to update the button background color conditionally, better to do it in a separate function, to avoid repetition.
function updateColor(el) {
if (el.style.backgroundColor === originalColor) {
el.style.backgroundColor = updatedColor;
} else {
el.style.backgroundColor = originalColor;
}
}
For this, you will need more than just one event. Each button should have a separate click event with a toggle for 'red' and 'default'.
optA.addEventListener('click', function (e) {
e.target.style.backgroundColor == 'red'?
e.target.style.backgroundColor = 'default':
e.target.style.backgroundColor = 'red';
});
optB.addEventListener('click', function (e) {
e.target.style.backgroundColor == 'red'?
e.target.style.backgroundColor = 'default':
e.target.style.backgroundColor = 'red';
});
optC.addEventListener('click', function (e) {
e.target.style.backgroundColor == 'red'?
e.target.style.backgroundColor = 'default':
e.target.style.backgroundColor = 'red';
});
I recommend creating a function to toggle the background instead of just ctrl+c and ctrl+v as I just did
And For the "All" button you will need an event that checks the background of all the other buttons.
optAll.addEventListener('click', function (e) {
(optA.style.backgroundColor == "red" &&
optB.style.backgroundColor == "red" &&
optC.style.backgroundColor == "red")?
e.target.style.backgroundColor = 'red':
e.target.style.backgroundColor = 'default';
});
I believe something like this should work.
let btn_all = document.getElementById("btn_all");
let btn_A = document.getElementById("btn_A");
let btn_B = document.getElementById("btn_B");
let btn_C = document.getElementById("btn_C");
let allButtons = [btn_all,btn_A,btn_B,btn_C];
function changeColor (e) {
let currentColor = e.target.style.backgroundColor;
if (currentColor != "red") {
e.target.style.backgroundColor = "red";
}
else {
e.target.style.backgroundColor = "";
}
}
function changeColorAll (e) {
let currentColor = e.target.style.backgroundColor;
if (currentColor != "red") {
allButtons.forEach( function(element, index) {
element.style.backgroundColor = 'red';
});
}
else {
allButtons.forEach( function(element, index) {
element.style.backgroundColor = '';
});
}
}
btn_all.addEventListener("click", changeColorAll);
btn_A.addEventListener("click", changeColor);
btn_B.addEventListener("click", changeColor);
btn_C.addEventListener("click", changeColor);
<button type="button" id="btn_all">All</button>
<button type="button" id="btn_A">A</button>
<button type="button" id="btn_B">B</button>
<button type="button" id="btn_C">C</button>
https://stackoverflow.com/questions/65630594/javascript-click-on-button-to-change-backgroud-color#
Try this code you only have to add two callbacks one for all the other buttons and one for the "all" button. And inside the "all" button callback you have to deal with a list that contains a reference to all your button objects
Here is the solution you need. This solution involves the for cycle.
To select All buttons, you define the first button as optAll[0]:
optAll[0].onclick = function() {}
To select each button - A, B and C, except for All in the for cycle, the numbering starts from 1, not from 0:
for (var i = 1; i < optAll.length; i++) {}
And you declare the button only once:
let opt = document.querySelectorAll('button');
let opt = document.querySelectorAll('button');
opt[0].onclick = function() {
for (var i = 0; i < opt.length; i++) {
opt[i].style.backgroundColor = "red";
}
}
for (var i = 1; i < opt.length; i++) {
opt[i].onclick = function(event) {
this.style.backgroundColor = "red";
}
}
<button type="button">All</button>
<button type="button">A</button>
<button type="button">B</button>
<button type="button">C</button>
I would create the class button-red in order be able to toggle the class back and forth between the original color and the red color.
Then I would add a listener to each button and also create a variable optAll where all buttons can be stored so that we can loop through it to change the color of the buttons when the ALL button is clicked. Then add a function changeColor(btn) to change the color of the button in question and make things cleaner overall, and call it in each listener.
const optAll= document.querySelectorAll('.buttons');
const optA=document.getElementById("optA");
const optB=document.getElementById("optB");
const optC=document.getElementById("optC");
function changeColor(btn){
btn.classList.toggle("button-red");
}
optAll[0].addEventListener('click', function (e) {
optAll.forEach( btn => {
changeColor(btn);
});
});
optA.addEventListener('click', function (e) {
changeColor(e.target);
});
optB.addEventListener('click', function (e) {
changeColor(e.target);
});
optC.addEventListener('click', function (e) {
changeColor(e.target);
});
.button-red {
background-color:red
}
<button id="optAll" class="buttons" type="button">All</button>
<button id="optA" class="buttons" type="button">A</button>
<button id="optB" class="buttons" type="button">B</button>
<button id="optC" class="buttons" type="button">C</button>
You could wrap the buttons inside of a <div> with a class i.e. button-set. This way you can access e.target.target.parentElement to get the wrapping div when clicking on a button.
If you activate the "All" option, it will deselect all active buttons. If you activate any other button, it will not deactivate any other button, unless the "All" button is already active.
In the example below, the button.dataset.active is used to set the data-active attribute for a given button. This attribute gets removed upon deactivation. The attribute is also used as a CSS rule to visually change the appearance of the button when active.
const ButtonSet = selector => {
const _buttonSet = document.querySelector(selector);
const handleClick = e => {
const button = e.target,
group = button.parentElement,
allButton = [...group.querySelectorAll('button')]
.find(button => button.textContent === 'All'),
allActive = allButton.dataset.active === 'true',
isActive = button.dataset.active === 'true';
if (isActive) { delete button.dataset.active; }
else { button.dataset.active = true; }
if (button === allButton) {
group.querySelectorAll('button').forEach(child => {
if (child !== button) { delete child.dataset.active; }
});
} else {
if (allActive) { delete allButton.dataset.active; }
}
};
_buttonSet.querySelectorAll('button')
.forEach(button => button.addEventListener('click', handleClick));
return _buttonSet;
}
const getButtonSetValue = ref => [...ref.querySelectorAll('button')]
.filter(button => button.dataset.active === 'true')
.map(button => button.textContent);
ButtonSet('.button-set').addEventListener('click', e => {
const value = getButtonSetValue(e.currentTarget);
document.querySelector('#result > span').textContent = value.join(', ');
});
.as-console-wrapper { max-height: 4em !important; }
:root {
--button-set-active-color: #AAF;
--button-set-active-hover-color: #CCF;
--button-set-button-color: #EEE;
--button-set-button-hover-color: #FFF;
}
.button-set {
display: flex;
flex-direction: row;
border: thin solid grey;
height: 1.5em;
}
.button-set button {
width: 25%;
border: none;
border-right: thin solid grey;
cursor: pointer;
background: var(--button-set-button-color);
}
.button-set button:hover {
background: var(--button-set-button-hover-color);
}
.button-set button:last-child {
border-right: none;
}
.button-set button:focus {
outline: 0;
}
.button-set button[data-active="true"] {
background: var(--button-set-active-color);
}
.button-set button[data-active="true"]:hover {
background: var(--button-set-active-hover-color);
}
#result {
margin-top: 0.5em;
}
#result label {
font-weight: bold;
}
<div class="button-set">
<button>All</button>
<button>A</button>
<button>B</button>
<button>C</button>
</div>
<div id="result">
<label>Selected:</label>
<span></span>
</div>

Variable values don't save in if loops

I'm attempting to create something that makes a button only work once. In order to do so, I created an if loop. In that if loop, I put it to a function called myFunction and then set a variable, button, to 0 (the if loop only runs if button is =2. It will not run in the first place. What am I doing wrong?
I've already attempted to recreate the variable(saying var button once out of the loop and then saying it again within).
function getRndInteger(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
var button = 2;
var x = 0
function ins() {
function removeElement(elementId) {
// Removes an element from the document
var element = document.getElementById(elementId);
element.parentNode.removeChild(element);
}
x = getRndInteger(0, window.innerWidth)
alert(x);
}
function button() {
if (button === 2) {
alert("k")
myFunction();
button = 0;
} else {}
}
function myFunction() {
var para = document.createElement("SPAN");
para.style.position = "absolute";
x = getRndInteger(0, (window.innerWidth - 60))
para.style.left = x + "px"
var p = getRndInteger(0, (window.innerHeight - 60))
para.style.top = p + "px"
para.style.display = "inline-block;"
para.style.height = "50px"
para.style.width = "50px"
para.style.backgroundColor = "red"
para.style.borderRadius = "50px"
para.style.border = "1px solid black"
para.style.animation = "1s a linear"
para.id = "a"
para.onclick = myFunction
document.getElementById("myDIV").appendChild(para);
}
#keyframes a {
0% {
opacity: 0;
}
100% {
opacity: 1;
}
}
button {
background-color: #010417;
border-radius: 10px;
border: 4px solid white;
color: white;
padding: 10px 26px;
font-size: 20px;
}
<div id="myDIV"></div>
<center>
<button id="button" onClick="button();">Start</button>
</center>
EDIT: Ignore the delete function, doesn't mean anything
The issue with this code is that your event handler function, and the flag (that changes value between 2 and 0) are both named 'button'. Javascript is a relatively accommodating language, so this kind of dual declaration might not throw error right away, but it will obviously cause unexpected behaviour.
Looking at your code:
var button = 2;
function button() {
if (button === 2) {
alert("k")
myFunction();
button = 0;
} else {}
}
In this case (depending on the JS engine), button either refers to the function or the number. If it's the number, then type error will be thrown when button is clicked. Since the code will try to call the number like a function.
If it's a function, then the button === 2 comparison will always be false, and the (empty) else block will get executed. Either way you wouldn't get the expected behavior. You can simply change the variable name to something else, and it should work.
Please note that, as someone pointed out in comments, you should prefer adding disabled attribute to the button over this logic. Unless the aim is to do something other than blocking multiple clicks of the button.

Scroll to position WITHIN a div (not window) using pure JS

PURE JS ONLY PLEASE - NO JQUERY
I have a div with overflow scroll, the window (html/body) never overflows itself.
I have a list of anchor links and want to scroll to a position when they're clicked.
Basically just looking for anchor scrolling from within a div, not window.
window.scrollTo etc. don't work as the window never actually overflows.
Simple test case http://codepen.io/mildrenben/pen/RPyzqm
JADE
nav
a(data-goto="#1") 1
a(data-goto="#2") 2
a(data-goto="#3") 3
a(data-goto="#4") 4
a(data-goto="#5") 5
a(data-goto="#6") 6
main
p(data-id="1") 1
p(data-id="2") 2
p(data-id="3") 3
p(data-id="4") 4
p(data-id="5") 5
p(data-id="6") 6
SCSS
html, body {
width: 100%;
height: 100%;
max-height: 100%;
}
main {
height: 100%;
max-height: 100%;
overflow: scroll;
width: 500px;
}
nav {
background: red;
color: white;
position: fixed;
width: 50%;
left: 50%;
}
a {
color: white;
cursor: pointer;
display: block;
padding: 10px 20px;
&:hover {
background: lighten(red, 20%);
}
}
p {
width: 400px;
height: 400px;
border: solid 2px green;
padding: 30px;
}
JS
var links = document.querySelectorAll('a'),
paras = document.querySelectorAll('p'),
main = document.querySelector('main');
for (var i = 0; i < links.length; i++) {
links[i].addEventListener('click', function(){
var linkID = this.getAttribute('data-goto').slice(1);
for (var j = 0; j < links.length; j++) {
if(linkID === paras[j].getAttribute('data-id')) {
window.scrollTo(0, paras[j].offsetTop);
}
}
})
}
PURE JS ONLY PLEASE - NO JQUERY
What you want is to set the scrollTop property on the <main> element.
var nav = document.querySelector('nav'),
main = document.querySelector('main');
nav.addEventListener('click', function(event){
var linkID,
scrollTarget;
if (event.target.tagName.toUpperCase() === "A") {
linkID = event.target.dataset.goto.slice(1);
scrollTarget = main.querySelector('[data-id="' + linkID + '"]');
main.scrollTop = scrollTarget.offsetTop;
}
});
You'll notice a couple of other things I did different:
I used event delegation so I only had to attach one event to the nav element which will more efficiently handle clicks on any of the links.
Likewise, instead of looping through all the p elements, I selected the one I wanted using an attribute selector
This is not only more efficient and scalable, it also produces shorter, easier to maintain code.
This code will just jump to the element, for an animated scroll, you would need to write a function that incrementally updates scrollTop after small delays using setTimeout.
var nav = document.querySelector('nav'),
main = document.querySelector('main'),
scrollElementTo = (function () {
var timerId;
return function (scrollWithin, scrollTo, pixelsPerSecond) {
scrollWithin.scrollTop = scrollWithin.scrollTop || 0;
var pixelsPerTick = pixelsPerSecond / 100,
destY = scrollTo.offsetTop,
direction = scrollWithin.scrollTop < destY ? 1 : -1,
doTick = function () {
var distLeft = Math.abs(scrollWithin.scrollTop - destY),
moveBy = Math.min(pixelsPerTick, distLeft);
scrollWithin.scrollTop += moveBy * direction;
if (distLeft > 0) {
timerId = setTimeout(doTick, 10);
}
};
clearTimeout(timerId);
doTick();
};
}());
nav.addEventListener('click', function(event) {
var linkID,
scrollTarget;
if (event.target.tagName.toUpperCase() === "A") {
linkID = event.target.dataset.goto.slice(1);
scrollTarget = main.querySelector('[data-id="' + linkID + '"]');
scrollElementTo(main, scrollTarget, 500);
}
});
Another problem you might have with the event delegation is that if the a elements contain child elements and a child element is clicked on, it will be the target of the event instead of the a tag itself. You can work around that with something like the getParentAnchor function I wrote here.
I hope I understand the problem correctly now: You have markup that you can't change (as it's generated by some means you have no control over) and want to use JS to add functionality to the generated menu items.
My suggestion would be to add id and href attributes to the targets and menu items respectively, like so:
var links = document.querySelectorAll('a'),
paras = document.querySelectorAll('p');
for (var i = 0; i < links.length; i++) {
links[i].href=links[i].getAttribute('data-goto');
}
for (var i = 0; i < paras.length; i++) {
paras[i].id=paras[i].getAttribute('data-id');
}

Categories

Resources