Restructure a Multi-Level Content Presentation System - javascript

I am new to javascript, and I'm developing educational materials for my students at our school in India. I have created this structure that allows for cycling through lists of words with prev/next buttons, changing categories, and the ability to click on any given word to hear it spoken via text-to-speech.
It works and it's the most complicated thing I've been able to do, but I know it has to be horribly inefficient and outdated in the way it's constructed. I'm trying to learn more about event listeners, and how to simplify structures in general. I've spent the last five hours trying to change this structure using what I've learned, but the prev/next nav part and the fact that my items are in an array makes it beyond my scope.
Hopefully it's not out of line to ask for general input about how to reconfigure this to be more economical. I have put a lot of effort into getting this far with my woeful skillset. Many thanks for any assistance. If snippet isn't working page can be seen [here].1
var levelone = [
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Apple</div>",
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Banana</div>",
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Grapes</div>",
];
var leveltwo = [
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Dog</div>",
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Cat</div>",
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Horse</div>",
];
var levelthree = [
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Red</div>",
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Yellow</div>",
"<div onclick=\"jQuery(this).articulate('rate',1).articulate('setVoice','language','hi').articulate('speak')\" data-articulate-append=\"\">Blue</div>",
];
var box = document.getElementById('box');
var i = -1;
function next() {
i = i >= start.length - 1 ? 0 : i + 1;
box.innerHTML = start[i];
}
function prev() {
i = i > 0 ? i - 1 : sav.length - 1;
box.innerHTML = start[i];
}
$("#nextBtn").click(function() {
var nextDiv = $(".step:visible").next(".step");
if (nextDiv.length == 0) { // wrap around to beginning
nextDiv = $(".step:first");
}
$(".step").hide();
nextDiv.show();
});
$("#prevBtn").click(function() {
var prevDiv = $(".step:visible").prev(".step");
if (prevDiv.length == 0) { // wrap around to end
prevDiv = $(".step:last");
}
$(".step").hide();
prevDiv.show();
});
function lone() {
start = levelone.slice(0);
box.innerHTML = levelone[0];
}
function ltwo() {
start = leveltwo.slice(0);
box.innerHTML = leveltwo[0];
}
function lthree() {
start = levelthree.slice(0);
box.innerHTML = levelthree[0];
}
body {
padding: 20px;
font-size: 20px;
line-height: 1.5em
}
div {
cursor: pointer
}
<meta http-equiv="Content-Language" content="hi">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="http://clients.brettcolephotography.com/test/jquery-3.1.1.min.js"></script>
<script src="http://clients.brettcolephotography.com/test/articulate.min.js"></script>
<div id="box">Pick a category. Then click on each word to hear it spoken.</div>
<div>
<button type="button" onclick="prev()">prev</button>
<button type="button" onclick="next()">next</button>
</div>
<div>
<div onclick="lone()">Fruits</div>
<div onclick="ltwo()">Animals</div>
<div onclick="lthree()">Colors</div>
</div>

I can suggest a design model:
Have a array , something like const items=[];
Have another array something like const categories=["fruit","animal"]
When your page loads fetch the top category from above the array, ex: fruits, then have a render function which will push data into items based upon the category. After the push call the render function again which loads the list item based upon data avilable in the items array.
attach eventhandler to the new list items rendered through above code.

Related

Outputting an object to a page that keeps close function

I posted this question before, among others. But it was suggested I need to ask a more specific or focused question.
I am working on an output history log on a single page. And I want to make it so each output it's self is contained in box object that can be closed or deleted individually. Like this.
Now I have managed to get everything working to the point where it will nicely output to a box with a close button. However the close button it's self will not function in this case.
So, I am trying to output it like this...
HTML:
<p>History log:</p><br><div style="white-space:pre-wrap"><ul
id="outputListItem" class="boxcontainer"></ul></div>
SCRIPT:
document.getElementById("Add").onclick = function(e) {
convertOutput();
}
function convertOutput(){
//this is the part I have been trying to get working
convertOutput.addEventListener('close', function() {
this.parentElement.style.display = 'none';
}
});
var output = document.getElementById("output").value;
var li = document.createElement('li');
li.className = "containedboxes";
var dateTime = todayDateTime();
li.innerHTML = "<time id='time'>" + dateTime +"</time><br /> <br />"+ output
+"<br /><br /><span class='close'>×</span>";
document.getElementById('outputListItem').prepend(li);
}
And the script to close the box:
var closebtns = document.getElementsByClassName("close");
var i;
for (i = 0; i < closebtns.length; i++) {
closebtns[i].addEventListener("click", function() {
this.parentElement.style.display = 'none';
});
}
It was suggested to me on the last question I posed I should use convertOutput() right after addEventListener() loop immediately after it. If this is how you do it, i am still quite new to JavaScript, so not sore how to properly do this. I created a fiddle for this also, but for some reason I can't get the script to run properly in the fiddle, But all the code is there to see.
I am looking to solve this using vanilla JavaScript.
I created an example for you. Hopefully this helps you get going :) A couple things to note, I use a data attribute to store the index for the item in the array, so you can delete it when you click on the list item.
document.addEventListener('DOMContentLoaded', function(){
let nameEl = document.querySelector("#name");
let submitEl = document.querySelector("#submit-name");
let historyEl = document.querySelector(".history-list");
let historyList = [
{ name: 'Mitch'},
{ name: 'Max'},
{ name: 'Mike'},
];
function addToList(arr) {
// Clear up list and then update it
while(historyEl.firstChild) {
historyEl.removeChild(historyEl.firstChild);
}
// Update the list with the historyList
for(let item in historyList) {
let name = historyList[item].name;
let listContent = document.createElement("li");
listContent.textContent = name;
// We will use the index to remove items from the list
listContent.setAttribute('data-value', item);
listContent.addEventListener("click", removeFromList)
historyEl.appendChild(listContent);
}
}
function removeFromList(index) {
// Takes the index of the object, and will later remove it
console.log("Removed Item " + this.dataset.value);
historyList.splice(index, 1);
addToList(historyList);
}
addToList(historyList);
submitEl.addEventListener("click", function(event) {
if(nameEl.value) {
// Add the name to the start of the history list array.
historyList.unshift({ name: nameEl.value})
nameEl.value = '';
// Update the dom with the new array
addToList(historyList);
}
});
});
<label for="name">Type Name</label>
<input type="text" name="name" id="name">
<button id="submit-name">Submit Name</button>
<ul class="history-list"></ul>
Hopefully this gives you a good idea on how to get the task done and let me know if you have any questions :)
Your boxes don't respond to the click event simply because your script crashes before the events even get attached to it.
The following block right at the beginning:
document.getElementById("Add").onclick = function(e) {
convertOutput();
}
tries to add a click listener to the HTML element Add which does not exist. If you either remove the code block or add the appropriate element your boxes will have it's click functionality.

Randomize the layout of a set of existing images with JavaScript

I have a grid of images (baseball and football teams), with names associated with them (like captions).
What I'm trying to do is randomize the placement of the images with the names so they appear at different spots of the grid.
I can get the images and names to display (the names come from a textarea and are stored into a names array) just fine, but as soon as I hit the random button, the img.src disappears, leaving me with blank spots.
The names still randomize though and work fine.
This is my code so far:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>sports</title>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<link rel="stylesheet" href="css/main.css">
</head>
<body>
<div class="container-fluid">
<h1>Sports Random</h1>
<br>
<div class="container">
<div class="row" id="parent">
</div>
<div class="row">
</div>
<textarea id="textarea"></textarea>
<div id="buttonGroup">
<button id="random">Random</button>
<button id="baseball">Baseball</button>
<button id="football">Football</button>
<button id="reset">Reset</button>
</div>
</div>
<br>
</div>
<script src="js/main.js"></script>
</body>
</html>
JavaScript:
bGroup.addEventListener("click", function images(e) {
tDisplay.innerHTML = "";
for (var i = 1; i < names.length; i++) {
var newDiv = document.createElement("div");
var newImg = document.createElement("img");
var userName = document.createElement("p");
newDiv.className = "col-sm-3 col-md-3 col-lg-2"
newDiv.appendChild(newImg);
newDiv.appendChild(userName);
userName.textContent = names[Math.random() * i | 0];
if (e.target.id === "baseball") {
newImg.src = "images\\baseball\\team" + i + ".jpg";
} else if (e.target.id === "football") {
newImg.src = "images\\football\\team" + i + ".gif";
}
tDisplay.appendChild(newDiv);
};
});
// random the images
random.addEventListener("click", function random() {
for (var i = 0; i < tDisplay.children.length; i++) {
tDisplay.appendChild(tDisplay.children[Math.random() * i | 0]);
tDisplay.getElementsByTagName("p")[Math.random() * i | 0].textContent =
names[Math.random() * i | 0];
}
});
Right now the buttons on my page are in a div group (bGroup) and then I delegate events depending on which button is clicked.
I had this working when I had separate functions for "baseball" and "football" images; just trying to reduce code.
The random button is included in the button group but I kept it separate just for the sake of organization; kind of wondering if that is good practice or not?
If possible I would like any answers strictly in JavaScript.
Interference
The cause of the problem is that your Random button has its own "click" event listener whilst being a child of bGroup which also has a "click" event listener.
The exact process that results in empty clones is kind of irrelevant, but could be examined (if interested) by use console.log().
When you click Random, you trigger both the randomization of the contents of tDisplay and the initialization/creation of <div><img><p></p></div> children.
You have two options:
Either stopPropagation() of the "click" event in the unique listener attached to Random.
Or include the randomization functionality in the function triggered by a "click" on bGroup.
I have built a simplified version of your code below, using the second option stated above.
Since the randomization code Math.random() * i | 0 is used many times, I've created a function to return the random result of any number parsed through it.
The const declarations at the top of the JS are to store values that will never change and that are repeatedly used throughout the script.
The const declaration creates a read-only reference to a value. It does not mean the value it holds is immutable, just that the variable identifier cannot be reassigned. For instance, in the case where the content is an object, this means the object's contents (e.g., its parameters) can be altered.
I've used let instead of var where suitable, since it's less leaky.
let allows you to declare variables that are limited in scope to the block, statement, or expression on which it is used. This is unlike the var keyword, which defines a variable globally, or locally to an entire function regardless of block scope.
I chose to use forEach() instead of a for loop to iterate through names, but retained the for loop to handle the randization.
Each has its own merits.
The arrow function called by forEach() uses two arguments; n and i, where n is the name on each loop, and i is the index of the name (a loop counter).
I also chose to use querySelectorAll() instead of getElementsByTagName() for brevity.
Those changes have benefits, but the only significant change is in moving the "click" handling for Random into the handler for clicks on bGroup.
Be aware that the randomization may not change the display every time (the nature of random), but keep clicking and you'll see that it does in fact work; a longer names Array (and thus more images) would likely randomize more noticeably.
const tDisplay = document.querySelector( ".container" ),
bGroup = document.querySelector( "#buttonGroup" ),
names = [ "foo", "bar", "baz", "qux" ];
function randIndex( i ) {
return ( Math.random() * i | 0 );
}
bGroup.addEventListener( "click", function( evt ) {
let trg = evt.target.id;
if ( trg !== "random" ) {
tDisplay.innerHTML = "";
names.forEach( ( n, i ) => {
let newDiv = document.createElement( "div" ),
newImg = document.createElement( "img" ),
userName = document.createElement( "p" );
//newDiv.className = "col-sm-3 col-md-3 col-lg-2";
newDiv.appendChild( newImg );
newDiv.appendChild( userName );
userName.textContent = names[ randIndex( i ) ];
if ( trg === "sports" ) {
newImg.src = "https://lorempixel.com/100/70/sports/" + i;
} else if ( trg === "cats") {
newImg.src = "https://lorempixel.com/100/70/cats/" + i;
}
tDisplay.appendChild( newDiv );
} );
} else {
for ( let i = 0; i < tDisplay.children.length; i++ ) {
tDisplay.appendChild( tDisplay.children[ randIndex( i ) ] );
tDisplay.querySelectorAll( "p" )[ randIndex( i ) ].textContent =
names[ randIndex( i ) ];
}
}
});
body {
font-family: sans-serif;
}
h1, p {
margin: .5em 0;
}
#buttonGroup {
margin-bottom: 1em;
}
.container div {
display: inline-block;
margin-right: 1em;
}
<h1>Images Random</h1>
<div id="buttonGroup">
<button id="random">Random</button>
<button id="sports">Sports</button>
<button id="cats">Cats</button>
</div>
<div class="container"></div>

JavaScript: how to give an HTML content to a variable?

I'm studying JavaScript basics and today I built a simple html page which let the user to add/remove a list item. Well, I think I could be there (I know that there are a lot of better solutions, but hey, I'm just learning).
// the function that adds a list item
function addListItem () {
var newLi = document.createElement("li");
newLi.className = "listItem";
// newLi.innerHTML = "<h3>List item</h3> <p>This is a simple list item</p>";
list.appendChild(newLi);
}
You can see full code here: https://jsfiddle.net/l_wel/cuvc0m5g/
The problem is: how you can see within the first function, I put a commented code. It inserts html content inside the new list item. Is there a better way to do it? I mean, what if i would the new list item to have the number of the list item into the ?
Something like that:
List item 1
List item 2
List item 3
etc.. etc..
I know I should use a counter, but I was not able to let the created list items to have all the original html content from the first list item without the need to rewrite it within the function.
Ok, sorry for my bad english and sorry if you think this is a very simple problem, but I tried for hours. I hope you understood what I'm trying to achieve. I think that without the comment it could work as well, depending on the project.
P.S.
I don't know jQuery yet, I wanted to solve this using vanilla js.
See if this works for you:
// store the list
var list = document.getElementById("list");
var number = 1;
// the function that adds a list item
function addListItem () {
number++;
var newLi = document.createElement("li");
newLi.className = "listItem";
newLi.innerHTML = "<h3>List item</h3> <p>This is a simple list item " + number + "</p>";
list.appendChild(newLi);
}
// the function that removes the last list item
function removeListItem () {
number--;
var ulList = document.querySelectorAll("listItem");
var lastLi = list.lastElementChild;
var containerLi = lastLi.parentNode;
containerLi.removeChild(lastLi);
}
// add a list item
var btnAdd = document.getElementById("btnAdd");
if(btnAdd.addEventListener) {
btnAdd.addEventListener("click", addListItem, false);
} else {
btnAdd.attachEvent("click", addListItem, false);
}
// remove the last list item
var btnRemove = document.getElementById("btnRemove");
if(btnRemove.addEventListener) {
btnRemove.addEventListener("click", removeListItem, false);
} else {
btnAdd.attachEvent("click", removeListItem, false);
}
body {
font-family: monospace;
background: #1e2530;
color: #cce8ff;
}
.top { text-align: center; }
#list {
list-style-type: none;
padding: 0;
margin: 10px;
}
.listItem {
background: #cce8ff;
color: #1e2530;
margin-bottom: 10px;
padding: 5px 0 5px 10px;
border-radius: 5px;
}
<body>
<div class="top">
<h2>challenge #8</h2>
<button id="btnAdd">Add an item list</button>
<button id="btnRemove">Remove an item list</button>
</div>
<ul id="list">
<li class="listItem">
<h3>List item</h3>
<p>This is a simple list item 1</p>
</li>
</ul>
</body>
addListItem is a function which can accept parameters. for example, the forEach command is iterating the array and calling the addListItem for each of the items, forEach is calling the callback with two arguments, the first argument is the item itself, and the second is the index of the item in the array...
then you can use the arguments to display the data...
var items = ['Dog','Cat','Mouse'];
function addListItem( title, index ) {
var newLi = document.createElement("li");
newLi.className = "listItem";
newLi.innerHTML = "<h3>"+title+"</h3> " + index;
list.appendChild(newLi);
}
items.forEach( addListItem );
I know you said you didn't want to use JQuery (http://api.jquery.com/append/), but it does make your life easier. For example, you could use the function below. Writing JavaScript is fun, but reading good open source JavaScript (like reading JQuery source) is a far better learning experience.
you are going to need to create a counter to get the list number:
var lst = $('ul.mylist') //class is my list, if ul.mylist doesn't exist use append to append it to the document
for(let i = 0; i < [number of elements]; i++) {
lst.append('<li>List Item' + i + '</li>);
}

Generating random divs multiple times on load

let me just give a quick story. I have made a page. (VERY simple - two divs with a different background image, see here.)
Anyway, I need to make it so that when a new page loads, the two divs that I have load in a random order over and over, filling the entire screen content. So there's no pattern of the first div and then the second, it's just randomly generated. Sort of like a huge grid, with the two divs repeated with no pattern.
My question is...is that possible? I assume I'd need to know PHP, but I have no knowledge of it.
Thanks guys, I appreciate all help!
http://jsfiddle.net/uYPRq/
jquery
var div1 = '<div class="one">';
var div2 = '<div class="two">';
var len =
Math.floor(window.innerWidth/30)*Math.floor(window.innerHeight/30);
for (x = 0; x < len; x++) {
if ( Math.random() > 0.5 ) {
$(div1).appendTo('body');
}
else {
$(div2).appendTo('body');
}
}
css
div.one, div.two {
height:30px;
width:30px;
float:left;
}
div.one { background-color:#EBE1E4; }
div.two { background-color:#F0F5DF; }
edit:
changed screen.availWidth to window.innerWidth
Something like so? Just loop through how ever many times you like and add elements in.
for (i = 0; i < 300; i++) {
var type1 = document.createElement("div");
var type2 = document.createElement("div");
type1.innerHTML = "div1";
type2.innerHTML = "div2";
type1.setAttribute("class", "type1");
type2.setAttribute("class", "type2");
document.body.appendChild(type1);
document.body.appendChild(type2);
}
No PHP needed. This can be done client-side using Javascript (Jquery might be easier).

Filtering the list of friends extracted by Facebook graph api ( more of a JavaScript/Jquery question than Facebook API question)

Hello there JavaScript and Jquery gurus, I am getting and then displaying list of a facebook user's friend list by using the following code:
<script>
function getFriends(){
var theword = '/me/friends';
FB.api(theword, function(response) {
var divInfo = document.getElementById("divInfo");
var friends = response.data;
divInfo.innerHTML += '<h1 id="header">Friends/h1><ul id="list">';
for (var i = 0; i < friends.length; i++) {
divInfo.innerHTML += '<li>'+friends[i].name +'</li>';
}
divInfo.innerHTML += '</ul></div>';
});
}
</script>
graph friends
<div id = divInfo></div>
Now, in my Facebook integrated website, I would eventually like my users to choose their friends and send them gifts/facebook-punch them..or whatever. Therefore, I am trying to implement a simple Jquery filter using this piece of code that manipulates with the DOM
<script>
(function ($) {
// custom css expression for a case-insensitive contains()
jQuery.expr[':'].Contains = function(a,i,m){
return (a.textContent || a.innerText || "").toUpperCase().indexOf(m[3].toUpperCase())>=0;
};
function listFilter(header, list) { // header is any element, list is an unordered list
// create and add the filter form to the header
var form = $("<form>").attr({"class":"filterform","action":"#"}),
input = $("<input>").attr({"class":"filterinput","type":"text"});
$(form).append(input).appendTo(header);
$(input)
.change( function () {
var filter = $(this).val();
if(filter) {
// this finds all links in a list that contain the input,
// and hide the ones not containing the input while showing the ones that do
$(list).find("a:not(:Contains(" + filter + "))").parent().slideUp();
$(list).find("a:Contains(" + filter + ")").parent().slideDown();
} else {
$(list).find("li").slideDown();
}
return false;
})
.keyup( function () {
// fire the above change event after every letter
$(this).change();
});
}
//ondomready
$(function () {
listFilter($("#header"), $("#list"));
});
}(jQuery));
</script>
Now, This piece of code works on normal unordered list, but when the list is rendered by JavaScript, it does not. I have a hunch that it has to do something with the innerHTML method. Also, I have tried putting the JQuery filter code within and also right before tag. Neither seemed to work.
If anyone knows how to resolve this issue, please help me out. Also, is there a better way to display the friends list from which users can choose from?
The problem is here:
$(list).find("a:not(:Contains(" + filter + "))").parent().slideUp();
$(list).find("a:Contains(" + filter + ")").parent().slideDown();
Since you're rendering this:
divInfo.innerHTML += '<li>'+friends[i].name +'</li>';
There is no anchor wrapper, the text is directly in the <li> so change the first two lines to look in those elements accordingly, like this:
$(list).find("li:not(:Contains(" + filter + "))").slideUp();
$(list).find("li:Contains(" + filter + ")").slideDown();
You could also make that whole section a bit faster by running your Contains() code only once, making a big pact for long lists, like this:
$(input).bind("change keyup", function () {
var filter = $(this).val();
if(filter) {
var matches = $(list).find("li:Contains(" + filter + ")").slideDown();
$(list).find("li").not(matches).slideUp();
} else {
$(list).find("li").slideDown();
}
});
And to resolve those potential (likely really) innerHTML issues, build your structure by using the DOM, like this:
function getFriends(){
var theword = '/me/friends';
FB.api(theword, function(response) {
var divInfo = $("#divInfo"), friends = response.data;
divInfo.append('<h1 id="header">Friends/h1>');
var list = $('<ul id="list" />');
for (var i = 0; i < friends.length; i++) {
$('<li />', { text: friends[i].name }).appendTo(list);
}
divInfo.append(list);
});
}
By doing it this way you're building your content all at once, the <ul> being a document fragment, then one insertion....this is also better for performance for 2 reasons. 1) You're currently adding invalid HTML with the .innerHTML calls...you should never have an unclosed element at any point, and 2) you're doing 2 DOM manipulations (1 for the header, 1 for the list) after the much faster document fragment creation, not repeated .innerHTML changes.

Categories

Resources