最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - How to wrap text inside elements with a span tag after AJAX call? - Stack Overflow

programmeradmin4浏览0评论

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
  • 1 Why would you want to change the HTML structure in this way? It seems to me that you are introducing an unnecessary layer of complexity here. If you want to equip the targeted button and a 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
  • The problem is, that I need to insert a span tag on mobile devices to hide the text and just show a background image. I could insert the span tag into the translation file which works fine for the last three buttons/links but not for the first one. The first one takes the content and copies it to the title attribute of the parent link so I have <a title="<span>whatever</span"><span>whatever</span></a>. So I can try to change the title attribute from the first button OR add a span with JS/JQuery. It would work this way, if there weren't an ajax request attached to each of the buttons/links. – user27734429 Commented Mar 26 at 13:33
  • @user27734429 then why don't you make the text transparent or font-size:0 just for example - instead of unnecessarily wrapping the text in <span>?! – Roko C. Buljan Commented Mar 27 at 8:15
Add a comment  | 

5 Answers 5

Reset to default 0

Each 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:

  1. Collect all <button>, and <a> into a NodeList:

    const targets = document.querySelectorAll("button, a");
    
  2. Then for each target, if it has text, create a <span>, copy it's content, place the content within the <span>, and then use replaceChildren().

    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="#">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;</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() @param

    1. tag {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".

    2. 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&amp;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>

发布评论

评论列表(0)

  1. 暂无评论