The following code basically shows/hides paragraph tags, I'm having to re-declare the paras variable. Is this because I'm dynamically injecting the button into the DOM, or is it to do with scope? How could I better construct this markup?
// vars
var revealContainer = document.querySelector('.reveal-more');
var paras = revealContainer.querySelectorAll('p');
var status = true;
// return
if (paras && paras.length <= 3) return;
// generate show more link
revealContainer.innerHTML += '<button class="button--text reveal-more__btn">Read more</button>';
var revealBtn = revealContainer.querySelector('.reveal-more__btn');
// click event
revealBtn.addEventListener('click', function () {
var paras = revealContainer.querySelectorAll('p');
// toggle show/hide class
for (var i = 0; i < paras.length; i++) {
var p = paras[i];
p.classList.toggle('is-shown');
}
// check status
if (status) {
this.textContent = 'Read less';
status = false;
} else {
this.textContent = 'Read more';
status = true;
}
});
You can use the live HTMLCollection returned by .getElementsByTagName() instead of the static NodeList returned by .querySelectorAll()
The getElementsByTagName method of Document interface returns an HTMLCollection of elements with the given tag name. The complete document is searched, including the root node. The returned HTMLCollection is live, meaning that it updates itself automatically to stay in sync with the DOM tree without having to call document.getElementsByTagName() again.
var paragraphs = document.getElementById("container").getElementsByTagName("p");
console.log(paragraphs.length);
setInterval(function() {
document.getElementById("container").insertAdjacentHTML("beforeend", "<p>p</p>");
}, 1000);
setInterval(function() {
console.log(paragraphs.length);
}, 2000);
<div id="container"></div>
Below is a really simple Snippet that demonstrates delegated events in pure Javascript, instead of using jQuery.
Here you can see I've attached the eventListener to the div with id elements, this will then listen for click events under this, a simple matches is used just in case you have other elements your not interested in..
document.querySelector("#elements").addEventListener("click", (e) => {
if (!e.target.matches('.element')) return
console.log(`Clicked ${e.target.innerText}`);
});
.element {
border: 1px solid black;
margin: 5px;
}
<div id="elements">
<div class="element">1</div>
<div class="element">2</div>
<div class="element">3</div>
<div>Clicking this does nothing.</div>
</div>
I've attached a MutationObserver to a DOM and am monitoring for changes.
I get the notification and receive a MutationRecord object containing a description of what was changed.
Is there a supported/standard/easy way to apply the changes in MutationRecord again? In other words, use the MutationRecord object to "replay" changes to the DOM?
Any help appreciated!
I couldn't help myself, after noticing that this was asked 6 years ago in 2016, so I made a "DomRecorder Lite". This program records three types on DOM mutation events that can happen inside a target element, in this case some div. You can play around with the tool and try to:
Add elements
Change text of a selected element
Remove selected element
Replay selected action
Replay all actions
There might be a bug or two, but I think that the Proof of Concept is there either way.
I also made a small video demonstrating how the program works (when I press edit-button, a prompt asking for a text appears, but that isn't recorded): https://www.veed.io/view/7325e363-ac5c-4c9b-8138-cd990b253372
We use two custom classes called MutationEvent and DOMRecorder:
MutationEvent
MutationEvent is responsible for storing information about an observer mutation event, such as what the event was (TEXTCHANGED | INSERT | REMOVE), DOM element data related to the event and just generic data, in case we need to attach some extra data to an event.
MutationEvent is also responsible for replaying itself depending on what the event type is. I made this PoC to support three types of events as written above.
The INSERT case is somewhat interesting because of the user's ability to replay one event (doesn't clear the recorded wrapper element) or to replay all events (clears the recorded wrapper element first).
When replaying only one INSERT event, we can't directly insert the element saved to the MutationEvent, because it would have the same id in DOM as the element from which this event originates in the first place, so we create this sort of "actor element", which is a clone of the original element (and its child content), but with a different id.
If we are replaying all the events instead, the recorded wrapper element is cleared first, which means we don't have to worry about this issue.
case 'INSERT':
elem = document.querySelector(`#${$targetid}`);
if(elem) {
let actorElement = null;
if(clean) {
actorElement = this.element.cloneNode(true);
actorElement.id = 'd-' + Math.random().toString(16).slice(2, 8);
}
else {
actorElement = this.element;
}
elem.append(actorElement);
success = true;
}
break;
DOMRecorder
DOMRecorder is kind of the meat and potatoes of everything. Its main resposibility is to use MutationObserver to observe changes in the DOM of the recorded element and to create MutationEvents from those changes. Other resposibilities include replaying a single or every MutationEvent it has stored. In this PoC, the DOMRecorder is also responsible for updating some of the select inputs on the page, which is probably not 100% legitimate way to separate concerns, but that's not relevant here.
Again, this is just a proof of concept how replaying of MutationEvents could be implemented, so there are only a limited number of supported events. This PoC also doesn't take a position regarding deep DOM subtree modifications, but after coding this, I am not too scared to think of it. see edit
Is there a supported/standard/easy way
Probably not. Can't think of many uses for such standard at least. There are some libraries out there that record user's interactions on a webpage, but those don't need to record/replay actual DOM modifications, only the interactions that could result in DOM modifications.
Should also keep in mind that, in general, it's a lot easier to implement a system that watches for changes and emits some kind of change events telling you that "this just changed", than it is to implement a system that is able to re-create those changes. The former doesn't really need to care about anything other than changes happening anywhere in any form, but with the latter, there are a lot of considerations when it comes to re-creation.
To list some:
Should you re-create something that should be unique? Is it really a replay, if the replayed changes are not exactly the same as the recorded ones?
What if the "replay" doesn't start from the same state as the "recording" started?
Should you also record what the replay does?
How do you handle local relations between events?
How do you handle global relations between events and the DOM?
If I make an element, change it's text, remove the element and want to replay the text change event again, what should happen?
What should happen when a parent of deeply nested element gets removed or changed in some way that it can no longer hold the nested element?
If we decided that deletion of a parent of deeply nested element should remove the whole element branch, should that be an event of itself, or perhaps even multiple removal events for each obsolete child on the branch?
How to keep track of CSS changes, after trying this out, it is not as simple as it seems
EDIT
So I added events for subtree insertion and removal. This means that one can now add child elements inside other elements. Functions that worked for the top-level elements should work for any child element as well, or child-of-a-child, or child-of-a-child-of-a-child, ...
This was a bit tricky to implement and the biggest issue here was keeping tracking of parent element ids. See, when an element is created, some id will be generated for it. To insert an element inside of another existing element, we obviously need to know its id. Now this seems like a no problem at first, the user selected parent element has some id, so we just get it and insert the element inside.
Everything seems to work, but we run into a problem when replaying the events and the subtree insertion just won't do anything. The reason for this was that when the program replays the events, specifically the SUBINSERT event, it uses a wrong id for the parent element. This is because the replay function clears the recorded wrapper before starting the replay and therefore all the elements created during replay will get new ids, so the SUBINSERT will not know what the parent element id is during the replay.
To fix this, I added a second, "static internal id" for each event:
constructor(recorderInstance, event, element, data)
{
this.static_replay_id = getId();
...
}
This id will not change from recording to replay, so when we want to insert an element as a child element to some parent element, we do the following event-wise:
Get the id of the mutation.target, which is the id of the parent element
Assume that since this parent element exists, there must also exist an event for it already
Get the static_replay_id of the event by the mutation.target id
Add the static_replay_id as subtree_target for this SUBINSERT event
getStaticIdForMutationTarget:
getStaticIdForMutationTarget(targetid)
{
return this.events.find((evt) => evt.element.id === targetid).static_replay_id;
}
And when we replay the SUBINSERT event, we get the current DOM id of the parent element by the saved static_replay_id:
elem = document.querySelector(`#${$targetid}div#${this.recorderInstance.getEventTargetByStaticReplayId(this.data.subtree_target)}`);
getEventTargetByStaticReplayId:
getEventTargetByStaticReplayId(replay_id)
{
let targetid = null;
for(let i = 0; i < this.events.length; i++) {
let evt = this.events[i];
if(!"static_replay_id" in evt.data)
continue;
if(evt.static_replay_id === replay_id) {
targetid = evt.element.id;
break;
}
}
return targetid;
}
EDIT 2
Added support for applying and replaying inline CSS style events and changed long event name strings, such as "SUBINSERT" or "SUBREMOVE" -> "SUBINS", "SUBREM", so that the UI works better.
Also fixed a couple of bugs regarding mutation event targets and subtree elements. Had to also re-implement how changing element text node text works. The most interesting takeaway here is that for some reason, if we try to change element's textnode's text with elem.childNodes[0].value = text, MutationObserver will not catch that - I guess it doesn't count as any kind of "change in the DOM tree".
// Can't use elem.textContent / innerText / innerHTML...
// might accidentally overwrite element's subtree
// Can't do it like this, because MutationObserver can't see this (why??)
// elem.childNodes[0].value = text;
// This is the way
elem.replaceChild(document.createTextNode(text), elem.childNodes[0]);
At this point I've noticed that a "cleaning" process is required when replaying events:
// Used to clean "dirtied" elements before replaying.
// Consider the event flow:
// 1. Create element X
// 2. Change X CSS or subinsert to X
// 3. Remove X
// 4. Replay
// Without cleanup, during replay at step 1, the created element
// will already have "later" CSS changes and subinserts applied to it ahead of time
// because of ""the way we do things"".
// This fixes that issue
__clean()
{
// OLD CLEANUP METHOD
/*while (this.element.lastElementChild) {
this.element.lastElementChild.style = "";
this.element.removeChild(this.element.lastElementChild);
}
this.element.style = "";
this.element.innerText = this.element.id;*/
// NEW CLEANUP METHOD (don't have to care what changed)
// Reset event's element
this.element = this.originalElement.cloneNode(true);
// Must remember to set the original id back
// Or else it will be the clone's id
this.element.id = this.originalId;
}
As explained in the code snippet comments, there are certain "event flows", which could lead to wrong replay results, such as some events/changes being applied to elements ahead of time so to speak.
To fix this, each event performs an internal "cleaning process" before actually replaying itself. This might not be needed, were the implementation a bit different, but for now it has to be done.
When it comes to the question "What exactly needs to be cleaned?", I am honestly not sure. As far as I know, we seem to have to clean everything that any event could possibly modify on an element.
OLD WAY
Currently that means:
Clearing element DOM subtree of children
Resetting element inline styles
Resetting element's innerText to what it originally is (it's id)
NEW WAY
I figured we can get around caring what needs to be reset in the cleaning process by just creating a clone of each even't original related element and then replacing the event's element with the original unchanged clone before replaying:
class MutationEvent
{
constructor(recorderInstance, event, element, data)
{
this.originalId = this.element.id;
this.originalElement = this.element.cloneNode(true);
// Important to change the id to "hide" the clone
this.originalElement.id = getId("o-");
...
}
}
Anyways, here's the code:
const $recordable = document.querySelector('#recordable');
const $eventlist = document.querySelector('#event-list');
const $elementlist = document.querySelector('#element-list');
function getId(prefix = "d-") {
return prefix + Math.random().toString(16).slice(2, 8);
}
// MutationEvent class
class MutationEvent
{
constructor(recorderInstance, event, element, data)
{
this.recorderInstance = recorderInstance;
this.event = event;
this.element = element.cloneNode(true);
// For cleanup
this.originalId = this.element.id;
this.originalElement = this.element.cloneNode(true);
// Important to change the id to "hide" the clone
this.originalElement.id = getId("o-");
this.data = data || { };
this.static_replay_id = getId("r-");
}
getStaticId()
{
return this.static_replay_id;
}
// Used to clean "dirtied" elements before replaying.
// Consider the event flow:
// 1. Create element X
// 2. Change X CSS or subinsert to X
// 3. Remove X
// 4. Replay
// Without cleanup, during replay at step 1, the created element
// will already have "later" CSS changes and subinserts applied to it ahead of time
// because of ""the way we do things"".
// This fixes that issue
__clean()
{
// OLD CLEANUP METHOD
/*while (this.element.lastElementChild) {
this.element.lastElementChild.style = "";
this.element.removeChild(this.element.lastElementChild);
}
this.element.style = "";
this.element.innerText = this.element.id;*/
// NEW CLEANUP METHOD (don't have to care what changed)
// Reset event's element
this.element = this.originalElement.cloneNode(true);
// Must remember to set the original id back
// Or else it will be the clone's id
this.element.id = this.originalId;
}
replay($targetid, useActor = false)
{
let elem = null;
let success = false;
this.__clean();
switch(this.event)
{
case 'TEXT':
elem = document.querySelector(`#${$targetid} div#${this.element.id}`);
if(elem) {
elem.replaceChild(document.createTextNode(this.data.text), elem.childNodes[0]);
success = true;
}
break;
case 'INSERT':
elem = document.querySelector(`#${$targetid}`);
if(elem) {
let actorElement = null;
if(useActor) {
actorElement = this.element.cloneNode(true);
actorElement.id = getId();
}
else {
actorElement = this.element;
}
elem.append(actorElement);
success = true;
}
break;
case 'SUBINS':
elem = document.querySelector(
`#${$targetid} div#${
this.recorderInstance.getEventTargetByStaticReplayId(this.data.subtree_target)
}`);
if(elem) {
let actorElement = null;
if(useActor) {
actorElement = this.element.cloneNode(true);
actorElement.id = getId();
}
else {
actorElement = this.element;
}
elem.append(actorElement);
success = true;
}
break;
case 'CSS':
elem = document.querySelector(`#${$targetid} div#${this.element.id}`);
if(elem && typeof this.data.css_rules !== 'undefined') {
this.data.css_rules.forEach((r) => {
elem.style[r.rule] = r.value;
});
success = true;
}
break;
case 'REMOVE':
elem = document.querySelector(`#${$targetid} div#${this.element.id}`);
if(elem) {
elem.remove();
success = true;
}
break;
case 'SUBREM':
elem = document.querySelector(
`#${$targetid} div#${
this.recorderInstance.getEventTargetByStaticReplayId(this.data.subtree_target)
}`);
if(elem) {
elem.remove();
success = true;
}
break;
default:
break;
}
return success;
}
toString()
{
return `${this.event}: ${this.element.id}`;
}
}
// Dom recorder / MutationObserver stuff
class DOMRecorder
{
constructor(targetNode, config)
{
this.recording = false;
this.replaying = false;
this.targetNode = targetNode;
this.config = config;
this.observer = null;
this.elementIds = [];
this.events = [];
}
start()
{
if(this.observer === null)
this.observer = new MutationObserver(this.recordEvent.bind(this));
this.observer.observe(this.targetNode, this.config);
this.recording = true;
}
getStaticIdForMutationTarget(targetid)
{
return this.events.find((evt) => evt.element.id === targetid).static_replay_id;
}
getEventByTargetId(targetid)
{
return this.events.find((evt) => evt.element.id === targetid);
}
getEventTargetByStaticReplayId(replay_id)
{
let targetid = null;
for(let i = 0; i < this.events.length; i++) {
let evt = this.events[i];
if(!"static_replay_id" in evt.data)
continue;
if(evt.static_replay_id === replay_id) {
targetid = evt.element.id;
break;
}
}
return targetid;
}
stop()
{
this.observer.disconnect();
this.recording = false;
}
update()
{
this.updateElementList();
this.updateEventList();
}
updateElementList()
{
let options = this.elementIds.map((id) => {
let option = document.createElement("option");
option.value = id;
option.innerText = id;
return option;
});
$elementlist.replaceChildren(...options);
}
updateEventList()
{
let options = this.events.map((e) => {
let option = document.createElement("option");
// This line breaks syntax-highlighting, if `` is used???
option.value = e.event + ";" + e.element.id;
option.innerText = e.toString();
return option;
});
$eventlist.replaceChildren(...options);
}
recordEvent(mutationList, observer)
{
for(const mutation of mutationList) {
// Element text changed event
if(mutation.type === 'childList' &&
mutation.addedNodes.length > 0 &&
mutation.addedNodes[0].nodeName === '#text') {
this.events.push(new MutationEvent(
this,
"TEXT",
mutation.addedNodes[0].parentElement,
{ text: mutation.addedNodes[0].nodeValue }
));
}
// Element added event
else if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
// Element added directly into the recorded element
if(mutation.target.id === this.targetNode.id) {
this.events.push(new MutationEvent(this, "INSERT", mutation.addedNodes[0]));
this.elementIds.push(mutation.addedNodes[0].id);
}
// Element added to a subtree of some element in the recorded element
else {
this.events.push(new MutationEvent(
this,
"SUBINS",
mutation.addedNodes[0],
{ subtree_target: this.getStaticIdForMutationTarget(mutation.target.id) }
));
this.elementIds.push(mutation.addedNodes[0].id);
}
}
// Element removed event
else if (mutation.type === 'childList' && mutation.removedNodes.length > 0) {
// Element removed directly from the recorded element
if(mutation.target.id === this.targetNode.id) {
this.events.push(new MutationEvent(this, "REMOVE", mutation.removedNodes[0]));
}
// Element removed from a subtree of some element in the recorded element
else {
this.events.push(new MutationEvent(
this,
"SUBREM",
mutation.removedNodes[0],
{ subtree_target: this.getStaticIdForMutationTarget(mutation.removedNodes[0].id) }
));
}
this.elementIds.splice(this.elementIds.indexOf(mutation.removedNodes[0].id), 1);
// Clean up element list, if we remove a parent element which has child elements
let childNodes = mutation.removedNodes[0].getElementsByTagName('*');
Array.from(childNodes).forEach((n) => {
this.elementIds.splice(this.elementIds.indexOf(n.id), 1);
});
}
// Element inline CSS changed
else if(mutation.type === 'attributes' && mutation.attributeName === "style") {
// Perform some magic to find specifically only the changed
// inline style names of the element, as well as their values
let addedStyles = Object.keys(mutation.target.style).filter((k) => {
return Number.isInteger(parseInt(k));
}).map((styleKey) => {
// Change rule names like "background-color" -> "backgroundColor"
let rule = mutation.target.style[styleKey]
.replace(/-(.)/g, (m,p) => p.toUpperCase());
let value = mutation.target.style[rule];
return { rule, value }
});
this.events.push(new MutationEvent(
this,
"CSS",
mutation.target,
{ css_rules: addedStyles }
));
}
}
this.update();
}
replayEvent(event, id, $targetid)
{
let replayEvent = this.events
.find((evt) => evt.event === event && evt.element.id === id);
if(!replayEvent) {
console.log(`Could not find event: "${event}", ID: "${id}"`);
return;
}
replayEvent.replay($targetid, true);
}
replayEvents(speed, $targetid)
{
if(this.replaying)
return;
this.replaying = true;
if(this.recording)
this.stop();
$recordable.innerHTML = '';
let i = 0;
let replayInterval = setInterval(() => {
if(i < this.events.length) {
this.events[i].replay($targetid);
i++;
}
else {
clearInterval(replayInterval);
this.replaying = false;
this.start();
}
}, speed);
}
}
// Example controls events
function addElement() {
let id = getId();
let div = document.createElement("div");
div.id = id;
div.innerHTML = id;
$recordable.append(div);
}
function addElementSubtree() {
let selected = $elementlist.value || null;
if(selected === null) {
console.log('No element selected');
return;
}
let id = getId();
let div = document.createElement("div");
div.id = id;
div.innerHTML = id;
let elem = document.querySelector(`#recordable div#${selected}`);
if(elem)
elem.append(div);
}
function changeCSS() {
let selected = $elementlist.value || null;
if(selected === null) {
console.log('No element selected');
return;
}
let userCSS = prompt("Input a single CSS rule like background-color: red");
userCSS = userCSS.length === 0 ? "background-color: red" : userCSS;
let [cssRuleName, cssRule] = userCSS.split(':').map((s) => s.trim());
cssRuleName = cssRuleName.replace(/-(.)/g, (m,p) => p.toUpperCase());
let elem = document.querySelector(`#recordable div#${selected}`);
if(elem)
elem.style[cssRuleName] = cssRule;
}
function removeElement() {
let selected = $elementlist.value || null;
if(selected === null) {
console.log('No element selected');
return;
}
let elem = document.querySelector(`#recordable div#${selected}`);
if(elem)
elem.remove();
}
function changeText()
{
let selected = $elementlist.value || null;
if(selected === null) {
console.log('No element selected');
return;
}
let elem = document.querySelector(`#recordable div#${selected}`);
if(elem) {
let text = prompt("Insert text for element");
// Can't use elem.textContent / innerText / innerHTML...
// might accidentally overwrite element's subtree
// Can't do it like this, because MutationObserver can't see this (why??)
// elem.childNodes[0].value = text;
// This is the way
elem.replaceChild(document.createTextNode(text), elem.childNodes[0]);
}
}
function replayOne()
{
let selected = $eventlist.value || null;
if(selected === null) {
console.log('No element selected');
return;
}
let [event, id] = selected.split(';');
recorder.replayEvent(event, id, $recordable.id);
}
function replayAll()
{
let speed = prompt("Input speed in ms for the replay") || 350;
recorder.replayEvents(speed, $recordable.id);
}
const recorder = new DOMRecorder(
$recordable,
{ attributes: true, childList: true, subtree: true }
);
recorder.start();
*
{
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body
{
width: 100%;
height: 100%;
}
#recordable
{
width: 100%;
height: calc(100% - 35px);
background-color: #7f8fa6;
padding: 5px;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: wrap;
-ms-flex-wrap: wrap;
flex-wrap: wrap;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-content: flex-start;
-ms-flex-line-pack: start;
align-content: flex-start;
-webkit-align-items: flex-start;
-ms-flex-align: start;
align-items: flex-start;
}
#controls
{
width: 100%;
height: 80px;
background-color: #273c75;
display: -ms-flexbox;
display: -webkit-flex;
display: flex;
-webkit-flex-direction: row;
-ms-flex-direction: row;
flex-direction: row;
-webkit-flex-wrap: nowrap;
-ms-flex-wrap: nowrap;
flex-wrap: nowrap;
-webkit-justify-content: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-align-content: stretch;
-ms-flex-line-pack: stretch;
align-content: stretch;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
}
button, select
{
padding: 2px;
margin: 0 3px 0 3px;
}
#recordable > div
{
width: 120px;
min-height: 35px;
padding: 4px;
line-height: calc(35px - 4px);
background-color: #2f3640;
color: #dcdde1;
font-family: "courier-new", Arial;
font-size: 10pt;
margin: 0px 5px 5px 5px;
text-align: center;
}
#recordable > div > *
{
padding: 0px;
line-height: calc(35px - 4px);
background-color: #192a56;
color: #dcdde1;
font-family: "courier-new", Arial;
font-size: 10pt;
margin: 5px 5px 5px 5px !important;
text-align: center;
}
<div id = "controls">
<button id = "add-control" onClick = "addElement()">Add</button>
<button id = "add-subtree-control" onClick = "addElementSubtree()">Add sub</button>
<button id = "add-control" onClick = "removeElement()">Remove</button>
<button id = "css-control" onClick = "changeCSS()">CSS</button>
<button id = "edit-control" onClick = "changeText()">Edit</button>
<select id = "element-list">
</select>
<select id = "event-list">
</select>
<button id = "replay-control" onClick = "replayOne()">Replay</button>
<button id = "replay-all-control" onClick = "replayAll()">Replay all</button>
</div>
<div id = "recordable">
</div>
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.
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
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;