Using either document.getElementsByName('spanName') or jQuery('[name="spanName"]') fails (returning []) when
called from within a Chrome extension (or console). However, document.getElementById('spanId') works, as does a CCS selector on a class name.
The span is already part of the DOM, prior to any intervention by the extension. However, the name attribute was added by the extension, the style attribute was modified, and a class name was added:
Original:
<div id="parentDiv">
<span style="background-color:Yellow">Some highlighted text</span>
</div>
Updated:
<div id="parentDiv">
<span id"spanId" name="spanName" class="highlighted" style="">Some highlighted text</span>
</div>
In addition, at one point the entire parentDiv's innerHTML is replaced and the spans are transferred
let spanNodeList = ...
let newInnerHTML = ...
let patterns = ...
for (let i = 0; i < spanNodeList.length; i++) {
newInnerHTML.replace(patterns[i], spanNodeList[i].outerHTML)
}
document.getElementById('parentDiv').innerHTML = newInnerHTML
I am performing this transfer before adding the name attribute. Could this innerHTML replacement be the source of my woes?
By the Way...
I'm updating the spans with:
let spans = document.getElementsByTagName('span')
spans[Symbol.Iterator] = [][Symbol.Iterator]
for (let span of spans) {
if (!/yellow/i.test(span.style.CSSText) continue;
span.style.CSSText = ''
span.id = <uniqueSpanId>
span.name = 'spanName'
span.className = "highlighted'
}
Per the spec, document.getElementsByName() returns an array of elements. You will either have to loop through the result or do document.getElementsByName('spanName')[0]
https://developer.mozilla.org/en-US/docs/Web/API/Document/getElementsByName
My testing agrees that:
document.getElementsByName(name) does not work from chrome extensions,
but
document.getElementById(id) works, and
document.getElementsByTagName(name) works too.
If you really need to use getElementsByName, a possible solution may be to inject a script into the page, like this:
function getEls() {
// This should work when it runs in the original page
let results = document.getElementsByName('spanname');
// but we have to shuttle the results back to the content script
document.body.setAttribute('data-myresults', my_encode(results));
}
// Runs "func" in the page we are attached to
function injectScript(func) {
const codeString = '(' + func + ')();';
const script = document.createElement('script');
script.textContent = codeString;
(document.head || document.documentElement).appendChild(script);
}
injectScript(getEls);
// then, after it has had enough time to asynchronously do the job...
let results = my_decode(document.body.getAttribute('data-myresults'));
(Also, you will have to write my_encode to convert your data to a string, and my_decode to do the opposite.)
Related
I am looking to replace an element in the DOM.
For example, there is an <a> element that I want to replace with a <span> instead.
How would I go and do that?
by using replaceChild():
<html>
<head>
</head>
<body>
<div>
<a id="myAnchor" href="http://www.stackoverflow.com">StackOverflow</a>
</div>
<script type="text/JavaScript">
var myAnchor = document.getElementById("myAnchor");
var mySpan = document.createElement("span");
mySpan.innerHTML = "replaced anchor!";
myAnchor.parentNode.replaceChild(mySpan, myAnchor);
</script>
</body>
</html>
A.replaceWith(span) - No parent needed
Generic form:
target.replaceWith(element)
Way better/cleaner than the previous method.
For your use case:
A.replaceWith(span)
Advanced usage
You can pass multiple values (or use spread operator ...).
Any string value will be added as a text element.
Examples:
// Initially [child1, target, child3]
target.replaceWith(span, "foo") // [child1, span, "foo", child3]
const list = ["bar", span]
target.replaceWith(...list, "fizz") // [child1, "bar", span, "fizz", child3]
Safely handling null target
If your target has a chance to be null, you can consider using the newish ?. optional chaining operator. Nothing will happen if target doesn't exist. Read more here.
target?.replaceWith?.(element)
Related DOM methods
Read More - child.before and child.after
Read More - parent.prepend and parent.append
Mozilla Docs
Supported Browsers - 97% Nov '22
var a = A.parentNode.replaceChild(document.createElement("span"), A);
a is the replaced A element.
This question is very old, but I found myself studying for a Microsoft Certification, and in the study book it was suggested to use:
oldElement.replaceNode(newElement)
I looked it up and it seems to only be supported in IE. Doh..
I thought I'd just add it here as a funny side note ;)
I had a similar issue and found this thread. Replace didn't work for me, and going by the parent was difficult for my situation. Inner Html replaced the children, which wasn't what I wanted either. Using outerHTML got the job done. Hope this helps someone else!
currEl = <div>hello</div>
newElem = <span>Goodbye</span>
currEl.outerHTML = newElem
# currEl = <span>Goodbye</span>
You can replace an HTML Element or Node using Node.replaceWith(newNode).
This example should keep all attributes and childs from origin node:
const links = document.querySelectorAll('a')
links.forEach(link => {
const replacement = document.createElement('span')
// copy attributes
for (let i = 0; i < link.attributes.length; i++) {
const attr = link.attributes[i]
replacement.setAttribute(attr.name, attr.value)
}
// copy content
replacement.innerHTML = link.innerHTML
// or you can use appendChild instead
// link.childNodes.forEach(node => replacement.appendChild(node))
link.replaceWith(replacement)
})
If you have these elements:
Link 1
Link 2
Link 3
Link 4
After running above codes, you will end up with these elements:
<span href="#link-1">Link 1</span>
<span href="#link-2">Link 2</span>
<span href="#link-3">Link 3</span>
<span href="#link-4">Link 4</span>
You can use replaceChild on the parent of the target element after creating your new element (createElement):
const newElement = document.createElement(/*...*/);
const target = document.getElementById("my-table");
target.parentNode.replaceChild(newElement, target);
If your starting point for the new element is HTML, you can use insertAdjacentHTML and then removeChild on the parent (or remove on the element itself, in modern environments):
const target = document.getElementById("my-table");
target.insertAdjacentHTML("afterend", theHTMLForTheNewElement);
target.parentNode.removeChild(target); // Or: `target.remove()`
Best way to do it. No parents need. Just use Element.outerHTML = template;
// Get the current element
var currentNode = document.querySelector('#greeting');
// Replace the element
currentNode.outerHTML =
'<div id="salutations">' +
'<h1>Hi, universe!</h1>' +
'<p>The sun is always shining!</p>' +
'</div>';
Example for replacing LI elements
function (element) {
let li = element.parentElement;
let ul = li.parentNode;
if (li.nextSibling.nodeName === 'LI') {
let li_replaced = ul.replaceChild(li, li.nextSibling);
ul.insertBefore(li_replaced, li);
}
}
Given the already proposed options the easiest solution without finding a parent:
var parent = document.createElement("div");
var child = parent.appendChild(document.createElement("a"));
var span = document.createElement("span");
// for IE
if("replaceNode" in child)
child.replaceNode(span);
// for other browsers
if("replaceWith" in child)
child.replaceWith(span);
console.log(parent.outerHTML);
NOTE: The first requirement for this is that it not use jQuery.
I also do not want to use .innerHTML if I can avoid it (even if that requires a bit more code)
I have divs within a page (multiple locations) that will be something like this:
<div class="p-user-content">John Smith current working on ticket LQ-1954</div>
... again, multiple locations ...
<div class="p-user-content">Sally Jones assigned GM-3398 yesterday</div>
There will be simple text, and tags like <a> mixed in with the text as shown.
The following script successfully identifies all the "text nodes":
var el = document.getElementsByClassName('p-user-content');
for (var i in el) {
if (typeof el[i].childNodes === 'undefined') continue
for (var k in el[i].childNodes) {
if (typeof el[i].childNodes[k] === 'function') continue
if (el[i].childNodes[k].nodeType !== Node.TEXT_NODE) continue
/**
This successfully gets the nodeValue
*/
console.log(el[i].childNodes[k].nodeValue)
}
}
What I want to do is split the text node word-by-word, and then convert values like LQ-1954 and GM-3398 to anchor elements, and then replace the modified text and anchor links back into the existing div. I'm able to split the text up and do the matching part, but how would I
build the nodes back with the new links and
replace it back into the same div?
You can use .replace
document.body.outerHTML = document.body.outerHTML.replace(/(LQ-1954)/g , '<a> $1 </a>')
If you must avoid manipulating html by changing the value of innerHTML,
create a new element and replace the old element with it:
function addLinks(){
var el = document.getElementsByClassName('p-user-content');
for (var i in el) {
if (typeof el[i].childNodes === 'undefined') continue
let value = el[i].innerHTML.replace(/[A-Z]{2}-[\d]{4}/g, (match)=>'' + match + '');
let newDiv = document.createElement('div')
newDiv.insertAdjacentHTML("beforeend", value);
el[i].replaceChildren(newDiv)
}
}
<div class="p-user-content">John Smith current working on ticket LQ-1954</div>
... again, multiple locations ...
<div class="p-user-content">Sally Jones assigned GM-3398 yesterday</div>
<button onclick="addLinks()">Add Links</button>
Simple way using innerHTML:
function addLinks(){
var el = document.getElementsByClassName('p-user-content');
for (var i in el) {
if (typeof el[i].childNodes === 'undefined') continue
el[i].innerHTML = el[i].innerHTML.replace(/[A-Z]{2}-[\d]{4}/g, (match)=>'' + match + '');
}
}
<div class="p-user-content">John Smith current working on ticket LQ-1954</div>
... again, multiple locations ...
<div class="p-user-content">Sally Jones assigned GM-3398 yesterday</div>
<button onclick="addLinks()">Add Links</button>
First, I'm creating a library for JavaScript and I can not use jQuery. I'm trying to get the text content of an HTML element without the text contents of its children.
Both attributes innerText and textContent don't give me what needed, please help.
You can solve using DOM API as childNodes and nodeType.
var elChildNode = document.querySelector("#hello").childNodes;
var sChildTextSum = "";
elChildNode.forEach(function(value){
if(value.nodeType === Node.TEXT_NODE) {
console.log("Current textNode value is : ", value.nodeValue.trim());
sChildTextSum += value.nodeValue;
}
});
console.log("All text value of firstChild : ", sChildTextSum);
I created a sample code as above.
https://jsfiddle.net/nigayo/p7t9bdc3/
To get Author's Name from the following element, excluding <span>...:
<div class="details__instructor">
Author's Name<span ng-show="job_title">, Entrepreneur</span>
</div>
use childNodes[0]. For example:
document.querySelector('div.details__instructor').childNodes[0].textContent
Using only JavaScript (you specified you cannot use jQuery), and given that you have provided and know the id for the parent element:
document.getElementById('parent_element_id').childNodes[0].nodeValue;
You can also use .trim() to remove any trailing space characters left behind from the removal of any child element text:
document.getElementById('parent_element_id').childNodes[0].nodeValue.trim();
var mydiv = getElementByID("id");
function Get_text(element) {
var selected = element.cloneNode(true);
var text;
while (selected.firstChild) {
if (selected.firstChild.nodeType == 3) text = selected.firstChild.nodeValue;
selected.removeChild(selected.firstChild);
}
return text;
}
Get_text(mydiv);
I know many good solutions here exist, but none of them actually achieved what I needed (get the textContent of a single node, none of its children), so sharing this for future searchers.
var html = document.getElementsByTagName("*");
for (var i = 0; i < html.length; i++) {
var el = html[i];
for (var j = 0; j < el.children.length; j++) {
var child = el.children[j],
childTextContent = child.innerHTML;
// Remove all children tags, leaving only the actual text of the node.
childTextContent = childTextContent.replace(/\<.*\>.*\<\/.*\>/gmi, "");
// Also remove <img /> type tags.
childTextContent = childTextContent.replace(/\<.*\ \/\>/gmi, "");
console.log(childTextContent);
// Now you can do any type of text matching (regex) on the result.
}
});
This sounds a little crazy, but I'm wondering whether possible to get reference to comment element so that I can dynamically replace it other content with JavaScript.
<html>
<head>
</head>
<body>
<div id="header"></div>
<div id="content"></div>
<!-- sidebar place holder: some id-->
</body>
</html>
In above page, can I get reference to the comment block and replace it with some content in local storage?
I know that I can have a div place holder. Just wondering whether it applies to comment block.
Thanks.
var findComments = function(el) {
var arr = [];
for(var i = 0; i < el.childNodes.length; i++) {
var node = el.childNodes[i];
if(node.nodeType === 8) {
arr.push(node);
} else {
arr.push.apply(arr, findComments(node));
}
}
return arr;
};
var commentNodes = findComments(document);
// whatever you were going to do with the comment...
console.log(commentNodes[0].nodeValue);
It seems there are legitimate (performance) concerns about using comments as placeholders - for one, there's no CSS selector that can match comment nodes, so you won't be able to query them with e.g. document.querySelectorAll(), which makes it both complex and slow to locate comment elements.
My question then was, is there another element I can place inline, that doesn't have any visible side-effects? I've seen some people using the <meta> tag, but I looked into that, and using that in <body> isn't valid markup.
So I settled on the <script> tag.
Use a custom type attribute, so it won't actually get executed as a script, and use data-attributes for any initialization data required by the script that's going to initialize your placeholders.
For example:
<script type="placeholder/foo" data-stuff="whatevs"></script>
Then simply query those tags - e.g.:
document.querySelectorAll('script[type="placeholder/foo"]')
Then replace them as needed - here's a plain DOM example.
Note that placeholder in this example isn't any defined "real" thing - you should replace that with e.g. vendor-name to make sure your type doesn't collide with anything "real".
Building off of hyperslug's answer, you can make it go faster by using a stack instead of function recursion. As shown in this jsPerf, function recursion is 42% slower on my Chrome 36 on Windows and 71% with IE11 in IE8 compatibility mode. It appears to run about 20% slower in IE11 in edge mode but faster in all other cases tested.
function getComments(context) {
var foundComments = [];
var elementPath = [context];
while (elementPath.length > 0) {
var el = elementPath.pop();
for (var i = 0; i < el.childNodes.length; i++) {
var node = el.childNodes[i];
if (node.nodeType === Node.COMMENT_NODE) {
foundComments.push(node);
} else {
elementPath.push(node);
}
}
}
return foundComments;
}
Or as done in TypeScript:
public static getComments(context: any): Comment[] {
const foundComments = [];
const elementPath = [context];
while (elementPath.length > 0) {
const el = elementPath.pop();
for (let i = 0; i < el.childNodes.length; i++) {
const node = el.childNodes[i];
if (node.nodeType === Node.COMMENT_NODE) {
foundComments.push(node);
} else {
elementPath.push(node);
}
}
}
return foundComments;
}
There is an API for document nodes traversal: Document#createNodeIterator():
var nodeIterator = document.createNodeIterator(
document.body,
NodeFilter.SHOW_COMMENT
);
// Replace all comment nodes with a div
while(nodeIterator.nextNode()){
var commentNode = nodeIterator.referenceNode;
var id = (commentNode.textContent.split(":")[1] || "").trim();
var div = document.createElement("div");
div.id = id;
commentNode.parentNode.replaceChild(div, commentNode);
}
#header,
#content,
#some_id{
margin: 1em 0;
padding: 0.2em;
border: 2px grey solid;
}
#header::after,
#content::after,
#some_id::after{
content: "DIV with ID=" attr(id);
}
<html>
<head>
</head>
<body>
<div id="header"></div>
<div id="content"></div>
<!-- sidebar placeholder: some_id -->
</body>
</html>
Edit: use a NodeIterator instead of a TreeWalker
If you use jQuery, you can do the following to get all comment nodes
comments = $('*').contents().filter(function(){ return this.nodeType===8; })
If you only want the comments nodes of the body, use
comments = $('body').find('*').contents().filter(function(){
return this.nodeType===8;
})
If you want the comment strings as an array you can then use map:
comment_strings = comments.map(function(){return this.nodeValue;})
Using document.evaluate and xPath:
function getAllComments(node) {
const xPath = "//comment()",
result = [];
let query = document.evaluate(xPath, node, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
for (let i = 0, length = query.snapshotLength; i < length; ++i) {
result.push(query.snapshotItem(i));
}
return result;
}
getAllComments(document.documentElement);
from my testing, using xPath is faster than treeWalker:
https://jsben.ch/Feagf
This is an old question, but here's my two cents on DOM "placeholders"
IMO a comment element is perfect for the job (valid html, not visible, and not misleading in any way).
However, traversing the dom looking for comments is not necessary if you build your code the other way around.
I would suggest using the following method:
Mark the places you want to "control" with markup of your choice (e.g a div element with a specific class)
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
<div class="placeholder"></div>
Find the placeholders the usual way (querySelector/classSelector etc)
var placeholders = document.querySelectorAll('placeholder');
Replace them with comments and keep reference of those comments:
var refArray = [];
[...placeholders].forEach(function(placeholder){
var comment = document.createComment('this is a placeholder');
refArray.push( placeholder.parentNode.replaceChild(comment, placeholder) );
});
at this stage your rendered markup should look like this:
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
<!-- this is a placeholder -->
Now you can access each of those comments directly with your built refArray and do whatevere it is you wanna do... for example:
replace the second comment with a headline
let headline = document.createElement('h1');
headline.innerText = "I am a headline!";
refArray[1].parentNode.replaceChild(headline,refArray[1]);
If you just want to get an array of all comments from a document or part of a document, then this is the most efficient way I've found to do that in modern JavaScript.
function getComments (root) {
var treeWalker = document.createTreeWalker(
root,
NodeFilter.SHOW_COMMENT,
{
"acceptNode": function acceptNode (node) {
return NodeFilter.FILTER_ACCEPT;
}
}
);
// skip the first node which is the node specified in the `root`
var currentNode = treeWalker.nextNode();
var nodeList = [];
while (currentNode) {
nodeList.push(currentNode);
currentNode = treeWalker.nextNode();
}
return nodeList;
}
I am getting over 50,000 operations per second in Chrome 80 and the stack and recursion methods both get less than 5,000 operations per second in Chrome 80. I had tens of thousands of complex documents to process in node.js and this worked the best for me.
https://jsperf.com/getcomments/6
I have a span:
<span class="attr attr-value">Brand Name</span>
And I want to replace that text with an image, based on the text
Here is what I have:
<script type="text/javascript">
var oldHTML = document.getElementsByClass('attr-value').innerHTML;
var filename = oldHTML.toLowerCase().replace(/ /g, '-').replace(/([^0-9a-z-])/g,'');
var newHTML = "<img src='http://www.example.com/" + filename + ".jpg'>";
document.getElementsByClass('attr-value').innerHTML = newHTML;
</script>
What am I doing wrong here?
This line is an issue:
var oldHTML = document.getElementsByClass('attr-value').innerHTML;
document.getElementsByClass should be document.getElementsByClassName, and it returns a NodeList, which doesn't have an innerHTML property. You'd want to loop through the NodeList to look at the innerHTML of each element.
Something like this (live example):
<script type="text/javascript">
(function() {
var list, index, element, filename;
list = document.getElementsByClassName('attr-value');
for (index = 0; index < list.length; ++index) {
element = list[index];
filename = element.innerHTML.toLowerCase().replace(/ /g, '-').replace(/([^0-9a-z-])/g,'');
element.innerHTML = "<img src='http://www.example.com/" + filename + ".jpg'>";
}
})();
</script>
Changes:
Put the whole thing in an anonymous function and immediately execute that function, to avoid creating global symbols.
Use the correct getElementsByClassName
Loop through them
Operate on the innerHTML of each element.
Notes:
IE doesn't have getElementsByClassName, so you'll want to be sure you're loading a script that provides it on IE. That's not provided in the live example above, use Firefox, Chrome, or Opera. (Just search for "getElementsByClassName for IE" and you'll find several implementations to choose from.)
The above script tag will need to be placed after all of the elements you want to process in the HTML file.
class="attr attr-value" and you're calling
document.getElementsByClass('attr-value').innerHTML
document.getElementsByClassName();
It should be, (e.g)
document.getElementsByClassName('attr-value')[0];