I select an element of the page:
$mainSection = $('#main');
then I add more Elements via AJAX into the <div id="main"></div> element. Next time I call $mainSection, the newly added elements are also in it. But I don't want that. I would like that the variable $mainSection only has the content in it from the initial rendering of the page. I can't find a way to prevent jQuery from updating.
I tried this:
$(document).ready(function(){
$mainSection = $('#main').clone(true);
Then I add new elements to #main and then I check if they get found via:
$foundElement = $($mainSection + ":not(:has(*)):not(script):contains('"+newlyAddedContent+"')");
On page load, they are not there. But after I add them, they get found.
I also tried:
$mainSection = $('#main').html();
$mainSection = $($mainSection);
didn't work also.
Here is a jsFiddle to illustrate my point:
http://jsfiddle.net/VEQ2E/2/
The problem is somewhere burried in this line:
$foundElement = $($mainSection + ":not(:has(*)):not(script):contains('"+newlyAddedContent+"')");
It somehow always searches through the whole document, when I do it like this.
You can use .clone(true):
$mainSection = $('#main').clone(true);
It every time takes the clone/copy of the initial state of this div.
Note:
.clone( [withDataAndEvents ] [, deepWithDataAndEvents ] )
withDataAndEvents : Boolean (default: false)
deepWithDataAndEvents : Boolean (default: value of withDataAndEvents)
A Boolean indicating whether event handlers and data for all children of the cloned element should be copied.
Your problem was not that your clone was getting changed, but rather the selector you were using to try finding something within the clone. Your code was like this:
$($mainSection + ":not(:has(*)):not(script):contains('"+newlyAddedContent+"')");
Concatenating an object with a string will turn the object into a string, simply "[object Object]", then your selector will just look at the ":not(:has..."
Instead, you should use filter:
$foundElement = $mainClone.filter(":not(:has(*)):not(script):contains('world')");
This will now only look within your $mainClone for items matching that filter.
JSFiddle
You can take several approaches:
Cache the contents of #main before the update. This gives you just the contents of the element without the element.:
mainsectionContents = $('#main').html();
Or Cache a copy of #main before the update. This will give you the content together with the element, and depending on whatever else you may want to copy feel free to check the api docs:
$mainsectionCopy = $('#main').clone();
Related
I have this code:
//Callback function
var done = arguments[arguments.length - 1];
//Take all the events
var array_events = []
var retour = (e) => {
array_events.push(e.target.outerHTML)
}
var quit = (key) => {
console.log(array_events);
(key.keyCode == 27 )? done(JSON.stringify(array_events)) : undefined
}
// Listen to the clicks
getPath = document.addEventListener("click", retour, true)
getIndex = document.addEventListener("click", detour, true)
// Listen to the key "esc" which means user has gathered all needed events
getKey = document.addEventListener("keydown", quit, true)
This gets me the value of outerHTML of an element clicked, so far so good.
I'm building a program that needs to find that element,but in the case where the outerHTML of the element is not unique (meaning there are multiple occurrences of that element in the DOM), I cannot find out which one of the elements the user wants, so I think the easiest way would be to include index of some sort.
My question is ->
How can I get the index of that element relative to whole HTML document?
If that is not possible, any index of some sort that I can take and calculate which one is first or second or last does help too!
I am not aware of any indexing of DOM elements that is accessible with JS.
Not sure how you want to use this information about what element has been selected. So here are several ideas, hope one will be useful.
If you need to do something with that element, like modify it in some way in your callback, then you may want to use:
e.target.parentElement
This will return the actual DOM element, and you can do whatever you want with it. It does not matter how many copies you have in the page. And if you actually need the HTML text, you can always use .innerHTML to get it.
If you need this for some type of analytics where you want to save this interaction somewhere instead of modifying the page you can do something like this:
e.target.parentElement.getBoundingClientRect()
This will return an object that looks something like this:
this way you will know the exact coordinates of the element that was selected, and even if you have multiple elements with the exact same HTML you can know that this is some specific copy of it.
If you do not have too many elements that need to be affected by this, maybe you can add the index yourself. When all elements are created, you can do something like this:
let maxId = 0;
const allElements = document.querySelectorAll('.SelectorForYourElement');
allElements.forEach(element => {
element.dataset.id = ++maxId;
});
I currently have the following jQuery code working. Here's what it does: when I click a button, it will swap between classes. Each of those classes contains a background image. So, as I click this single button, it cycles through the background images. (edit: I just need to store whatever the current variable is, so when they swap pages, that variable is what's loaded).
$('.button').mousedown(function () {
$('#backgrounds').each(function(){
var classes = ['bg1','bg2','bg3','bg4'];
this.className = classes[($.inArray(this.className, classes)+1)%classes.length];
});
});
However, my website has multiple pages. So, what I need to do is store the current variable to the session storage or local storage, and then retrieve that variable on page load. That way as I jump from one page to another, the same class (and therefore background image) will be displayed. I don't need to (nor want to) use cookies -- I just need this to last the current session.
Additionally, if possible (though much less important than storing the variable), I'd also like this jQuery function to neatly fade when swapping from one background image to another. Right now it "snaps" from one image to the next as it swaps out the classes.
Thanks in advance -- any help greatly appreciated!
Use the Window.sessionStorage API for a single session.
Just set by getting the value from the sessionStorage if applicable (if not, set a default value):
var classes = sessionStorage.getItem(classes) || ['bg1','bg2','bg3','bg4'];
and set it when you need to:
sessionStorage.setItem("classes", classes);
The selector #backgrounds should only match one element. So it is unnecessary to use each:
// Define classes & background element.
var classes = ['bg1','bg2','bg3','bg4'],
$bg = document.getElementById('backgrounds');
// On first run:
$bg.className = sessionStorage.getItem('currentClass') || classes[0];
// On button click:
$('.button').mousedown(function () {
// (1) Get current class of background element,
// find its index in "classes" Array.
var currentClassIndex = classes.indexOf($bg.className);
// (2) Get new class from list.
var nextClass = classes[(currentClassIndex + 1)%classes.length];
// (3) Assign new class to background element.
$bg.className = nextClass;
// (4) Save new class in sessionStorage.
sessionStorage.setItem('currentClass', nextClass);
});
In this demo I've used getElementById to select the #backgrounds DOM element, rather than selecting it using jQuery. This is because jQuery element representations don't actually have a className property, and the ordinary JavaScript API is simpler in this case.
I've noticed that when I change the ID or name of a form element via JavaScript, the jQuery events that were tied to it are no longer there. I've tried this with Firefox 17 and IE 10. Is this by design? And if so, is there any way to prevent it?
UPDATE: Please check http://jsfiddle.net/qHH7P/2/ for an example.
I'm adding the button to remove the row via jQuery. When I remove the first row, I give the elements in the second row a new name and ID. Then the remove button for that remaining row doesn't fire the event anymore. I need to rename the elements because ASP.NET MVC expects a certain naming convention for the collection of objects when binding. That's why I need to rename them with a "0" instead of "1". I'm doing my rename with
var regexpattern = new RegExp("WorkspaceQuestionSets\\[.+\\]", "g");
$(this).html($(this).html().replace(regexpattern, "WorkspaceQuestionSets[" + index + "]"));
var regexpattern = new RegExp("WorkspaceQuestionSets_.+__", "g");
$(this).html($(this).html().replace(regexpattern, "WorkspaceQuestionSets_" + index + "__"));
I just realized that I'm not even renaming the buttons. So it makes even less sense that the event is gone. But if I comment out the code to rename the elements, the event remains.
You are rewriting the HTML. .html() returns a string, which you then modify and set again. The browser will parse that HTML string and create new DOM elements from it. In this process, your destroying DOM elements and consequently loose the event handlers you bound to them before.
I just realized that I'm not even renaming the buttons. So it makes even less sense that the event is gone.
You are destroying and recreating every single element that is inside this (each row I assume), no matter whether you modified its HTML representation or not.
You have to possibilites to solve this:
Use event delegation: Instead of binding the event handlers directly to the elements, bind them to an ancestor, which will always exist. Read more about event delegation in the .on *[docs] documentation, section Direct and delegated events.
Don't rewrite the HTML. Select the elements whose name attribute you want to modify and modify it. For example:
var name_exp1 = /WorkspaceQuestionSets\[.+\]/g;
var name_exp2 = /WorkspaceQuestionSets_.+__/g;
sourceEle.closest(".table").find(".row.set").each(function(index) {
// Edit the name attribute of all `select` and `input` elements
$(this).find('select, input').prop('name', function(i, name) {
if (name_exp1.test(name)) {
return name.replace(
name_exp1,
"WorkspaceQuestionSets[" + index + "]"
);
}
else if (name_exp2.test(name)) {
return name.replace(
name_exp1,
"WorkspaceQuestionSets_" + index + "__"
);
}
return name;
});
});
In JavaScript, forget the HTML and work with the DOM.
This does not happen to me with plain JavaScript in Chrome 24.
Here's what I typed in the JS console:
document.write("<button id='bb'/>")
//undefined
var bb=document.getElementById('bb')
//undefined
bb
//<button id="bb"></button>
bb.addEventListener('click',function(){alert('hi');});
//undefined
bb
//<button id="bb"></button>
bb.id
//"bb"
bb.id="qq"
//"qq"
bb
//<button id="qq"></button>
document.getElementById("bb")
//null
document.getElementById("qq")
//<button id="qq"></button>
And importantly clicking the button made the event trigger both before and after the assignment to bb.id.
Also in Firefox 17 I didn't quite see that happening with plain JavaScript. I was on the page: http://start.ubuntu.com/12.04/Google/?sourceid=hp
And in the console here's what I issued:
[19:29:41.261] var bb=document.getElementById('sbtn')
[19:29:41.267] undefined
[19:29:45.501] bb.addEventListener('click',function(){alert('hi')});
[19:29:45.507] undefined
[19:29:48.292] bb.id
[19:29:48.298] "sbtn"
[19:29:59.613] bb.id="sbttttttn"
[19:29:59.619] "sbttttttn"
The alert still occurred after the id was changed.
This must be specific to either IE, which I cannot try, or the way jQuery sets up events or handles ID changes (perhaps invisibly destroying the object?).
Say I have a list like this:
<ul id='dom_a'>
<li>foo</li>
</ul>
I know how to insert elements in the ul tag with:
Element.insert('dom_a', {bottom:"<li>bar</li>"});
Since the string I receive contains the dom id, I need to insert the inner HTML instead of the whole element. I need a function to do this:
insert_content('dom_a', {bottom:"<ul id='dom_a'><li>bar</li></ul>"});
And obtain:
<ul id='dom_a'>
<li>foo</li>
<li>bar</li>
</ul>
How should I do this with Prototype ?
Here is the solution I have come up with, can anyone make this better ?
Zena.insert_inner = function(dom, position, content) {
dom = $(dom);
position = position.toLowerCase();
content = Object.toHTML(content);
var elem = new Element('div');
elem.innerHTML = content; // strip scripts ?
elem = elem.down();
var insertions = {};
$A(elem.childElements()).each(function(e) {
insertions[position] = e;
dom.insert(insertions);
});
}
I think you could parse the code block in your variable, then ask it for its innerHTML, and then use insert to stick that at the bottom of the actual node in the DOM.
That might look like this:
var rep_struct = "<ul id='dom_a'><li>bar</li></ul>";
var dummy_node = new Element('div'); // So we can easily access the structure
dummy_node.update(rep_struct);
$('dom_a').insert({bottom: dummy_node.childNodes[0].innerHTML});
I think you can slim down the code a bit by simply appending the innerHTML of the first child of temporary element:
Zena.insert_inner = function(dom, position, content) {
var d = document.createElement('div');
d.innerHTML = content;
var insertions = {};
insertions[position] = d.firstChild.innerHTML;
Element.insert(dom, insertions);
}
Not too much of an improvement though, example here.
I've been looking into the Prototype Documentation and I found this: update function.
By the way you described it, you could use the update function in order to find the current bottom content and then update it (just like innerHTML) by adding the desired code plus the previous stored code.
You could use regular expression to strip the outer element.
Element.Methods.insert_content = function(element, insertions) {
var regex = /^<(\w+)[^>]*>(.*)<\/\1>/;
for (key in insertions) {
insertions[key] = regex.exec(insertions[key])[2];
}
Element.insert(element, insertions);
};
Element.addMethods();
$('dom_a').insert_content({bottom:"<ul id='dom_a'><li>bar</li></ul>"});
If you are using PrototypeJS, you might also want to add script.aculo.us to your project. Builder in script.aculo.us provides a nice way to build complex DOM structures like so:
var myList = Builder.node("ul", {
id: "dom_a"
},[
Builder.node("li", "foo"),
Builder.node("li", "bar"),
]);
After this, you can insert this object which should be rendered as HTML anywhere in the DOM with any insert/update functions (of PrototypeJS) or even standard JavaScript appendChild.
$("my_div").insert({After: myList});
Note that in PrototypeJS insert comes in 4 different modes: After, Before, Top and Bottom. If you use insert without specifying a "mode" as above, the default will be Bottom. That is, the new DOM code will be appended below existing contents of the container element as innerHTML. Top will do the same thing but add it on top of the existing contents. Before and After are also cool ways to append to the DOM. If you use these, the content will be added in the DOM structure before and after the container element, not inside as innerHTML.
With Builder however, there is one thing to keep in mind, .. okay two things really:
i. You cannot enter raw HTML in the object as content... This will fail:
Builder.node("ul", "<li>foo</li>");
ii. When you specify node attributes, keep in mind that you must use className to signify HTML attribute class (and possibly also htmlFor for for attribute... although for attribute seems to be deprecated in HTML5(?), but who does not want to use it for labels)
Builder.node("ul", {
id: "dom_a",
className: "classy_list"
});
I know you are scratching your head because of point i. > What, no raw HTML, dang!
Not to worry. If you still need to add content which might contain HTML inside a Builder created DOM, just do it in the second stage using the insert({Before/After/Top/Bottom: string}). But why'd you want to do it in the first place? It would be really good practice if you wrote an once for all function that generates all kinds of DOM elements rather than stitching in all sorts of strings. The former approach would be neat and elegant. This is something like the inline style versus class type of question. Good design should after all separate content from meta content, or formatting markup / markdown.
One last thing to keep handy in your toolbox is Protype's DOM traversal in case you want to dynamically insert and delete content like a HTML Houdini. Check out the Element next, up, down, previous methods. Besides the $$ is also kinda fun to use, particularly if you know CSS3 selectors.
Does anyone know how to tell if a cached jQuery object has gone stale, e.g. is no longer in the DOM? For example:
var $cached_elem = $('.the_button');
// .. and then later
$cached_elem.text('updating...');
I have recently encountered the situation where the $cached_elem is removed from the DOM due to some other event. So what I would like to do:
if ( $cache_elem.isStillInDOM() ){
// now do time consuming stuff with $cached_elem in DOM
}
Before anyone offers, I have already employed this, which is a fair analog for what I'm trying to do:
if ( $cached_elem.is(':visible') === true ){ ... }
However, this is not really the same thing and could fail in some cases.
So can anyone think of a simple way to check directly if a cached jQuery object is "stale"? I may be forced to write a plugin if not ...
if($elem.closest('body').length > 0) seems like it could do the trick.
$(function() {
var $button = $(".the_button");
alert (isStale($button));
$button.remove();
alert (isStale($button));
});
function isStale($elem)
{
return $elem.closest("body").length > 0;
};
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<span class="the_button">Hello World</span>
</div>
Edit: Updated in response to Yi Jiang's comment so that it will return correctly if its parent element is removed
Edit 2: Updated in response to lonesomeday's comment - changed parents() to 'closest()` for performance improvement
The native document.contains() method should be faster than jQuery to determine if a cached jQuery element exists in the DOM.
if (document.contains($cached_elem[0])) {
// Element is still in the DOM
}
If the selector hasn't changed, just reinitialize it:
var $cached_elem = $('.the_button');
// code that might remove some elements of $cached_elem
$cached_elem = $('.the_button');
If you want to avoid duplicating the selector string, use the selector property of the original jQuery object:
var $cached_elem = $('.the_button');
// code that might remove some elements of $cached_elem
$cached_elem = $($cached_elem.selector);
if ($cached_elem.length) {
// element still exists
}
In a slightly variant case, one can directly compare the native DOM element wrapped by the jQuery object $cached_elem.
$cached_elem[0] === $($cached_elem.selector)[0]
Besides checking if the $cached_elem is still in DOM, this can tell you if it's still the original DOM element, rather than a re-created one with same attributes after certain/partial page refresh.