Javascript - animation stops as new one is started - javascript

I have a UL on a page and on the click of a button I am appending an li and fading it in. This works fine for single clicks. However, when the button is clicked twice, the first animation immediately stops and the second completes.
I have setup a test page to demonstrate this: http://anttears.co.uk/test (the test page has been tested in FF only)
Putting a console.log in the setOpacity function for the value of elem seems to show the javascript working as expected - both li's reaching opacity=1. However, the first li seems to be a fragment dissasoiated with the actual DOM.
I am deliberately trying to get this working without the usual libraries, as a learning experience.
Any help greatfully appreciated...
Ant

this.prepend = function(string, elem) {
var content = elem.innerHTML;
content = string + content;
elem.innerHTML = content;
}
Never do this.
You're taking the DOM content of the elem (the growlList), serialising it to a new HTML string, prepending some HTML content, and parsing the joined HTML back into completely new DOM nodes. This loses all non-HTML-serialisable content, such as form field values, event handlers and JavaScript references. The animation still has a reference to the old HTMLElement node, which is no longer in the DOM, having been replaced by newly-parsed-from-content elements.
Indeed, you usually want to avoid generating HTML strings at all:
growl = '<li id="' + gid + '" class="' + className + ' growl" style="display: none"><span class="close" title="close">x</span><h2>' + heading + '</h2><div class="growlContent">' + content + '</div></li>',
Any unescaped HTML-special characters in that content and you've broken your markup at best. At worst, the data comes from a malicious user and you've given yourself an XSS security hole.
Instead, use DOM-style methods, eg:
var span= document.createElement('span');
span.className=span.title= 'close';
span.appendChild(document.createTextNode('x'));
var h2= document.createElement('h2');
h2.appendChild(document.createTextNode(heading));
var div= document.createElement('div');
div.className= 'growlContent';
div.appendChild(document.createTextNode(content))
var li= document.createElement('li');
li.id= gid;
li.className= className;
li.appendChild(span);
li.appendChild(h2);
li.appendChild(div);
gl.insertBefore(li, gl.firstChild);
This is quite wordy, but it's easy to write a helper function to cut down on typing, eg:
gl.insertBefore(el('li', {id: gid, className: className), [
el('span', {className: 'close', title: 'close'}, 'x'),
el('h2', {}, heading),
el('div', {className: 'growlContent'}, content)
]), gl.firstChild);
// Element creation convenience function
//
function el(tag, attrs, content) {
var el= document.createElement(tag);
if (attrs!==undefined)
for (var k in attrs)
if (attrs.hasOwnProperty(k))
el[k]= attrs[k];
if (content!==undefined) {
if (typeof(content)==='string')
el.appendChild(document.createTextNode(content));
else
for (var i= 0; i<content.length; i++)
el.appendChild(content[i]);
}
return el;
};

Related

Chrome is outputting something different than other browsers/IDE

Problem
I am making a wikipedia viewer for FreeCodeCamp. When I execute my code using Jetbrains Webstorm IDE, I get a total of 10 extra divs created at the end of my search results.
When I do so using JSfiddle, I see the correct results, which should just be the initial 10 search results, and not +10 more empty div containers.
Any idea what's going on?
Troubleshooting
After some more digging into it, it seems like this is more of a browser issue than an IDE issue. It looks like it's just Chrome that's doing this? I tried on Firefox and IE, and it runs as normal, just like the JSfiddle. I have also tried clearing all cookies and cache on Chrome.
When I open the console log and look at the elements, in Chrome, there are clearly 10 extra empty divs, as shown here.
But in Firefox, the same code does NOT create the divs, as shown here.
Edit: Okay, so in Chrome, if you hit enter, instead of clicking on the search button, it creates the extra divs. But if you click on the search icon, the results appear correctly. Can anyone explain why? Did I place/write the .keyup() code block incorrectly?
Here's a picture of said issue.
Javascript portion:
$(function(){
$("#search-bar").keyup(function(event) {
if(event.keyCode === 13) {
$(".btn").click();
}
});
$(".btn").click(function() {
$("#results").empty();
var searchTerm = $("#search-bar").val();
var url = "https://en.wikipedia.org/w/api.php?action=query&list=search&srsearch=" + searchTerm + "&utf8=&format=json&origin=*";
console.log(url);
$.ajax({
cache: false,
url: url,
type: 'GET',
success: function(data) {
// Create a blank array to store the specific URLs in
var urlArr = [];
for (var i=0; i <= data.query.search.length; i++) {
// Adding the variables for use here
var headerData = data.query.search[i].title;
var urlSearchTerm = headerData.replace(/\s/gi, '_');
var snippetData = data.query.search[i].snippet + "...";
var createDiv = document.createElement("div");
var createHeader = document.createElement("h1");
var createSnippet = document.createElement("p");
var divId = "div" + i.toString();
var headerId = "header" + i.toString();
var snippetId = "snippet" + i.toString();
var resultUrl = "https://en.wikipedia.org/wiki/";
urlArr[i] = resultUrl + urlSearchTerm;
// Create the div element, give it an id
// Create <h1> element, give it an id
// Create <p> element, give it an id
// Give each <div> a class
createDiv.setAttribute("id", divId);
createHeader.setAttribute("id", headerId);
createSnippet.setAttribute("id", snippetId);
createDiv.setAttribute("class", "each-result");
// appending <div> elements in the #results id element in body
// appending <h1> element to the div element
// appending <p> element to the div element
document.getElementById("results").appendChild(createDiv);
document.getElementById(divId).appendChild(createHeader);
document.getElementById(divId).appendChild(createSnippet);
// populate json data into <h1> element
// populate json data into <p> element
$("#header" + i.toString()).text(headerData);
$("#snippet" + i.toString()).html(snippetData);
// create a click event handler that does 2 things
// 1. Gives each div an element that has a URL in the resultUrl array
// 2. Opens that url in a new window
(function(i) {
$("#div" + i.toString()).click(function() {
window.open(urlArr[i]);
});
}(i));
}
}
});
});
$("#random-wiki-button").click(function() {
window.open("https://en.wikipedia.org/wiki/Special:Random");
});
});
Actual full code including html/css + JS: JSfiddle
Maybe try updating chrome?
I would try to change your for loop to for (var i=0; i < 10; i++) { that way the increment stops at the 10th item.
edited.

To what HTML element can I append all possible HTML elements using innerHTML?

I have to pass HTML around in as a string (as I'm using postmessage for communication). To apply modifications to the html, I'm doing:
function foo(my_string) {
var temp, element_list;
temp = document.createElement("div")
temp.innerHTML = my_string;
element_list = temp.querySelectorAll(".foo");
...
My problem is that my_string can be anything and in case I'm passing a string with table rows and cells like this:
'<tr>' +
'<td>' +
'<a href="#gadget._key=project_module%2F1&gadget.view=view">' +
'My Test Project 2014/12/16 14:24:48.930904 GMT' +
'</a>' +
'</td>' +
'...' +
'</tr>'
appending this to a <div> removes the table rows and cells and I'm left with links only. Something like this:
'<a href="#gadget._key=project_module%2F1&gadget.view=view">' +
'My Test Project 2014/12/16 14:24:48.930904 GMT' +
'</a>' +
Question:
Is there a generic element, which accepts any type of child elements and does not modify whatever it's passed via innerHTML?
Thanks!
Edit:
The method is used to translate html snippets. When I'm updating a table, it will only pass the generated table rows vs receiving the whole table on the initial page rendering.
There isn't such an element. <tr> is a very good example of this. According to W3C standards, the "Permitted parent elements" for <tr> are "A <table>, <thead>, <tbody> or <tfoot> element."
If you must have these strings coming in as they are, your best bet is to perform some sort of detection as to the type of element(s) you are inserting, and wrap them in the appropriate HTML if required.
For example: (View as a CodePen)
HTML
<div id="container"></div>
JavaScript
var anyone = "<div>In a Div</div>";
var tableOnly = "<tr><td>In a..</td></tr>" +
"<tr><td>...table</td></tr>";
$(function () {
var $container = $("#container");
appendContent(anyone);
appendContent(tableOnly);
function appendContent(html) {
var $html = $(html),
$parent = $(),
lastParent = "";
$html.each(function () {
var parent = parentTag(this.tagName);
if(parent !== lastParent)
{
$container.append($parent);
$parent = $(parent);
}
$parent.append(this);
lastParent = parent;
});
$container.append($parent);
}
function parentTag(tagName) {
switch (tagName.toLowerCase()) {
case "tr":
return "<table></table>";
default:
return "<div></div>";
}
}
});
Edit: Note that the technique used here to detect the tags used in your HTML can have problems if your HTML contains content that cannot be part of the same parent. For example, the following code would fail:
appendContent("<tr><td>Also in a table</td></tr><div>Also in a div</div>");
This is because of how jQuery internally builds its selectors. Since you can't have a div tag as a sibling to a tr, effectively the div element gets dropped. Here's a CodePen demonstrating this, but from the sound of things, this wouldn't be an issue for the OP's needs. If it is, you could use some alternative method of detecting the tags such as Regular Expressions.
If you append the mal-formatted HTML data (as you've noticed) with missing tags you're at the Browser DOM parser mercy removing every single one till a conformable HTML is returned.
If your main concern (project-wise) is just about table HTML content than you could
treat the string as an XML data structure and get the needed wrapping tag and act accordingly:
jsBin demo
function sanitizeHTML( string ) {
// Treat friendly a HTMLString as XML structure:
var par = new DOMParser();
var doc = par.parseFromString(string, 'text/xml');
var chd = doc.firstChild;
var tag = chd.nodeName.toUpperCase(); // Get the tag
var ele;
function wrapWith(parent, childEl){ // Wrap a node into a parent
var p = document.createElement(parent);
p.appendChild(childEl);
return p; // And return that parent element.
}
if(/^(THEAD|TBODY|TR)$/.test(tag)){ // If THEAD or TBODY or TR
ele = wrapWith("table", chd); // just wrap in TABLE.
}else if(/^(TD|TH)$/.test(tag)){ // Else if TD or TH
ele = wrapWith("tr", chd); // wrap first in TR
ele = wrapWith("table", ele); // and than in TABLE.
}else{
// All fine. Do we need something here?
}
return ele || chd; // Returns a HTMLElement
}
// This will return the final HTMLElement:
// var myEl = sanitizeHTML( str );
// Let's see in console:
console.log( sanitizeHTML( str ).outerHTML );
For simplicity sake the above code will consider strings with only one children.
To extend it - loop all the children of the doc object.
See this jsfiddle: http://jsfiddle.net/Grezzo/x1qxjx5y/
With <tr>s in <table>s they are ok.
It's because you are putting a <tr> in a <div> which isn't valid.
Putting unsanitized content in the page like this is a real security risk
Update: I updated the jsfiddle to include two <div>s that were not modified by javascript and you can see that the <tr>s are stripped if they are not in a <table> parent: http://jsfiddle.net/Grezzo/x1qxjx5y/1/

Removing elements from a document in Javascript [duplicate]

This question already has answers here:
JavaScript DOM remove element
(4 answers)
Closed 8 years ago.
Working on building this to-do list app to learn JS better.
I am able to insert text into box and have it add a new div element to the page with the relevant text residing within the div. The code that adds the div to the page automatically applies the class .newItem then adds an incrementing id i0, i1, i2, etc each time it's clicked. Everything works without an issue. Now, I have been fiddling around with the code to be able to click a single div element and have it remove itself from the page, but can't seem to get it to work.
var iDN = 0;
//Function that adds a new div with a custom incrementing id number
document.getElementById('add_button').onclick = function () {
var taskName = document.getElementById('add_task').value; // Store the value in the textbox into a variable
document.querySelector('.shit_to_do').innerHTML += '<div class = "newItem" id = "i' + iDN + '"' + 'onclick = removeEl()' + '>' + taskName + '</div>';
iDN += 1;
};
document.getElementById('testing').onclick = function () {
var parentNode = document.querySelector('.shit_to_do');
parentNode.removeChild(parentNode.children[0]);
}
function removeEl() {
for (i = 0; i < iDN; i++) {
if (document.getElementById('i' + i).onclick) {
document.getElementById('i' + i).display.style = 'none';
}
alert(i);
}
}
The for loop was really some random option I was trying to figure out how things were working onclick for each div, but didn't get to far with that one.
tl;dr:
I want to add click events to the new divs added onto the page in a single, universal function.
The value of document.getElementById('i' + i).onclick will be null if you've not set a handler to this attribute/property, otherwise it will be a function. null is always falsy, a function is always truthy.
To remove your element, you'll either have to look at this or e.target where e is the click event, and then call the DOM method node.removeChild(child).
The "quick and dirty" solution is to pass this into removeEl and remove it that way,
// ...
document.querySelector('.shit_to_do').innerHTML += '<div class="newItem" id="i' + iDN + '" onclick="removeEl(this)">' + taskName + '</div>';
// ...
function removeEl(elm) {
elm.parentNode.removeChild(elm);
}
I also removed the strange spacing between attribute names and values in your HTML
A perhaps "cleaner" solution is to create your nodes and attach listeners all by using DOM methods
function createDiv(index, taskname) {
var d = document.createElement('div');
d.setAttribute('id', 'i' + index);
d.textContent = taskname;
return d;
}
function removeElm() {
this.parentNode.removeChild(this);
}
var iDN = 0;
document.getElementById('add_button').addEventListener('click', function () {
var taskName = document.getElementById('add_task').value,
list = querySelector('.shit_to_do'),
div = createDiv(iDN, taskName);
div.addEventListener('click', removeElm);
list.appendChild(div);
iDN += 1;
});
This way means the browser does not re-parse any HTML as it not use element.innerHTML, which is a dangerous property may destroy references etc (along with usually being a bit slower)
Helpful links
node.addEventListener
document.createElement
node.appendChild

How to append an element, all its children, and all classes of the parent and children with jQuery

I have a function that is successful in removing an element and appending it elsewhere on the page as successful. The problem is that as soon as the document is ready jQuery adds classes and attributes to the children that upon moving are lost. I need these classes and attributes to remain after removing and appending. I have thought about calling the original function that adds the classes, but the problem is they are key based and rely on their position prior to the move, calling it after changes the key and thus will add brand new and different classes.
The classes adding jQuery is pretty standard:
$(function(){
$("div").each(function(key){
if ($(this).hasClass("container")){
$(this).find("ul").addClass("parent" + key);
$(this).find(".container-item").attr("parentClass", ".parent" + key);
};
});
});
The remove/append function:
function copy_item(draggable, target){
var account = clone_items(draggable);
//$('#'+name.uid).remove();
$('#'+name.uid).hide();
target.append(make_div(name, true, true));
//$(draggable).children().attr("class", ($(draggable).children().attr("class")));
}
function make_div(name, drag, drop){
var newdiv = document.createElement('div');
newdiv.setAttribute('id', name.uid);
newdiv.appendChild(make_h3(name.username));
ul = document.createElement('ul');
ul.setAttribute("class", "domain_list");
newdiv.appendChild(ul);
for (j = 0; j < name.domains.length; ++j) {
ul.appendChild(make_li(name.domains[j], drag));
}
return newdiv;
}
The end result in the HTMl is basically:
<div class="container">
<ul class="parent0">
<li parentClass="parent0">
<li parentClass="parent0">
When recreating this structure, I need to have the class "parent0" and the parentClass attribute intact. Above you can see I've tried hiding the element, ensuring that it still stays a valid element with the correct classes/attributes, but in the end that still didn't work out. Ideally, I could remove the element entirely and recreate it with the correct classes.
If I am correct in my understanding of what you are trying to do, you do not need to .remove() and recreate the element in order to move it. You can just do this:
function copy_item(draggable, target) {
// not sure what this variable is for
// as you don't seem to be using it?
var account = clone_items(draggable);
// ...however, appending an existing
// element to another will 'move' it
// and preserve all of it's properties
target.append($('#' + name.uid));
}

(jquery) change nested same html tag to other bbcode tag

ok here is what i have:
<div id="mydiv">
<font color="green"><font size="3"><font face="helvetica">hello world</font></font></font>
</div>
I know the tags are strange, but that's what produced by the website.
So basically I want to change the font tag to bbcdoe tag, the jquery code I wrote:
$("#mydiv").find("font").text(function(){
var text = $(this).text();
var size = $(this).attr("size");
var color = $(this).attr("color");
var face = $(this).attr("face");;
if(size!=undefined){
return '[size="'+size+'"]'+text+'[/size]';
}
if(color!=undefined){
return '[color="'+color+'"]'+text+'[/color]';
}
if(face!=undefined){
return '[type="'+face+'"]'+text+'[/type]';
}
});
so what I got is only: [color="green"] hello world [/color]. always only the first tag. any idea?
ps: I tried each, replaceWith, html(), all the same result. only the first tag is change.
The reason it doesn't work is because when you call
$("#mydiv").find("font").text("New text")
For each font tag, starting from the first tag, it will replace the text within that tag.
Here is an example to show you what's going on.
Example | Code
$fonts = $("font","#mydiv");
console.log($fonts.text());
$fonts.text(function(){
return "New text";
});
console.log($fonts.text());
Here is an example of how you could do it instead
Example | Code
jQuery.fn.reverse = [].reverse;
var attributes= ["size", "color", "face"];
var text = $.trim($("#mydiv").text());
$("font","#mydiv").reverse().each(function(i, e) {
for (var i = 0; i < attributes.length; ++i){
var attr = $(e).attr(attributes[i]);
if( typeof attr != "undefined")
text = "["+attributes[i]+"="+attr+"]"+text+"[/"+attributes[i]+"]";
}
});
$("#mydiv").text(text);
A room full of sad, wailing kittens wishes that you'd get rid of those <font> tags, but you could probably make it work by explicitly working your way down through the nested tags.
It does what it does now because the outer call to .text() runs for the very first <font> tag, and it obliterates the other tags.
edit — to clarify, when you call
$('#mydiv').find('font')
jQuery will find 3 font tags. The library will therefore call the function you passed into .text() for each of those elements. However, the first call will have the effect of removing the other two <font> elements from the DOM. Even though the library proceeds to call your callback for those elements, there's no effect because they're not on the page anymore.
Here's what could work:
var $fonts = $('#mydiv').find('font');
var text = $fonts.text();
var attrs = {};
$fonts.each(function(_, font) {
var names = ["size", "color", "face"];
for (var i = 0; i < names.length; ++i)
if (font[names[i]]) attrs[names[i]] = font[names[i]];
});
var newText = "";
for (var name in attrs) {
if (attrs.hasOwnProperty(name))
newText += '[' + name + '=' + attrs[name] + ']';
}
newText += text;
for (var name in attrs) {
if (attrs.hasOwnProperty(name))
newText += '[/' + name + ']';
}
$('#mydiv').text(newText);
Note that I'm not really sure why you want to put the BBCode onto the page like that, but it seems to be the intention.
Seems to me your first line should be:
$("#mydiv").find("font").each(function(){

Categories

Resources