Remove class and change aria-hidden value when clicking on body - javascript

There is a project in the code pen called "Flex Priority Menu # 1.2" is a magnificent menu that when it has many links or the "resized" window, it is hiding and putting in a dropdown [More ...]. pure, the dropdown opens and closes with a click on itself. But I'd also like this dropdown menu to close when clicked anywhere else in the body.
Could someone give me a hand?
Thank you.
// Variables for the priority nav
const priorityContainer = document.querySelector('.js-priority-menu');
const priorityMenu = priorityContainer.querySelector('.priority');
const overflowMenu = priorityContainer.querySelector('.overflow');
const overflowTrigger = priorityContainer.querySelector('.js-overflow-trigger');
const menuItems = priorityMenu.children;
const delay = 100;
let throttled = false;
let breakout = [];
let timeout = 0;
// Variables for the trigger svg
const mySvg = document.querySelector('.js-svg-gradient');
const myGradient = mySvg.querySelector('.linearGradient');
const colours = [];
// Helpers could be shipped as part of an Object Literal/Module
const throttle = (callback, delay, scope) => {
if (!throttled) {
callback.call(scope);
throttled = true;
setTimeout(function() {
throttled = false;
}, delay);
}
};
// Initialise
const initialise = () => {
checkNav();
addListeners();
makeStepGradient(colours);
}
/* return the current width of the Navigation */
const getPriorityWidth = () => {
return priorityMenu.offsetWidth + 1;
}
/* return the break point of a menu item */
const getItemBreakPoint = item => {
return item.offsetLeft + item.offsetWidth;
}
/* test breakpoint against menu width */
const itemBreaks = (breakPoint, menuWidth) => {
return breakPoint > menuWidth;
}
/* test menuWidth against breakOut */
const itemBreaksOut = (index, priorityWidth) => {
if (breakout[index] < priorityWidth) {
return true;
}
}
/* move item to overflow menu */
const addToOverflow = (item, itemBreakPoint) => {
overflowMenu.insertBefore(item, overflowMenu.firstChild);
breakout.unshift(itemBreakPoint);
}
/* remove from the overflwo menu */
const removeFromOverflow = (breaksOut) => {
for (let item of breaksOut) {
breakout.shift();
priorityMenu.appendChild(item);
}
}
/* Set button visibility */
const checkTriggerHidden = (value) => {
if (value.toString() != overflowTrigger.getAttribute('aria-hidden')) {
overflowTrigger.setAttribute('aria-hidden', value);
checkNav();
}
}
/* Check priority and overflow */
const checkNav = () => {
/* check priorityMenu */
let priorityWidth = getPriorityWidth();
/* Iterate over the priority menu */
let priorityIndex = menuItems.length;
while (priorityIndex--) {
let item = menuItems[priorityIndex];
let itemBreakPoint = getItemBreakPoint(item);
if (itemBreaks(itemBreakPoint, priorityWidth)) {
addToOverflow(item, itemBreakPoint);
//add colour to svg
console.log(item);
console.log(window.getComputedStyle(item).backgroundColor);
let bgColour = window.getComputedStyle(item).backgroundColor;
addToSVG(bgColour);
};
};
/* iterate the overflow */
let overflowIndex = overflowMenu.children.length;
let breaksOut = [];
while (overflowIndex--) {
if (itemBreaksOut(overflowIndex, priorityWidth)) {
breaksOut.unshift(overflowMenu.children[overflowIndex]);
// remove colour from svg
removeFromSVG();
}
}
removeFromOverflow(breaksOut);
/* check the trigger visibility */
checkTriggerHidden(breakout.length == 0);
makeStepGradient(colours);
}
/* Add Event listeners */
const addListeners = () => {
window.addEventListener('resize', () => {
//throttle
throttle(checkNav, delay);
//debounce
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
checkNav();
}, delay);
})
overflowTrigger.addEventListener('click', () => {
overflowMenu.setAttribute('aria-hidden', overflowMenu.getAttribute('aria-hidden') === 'true' ? 'false' : 'true');
overflowTrigger.classList.toggle('active');
})
}
/* SVG indicator on the trigger */
/* Add to colour array */
const addToSVG = bgColour => {
colours.unshift(bgColour);
}
const removeFromSVG = bgColor => {
colours.shift();
}
const createStop = (offset, index, colour) => {
let offsetValue = offset * index;
let stop = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
stop.setAttribute('offset', offsetValue + '%');
stop.setAttribute('stop-color', colour);
return stop;
}
const makeStepGradient = colourArray => {
// Calculate offset values (%)
const colourCount = colourArray.length;
const offset = 100/colourCount;
myGradient.innerHTML = "";
// Loop the colours
colourArray.forEach((colour, index) => {
let stop1 = createStop(offset, index, colour);
let stop2 = createStop(offset, index + 1, colour);
myGradient.appendChild(stop1);
myGradient.appendChild(stop2);
});
}
/* Initilaise the menu */
initialise();
/* Structural */
.priority-menu {
display: flex;
flex-flow: row nowrap;
position: relative;
}
.priority-menu nav {
flex-basis: 100%;
display: flex;
text-align: center;
}
.priority-menu nav a {
white-space: nowrap;
text-align: center;
}
.priority-menu .priority {
overflow: hidden;
}
.priority-menu .priority a {
flex-basis: 100%;
display: block;
}
.priority-menu .overflow {
position: absolute;
top: 100%;
right: 0;
display: block;
}
.priority-menu .overflow a {
display: block;
}
.priority-menu [aria-hidden="true"] {
display: none;
}
.priority-menu [aria-hiiden="false"] {
display: true;
}
/* Decorative */
body {
padding: 12px;
background: #2B3440;
font-family: Arial, Helvetica, sans-serif;
color: #cbe4ed;
}
a {
color: white;
}
:focus {
outline: none;
}
.priority-menu nav a {
text-decoration: none;
padding: 15px 30px;
color: #2B3440;
}
.priority-menu nav a.menu10 {
background-color: #A8E6CE;
}
.priority-menu nav a.menu09 {
background-color: #FFD3B5;
}
.priority-menu nav a.menu08 {
background-color: #f7e651;
}
.priority-menu nav a.menu07 {
background-color: #DCEDC2;
}
.priority-menu nav a.menu06 {
background-color: #A8E6CE;
}
.priority-menu nav a.menu05 {
background-color: #FF8C94;
}
.priority-menu nav a.menu04 {
background-color: #FFAAA6;
}
.priority-menu nav a.menu03 {
background-color: #FFD3B5;
}
.priority-menu nav a.menu02 {
background-color: #DCEDC2;
}
.priority-menu nav a.menu01 {
background-color: #A8E6CE;
}
.priority-menu .overflow {
border-top: 2px solid #2B3440;
}
.priority-menu .overflow-trigger {
position: relative;
background: #2B3440;
padding: 12px 10px;
border: none;
border-left: 2px solid #2B3440;
font-size: 16px;
}
.priority-menu .overflow-trigger .svg-gradient {
width: 73px;
position: absolute;
top: 0;
left: 0;
height: 100%;
fill: url(#linearGradient);
}
.priority-menu .overflow-trigger span {
position: relative;
}
.priority-menu .overflow-trigger.active {
color: #cbe4ed;
}
.priority-menu .overflow-trigger.active .svg-gradient {
fill: #2B3440;
height: 46px;
border: 1px solid #cbe4ed;
}
.blurb {
margin: 50px auto;
width: 400px;
}
<div class="js-priority-menu priority-menu">
<nav class="priority">
Menu Item 1
Menu Item 2
Menu Item 3
Item Item 4
Menu Item 5
Item Item 6
Menu Item 7
</nav>
<button class="js-overflow-trigger overflow-trigger" aria-hidden="true">
<svg class="js-svg-gradient svg-gradient" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<linearGradient id="linearGradient" class="linearGradient"
x1="0" y1="0" x2="100%" y2="0" spreadMethod="pad">
<stop offset="50%" stop-color="#000"></stop>
</linearGradient>
</defs>
<rect x="0" y="0" width="100%" height="100%" rx="0" ry="0"/>
</svg>
<span>More...</span>
</button>
<nav class="overflow" id="overflow" aria-hidden="true" data-behaviour="menu"></nav>
</div>
<div class="blurb">
<h1>Flex Priority Menu #1.2</h1>
<p> Resize window to trigger the overflow nav drop down</p>
<p>Pretty much the same as <a href='https://codepen.io/kungfuyou/pen/RaJaNZ'>Flex Priority Nav #1.1</a> but with a dynamic svg on as a menu indicator.</p>
<p>To do: open / close state for the Menu trigger.</p>
</div>

You can attach a listener to whole document body and hide the overflow menu when user clicks anywhere but on the More... button (which is handled by another function).
const addListeners = () => {
//Other Listeners ...
document.body.addEventListener('click', (e) => {
if(e.target != overflowTrigger && e.target.parentNode != overflowMenu) {
overflowMenu.setAttribute('aria-hidden', 'true');
overflowTrigger.classList.remove('active');
}
})
}
In case you want it to work with right clicks too:
const addListeners = () => {
//Other Listeners ...
(['click','contextmenu']).forEach(event => {
document.body.addEventListener(event, (e) => {
if(e.target != overflowTrigger && e.target.parentNode != overflowMenu) {
overflowMenu.setAttribute('aria-hidden', 'true');
overflowTrigger.classList.remove('active');
}
})
})
}
Edited Pen

Related

Use IntersectionObserver To Trigger Event AFTER Element Completely Passes Threshold

I have a few IntersectionObserver's set up. observer toggles new boxes to be made as the user scrolls down the page. lastBoxObserver loads new boxes as this continuous scrolling happens.
What I would like to do is change the color of a box once it leaves the threshold set in the first observer (observer - whose threshold is set to 1). So, once box 12 enters the viewport, it passes through the observer, and once it has completely passed outside of the threshold for this observer and box 13 enters the observer, box 12's background changes from green to orange, perhaps.
Is there a way to make this happen? Maybe by adding an additional observer, or adding code to observer? Any help is greatly appreciated.
Codepen: https://codepen.io/jon424/pen/NWwReEJ
JavaScript
const boxes = document.querySelectorAll(".box");
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
entry.target.classList.toggle("show", entry.isIntersecting);
if (entry.isIntersecting) observer.unobserve(entry.target);
});
},
{
threshold: 1,
}
);
const lastBoxObserver = new IntersectionObserver((entries) => {
const lastBox = entries[0];
if (!lastBox.isIntersecting) return;
loadNewBoxes();
lastBoxObserver.unobserve(lastBox.target);
lastBoxObserver.observe(document.querySelector(".box:last-child"));
}, {});
lastBoxObserver.observe(document.querySelector(".box:last-child"));
boxes.forEach((box) => {
observer.observe(box);
});
const boxContainer = document.querySelector(".container");
function loadNewBoxes() {
for (let i = 0; i < 1000; i++) {
const box = document.createElement("div");
box.textContent = `${i + 1}`;
box.classList.add("box");
observer.observe(box);
boxContainer.appendChild(box);
}
}
HTML
<div class="container">
<div class="box">0</div>
</div>
CSS
.container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.box {
background: green;
color: white;
font-size: 4rem;
text-align: center;
margin: auto;
height: 100px;
width: 100px;
border: 1px solid black;
border-radius: 0.25rem;
padding: 0.5rem;
transform: translateX(100px);
opacity: 0;
transition: 150ms;
}
.box.show {
transform: translateX(0);
opacity: 1;
}
.box.show.more {
background-color: orange;
}
You just need to add color change logic in your first observer.
I tested with the following changes to your code,
In css, change .box.show.more to -
.box.more {
background-color: orange;
}
In javascript -
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
entry.target.classList.toggle("show", entry.isIntersecting);
if (entry.isIntersecting) {
observer.unobserve(entry.target);
if(entry.target.textContent === '13')
entry.target.previousSibling.classList.toggle('more');
}
});
},
{
threshold: 1,
}
);
As you can see, the only change is that I added this part -
if(entry.target.textContent === '13')
entry.target.previousSibling.classList.toggle('more');
I also changed the number of new div to add from 1000 to 20 for easy test.
If you want to change box 12 color as soon as box 13 enters viewport, you can simply change "threshold" from 1 to 0.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<style>
.container {
display: flex;
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.box {
background: green;
color: white;
font-size: 4rem;
text-align: center;
margin: auto;
height: 100px;
width: 100px;
border: 1px solid black;
border-radius: 0.25rem;
padding: 0.5rem;
opacity: 0;
}
.box.show {
opacity: 1;
}
.box.show.more {
background-color: orange;
}
.mytest{
border:solid 10px #000;
background-color: orange;
}
</style>
<div class="container">
<div class="box">0</div>
<div class="sentinel"></div>
</div>
<script>
/* https://stackoverflow.com/questions/70994897/use-intersectionobserver-to-trigger-event-after-element-completely-passes-thresh/71054028#71054028 */
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add("show");
}
if (entry.boundingClientRect.top < entry.rootBounds.top) {
/* entry is above viewport */
entry.target.classList.add("mytest");
observer.unobserve(entry.target);
}
});
},
{
threshold: 0, /* top is 200 below top of viewport - something for the box to scroll into*/
rootMargin: "-200px 0px 0px 0px"
}
);
/* last div sentinel indicates last box */
const mysentinel = new IntersectionObserver((entries) => {
var entry = entries[0];
if (entry.isIntersecting) {
loadNewBoxes();
}
},{
threshold: 0
});
const boxContainer = document.querySelector(".container");
const sentinel = document.querySelector(".sentinel");
/* setup - create extra boxes, sentinel is last after boxes */
loadNewBoxes();
mysentinel.observe(sentinel);
function loadNewBoxes() {
for (let i = 0; i < 10; i++) {
const box = document.createElement("div");
box.textContent = `${i + 1}`;
box.classList.add("box");
box.classList.add("show");
observer.observe(box);
boxContainer.appendChild(box);
}
boxContainer.appendChild(sentinel);
}
</script>
</body>
</html>
IntersectionObserver callback is async, if you scroll fast enough then some boxes will be missed. I've simplified your code. If the box/observed target is above the root top then I add a class and unobserve it. If the box is transitioning I add a class to show the box. I use sentinel to indicate the last box and to add more boxes when the sentinel hits the viewport.

SortableJS isn't saving my sorted todo list in localStorage

I have a todo project with localStorage and SortableJS. I am having problem where when I sort my todo list, it won't update the localStorage. Can somebody help me figure out a way to save the sorted list? The code is below but would be nice to visit the codepen link under the snippet.
const clear = document.querySelector(".clear");
const dateElement = document.getElementById("date");
const list = document.getElementById("list");
const input = document.getElementById("input");
// Class names
const CHECK = "fa-check-circle";
const UNCHECK = "fa-circle-thin";
const LINE_THROUGH = "lineThrough";
// Variables
let LIST, id;
// Get item from localStorage
let data = localStorage.getItem("TODO");
// Check if data is not empty
if (data) {
LIST = JSON.parse(data);
id = LIST.length;
loadList(LIST);
} else {
LIST = [];
id = 0;
}
// Load items to the user's interface
function loadList(array) {
array.forEach(function(item) {
addToDo(item.name, item.id, item.done, item.trash);
});
}
// Clear the localStorage
clear.addEventListener("click", function() {
localStorage.clear();
location.reload();
})
// Show today's date
const options = {
weekday: "long",
month: "short",
day: "numeric"
};
const today = new Date();
dateElement.innerHTML = today.toLocaleDateString("en-US", options);
// Add to do function
function addToDo(toDo, id, done, trash) {
if (trash) {
return;
}
const DONE = done ? CHECK : UNCHECK;
const LINE = done ? LINE_THROUGH : "";
const item = `<li class="item">
<i class="fa ${DONE}" job="complete" id="${id}"></i>
<p class="text ${LINE}">${toDo}</p>
<i class="fa fa-trash-o de" job="delete" id="${id}"></i>
</li>
`;
const position = "beforeend";
list.insertAdjacentHTML(position, item);
}
// Add an item to the list when the user cick the enter key
document.addEventListener("keyup", function(event) {
if (event.keyCode == 13) {
const toDo = input.value;
// If the input isn't empty
if (toDo) {
addToDo(toDo);
LIST.push({
name: toDo,
id: id,
done: false,
trash: false
});
// Add item to localStorage
localStorage.setItem("TODO", JSON.stringify(LIST));
id++;
}
input.value = ""
}
});
// complete to do
function completeToDo(element) {
element.classList.toggle(CHECK);
element.classList.toggle(UNCHECK);
element.parentNode.querySelector(".text").classList.toggle(LINE_THROUGH);
LIST[element.id].done = LIST[element.id].done ? false : true;
}
// Remove to do
function removeToDo(element) {
element.parentNode.parentNode.removeChild(element.parentNode);
LIST[element.id].trash = true;
// Add item to localStorage
localStorage.setItem("TODO", JSON.stringify(LIST));
}
// Target the items created dynamically
list.addEventListener("click", function(event) {
const element = event.target;
const elementJob = element.attributes.job.value;
if (elementJob == "complete") {
completeToDo(element);
} else if (elementJob == "delete") {
removeToDo(element);
}
// Add item to localStorage
localStorage.setItem("TODO", JSON.stringify(LIST));
});
// For sorting the list
Sortable.create(list, {
animation: 100,
group: 'list-1',
draggable: '#list li',
handle: '#list li',
sort: true,
filter: '.sortable-disabled',
chosenClass: 'active'
});
/* ------------ youtube.com/CodeExplained ------------ */
body {
padding: 0;
margin: 0;
background-color: rgba(0, 0, 0, 0.1);
font-family: 'Titillium Web', sans-serif;
}
/* ------------ container ------------ */
.container {
padding: 10px;
width: 380px;
margin: 0 auto;
}
/* ------------ header ------------ */
.header {
width: 380px;
height: 200px;
background-image: url('');
background-size: 100% 200%;
background-repeat: no-repeat;
border-radius: 15px 15px 0 0;
position: relative;
}
.clear {
width: 30px;
height: 30px;
position: absolute;
right: 20px;
top: 20px;
}
.clear i {
font-size: 30px;
color: #FFF;
}
.clear i:hover {
cursor: pointer;
text-shadow: 1px 3px 5px #000;
transform: rotate(45deg);
}
#date {
position: absolute;
bottom: 10px;
left: 10px;
color: #FFF;
font-size: 25px;
font-family: 'Titillium Web', sans-serif;
}
/* ------------ content ------------ */
.content {
width: 380px;
height: 350px;
max-height: 350px;
background-color: #FFF;
overflow: auto;
}
.content::-webkit-scrollbar {
display: none;
}
.content ul {
padding: 0;
margin: 0;
}
.item {
width: 380px;
height: 45px;
min-height: 45px;
position: relative;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
list-style: none;
padding: 0;
margin: 0;
}
.item i.co {
position: absolute;
font-size: 25px;
padding-left: 5px;
left: 15px;
top: 10px;
}
.item i.co:hover {
cursor: pointer;
}
.fa-check-circle {
color: #6eb200;
}
.item p.text {
position: absolute;
padding: 0;
margin: 0;
font-size: 20px;
left: 50px;
top: 5px;
background-color: #FFF;
max-width: 285px;
}
.lineThrough {
text-decoration: line-through;
color: #ccc;
}
.item i.de {
position: absolute;
font-size: 25px;
right: 15px;
top: 10px;
}
.item i.de:hover {
color: #af0000;
cursor: pointer;
}
/* ------------ add item ------------ */
.add-to-do {
position: relative;
width: 360px;
height: 40px;
background-color: #FFF;
padding: 10px;
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.add-to-do i {
position: absolute;
font-size: 40px;
color: #4162f6;
}
.add-to-do input {
position: absolute;
left: 50px;
height: 35px;
width: 310px;
background-color: transparent;
border: none;
font-size: 20px;
padding-left: 10px;
}
.add-to-do input::-webkit-input-placeholder {
/* Chrome/Opera/Safari */
color: #4162f6;
font-family: 'Titillium Web', sans-serif;
font-size: 20px;
}
.add-to-do input::-moz-placeholder {
/* Firefox 19+ */
color: #4162f6;
font-family: 'Titillium Web', sans-serif;
font-size: 20px;
}
.add-to-do input:-ms-input-placeholder {
/* IE 10+ */
color: #4162f6;
font-family: 'Titillium Web', sans-serif;
font-size: 20px;
}
.add-to-do input:-moz-placeholder {
/* Firefox 18- */
color: #4162f6;
font-family: 'Titillium Web', sans-serif;
font-size: 20px;
}
<script src="https://kit.fontawesome.com/ed2e310181.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/RubaXa/Sortable/Sortable.min.js"></script>
<div class="container">
<div class="header">
<div class="clear">
<i class="fa fa-refresh"></i>
</div>
<div id="date"></div>
</div>
<div class="content">
<ul id="list">
<!-- <li class="item">
<i class="fa fa-circle-thin co" job="complete" id="0"></i>
<p class="text"></p>
<i class="fa fa-trash-o" job="delete" id="1"></i>
</li> -->
</ul>
</div>
<div class="add-to-do">
<i class="fa fa-plus-circle"></i>
<input type="text" id="input" placeholder="Add a to-do">
</div>
</div>
Please visit my codepen for a working project.
Try to add 2 or more todos then sort, on refresh was hoping to keep the sorted list.
https://codepen.io/Foxseiz/pen/ZEGadWZ
Sortable.create(list, {
group: "TODO2",
options: {
animation: 100,
draggable: "#list li",
handle: "#list li",
sort: true,
filter: ".sortable-disabled",
chosenClass: "active"
},
store: {
/**
* Get the order of elements. Called once during initialization.
* #param {Sortable} sortable
* #returns {Array}
*/
get: function(sortable) {
var order = localStorage.getItem(sortable.options.group.name);
return order ? order.split("|") : [];
},
/**
* Save the order of elements. Called onEnd (when the item is dropped).
* #param {Sortable} sortable
*/
set: function(sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.group.name, order.join("|"));
}
}
});
That would work in your case
For your Sortable.create option you can do the following:
// For sorting the list
Sortable.create(list, {
animation: 100,
group: 'list-1',
draggable: '#list li',
handle: '#list li',
sort: true,
filter: '.sortable-disabled',
chosenClass: 'active',
onSort: function(e) {
var items = e.to.children;
var result = [];
for (var i = 0; i < items.length; i++) {
result.push(items[i].id);
}
var lsBefore = JSON.parse(localStorage.getItem("TODO"));
var lsAfter = [];
for (var i = 0; i < result.length; i++) {
var found = false;
for (var j = 0; j < lsBefore.length && !found; j++) {
if (lsBefore[j].id == result[i]) {
lsAfter.push(lsBefore[j]);
lsBefore.splice(j, 1);
found = true;
}
}
}
localStorage.setItem("TODO", JSON.stringify(lsAfter));
console.log(result);
console.log(lsBefore);
console.log(lsAfter);
}
The lsAfter is your re-sorted set of objects that you can store/update in local storage.
My solution also requires that your const item looks like this (I added the id attribute to the <li> element:
const item = `<li class="item" id="${id}">
<i class="fa ${DONE}" job="complete" id="${id}"></i>
<p class="text ${LINE}">${toDo}</p>
<i class="fa fa-trash-o de" job="delete" id="${id}"></i>
</li>
`;
You need use onSort callback.
example code:
const clear = document.querySelector(".clear");
const dateElement = document.getElementById("date");
const list = document.getElementById("list");
const input = document.getElementById("input");
// Class names
const CHECK = "fa-check-circle";
const UNCHECK = "fa-circle-thin";
const LINE_THROUGH = "lineThrough";
// Variables
let LIST, id;
// Get item from localStorage
let data = localStorage.getItem("TODO");
// Check if data is not empty
if(data) {
LIST = JSON.parse(data);
id = LIST.length;
loadList(LIST);
}else{
LIST =[];
id = 0;
}
// Load items to the user's interface
function loadList(array) {
array.forEach(function(item){
addToDo(item.name, item.id, item.done, item.trash);
});
}
// Clear the localStorage
clear.addEventListener("click", function() {
localStorage.clear();
location.reload();
})
// Show today's date
const options = {weekday : "long", month : "short", day : "numeric"};
const today = new Date();
dateElement.innerHTML = today.toLocaleDateString("en-US", options);
// Add to do function
function addToDo(toDo, id, done, trash) {
if(trash) { return; }
const DONE = done ? CHECK : UNCHECK;
const LINE = done ? LINE_THROUGH : "";
const item = `<li class="item">
<i class="fa ${DONE}" job="complete" id="${id}"></i>
<p class="text ${LINE}">${toDo}</p>
<i class="fa fa-trash-o de" job="delete" id="${id}"></i>
</li>
`;
const position = "beforeend";
list.insertAdjacentHTML(position, item);
}
// Add an item to the list when the user cick the enter key
document.addEventListener("keyup", function(event) {
if(event.keyCode == 13) {
const toDo = input.value;
// If the input isn't empty
if(toDo) {
addToDo(toDo);
LIST.push({
name : toDo,
id : id,
done : false,
trash : false
});
// Add item to localStorage
localStorage.setItem("TODO", JSON.stringify(LIST));
id++;
}
input.value = ""
}
});
// complete to do
function completeToDo(element) {
element.classList.toggle(CHECK);
element.classList.toggle(UNCHECK);
element.parentNode.querySelector(".text").classList.toggle(LINE_THROUGH);
LIST[element.id].done = LIST[element.id].done ? false : true;
}
// Remove to do
function removeToDo(element) {
element.parentNode.parentNode.removeChild(element.parentNode);
LIST[element.id].trash = true;
// Add item to localStorage
localStorage.setItem("TODO", JSON.stringify(LIST));
}
// Target the items created dynamically
list.addEventListener("click", function(event) {
const element = event.target;
const elementJob = element.attributes.job.value;
if(elementJob == "complete") {
completeToDo(element);
}else if(elementJob == "delete"){
removeToDo(element);
}
// Add item to localStorage
localStorage.setItem("TODO", JSON.stringify(LIST));
});
function swapArrayElements(arr, indexA, indexB) {
var temp = arr[indexA];
arr[indexA] = arr[indexB];
arr[indexB] = temp;
};
function orderList(oldIndex, newIndex) {
swapArrayElements(LIST, oldIndex, newIndex)
localStorage.setItem("TODO", JSON.stringify(LIST));
}
// For sorting the list
Sortable.create(list, {
animation: 100,
group: 'list-1',
draggable: '#list li',
handle: '#list li',
sort: true,
filter: '.sortable-disabled',
chosenClass: 'active',
onSort: function (/**Event*/evt) {
orderList(evt.oldIndex, evt.newIndex);
},
});
When you call
Sortable.create(list, {
animation: 100,
group: 'list-1',
draggable: '#list li',
handle: '#list li',
sort: true,
filter: '.sortable-disabled',
chosenClass: 'active'
});
There is actually a store option you can add. Like this:
Sortable.create(list, {
store: {
//Get the order of elements. Called once during initialization.
// #param {Sortable} sortable
// #returns {Array}
get: function (sortable) {
var order = localStorage.getItem(sortable.options.group.name);
return order ? order.split('|') : [];
},
// Save the order of elements.
// #param {Sortable} sortable
set: function (sortable) {
var order = sortable.toArray();
localStorage.setItem(sortable.options.group.name, order.join('|'));
}
},
...rest of your options
});
Also Sortable.create returns a "Sortable" object for your list so building on the code you have above you now have access to the Sortable object
var mySortable = Sortable.create(list, {...your options});
Now you can call mySortable.Save() after any event and your store's set function will get called. For example put mysortable.Save() in your document.addEventListener("keyup") function

Check if DOM elements are present inside a DIV then run functions assigned to those elements in order

i'm trying to develop a game using html, css and js. At the moment I'm focusing on manipulating DOM elements without using the canvas tag. My idea is to create a pseudo graphical programming language, similar to the Blockly environment. So far I have inserted 3 clickable elements inside #toolbox that create their copies in #workspace.
Now, I am trying to assign functions to the elements present in #workspace, which once pressed the Run button are executed in order of appearance, so as to create a queue of commands that is able to move the pink square inside #output_section.
Therefore I cannot understand how to write the function that is able to verify the presence of the elements and then be able to perform the different functions assigned to these elements.
Any ideas? :D
I'm using Jquery 3.3.1
function addRed() {
var redWorkspace = document.createElement("DIV");
redWorkspace.className = "remove-block block red";
document.getElementById("workspace").appendChild(redWorkspace);
};
function addBlue() {
var blueWorkspace = document.createElement("DIV");
blueWorkspace.className = "remove-block block blue";
document.getElementById("workspace").appendChild(blueWorkspace);
};
function addGreen() {
var greenWorkspace = document.createElement("DIV");
greenWorkspace.className = "remove-block block green";
document.getElementById("workspace").appendChild(greenWorkspace);
};
$("#clear_workspace").click(function () {
$("#workspace").empty();
});
$(document).on("click", ".remove-block", function () {
$(this).closest("div").remove();
});
html,
body {
margin: 0;
padding: 0;
}
#workspace {
display: flex;
height: 100px;
padding: 10px;
background: black;
}
#toolbox {
display: flex;
padding: 10px;
width: 300px;
}
#output_section {
height: 500px;
width: 500px;
border: solid black;
margin: 10px;
position: relative;
}
#moving_square {
position: absolute;
bottom: 0;
right: 0;
width: 100px;
height: 100px;
background: pink;
}
.block {
height: 100px;
width: 100px;
}
.red {
background: red;
}
.blue {
background: cyan;
}
.green {
background: green;
}
.grey {
background: #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<html>
<body>
<div id="workspace"></div>
<div id="workspace-menu">
<button id="run_workspace">Run</button>
<button id="clear_workspace">Clear</button>
</div>
<div id="toolbox" class="grey">
<div onclick="addRed()" class="block red">Left</div>
<div onclick="addBlue()" class="block blue">Up</div>
<div onclick="addGreen()" class="block green">Right</div>
</div>
<div id="output_section">
<div id="moving_square"></div>
</div>
</body>
</html>
Completely untested but run button does something along the lines of:
$("#run_workspace").click(function() {
$("#workspace .block").each(function(elem) {
if (elem.hasClass("red")) {
moveObjectLeft();
} else if (elem.hasClass("green")) {
moveObjectRight();
} else if (elem.hasClass("blue")) {
moveObjectUp();
}
});
});
Commonly, it's a good idea to store all required information in arrays and objects, and use HTML only to display your data.
Also, if you are already using jQuery - use it for all 100%)
Made some improvements:
let mobs = {
pinky: {
node: $('#moving_square'),
coors: { top: 400, left: 400 },
step: 30,
moveQueue: [],
// moveTimeout ???
},
}; // storing here all created objects, that must move.
/* Each [moveQueue] array will store the chain of moves, like ["up", "up", "left"]
You can take each "key-word" of move, and get required function buy that key,
from the 'move' object */
let move = { // Think about how to simlify this object and functions. It's possible!)
left: function (obj) {
let left = obj.coors.left = (obj.coors.left - obj.step);
obj.node.css('left', left + 'px');
},
up: function (obj) {
let top = obj.coors.top = (obj.coors.top - obj.step);
obj.node.css('top', top + 'px');
},
right: function (obj) {
let left = obj.coors.left = (obj.coors.left + obj.step);
obj.node.css('left', left + 'px');
}
};
let stepTimeout = 1000;
let running = false;
let timeouts = {}; // store all running timeouts here,
// and clear everything with for( key in obj ) loop, if required
$('#toolbox .block').on('click', function () {
let color = $(this).attr('data-color');
let workBlock = '<div class="remove-block block ' + color + '"></div>';
$('#workspace').append(workBlock);
mobs.pinky.moveQueue.push( $(this).text().toLowerCase() ); // .attr('data-direction');
// instead of pinky - any other currently selected object
// $(this).text().toLowerCase() — must be "left", "up", "right"
});
$('#run_workspace').on('click', function () {
running = true;
runCode();
function runCode() {
for (let obj in mobs) { // mobile objects may be multiple
// Inside the loop, obj == mobs each key name. Here it's == "pinky"
let i = 0;
let pinky = mobs[obj];
localRun();
function localRun() {
let direction = pinky.moveQueue[i]; // getting direction key by array index.
move[direction](pinky); // calling the required function from storage.
if (pinky.moveQueue[++i] && running ) {
// self-calling again, if moveQueue has next element.
// At the same time increasing i by +1 ( ++i )
timeouts[obj] = setTimeout(localRun, stepTimeout);
}
}
}
}
});
$("#clear_workspace").click(function () {
$("#workspace").empty();
});
$('#workspace').on("click", ".remove-block", function () {
$(this).closest("div").remove();
});
html,
body {
margin: 0;
padding: 0;
}
#workspace {
display: flex;
height: 100px;
padding: 10px;
background: black;
}
#toolbox {
display: flex;
padding: 10px;
width: 300px;
}
#output_section {
height: 500px;
width: 500px;
border: solid black;
margin: 10px;
position: relative;
}
#moving_square {
position: absolute;
top: 400px;
left: 400px;
width: 100px;
height: 100px;
background: pink;
}
.block {
height: 100px;
width: 100px;
}
.red {
background: red;
}
.blue {
background: cyan;
}
.green {
background: green;
}
.grey {
background: #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="workspace"></div>
<div id="workspace-menu">
<button id="run_workspace">Run</button>
<button id="clear_workspace">Clear</button>
</div>
<div id="toolbox" class="grey">
<div data-color="red" class="block red">Left</div>
<div data-color="blue" class="block blue">Up</div>
<div data-color="green" class="block green">Right</div>
</div>
<div id="output_section">
<div id="moving_square"></div>
</div>
But... jQuery was used only for clicks... Translation to JS:
let mobs = {
pinky: {
node: document.getElementById('moving_square'),
coors: { top: 400, left: 400 },
step: 30,
moveQueue: [],
},
};
let move = {
left: function (obj) {
let left = obj.coors.left = (obj.coors.left - obj.step);
obj.node.style.left = left + 'px';
},
up: function (obj) {
let top = obj.coors.top = (obj.coors.top - obj.step);
obj.node.style.top = top + 'px';
},
right: function (obj) {
let left = obj.coors.left = (obj.coors.left + obj.step);
obj.node.style.left = left + 'px';
}
};
let stepTimeout = 1000;
let running = false;
let timeouts = {};
let blocks = document.querySelectorAll('#toolbox .block');
let workSpace = document.getElementById('workspace');
blocks.forEach(function(block){
block.addEventListener('click', function(){
let color = this.dataset.color;
let workBlock = '<div class="remove-block block ' + color + '"></div>';
workSpace.insertAdjacentHTML('beforeend', workBlock);
mobs.pinky.moveQueue.push( this.textContent.toLowerCase() );
});
});
document.getElementById('run_workspace').addEventListener('click', function () {
running = true;
runCode();
function runCode() {
for (let obj in mobs) { // mobile objects may be multiple
// Inside the loop, obj == mobs each key name. Here it's == "pinky"
let i = 0;
let pinky = mobs[obj];
localRun();
function localRun() {
let direction = pinky.moveQueue[i]; // getting direction key by array index.
move[direction](pinky); // calling the required function from storage.
if (pinky.moveQueue[++i] && running ) {
// self-calling again, if moveQueue has next element.
// At the same time increasing i by +1 ( ++i )
timeouts[obj] = setTimeout(localRun, stepTimeout);
}
}
}
}
});
document.getElementById("clear_workspace").addEventListener('click', function () {
workSpace.textContent = "";
});
workSpace.addEventListener('click', function (e) {
if( e.target.classList.contains('remove-block') ){
e.target.remove();
}
});
html,
body {
margin: 0;
padding: 0;
}
#workspace {
display: flex;
height: 100px;
padding: 10px;
background: black;
}
#toolbox {
display: flex;
padding: 10px;
width: 300px;
}
#output_section {
height: 500px;
width: 500px;
border: solid black;
margin: 10px;
position: relative;
}
#moving_square {
position: absolute;
top: 400px;
left: 400px;
width: 100px;
height: 100px;
background: pink;
}
.block {
height: 100px;
width: 100px;
}
.red {
background: red;
}
.blue {
background: cyan;
}
.green {
background: green;
}
.grey {
background: #ccc;
}
<div id="workspace"></div>
<div id="workspace-menu">
<button id="run_workspace">Run</button>
<button id="clear_workspace">Clear</button>
</div>
<div id="toolbox" class="grey">
<div data-color="red" class="block red">Left</div>
<div data-color="blue" class="block blue">Up</div>
<div data-color="green" class="block green">Right</div>
</div>
<div id="output_section">
<div id="moving_square"></div>
</div>

How to have two different bgcolor changing events

I'm trying to have a bgcolor change for an element on mouseover, mouseout, and onclick. The problem is Javascript overwrites my onclick with mouseout, so I can't have both. So is there any way to have mouseover reset after mouseout?
function init() {
document.getElementById('default').onmouseover = function() {
tabHoverOn('default', 'grey')
};
document.getElementById('default').onmouseout = function() {
tabHoverOff('default', 'yellow')
};
document.getElementById('section2').onmouseover = function() {
tabHoverOn('section2', 'grey')
};
document.getElementById('section2').onmouseout = function() {
tabHoverOff('section2', 'yellow')
};
document.getElementById('section3').onmouseover = function() {
tabHoverOn('section3', 'grey')
};
document.getElementById('section3').onmouseout = function() {
tabHoverOff('section3', 'yellow')
};
}
function tabHoverOn(id, bgcolor) {
document.getElementById(id).style.backgroundColor = bgcolor;
}
function tabHoverOff(id, bgcolor) {
document.getElementById(id).style.backgroundColor = bgcolor;
}
var current = document.getElementById('default');
function tab1Highlight(id) {
if (current != null) {
current.className = "";
}
id.className = "tab1highlight";
current = id;
}
function tab2highlight(id) {
if (current != null) {
current.className = "";
}
id.className = "tab2highlight";
current = id;
}
function tab3highlight(id) {
if (current != null) {
current.className = "";
}
id.className = "tab3highlight";
current = id;
}
window.onload = init();
body {
width: 900px;
margin: 10px auto;
}
nav {
display: block;
width: 80%;
margin: 0 auto;
}
nav > ul {
list-style: none;
}
nav > ul > li {
display: inline-block;
margin: 0 3px;
width: 150px;
}
nav > ul > li > a {
width: 100%;
background-color: #ffff66;
border: 1px solid #9b9b9b;
border-radius: 12px 8px 0 0;
padding: 8px 15px;
text-decoration: none;
font-weight: bold;
font-family: arial, sans-serif;
}
main {
display: block;
width: 80%;
margin: 0 auto;
border: 1px solid #9b9b9b;
padding: 10px;
}
main > h1 {
font-size: 1.5em;
}
.tab1highlight {
background-color: #339966;
color: white;
}
.tab2highlight {
background-color: #ff6666;
color: white;
}
.tab3highlight {
background-color: #6600ff;
color: white;
}
main img {
border: 5px solid #eeefff;
width: 80%;
margin-top: 20px;
}
<body>
<nav>
<ul>
<li>Section 1</li>
<li>Section 2</li>
<li>Section 3</li>
</ul>
</nav>
<main>
<h1>Exercise: Navigation Tab #5</h1>
<ul>
<li>
Combine the navigation tab exercises #1, #3, and #4 in one file, including <br>
<ul>
<li>temporarily change the background color of a tab when the cursor is hovering on it.</li>
<li>set the foreground and background color of the tab being clicked.</li>
<li>change the background color of the main element based on the selected tab.</li>
</ul>
<p>
To test, click on a tab and then move your mouse around. For example, the third tab is clicked, the tab background color is switched to blue. Then hover the mouse over the third tab, the background color of the tab should be switch to light green and then back to blue after the mouse moves out.
</p>
<img src="menu_tab5.jpg">
</li>
</ul>
</main>
It's generally a good idea to keep CSS out of JavaScript completely if you can help it. A better strategy for solving the hover problem is to use the CSS pseudo selector :hover rather than coding the color changes in JavaScript. If you give all your tabs the same class, you only have to write the CSS once:
.tab {
background-color: yellow;
}
.tab:hover {
background-color: grey;
}
Once you've done that, you can also relegate the click styling to CSS by creating an event handler that adds and removes a special class each time a tab is clicked.
In the CSS file:
.tab.clicked {
background-color: blue;
}
And then in JavaScript, something like:
var tabs = document.getElementsByClassName('tab');
for (i = 0; i < tabs.length; i ++) {
tabs[i].onclick = function (ev) {
for (i = 0; i < tabs.length; i ++) {
tabs[i].classList.remove('clicked');
}
ev.currentTarget.classList.add('clicked');
};
}
I've created a JSFiddle to illustrate.
Try updating a Boolean variable.
var Ele = document.getElementById('default');
var clicked = false;
Ele.onclick = function(){
clicked = true;
// add additional functionality here
}
Ele.onmouseover = function(){
clicked = false;
// add additional functionality here
}
Ele.onmouseout = function(){
if(!clicked){
// add additional functionality here
}
}

Semi-fixed text in a scrolling container

I've got a bunch of horizontal boxes containing text. The boxes are all in a horizontally scrolling container:
// generate some random data
var model = {
leftEdge: ko.observable(0)
};
model.rows = populateArray(10 + randInt(20), randRow);
ko.applyBindings(model);
$(function() {
$('.slide').on('scroll', function() {
model.leftEdge(this.scrollLeft);
})
})
function randRow() {
var events = populateArray(50 + randInt(100), randEvent);
var left = randInt(1000);
events.forEach(function(event) {
event.left = left;
left += 10 + event.width + randInt(1000);
});
return {
events: events
}
}
function randEvent() {
var word = randWord()
var width = 50 + Math.max(8 * word.length, randInt(200));
var event = {
left: 0,
width: width,
label: word
};
event.offset = ko.computed(function() {
// reposition the text to stay
// * within its container
// * fully on-screen (if possible)
var leftEdge = model.leftEdge();
return Math.max(0, Math.min(
leftEdge - event.left,
event.width - 8 * event.label.length
));
});
return event;
}
function randWord() {
var n = 2 + randInt(5);
var ret = "";
while (n-- > 0) {
ret += randElt("rmhntsk");
ret += randElt("aeiou");
}
return ret;
}
function randElt(arr) {
return arr[randInt(arr.length)];
}
function populateArray(n, populate) {
var arr = new Array(n);
for (var i = 0; i < n; i++) {
arr[i] = populate();
}
return arr;
}
function randInt(n) {
return Math.floor(Math.random() * n);
}
.slide {
max-width: 100%;
overflow: auto;
border: 5px solid black;
}
.row {
position: relative;
height: 25px;
}
.event {
position: absolute;
top: 2.5px;
border: 1px solid black;
padding: 2px;
background: #cdffff;
font-size: 14px;
font-family: monospace;
}
.event > span {
position: relative;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="slide" data-bind="foreach: rows">
<div class="row" data-bind="foreach: events">
<div class="event" data-bind="style: { left: left+'px', width: width+'px' }"><span data-bind="text:label, style: { left: offset() + 'px' }"></div>
</div>
</div>
What I'd like to do is as the user scrolls from left-to-right, reposition the text within each box that partially overlaps the left border of the visible window to keep the text as visible as possible.
Currently I'm doing this by manually repositioning each item of text.
Is there a cleaner way to do this using CSS?
A friend helped me come up with this solution.
In English, the idea is to add an overlay to each row that is positioned relatively to the frame of the scrolling box, rather than the contents.
Then we can place a label for any box that overlaps the left edge in this overlay and it will appear to smoothly move as the box underneath it scrolls.
// generate some random data
var model = {
leftEdge: ko.observable(0),
};
model.rows = populateArray(10 + randInt(20), randRow);
model.width = Math.max.apply(Math, $.map(model.rows, function(row) {
return row.width
}));
ko.applyBindings(model);
$(function() {
$('.slide').on('scroll', function() {
model.leftEdge(this.scrollLeft);
})
})
function randRow() {
var events = populateArray(50 + randInt(100), randEvent);
var left = randInt(1000);
events.forEach(function(event) {
event.left = left;
left += 10 + event.width + randInt(1000);
});
return {
events: events,
width: left
}
}
function randEvent() {
var word = randWord()
var width = 50 + Math.max(8 * word.length, randInt(200));
var event = {
width: width,
label: word,
};
event.tense = ko.computed(function() {
// reposition the text to stay#
// * within its container
// * fully on-screen (if possible)
var leftEdge = model.leftEdge();
return ['future', 'present', 'past'][
(leftEdge >= event.left) +
(leftEdge > event.left + event.width - 8 * event.label.length)
];
});
return event;
}
function randWord() {
var n = 2 + randInt(5);
var ret = "";
while (n-- > 0) {
ret += randElt("rmhntsk");
ret += randElt("aeiou");
}
return ret;
}
function randElt(arr) {
return arr[randInt(arr.length)];
}
function populateArray(n, populate) {
var arr = new Array(n);
for (var i = 0; i < n; i++) {
arr[i] = populate();
}
return arr;
}
function randInt(n) {
return Math.floor(Math.random() * n);
}
.wrapper {
position: relative;
border: 5px solid black;
font-size: 14px;
font-family: monospace;
}
.slide {
max-width: 100%;
overflow: auto;
}
.slide > * {
height: 25px;
}
.overlay {
position: absolute;
width: 100%;
left: 0;
}
.overlay .past {
display: none
}
.overlay .present {
position: absolute;
z-index: 1;
top: 5.5px;
left 0;
}
.overlay .future {
display: none
}
.row {
position: relative;
}
.event {
position: absolute;
top: 2.5px;
border: 1px solid black;
padding: 2px;
background: #cdffff;
height: 14px;
}
.event .past {
float: right;
}
.event .present {
display: none;
}
.event .future {
float: left;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.2.0/knockout-min.js"></script>
<div class="wrapper">
<div class="slide" data-bind="foreach: rows, style: { width: width + 'px' }">
<div class="overlay" data-bind="foreach: events">
<span data-bind="text:label, css: tense"></span>
</div>
<div class="row" data-bind="foreach: events">
<div class="event" data-bind="style: { left: left+'px', width: width+'px' }"><span data-bind="text:label, css: tense"></div>
</div>
</div></div>
This doesn't result in less javascript, but it does result in more efficient javascript, as class changes happen much less often than offset changes, so fewer updates to DOM elements are required.
You can avoid processing every "event" (in the above example) by doing some pre-partitioning of the horizontal space, and only updating events in the relevant partition.

Categories

Resources