Clone and restore "tooltiped" elements - javascript

I'm in trouble with restoring DOM structure that has elements passed to Bootstrap's .fn.tooltip() method.
To be specific: $('footer p') is passed to tooltip on document ready event, like this:
$(function(){
$('footer p').tooltip();
$('footer p').on('click', function(){
console.log('Just to test events')
});
})
I check it out, tooltip works, on click console message appears. Now I take backup of what am I about to delete and delete it, from console, by calling function:
function experiment_destroy() {
window.backup = $('footer').clone(true, true);
$('footer p').remove();
}
as expected, footer's p disappears.
Now I restore what is cloned and cached in window.backup variable with:
function experiment_restore(){
$('footer').empty();
$('footer').replaceWith(window.backup);
}
also called from console and here's what happens:
footer p element is back as it should be
footer p on click produces console message 'Just to test events'
message, so this event is restored along with element
no tooltip is restored.
Even if I re-call tooltip method in function experiment_restore I get nothing. Does anyone have some idea?
UPDATE:
I've made one more variation. Tried with different - totally minimal DOM environment with just p for tooltip and parent container element. Results are the same. Definitely there isn't just something in my complex DOM structure that was messing things up.
Here is very simple Fiddle.

You need to call the tooltip() method again. Optionally you should destroy the tooltip before cloning / removing the item for cleaning up the data.
Working Fiddle
$('footer p').tooltip();
$('#destroy').click(function(){
// optionally remove bindings
$('footer p').tooltip('destroy');
window.backup = $('footer').clone();
$('footer p').remove();
})
$('#restore').click(function(){
$('footer').replaceWith(window.backup);
// apply tooltip again
//window.backup.find("p").tooltip();
$('footer p').tooltip();
});

For the scenario you've shown in your question, I would use $().detach() to remove it from the DOM while at the same time keeping the event handlers and the data added to it with $().data() intact. In terms of the fiddle you've put in the question:
$('#destroy').click(function(){
var $footer_p = $('footer p');
window.backup = $footer_p;
$footer_p.detach();
})
$('#restore').click(function(){
var $footer = $('footer');
$footer.append(window.backup);
});
Here's an updated fiddle
What happens behind the scenes is that Bootstrap uses $().data() to add a JavaScript object of class Tooltip to your DOM element, and adds a bunch of event handlers. You need to preserve these.
If for some reason, you cannot use $().detach(), then you would have to recreate the tooltip by calling $().tooltip().
Why is $().clone(true, true) not working?
You call $().clone() with parameters to deep clone the DOM hierarchy and preserve the event handlers and the data set with $().data() so why is it not working? Is it not the case that the clone should have a reference to the Tooltip object created by Bootstrap?
Yes, the event handlers are preserved, and the clone does have a reference to the Tooltip object. However, this object it itself not cloned. More importantly, it is not adapted to refer to the new DOM node created by $().clone(). (So even if jQuery would clone it, it would still not work.) It does receive the event that would trigger the tooltip but Tooltip.prototype.show performs this check:
var inDom = $.contains(this.$element[0].ownerDocument.documentElement, this.$element[0])
if (e.isDefaultPrevented() || !inDom) return
The inDom variable will be true if this.$element is in the DOM. However, this refers to the original element for which the tooltip was created, not the clone. Since that element is no longer in the DOM, then inDom is false and the next line returns, so the tooltip is never shown.
For giggles, take a clone of a DOM element on which you created a Bootstrap tooltip, do not remove the original element but add the clone somewhere else on the page. Then trigger the tooltip on the clone. The tooltip will appear on the original element. :)
What I described above is the general way Bootstrap's jQuery plugins work: they use $().data() to add JavaScript objects to the elements on which they operate. There's also a Dropdown class for dropdowns, a Modal class for modals, etc.

As an added answer, I used JQuery's clone method, but I copied all event listeners like .clone(true, true). My issue was that the tooltips were the exact same from the old and cloned elements, but they were in different positions (so hovering over the new one would show a tooltip in the top left corner of my browser).
The easiest fix I could think of that should work for all Javascript, Bootstrap, JQuery forever is:
const target = document.getElementById("random-div-you-want-to-clone-to")
$("selecting").clone(true, true).appendTo(target);
// if you only want .innerHTML of $("selecting")
// you can do $("selecting").children()
const _tooltips = target.querySelectorAll("[data-toggle='tooltip']");
for (const x of _tooltips) {
const build = x.cloneNode(true);
$(build).tooltip();
x.parentNode.replaceNode(build, x);
}
So the .clone(true, true) will grab all the event listeners, including "mousedown" which is the listener for the tooltips. When you use native ECMAScript's cloneNode method, you aren't getting the event listeners, so you need to reset the tooltips.
It's not the most efficient, but I had been working on this for an hour trying to think of something... be my guest in find a more efficient way because this method isn't, but it works. (e.g. use forEach, simply using JQuery directly, etc.).
Edit: you can also use .children() to get the innards of $("selecting") (i.e. its children) when cloning rather than getting it AND its children.

For those who use Bootstrap 4, the method $.tooltip("destroy") was replaced by $.tooltip("dispose")

Related

Dojo - destroying tooltip widget when DOM node is destroyed

I use dijit/tooltips on a page that has a lot of domConstuct.destroy() and domConstuct.place() going on. So each time I remove some nodes from the DOM, I want to remove tooltips attached to those nodes. Currently the number of tooltip widgets is constantly growing on my page.
Is there a way to automatically remove a widget when corresponding DOM node is removed, or to check if existing tooltip widget's connect DOM node still exists?
You can attach a single Tooltip widget to multiple nodes at once, this may be the solution for you as you don't have to "manage" your tooltips anymore then. There's only one tooltip widget created for all tooltips, so you don't have to destroy it anymore.
The best way to achieve this is by using the selector property as described in the reference guide.
new Tooltip({
connectId: "myTable",
selector: "tr",
getContent: function(matchedNode){
return matchedNode.getAttribute("tooltipText");
}
});
If they don't have a common connectId and/or selector, then you can still use a single tooltip by adding the target to the same tooltip instance by using the addTarget() function.
To remove a target you can also use removeTarget() which accepts a DOM node (so you just pass the DOM node you want to remove).
If neither of these solutions is able to help you I'd like to know how you instantiate your tooltips, there are multiple ways to do that. For example by using connectId or by creating an ad hoc tooltip using the show() function.
I found a solution to my problem with a help of Dimitri's answer. I don't create separate Tooltip widget for each tooltip any more, now I put all the tooltips in one Tooltip using it's .addTarget() method. The second part of the solution is iterating through Tooltip's connectId property and checking if the DOM node still exists. I had to do it using Javascript native methods .contains() and .getElementById(), because Dojo's dom.byId() and query() gave me false positives. So, my code now looks like this:
// creating Tooltip
var tooltips = new Tooltip({
getContent: function(matchedNode){
return matchedNode.getAttribute("tooltiptext");
}
});
// adding tooltips
tooltips.addTarget(nameNode);
// deleting sufficient connects
for(var i = tooltips.connectId.length -1; i >= 0 ; i--){
if(!document.contains(tooltips.connectId[i]) && !document.getElementById(tooltips.connectId[i])){
tooltips.removeTarget(tooltips.connectId[i]);
}
}
The reason I had to use both .contains() and .getElementById() is that some of the nodes I attached tooltips to have ids and some don't, and Tooltip widget stores some of them as strings (id) and some as DOM nodes.

How to get the contents of an elemnt including the listeners

My current code is this
var existing = element.children().clone(true);
element.empty();
// Do some stuff with element
/* ... */
// Restore the previous content.
element.empty().append(existing);
This works well as long as element doesn't contains text directly under it. I mean that it is like that:
// Works great, because .children() will get tag2 and tag3, etc.
<element><tag2>...</tag2><tag3>...</tag3></element>
// Doesn't work, because .children() doesn't get the text node.
<element>some_text<tag2>...</tag2></element>
So I tried with .contents(), however I have some events attached to my initial element (that's why i used .clone(true) in the current code) and .contents() doesn't get the element with their event.
Do you know any other methode that I could use ? Or can you help me to write some code that will do what I want without being too complicated ?
Thanks a lot.
EDIT
I created a this: http://jsfiddle.net/Jv6rW/ to show you. I want that when you click on 'click' it should show the alert message.
Rather than trying to clone event listeners, why not use on() event delegation to do that for you?
http://api.jquery.com/on/
You should use .on() method to bind events. Then you can achieve what you want to do like this way
var existing = element.html();
element.empty();
// Do some stuff with element
/* ... */
// Restore the previous content.
element.append(existing);

Handle stuff after dom changes

I've got a page with some Javascript / jQuery stuff, for example:
(function()
{
$('.tip').tooltip();
$('.test').click(function()
{
alert('Clicked!')
});
}();
On the page I insert some HTML with jQuery so the DOM changes. For example, I insert a extra element with the class "tip" or "test". The just new inserted elements doesn't work because jQuery is working with the non-manipulated DOM and the just inserted elements aren't there. So I've searched around and came to this solution for the "click":
$('body').on('click','.click',function()
{
alert('Clicked!')
});
I don't understand why, but this way it's working with the manipulated DOM and the jQuery stuff works on the new inserted elements. So my first question is, why does this work and just the click() function not? And the second question, why do I have to point to the "body"?
Finally, my third question is, how get this done with the tooltip?
I know that there is so many information about this subject (previous the delegate() and live() function I've read) but I can't found a explanation about it. And I can't get my third question solved with the information I found.
I'm looking forward to your responses!
Extra question:
4) Is it recommended to point always to the "body" for this kind of situations? It's always there but for possible performance issues?
So my first question is, why does this work and just the click()
function not?
Because the event handler is now delegated to a parent element, so it remains even after replacing/manipulating child elements.
Ancient article on event delegation for your perusal - but the concepts remain the same:
http://www.sitepoint.com/javascript-event-delegation-is-easier-than-you-think/
And the second question, why do I have to point to the "body"
You don't, any suitable parent element will do. For example, any direct parent (a div wrapper, for instance) which does not get replaced.
Finally, my third question is, how get this done with the tooltip?
You need to re-initialize your tooltip plugin on the newly inserted elements. For example:
$.get("foo.html", function (html) {
$("#someDiv").html(html);
$("#someDiv").find(".tip").tooltip();
});
The click() event doesn't work when you manipulate the DOM because JQuery is not watching for DOM changes. When you bind the click() event it is selecting the elements that are on the page at that time. New ones are not in the list unless you explicitly bind the event.
Because you have pointed the click() event on the body. JQuery then checks to see if the target of the click matches any of the event handlers (like what you have created) match the element clicked. This way any new elements will get the event 'associated' with them.
Because the tooltip isn't an event that you can place on the body, you will need to re-initialize it when the element is created.
EDIT:
For your fourth question, is it depends. The advantage of binding to the body is that you don't accidentally bind an event to an element more than once. The disadvantage is that you are adding event handlers that need to be checked on each event and this can lead to performance issues.
As for your concerns about DRY, put the initialization of the tooltips into a function and call that when you add them. Trying to avoid having the same function call is a little overkill in this regard, IMO.
Events are bound to the specific object you are binding it to.
So something like $('.tip').tooltip() will perform the tooltip() functionality on $('.tip') which is actually just a collection of objects that satisfies the css selector .tip. The thing you should take note of is, that collection is not dynamic, it is basically a "database" query of the current page, and returns a resultset of HTML DOM objects wrapped by jQuery.
Therefore calling tooptip() on that collection will only perform the tooltip functionality on the objects within that collection, anything that was not in that collection when tooltip is called will not have the tooltip functionality. So adding an element that satisfies the .tip selector, after the tooltip() call, will not give it the tooltip functionality.
Now, $('body').on('click','.click', func) is actually binding the click event to the body tag (which should always exist :P), but what happens is it captures whether the click event has passed through an element your target css selector (.click in this case), so since the check is done dynamically, new elements will be captured.
This is a relatively short summary of what's going on... I hope it helped
UPDATE:
Best way for your tooltip thing is to bind tooltip after you have added elements, e.g.
$('#container').load('www.example.com/stuff', function() {
$('.tip', $(this)).tooltip();
});

Recursive jQuery function to amend select option values

I have a form that I am trying to alter with jQuery. Basically, my form has two elements and I need to change the value of the first option in each of them. However, there is an "add more" option that uses AJAX to dynamically generate another element that also needs changed. This add more button can be clicked an unlimited amount of times.
Right now I have this:
$(document).ready(function(){
$("#myname-0-field option:first").val("None");
$("#myname-1-field option:first").val("None");
});
This works fine, but once the "add more" button is clicked, I have more elements called "#myname-2-field", "#myname-3-field", "#myname-4-field" etc. These obviously aren't affected by adding another line into my jQuery as the document has already loaded when they are added.
So the real question is, can someone point me in the right direction of writing a function that can react when the new element is added and change it. If possible, I'm also looking for the function to be aware and look for "#myname-X-field option:first" for tidyness.
use live() function
Then using each function set value
From the jQuery API look live function
Maybe you could add class to your element, so that finding particular element would be easier and it would not add event to other similar elements.
In the example I have a Li with class
$('li.myClass').live('click', function() {
$(this).val(); // this is the getter for clicked value
$(this).val("some_value_here"); // this is the setter for clicked value
});
Now you can add more elements (that has myClass class) and it will have a click event.
Btw. if you know that all elements are inside some container (div for example) then you can write more efficient jQuery using delegate.
$('#container_id').delegate('li.myClass', 'click', function () {
});
This is more efficient because it looks your new elements only under "containter" not from the whole DOM structure.

JQuery click anywhere except certain divs and issues with dynamically added html

I want this webpage to highlight certain elements when you click on one of them, but if you click anywhere else on the page, then all of these elements should no longer be highlighted.
I accomplished this task by the following, and it works just fine except for one thing (described below):
$(document).click(function() {
// Do stuff when clicking anywhere but on elements of class suggestion_box
$(".suggestion_box").css('background-color', '#FFFFFF');
});
$(".suggestion_box").click(function() {
// means you clicked on an object belonging to class suggestion_box
return false;
});
// the code for handling onclicks for each element
function clickSuggestion() {
// remove all highlighting
$(".suggestion_box").css('background-color', '#FFFFFF');
// then highlight only a specific item
$("div[name=" + arguments[0] + "]").css('background-color', '#CCFFCC');
}
This way of enforcing the highlighting of elements works fine until I add more html to the page without having a new page load. This is done by .append() and .prepend()
What I suspected from debugging was that the page is not "aware" of the new elements that were added to the page dynamically. Despite the new, dynamically added elements having the appropriate class names/IDs/names/onclicks ect, they never get highlighted like the rest of the elements (which continue to work fine the entire time).
I was wondering if a possible reason for why my approach does not work for the dynamically added content is that the page is not able to recognize the elements that were not present during the pageload. And if this is a possibility, then is there a way to reconcile this without a pageload?
If this line of reasoning is wrong, then the code I have above is probably not enough to show what's wrong with my webpage. But I'm really just interested in whether or not this line of thought is a possibility.
Use .live to "Attach a handler to the event for all elements which match the current selector, now and in the future". Example:
$(".suggestion_box").live("click", function() {
// means you clicked on an object belonging to className
return false;
});
Also see .delegate, which is similar.
Since the .live() method handles events once they have propagated to the top of the document, it is not possible to stop propagation of live events. Similarly, events handled by .delegate() will always propagate to the element to which they are delegated; event handlers on any elements below it will already have been executed by the time the delegated event handler is called.
from the jQuery documentation =)
(only to explain better why #karim79 also suggested the delegate method ;P )

Categories