I have basically this HTML structure:
<div id="whatever_1">
<button id="something1">
Some text 1
</button>
</div>
<div id="whatever_2">
<a href="..." class="something2">
Some text 2
</a>
</div>
<div id="whatver_3">
<a href="..." class="something3">
Some text 3
</a>
</div>
</div>
I need to wrap all the text which is inside the button- or a-tags with a span tag. So that it looks like this afterwards:
<div id="whatever_1">
<button id="something1">
<span>Some text 1</span>
</button>
</div>
<div id="whatever_2"><a href="..." class="something">
<span>Some text 2</span>
</a>
</div>and so on...
Click on link/button lead to an ajax call.
I tried using querySelector() which works but just on the first load of the page. If someone presses the button after an ajax call, the content of the button is again, without the span tag and a error message appears:
Uncaught (in promise) Error: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received
var text = document.querySelector('.something2 ').textContent;
var text_wrapped = text.replace(text, '<span id="add_friend">$&</span>');
document.querySelector('.something2 ').innerHTML = text_wrapped;
How to solve this?
I have basically this HTML structure:
<div id="whatever_1">
<button id="something1">
Some text 1
</button>
</div>
<div id="whatever_2">
<a href="..." class="something2">
Some text 2
</a>
</div>
<div id="whatver_3">
<a href="..." class="something3">
Some text 3
</a>
</div>
</div>
I need to wrap all the text which is inside the button- or a-tags with a span tag. So that it looks like this afterwards:
<div id="whatever_1">
<button id="something1">
<span>Some text 1</span>
</button>
</div>
<div id="whatever_2"><a href="..." class="something">
<span>Some text 2</span>
</a>
</div>and so on...
Click on link/button lead to an ajax call.
I tried using querySelector() which works but just on the first load of the page. If someone presses the button after an ajax call, the content of the button is again, without the span tag and a error message appears:
Uncaught (in promise) Error: A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received
var text = document.querySelector('.something2 ').textContent;
var text_wrapped = text.replace(text, '<span id="add_friend">$&</span>');
document.querySelector('.something2 ').innerHTML = text_wrapped;
How to solve this?
Share Improve this question edited Mar 25 at 17:49 user27734429 asked Mar 24 at 21:18 user27734429user27734429 155 bronze badges 3 |5 Answers
Reset to default 0Each all the elements, check only non-empty text nodes, and replace them with span
with current text, using replaceWith
, something like this:
$('*:not(:empty)').contents().filter(function() {
return this.nodeType === 3 && this.nodeValue.trim() !== '';
}).each(function() {
$(this).replaceWith(`<span>${this.nodeValue.trim()}</span>`);
});
<script src="https://cdnjs.cloudflare/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<div id="whatever_1">
<button id="something1">Some text 1</button>
</div>
<div id="whatever_2">
<a
href="#"
class="something2"
>
Some text 2
</a>
</div>
<div id="whatver_3">
<a
href="#"
class="something3"
>
Some text 3
</a>
</div>
No external libraries required.
Just find all <a>
and <button>
elements within a container; and replace their inner text nodes with span elements.
const wrapTextInSpan = (root, selector) => {
Array
.from(root.querySelectorAll(selector))
.forEach((element) => {
const textNode = element.firstChild;
if (textNode.nodeType !== Node.TEXT_NODE) return;
const span = document.createElement('span');
span.className = 'spanned';
span.id = `id-under-${element.parentNode.id}`;
span.textContent = textNode.textContent;
element.replaceChild(span, textNode);
});
};
wrapTextInSpan(document.getElementById('example'), 'a, button');
.spanned {
color: green;
}
<div id="example">
<div id="whatever_1">
<button id="something1">
Some text 1
</button>
</div>
<div id="whatever_2">
<a href="#" class="something2">
Some text 2
</a>
</div>
<div id="whatver_3">
<a href="#" class="something3">
Some text 3
</a>
</div>
</div>
Select all the button and anchor elements, create a <span>
for each one,give unique id based on index to <span>
tag and wrapped their content inside <span>
tag.
const arr = document.querySelectorAll('button, a');
arr.forEach((item,index) => {
const span = document.createElement('span');
span.id = `span-${(index + 1)}`;
span.textContent = item.textContent;
item.textContent = '';
item.appendChild(span);
});
Update
When you use querySelector()
, once it finds a match it stops searching, so if you have more than one target you'll need querySelectorAll()
which will collect all matches in an array-like object called a NodeList.
For some reason I thought OP needed all text to be wrapped, but it appears only the text of buttons and links need to be wrapped. So just simply do the following:
Collect all
<button>
, and<a>
into a NodeList:const targets = document.querySelectorAll("button, a");
Then for each target, if it has text, create a
<span>
, copy it's content, place the content within the<span>
, and then usereplaceChildren()
.targets.forEach(target => { if (target.textContent.trim().length > 0) { const span = document.createElement("span"); span.insertAdjacentHTML("beforeend", target.innerHTML); target.replaceChildren(span); } });
The example below uses <mark>
instead of <span>
.
const targets = document.querySelectorAll("button, a");
targets.forEach(target => {
if (target.textContent.trim().length > 0) {
const mark = document.createElement("mark");
mark.insertAdjacentHTML("beforeend", target.innerHTML);
target.replaceChildren(mark);
}
});
<form>
<fieldset>
<legend>Text NOT in a Button or Link</legend>
<textarea>This is text NOT inside a button or link</textarea><br>
<button>Text in a button</button>
</fieldset>
</form>
<dl>
<dt>Text Not in a button or link</dt>
<dd>Text Not in a button or link
<a href="#">Text in a link</a>
</dd>
</dl>
<div>
<p>The link below has only spaces so its not highlighted</p>
<a href="#"> </a>
<p>The following button has an inline event handler. If it reveals text when you click it then the event handler is still intact.</p>
<button onclick="this.nextElementSibling.textContent='Text after button'">Click</button> <b></b>
</div><br><br>
createTreeWalker()
This answer wraps all text under a target element, the section above answers the OP's question: How to wrap spans around the text of buttons and links?.
A TreeWalker
object (or a NodeIterator
) was designed to "walk" the DOM tree in what appears to be a linear fashion (under the hood it's most likely recursively traversing the DOM). It leaves no stone unturned so any children nodes of the root (the parent node being searched) its searching will be found according to whatever is configured by its NodeFilter
.
The relevant things that the NodeFilter
can target are ELEMENT, TEXT, and COMMENT nodes. The example below is using NodeFilter.SHOW_TEXT
. The following are the highlights:
wrapText()
@paramtag
{string}A string of a valid
tagName
to wrap text with.
ex."span"
,"div"
,"p"
,"mark"
, etc... If nothing is given or its just incorrect type,undefined
,null
, etc. it will default to a"span"
.sel
{string}A valid CSS selector of the parent element to be searched.
ex."#id"
,".class"
,"span"
,"[attribute]"
, etc. If nothing is given,"body"
is the default.
Instead of a <span>
I used a <mark>
to wrap text with so you can actually see everything highlighted (even the closed <details>
content was highlighted).
const wrapText = (tag, sel = "body") => {
const root = document.querySelector(sel);
const treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
while (treeWalker.nextNode()) {
const node = treeWalker.currentNode;
if (node.textContent.trim().length > 0) {
const elem = document.createElement(tag) || document.createElement("span");
node.after(elem);
elem.appendChild(node);
}
}
};
wrapText("mark");
<article class="main-page-content" lang="en-US">
<header>
<h1>Document: createTextNode() method</h1>
<details class="baseline-indicator high">
<summary><span class="indicator" role="img" aria-label="Baseline Check"></span>
<div class="status-title">Baseline
<!-- --> <span class="not-bold">Widely available</span>
</div>
<div class="browsers"><span class="engine" title="Supported in Chrome and Edge"><span class="browser chrome supported" role="img" aria-label="Chrome check"></span><span class="browser edge supported" role="img" aria-label="Edge check"></span></span><span class="engine" title="Supported in Firefox"><span class="browser firefox supported" role="img" aria-label="Firefox check"></span></span><span class="engine" title="Supported in Safari"><span class="browser safari supported" role="img" aria-label="Safari check"></span></span></div><span class="icon icon-chevron "></span>
</summary>
<div class="extra">
<p>This feature is well established and works across many devices and browser versions. It’s been available across browsers since
<!-- -->
<!-- -->July 2015
<!-- -->.
</p>
<ul>
<li><a href="/en-US/docs/Glossary/Baseline/Compatibility" data-glean="baseline_link_learn_more" target="_blank" class="learn-more">Learn more</a></li>
<li><a href="#browser_compatibility" data-glean="baseline_link_bcd_table">See full compatibility</a></li>
<li><a href="https://survey.alchemer/s3/7634825/MDN-baseline-feedback?page=%2Fen-US%2Fdocs%2FWeb%2FAPI%2FDocument%2FcreateTextNode&level=high" data-glean="baseline_link_feedback" class="feedback-link" target="_blank" rel="noreferrer">Report feedback</a></li>
</ul>
</div>
</details>
</header>
<div class="section-content">
<p>Creates a new <a href="/en-US/docs/Web/API/Text"><code>Text</code></a> node. This method can be used to escape HTML</p>
</div>
</article>
Although it is true that .innerHTML
applied on an element will re-create all internal elements that are described by the html code and will thereby delete their event listener bindings, it is also true that the bindings of the actual element will stay intact. This is demonstrated below.
I initially added event listeners to all a
and button
elements and then inserted the spans by using the .innerHTML()
method on each element.
// pre-existing event listener bindings:
const elems=document.querySelectorAll("a,button");
elems.forEach(el=>{
const txt=el.textContent;
el.addEventListener("click",ev=>{
ev.preventDefault();
console.log(txt);
})});
// === span insertion happens here: ===
elems.forEach(el=>{
el.innerHTML=`<span>new span inside with: ${el.textContent}</span>`;
});
<div id="whatever_1">
<button id="something1">
Some text 1
</button>
</div>
<div id="whatever_2">
<a href="..." class="something2">
Some text 2
</a>
</div>
<div id="whatver_3">
<a href="..." class="something3">
Some text 3
</a>
</div>
</div>
button
anda
elements with an event listener you can do that directly, either through event delegation or by using something like[...document.querySelectorAll("a,button")].forEach(el=>el.addEventListener("click", ev=>{...})
. – Carsten Massmann Commented Mar 26 at 6:23<span>
?! – Roko C. Buljan Commented Mar 27 at 8:15