Restore Node/Element after it has been modified/removed from DOM - javascript

I am in need of a way to detect if any DOM Node/Element has been removed or modified and instantly restore that element to the state in which it was before.
I tried to "backup" the body node and set the body.innerHTML to its original state every time MutationObserver is fired after the first run but that crashes the browser.
Is there any fast way to restore elements that have been modified or removed?

This is all I can come with (a bit hacky, but it works). Click test or test #2 for removing nodes: http://codepen.io/zvona/pen/BowXaN?editors=001
HTML:
<div class='preserve'>
<h1>There can be anything inside this</h1>
<p>Some content</p>
</div>
<div class='preserve'>
<p>Some more content</p>
</div>
JS:
var preserved = document.querySelectorAll('.preserve');
var config = { attributes: true, childList: true, characterData: true };
var createFragment = function(elem, i) {
var frag = document.createElement('div');
var id = 'id-'+ new Date().getTime() +'-'+ i;
frag.setAttribute('id', id);
elem.parentNode.insertBefore(frag, elem);
elem.dataset.frag = id;
observer.observe(elem.parentNode, config);
}
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (!mutation.addedNodes.length && mutation.removedNodes.length) {
Array.prototype.forEach.call(mutation.removedNodes, function(elem) {
var frag = document.querySelector('#'+ elem.dataset.frag);
frag.parentNode.replaceChild(elem, frag);
createFragment(elem, frag.id.split('-')[2]);
});
}
});
});
Array.prototype.forEach.call(preserved, function(preserve, i) {
createFragment(preserve, i);
});
If you want to preserve all the nodes (aka document.querySelectorAll('*');), then I think it becomes very heavy from performance point of view.

The problem is to record the removed nodes.
In my case, I generate a xpath for every nodes in the document. When childList triggered, generate again.
So that I can know the removed node's xpath, and can use the xpath to restore the node.
mutation.removedNodes.map((node) => {
const xpath = node.xpath // which is generated each time `childList` triggered
})
Hope to help you.

Related

Javascript Typewriter Effect from element childNodes

I am trying to create a type writer effect that will get the nodes of an element and then display the values of those nodes sequentially at a given speed. If the node is a text node I want it to go in and sequentially display each character in that text.
HTML:
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<!-- item will be appened to this layout -->
<div id="log" class="sl__chat__layout">
</div>
<!-- chat item -->
<script type="text/template" id="chatlist_item">
<div data-from="{from}" data-id="{messageId}" id="messageID">
<div id="messageBox">
<span id="message">
{message}
</span>
</div>
</div>
</script>
Javascript:
// Please use event listeners to run functions.
document.addEventListener('onLoad', function(obj) {
// obj will be empty for chat widget
// this will fire only once when the widget loads
});
document.addEventListener('onEventReceived', function(obj) {
// obj will contain information about the event
e++
typeEffect(e);
});
var speed = 50;
var e = 1;
function typeEffect(inp) {
var o = inp;
document.getElementById("messageID").id= "messageID"+o;
document.getElementById("message").id= "message"+o;
var text = $("#message"+o).text();
$("#message"+o).text('');
var i = 0;
var timer = setInterval(function() {
if(i < text.length) {
$("#message"+o).append(text.charAt(i));
i++;
}
else{
clearInterval(timer);
};
}, speed);
}
Here is an example of an element with the id "message2". As you can see it contains some text, then a span containing an image and then some more text.
<span id="message2">
Hello
<span class="emote">
<img src="https://static-cdn.jtvnw.net/emoticons/v1/1251411/1.0">
</span>
There
</span>
In my code posted above I am able to create the typewriter effect of the text. However, using the above example, I can't figure out a way to type "Hello" then the span with the image and then "There".
I have tried to get the nodes like this:
var contents = document.getElementById("message"+o).childNodes;
When I log that to the console I get: NodeList(3) [text, span.emote, text]
From there however I am having trouble accessing the nodeValues. I keep getting errors thrown. I am not sure exactly what I am doing wrong. From there I am also not sure the proper way to empty the "message"+o element and then refill it with the information.
Hopefully that explains everything!
By using $.text(), you are getting your Element's textContent, and all its markup content is gone (actually all its children).
In order to retain this content, you need to store the DOM nodes instead of just their textContent.
From there, you will have to detach the DOM tree and walk it while appending every Element, iterating slowly over each TextNode's textContent.
However, doing so is not that easy. Indeed, the fact that we will re-append the DOM nodes inside the document means that the detached DOM tree
we were walking will get broken.
To circumvent that, we thus need to create a copy of the detached DOM tree, that we will keep intact, so we can continue walking it just like if it were the original one.
And in order to know where to place our elements, we need to store each original node as a property of the cloned one.
To do so, we'll create two TreeWalkers, one for the original nodes, and one for the cloned version. By walking both at the same time, we can set our clones' .original property easily.
We then just have to go back to the root of our clones TreeWalker and start again walking it, this time being able to append the correct node to its original parentNode.
async function typeWrite(root, freq) {
// grab our element's content
const content = [...root.childNodes];
// move it to a documentFragment
const originals = document.createDocumentFragment();
originals.append.apply(originals, content);
// clone this documentFragment so can keep a clean version of the DOM tree
const clones = originals.cloneNode(true);
// every clone will have an `original` node
// clones documentFragment's one is the root Element, still in doc
clones.original = root;
// make two TreeWalkers
const originals_walker = document.createTreeWalker(originals, NodeFilter.SHOW_ALL, null);
const clones_walker = document.createTreeWalker(clones, NodeFilter.SHOW_ALL, null);
while(originals_walker.nextNode() && clones_walker.nextNode()) {
// link each original node to its clone
clones_walker.currentNode.original =
originals_walker.currentNode
}
while(clones_walker.parentNode()) {
// go back to root
}
// walk down only our clones (will stay untouched now)
while(clones_walker.nextNode()) {
const clone = clones_walker.currentNode;
const original = clone.original;
// retrieve the original parentNode (which is already in doc)
clone.parentNode.original
.append(original); // and append the original version of our currentNode
if(clone.nodeType === 3) { // TextNode
const originalText = original.textContent;
// we use a trimmed version to avoid all non visible characters
const txt = originalText.trim().replace(/\n/g, '');
original.textContent = ''; // in doc => empty for now
let i = 0;
while(i < txt.length) {
await wait(freq); // TypeWriting effect...
original.textContent += txt[i++];
}
// restore original textContent (invisible to user)
original.textContent = originalText;
}
}
}
typeWrite(message2, 200)
.catch(console.error);
function wait(time) {
return new Promise(res => setTimeout(res, time));
}
<span id="message2">
Hello
<span class="emote">
<img src="https://static-cdn.jtvnw.net/emoticons/v1/1251411/1.0">
</span>
There
</span>

if div element is present on page, hide other element

I have a dynamic web form, I'd like to detect if an element is visible; and if it is hide another element of mine. I have the below attempt, but this is not working stabilily; i.e. the element isn't always hiding. A better technique out there?
setInterval( myValidateFunction2, 1000);
function myValidateFunction2 () {
var inElgbl = document.getElementById('field_52_116');
if (typeof(inElgbl) != 'undefined' && inElgbl != null)
{
document.getElementById('field_52_24').style.display = "none";
}
};
It is by default display: none; but may become display: block; if it becomes display: block; I would like to display: none; my other div elem.
Consider an element to be visible if it consumes space in the document. For most purposes, this is exactly what you want.
Try this:
setInterval( myValidateFunction2, 1000);
function myValidateFunction2 () {
var inElgbl = document.getElementById('field_52_116');
if (inElgbl.offsetWidth <= 0 && inElgbl.offsetHeight <= 0)
{
document.getElementById('field_52_24').style.display = "none";
}
};
Probably the most stable way to do this would be using a DOM Mutation Observer and setting it up to watch the document or section of the document that could get the element in question.
In the example below, I'll set up an observer to watch an initially empty div and after I've set it up, I'll dynamically add the element we're supposed to be on the lookout for. You'll see that the element does not wind up getting displayed.
// Select the node that will be observed for mutations
var targetNode = document.getElementById('parent');
// Options for the observer (which mutations to observe)
var config = { attributes: true, childList: true, subtree: true };
// Callback function to execute when mutations are observed
function callback(mutationsList, observer) {
// We only need to test to see if node is truthy, which it will be if it exists
if (document.getElementById('field_52_116')){
document.getElementById('field_52_24').style.display = "none";
console.log("Node detected! Removing....");
}
};
// Create an observer instance linked to the callback function
var observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
// So, we'll add the node to test
let newNode = document.createElement("div");
newNode.textContent = "I'm here so the other node should be hidden!";
newNode.id = "field_52_116";
targetNode.appendChild(newNode);
// Later, you can stop observing if needed
// observer.disconnect();
<div id="parent"></div>
<div id='field_52_24'>ELement to hide</div>

Changing element's HTML before it's inserted in the DOM

Is it possible to change an element's inner HTML before it is inserted in the DOM?
I already tried doing this with MutationObserver but the problem is that you can see the element's HTML visually changing, is there a way to do this before DOM insertion altogether?
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
mutation.addedNodes[0].innerHTML = "....";
});
});
// Notify me of everything!
var observerConfig = {
attributes: true,
childList: true,
characterData: true
};
var targetNode = document.querySelector("ul#myElement");
observer.observe(targetNode, observerConfig);
You can try having the whole body as CSS display:none, change whatever you want and then bring it back to display:block

Grabbing innerHTML into a variable

I am trying to grab the text in a p tag into a variable. The p tag is called #musCardImageTitle and is a description of the background image on that page. However, it's not working. I dont know why
var desc = document.getElementById("musCardImageTitle").innerHTML;
document.getElementById("sb_form_q").placeholder = desc
//the second line is putting that text into a searchbox as placeholder text
This is for the Bing homepage if it helps. I've included an image of what I'm trying to do if it helps
http://i.stack.imgur.com/bJeU8.jpg
I think this should be easy but for some reason I cant get it to work...
Try this one
// select the target node
var target = document.querySelector('#musCardImageTitle');
// create an observer instance
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
//document.getElementById("sb_form_q").value = mutation.target.textContent ;
document.getElementById("sb_form_q").placeholder = mutation.target.textContent;
observer.disconnect();
});
});
// configuration of the observer:
var config = { attributes: true, childList: true, characterData: true };
// pass in the target node, as well as the observer options
observer.observe(target, config);
This example is on MutationObserver doc
Try this
var desc = document.getElementById("musCardImageTitle").value;

Pure javascript method to wrap content in a div

I want to wrap all the nodes within the #slidesContainer div with JavaScript. I know it is easily done in jQuery, but I am interested in knowing how to do it with pure JS.
Here is the code:
<div id="slidesContainer">
<div class="slide">slide 1</div>
<div class="slide">slide 2</div>
<div class="slide">slide 3</div>
<div class="slide">slide 4</div>
</div>
I want to wrap the divs with a class of "slide" collectively within another div with id="slideInner".
If your "slide"s are always in slidesContainer you could do this
org_html = document.getElementById("slidesContainer").innerHTML;
new_html = "<div id='slidesInner'>" + org_html + "</div>";
document.getElementById("slidesContainer").innerHTML = new_html;
Like BosWorth99, I also like to manipulate the dom elements directly, this helps maintain all of the node's attributes. However, I wanted to maintain the position of the element in the dom and not just append the end incase there were siblings. Here is what I did.
var wrap = function (toWrap, wrapper) {
wrapper = wrapper || document.createElement('div');
toWrap.parentNode.appendChild(wrapper);
return wrapper.appendChild(toWrap);
};
How to "wrap content" and "preserve bound events"?
// element that will be wrapped
var el = document.querySelector('div.wrap_me');
// create wrapper container
var wrapper = document.createElement('div');
// insert wrapper before el in the DOM tree
el.parentNode.insertBefore(wrapper, el);
// move el into wrapper
wrapper.appendChild(el);
or
function wrap(el, wrapper) {
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
}
// example: wrapping an anchor with class "wrap_me" into a new div element
wrap(document.querySelector('div.wrap_me'), document.createElement('div'));
ref
https://plainjs.com/javascript/manipulation/wrap-an-html-structure-around-an-element-28
If you patch up document.getElementsByClassName for IE, you can do something like:
var addedToDocument = false;
var wrapper = document.createElement("div");
wrapper.id = "slideInner";
var nodesToWrap = document.getElementsByClassName("slide");
for (var index = 0; index < nodesToWrap.length; index++) {
var node = nodesToWrap[index];
if (! addedToDocument) {
node.parentNode.insertBefore(wrapper, node);
addedToDocument = true;
}
node.parentNode.removeChild(node);
wrapper.appendChild(node);
}
Example: http://jsfiddle.net/GkEVm/2/
A general good tip for trying to do something you'd normally do with jQuery, without jQuery, is to look at the jQuery source. What do they do? Well, they grab all the children, append them to a a new node, then append that node inside the parent.
Here's a simple little method to do precisely that:
const wrapAll = (target, wrapper = document.createElement('div')) => {
;[ ...target.childNodes ].forEach(child => wrapper.appendChild(child))
target.appendChild(wrapper)
return wrapper
}
And here's how you use it:
// wraps everything in a div named 'wrapper'
const wrapper = wrapAll(document.body)
// wraps all the children of #some-list in a new ul tag
const newList = wrapAll(document.getElementById('some-list'), document.createElement('ul'))
I like to manipulate dom elements directly - createElement, appendChild, removeChild etc. as opposed to the injection of strings as element.innerHTML. That strategy does work, but I think the native browser methods are more direct. Additionally, they returns a new node's value, saving you from another unnecessary getElementById call.
This is really simple, and would need to be attached to some type of event to make any use of.
wrap();
function wrap() {
var newDiv = document.createElement('div');
newDiv.setAttribute("id", "slideInner");
document.getElementById('wrapper').appendChild(newDiv);
newDiv.appendChild(document.getElementById('slides'));
}
jsFiddle
Maybe that helps your understanding of this issue with vanilla js.
To simply wrap a div without the need of the parent:
<div id="original">ORIGINAL</div>
<script>
document.getElementById('original').outerHTML
=
'<div id="wrap">'+
document.getElementById('original').outerHTML
+'</div>'
</script>
Working Example: https://jsfiddle.net/0v5eLo29/
More Practical Way:
const origEle = document.getElementById('original');
origEle.outerHTML = '<div id="wrap">' + origEle.outerHTML + '</div>';
Or by using only nodes:
let original = document.getElementById('original');
let wrapper = document.createElement('div');
wrapper.classList.add('wrapper');
wrapper.append(original.cloneNode(true));
original.replaceWith(wrapper);
Working Example: https://jsfiddle.net/wfhqak2t/
A simple way to do this would be:
let el = document.getElementById('slidesContainer');
el.innerHTML = `<div id='slideInner'>${el.innerHTML}</div>`;
Note - below answers the title of the question but is not specific to the OP's requirements (which are over a decade old)
Using the range API is making wrapping easy, by creating a Range which selects only the node wished to be wrapped, and then use the surroundContents API to wrap it.
Below code wraps the first (text) node with a <mark> element and the last node with a <u> element:
const wrapNode = (nodeToWrap, wrapWith) => {
const range = document.createRange();
range.selectNode(nodeToWrap);
range.surroundContents(wrapWith);
}
wrapNode(document.querySelector('p').firstChild, document.createElement('mark'))
wrapNode(document.querySelector('p').lastChild, document.createElement('u'))
<p>
first node
<span>second node</span>
third node
</p>
From what I understand #Michal 's answer is vulnerable to XXS attacks (using innerHTML is a security vulnerability) Here is another link on this.
There are many ways to do this, one that I found and liked is:
function wrap_single(el, wrapper) {
el.parentNode.insertBefore(wrapper, el);
wrapper.appendChild(el);
}
let divWrapper;
let elementToWrap;
elementToWrap = document.querySelector('selector');
// wrapping the event form in a row
divWrapper = document.createElement('div');
divWrapper.className = 'row';
wrap_single(elementToWrap, divWrapper);
This works well. However for me, I sometimes want to just wrap parts of an element. So I modified the function to this:
function wrap_some_children(el, wrapper, counter) {
el.parentNode.insertBefore(wrapper, el);
if ( ! counter ) {
counter = el.childNodes.length;
}
for(i = 0; i < counter; i++) {
wrapper.appendChild( el.childNodes[0] );
}
}
// wrapping parts of the event form into columns
let divCol1;
let divCol2;
// the elements to wrap
elementToWrap = document.querySelector('selector');
// creating elements to wrap with
divCol1 = document.createElement('div');
divCol1.className = 'col-sm-6';
divCol2 = document.createElement('div');
divCol2.className = 'col-sm-6';
// for the first column
wrap_some_children(elementToWrap, divCol1, 13); // only wraps the first 13 child nodes
// for the second column
wrap_some_children(elementToWrap, divCol2);
I hope this helps.
wrapInner multiple tag content
function wilWrapInner(el, wrapInner) {
var _el = [].slice.call(el.children);
var fragment = document.createDocumentFragment();
el.insertAdjacentHTML('afterbegin', wrapInner);
var _wrap = el.children[0];
for (var i = 0, len = _el.length; i < len; i++) {
fragment.appendChild(_el[i]);
}
_wrap.appendChild(fragment);
}
Link Demo Jsbin

Categories