I have an HTML string. i want to select all span elements and remove spans with the same IDs and only let one of them remain in the string. The text between the span elements should not be deleted.
The spans with the same IDs are all after each other.
The remained one should wrap all the removed ones text.
e.g:
Input:
<p>
Hi,<span id="1">this is just a simple text and we</span>
<span id="1">want to save me because i had a lot of</span>
<span id="1">pressure on and i want to live a better life. ðŸ˜</span>
I researched a lot about it but i could't find anything helpful
<span id="2">just another rant. ðŸ˜</span>
</p>
Output:
<p>
Hi,
<span id="1">
this is just a simple text and we want to save me because i had a lot of
pressure on and i want to live a better life ðŸ˜
</span>
I researched a lot about it but i could't find anything helpful
<span id="2">just another rant. ðŸ˜</span>
</p>
A short solution
A short solution would be to select all the spans with an id and use Array.reduce() to concat the content of spans with the same id.
[...document.querySelectorAll("span[id]")].reduce((last,span) => {
if (span.id === last.id) {
last.innerHTML += ' \n' + span.innerHTML;
span.remove();
return last;
}
return span;
}, {});
Snippet
before.value = document.querySelector('p').innerHTML;
[...document.querySelectorAll("span[id]")].reduce((last,span) => {
if (span.id === last.id) {
last.innerHTML += ' \n' + span.innerHTML;
span.remove();
return last;
}
return span;
}, {});
after.value = document.querySelector('p').innerHTML;
body {
font-family: sans-serif;
font-size: 10px;
}
textarea {
width: 100%;
height: 7rem;
font-size: 10px;
}
p {
display: none;
}
<p>
Hi,<span id="1">this is just a simple and we</span>
<span id="1">want to save me for becuase i had a lot of</span>
<span id="1">presure on and i want to live a betteer life. ðŸ˜</span>
I researched a lot about it but i could't find anything helpful
<span id="2">just another rant. ðŸ˜</span>
<span id="3">Hello World</span>
<span id="3">Duplicate World</span>
<span>A span without an ID</span>
</p>
Before:
<textarea id="before"></textarea>
After:
<textarea id="after"></textarea>
While id is unique, you can still achieve your result without switching to using class instead. (if you can switch to using class, do.)
The following code finds the first instance of your duplicate id, copies the text content then deletes the element. this repeats until there are no more. A new element is then added and populated with the text content stored earlier.
let e = []
while (i = document.getElementById("one")) {
e.push(i.textContent);
i.parentNode.removeChild(i)
}
let x = document.createElement("div")
x.setAttribute("id", "one")
x.innerHTML = e.join("<br>")
document.body.appendChild(x)
<div id="one">One</div>
<div id="one">Two</div>
<div id="one">Three</div>
I hope I understood correctly what you want to achieve.
Since you can't have more than one element with the same id.
In Example 2 I replaced ID with a class.
The algorithm takes all the span elements in the code.
It then scrolls through all the elements of the collected sheet and takes their contents.
Deletes all elements of a given class except the first! In the first element, put the collected information.
I hope I've been helpful
Example with ID:
In this example I use jQuery
let list = [...$('span')];
let tempText = '';
list.forEach((el, i) => {
let currElID = el.getAttribute('id');
let nextElID = null;
if (list[i + 1]) {
nextElID = list[i + 1].getAttribute('id');
} else {
nextElID = null;
};
tempText += el.innerText;
if (currElID !== nextElID) {
let listByClass = [...$(`[id=${currElID}]`)];
if (listByClass.length > 0) {
listByClass.forEach((element, index) => {
if (index > 0) {
element.remove();
}
});
};
listByClass[0].innerText = tempText;
tempText = '';
};
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<p>
Hi,
<span id="1">this is just a simple and we</span>
<span id="1">want to save me for becuase i had a lot of</span>
<span id="1">presure on and i want to live a betteer life. ðŸ˜</span>
I researched a lot about it but i could't find anything helpful
<span id="2">just another rant. ðŸ˜</span>
</p>
Example with CLASS:
let list = document.querySelectorAll('span');
let tempText = '';
list.forEach((el, i) => {
let currElClass = el.getAttribute('class');
let nextElClass = null;
if (list[i + 1]) {
nextElClass = list[i + 1].getAttribute('class');
} else {
nextElClass = null;
};
tempText += el.innerText;
if (currElClass !== nextElClass) {
let listByClass = [...document.getElementsByClassName(currElClass)];
if (listByClass.length > 0) {
listByClass.forEach((element, index) => {
if (index > 0) {
element.remove();
}
});
};
listByClass[0].innerText = tempText;
tempText = '';
};
});
<p>
Hi,
<span class="1">this is just a simple and we</span>
<span class="1">want to save me for becuase i had a lot of</span>
<span class="1">presure on and i want to live a betteer life. ðŸ˜</span>
I researched a lot about it but i could't find anything helpful
<span class="2">just another rant. ðŸ˜</span>
</p>
First, find all spans with id set:
let spans = document.querySelectorAll("span[id]");
For each span:
for (let span of spans)
If next element of a given span is another span with the same id:
(nextElement && nextElement.nodeName === "SPAN" && span.id === nextElement.id)
then move all nodes between the spans into the first span:
let nextNode = span.nextSibling;
while(nextNode !== nextElement) {
span.append(nextNode);
nextNode = span.nextSibling;
}
as well as all the nodes from the second span:
span.append(...nextElement.childNodes);
remove the second span:
nextElement.remove();
and then continue the same procedure for the new sibling:
nextElement = span.nextElementSibling;
Since there's no point to process already removed spans with duplicate ids, skip them:
if (!span.parentNode) {
continue;
}
Finally, the spans NodeList should be released to avoid memory leaks:
spans = null;
Normally all DOM manipulations should be batched (moved to the very end of the code and done all at once) for performance reasons but this would complicate the code a bit so I didn't include that in the snippet below to make the code easier to follow.
let spans = document.querySelectorAll("span[id]");
for (let span of spans) {
if (!span.parentNode) {
continue;
}
let nextElement = span.nextElementSibling;
while (nextElement && nextElement.nodeName === "SPAN" && span.id === nextElement.id) {
let nextNode = span.nextSibling;
while (nextNode !== nextElement) {
span.append(nextNode);
nextNode = span.nextSibling;
}
span.append(...nextElement.childNodes);
nextElement.remove();
nextElement = span.nextElementSibling;
}
}
spans = null;
<p>
Hi,<span id="1">this is just a simple and we</span>
<span id="1">want to save me for becuase i had a lot of</span>
<span id="1">presure on and i want to live a betteer life. ðŸ˜</span> I researched a lot about it but i could't find anything helpful
<span id="2">just another rant. ðŸ˜</span>
</p>
Related
I have written the following JS for my chrome project which allows you to bold random letters in a word or sentence.
The problem is that whenever there is a hyperlink in a paragraph the code only bolds random letters up until that point and the rest of the text is unaffected.
let all = document.querySelectorAll("*");
all.forEach(a => a.childNodes.forEach(b => makeRandomBold(b)));
function makeRandomBold(node) {
if (node.nodeType !== 3) {
return;
}
let text = node.textContent;
node.textContent = "";
text.split('').forEach(s => {
if (s !== " " && Math.random() > .49) {
let strong = document.createElement("strong");
strong.textContent = s;
node.parentNode.insertBefore(strong, node);
} else {
node.parentNode.insertBefore(document.createTextNode(s), node);
}
I have tried to change from the universal selector tag, to individually selecting HTML elements such as p a span. Which did not work.
Might finding random intervals and putting them in a single strong tag work?
What might be causing this?
Here you have one way to do it.
I added comments to the code:
// The selector * will pull everything: html, head, style, script, body, etc.
// Let's indicate that we only want elements inside the body that aren't scripts,
// styles, img, or any element that wont hold a string.
let allElements = document.querySelectorAll("body > *:not(style):not(script):not(img)");
// Now, lets iterate:
allElements.forEach(elem => {
traverseDOMToBold(elem);
});
// We want to visit all the futher children first,
// then we can change the content
function traverseDOMToBold(elem){
if (elem.hasChildNodes()){
[...elem.children].forEach(child => {
traverseDOMToBold(child);
});
}
boldInnerHTML(elem);
}
// I like to try to create function that have a Single Responsability if possible.
function boldInnerHTML(elem){
if (elem.innerHTML.length > 0){
let currentString = elem.innerHTML;
let newString = `<strong>${currentString}</strong>`;
elem.innerHTML = currentString.replace(currentString, newString)
}
}
<h1>Title</h1>
<p>Paragraph</p>
<div>Div</div>
<span id="parent">
Parent Span
<span id="child">
Child Span
</span>
</span>
<div>
1.
<div>
2.
<div>
3.
</div>
</div>
</div>
<ul>
<li>A.</li>
<li>B.</li>
<li>
<ul>C
<li>C1.</li>
<li>C2.</li>
<li>C3.</li>
</ul>
</li>
</ul>
I want a code snippet that will search for a keyword text inside HTML element - pre -> code and replace the color of all occurrences of those keywords.
For example, If my text inside pre contains a text - SET, I want to replace it with red color.
I have tried a few codes but it just prints "externalHtml" in red color.
Also, what will be the efficient way to write this code. I may have around 10 to 15 of those keywords and I want to change all of those to just one color. There is no other group of keywords or colors.
var keyword = document.getElementsByClassName('language-sas');
var externalHtml = '<span style="color:red">'+keyword[0]+'</span>'
keyword[0].innerHTML = keyword[0].innerHTML.replace('set',externalHtml );
/*
code.html(code.html().replace(/set/, ' <
span style = "color: red" > $ & < /span>'
));
}
*/
<!DOCTYPE html>
<html>
<head>
</head>
<body>
<pre class="language-sas1">
<code class=" language-sas">
<span class="token keyword">data</span> keep_vars <span class="token punctuation">;</span>
<span class="t">set</span> sashelp<span class="token punctuation">.</span>citimon<span class="token punctuation">;</span>
</div></div></pre>
</body>
</html>
Any thoughts or suggestions will be really helpful.
Whatever you have written is mostly correct. You forgot to write .innerHTML in second line of your JS code.
var externalHtml = '<span style="color:red">'+keyword[0].innerHTML+'</span>'
I avoid using innerHTML when ever possible.
I've made something similar before for a relatable question:
https://stackoverflow.com/a/31852811/1519836
You could adapt it to your problem like this:
// This function will allow you to process DOM Trees
// and run a defined function on each Node.
const textWalker = function (node, callback) {
const nodes = [node];
while (nodes.length > 0) {
node = nodes.shift();
const children = [...node.childNodes];
for (let i = 0; i < children.length; i++) {
var child = children[i];
if (child.nodeType === HTMLElement.TEXT_NODE)
callback(child);
else
nodes.push(child);
}
}
};
// This will run a callback for each text node matching a given regexp pattern
// The callback function can return an array of elements and strings.
// The callback return value will be constructed in DOM
const textSplit = function (source, regexp, callback) {
textWalker(source, function (node) {
let text = node.nodeValue;
if (!regexp.test(text)) {
return;
}
const out = callback(regexp, text);
if (!(out instanceof Array)) {
node.nodeValue = ""+out;
}
else {
let nnode = out.shift();
if (nnode instanceof HTMLElement || nnode.nodeType === HTMLElement.TEXT_NODE) {
// Doesn't work with text nodes yet: node.insertAdjacentElement("afterend", nnode);
node.parentNode.insertBefore(nnode, node);
node.parentNode.removeChild(node);
node = nnode;
}
else {
node.textContent = ""+nnode;
}
while (out.length > 0) {
nnode = out.shift();
if (!(nnode instanceof HTMLElement) && nnode.nodeType !== HTMLElement.TEXT_NODE) {
nnode = document.createTextNode(""+nnode);
}
// Doesn't work with text nodes yet: node.insertAdjacentElement("afterend", nnode);
node.parentNode.insertBefore(nnode, node);
node.parentNode.insertBefore(node, nnode);
node = nnode;
}
}
});
};
// put this function call into your onload function:
const template = document.createElement("span");
template.style.color = "red";
// You could obviously simplify any of this how ever you want.
textSplit(document.querySelector("pre"), /(set)/g, function(regexp, text) {
const out = [];
text.split(regexp).forEach((text, i) => {
if (i % 2 === 0) {
out.push(text);
return;
};
const node = template.cloneNode(true);
node.textContent = text;
out.push(node);
});
return out;
});
<pre class="language-sas1">
<code class=" language-sas">
<span class="token keyword">data</span> keep_vars <span class="token punctuation">;</span>
<span class="t">set</span> sashelp<span class="token punctuation">.</span>citimon<span class="token punctuation">;</span>
</div></div></pre>
Maybe one day I extend this past one text node barrier.
For now you can only use it for self contained text nodes.
Alternatively you could also execute color calls on selected text through the contentEditable stuff.
I'd recommend using an editor for that tho.
Closed. This question needs debugging details. It is not currently accepting answers.
Edit the question to include desired behavior, a specific problem or error, and the shortest code necessary to reproduce the problem. This will help others answer the question.
Closed 1 year ago.
Improve this question
Suppose I have some HTML on a forum:
<div class="sign">username <span class="info">info</span></div>
I want to write a user script that changes it to something like this:
<div class="sign"><a itemprop="creator">username</a> <span class="info">info</span></div>
(The a element will have href as well. I omitted it intentionally to make the code shorter.)
I know how to create an a element, assign it a custom attribute, and add it to the DOM.
But I don't understand how to wrap username with it. That is, how to convert username from the 1st snippet to <a itemprop="creator">username</a> in the second snippet.
Doing this with vanilla DOM APIs is a little involved, but not too hard. You will need to locate the DOM text node which contains the fragment you want to replace, split it into three parts, then replace the middle part with the node you want.
If you have a text node textNode and want to replace the text spanning from index i to index j with a node computed by replacer, you can use this function:
function spliceTextNode(textNode, i, j, replacer) {
const parent = textNode.parentNode;
const after = textNode.splitText(j);
const middle = i ? textNode.splitText(i) : textNode;
middle.remove();
parent.insertBefore(replacer(middle), after);
}
Adapting your example, you will have to use it something like this:
function spliceTextNode(textNode, i, j, replacer) {
const parent = textNode.parentNode;
const after = textNode.splitText(j);
const middle = i ? textNode.splitText(i) : textNode;
middle.remove();
parent.insertBefore(replacer(middle), after);
}
document.getElementById('inject').addEventListener('click', () => {
// XXX: locating the appropriate text node may vary
const textNode = document.querySelector('div.sign').firstChild;
const m = /\w+/.exec(textNode.data);
spliceTextNode(textNode, m.index, m.index + m[0].length, node => {
const a = document.createElement('a');
a.itemprop = 'creator';
a.href = 'https://example.com/';
a.title = "The hottest examples on the Web!";
a.appendChild(node);
return a;
})
}, false);
/* this is to demonstrate other nodes underneath the <div> are untouched */
document.querySelector('.info').addEventListener('click', (ev) => {
ev.preventDefault();
alert('hello');
}, false);
<div class="sign">#username: haha, click me too</div>
<p> <button id="inject">inject link</button>
Note how the ‘click me too’ handler is still attached to the link after the ‘username’ link is injected; modifying innerHTML would fail to preserve this.
You can try replacing the innerHTML:
var el = document.querySelector('.sign');
el.innerHTML = el.innerHTML.replace('username', '<a itemprop="creator">username</a>');
<div class="sign">username <span class="info">info</span></div>
this way ?
let parentElm = document.querySelector('div.sign')
, refNod = parentElm.childNodes[0]
;
if ( refNod.nodeValue === 'username')
{
let newNod = document.createElement('a')
// newNod.href = '/...'
newNod.setAttribute('itemprop','creator')
newNod.textContent = refNod.nodeValue
parentElm.replaceChild( newNod, refNod )
}
/*****/
console.log( parentElm.innerHTML )
<div class="sign">username<span class="info">info</span></div>
Find all elements with class .sign and replace it's inner HTML with a wrapped text before the span tag.
var elms = document.querySelectorAll('.sign');
for (var i = 0; i < elms.length; i++) {
let item = elms[i];
var html = item.innerHTML;
item.innerHTML = html.replace(/(.*)?<span/, "<a itemprop=\"creator\" href='#'>$1</a><span");
}
<div class="sign">username <span class="info">info</span></div>
<div class="sign">other name <span class="info">info</span></div>
<div class="sign">other more and more <span class="info">info</span></div>
A little bit of regex saves the day sometimes.
If you want to preserve event attached to the span element-
var elms = document.querySelectorAll('.sign');
elms.forEach(item => {
let span = item.querySelector('span');
var html = item.innerHTML;
let txt = html.match(/(.*)?<span/)[0].replace("<span", "");
item.innerHTML = `<a itemprop="creator" href='#'>${txt}</a>`;
item.appendChild(span);
});
I have a text on an HTML page. If the user selects a word, I can retrieve the selected word and know exactly what he or she selected. However, I now want to also modify this word on the screen and make it bold. But I do not know how I would do this, since I only know the word clicked, but do not see an easy way to find the word in the HTML and modify it. I could of course search for it, but there is the risk of a word appearing multiple times.
A different way I thought about would be to give each word a unique idea and have something like this:
<span id="1">The</span>
<span id="2">fox</span>
<span id="3">jumps</span>
<span id="4">over</span>
<span id="5">the</span>
<span id="6">fence</span>
But I do not like this solution. This, too, seems overly complicated, does it not? Any suggestions how else I could access the exact words selected?
You can dynamically create a span surrounding the selected word:
const p = document.querySelector('p');
p.addEventListener('mouseup', () => {
const range = document.getSelection().getRangeAt(0);
do {
const charBefore = range.startContainer.textContent[range.startOffset - 1];
if (charBefore.match(/\s/)) break;
range.setStart(range.startContainer, range.startOffset - 1);
} while (range.startOffset !== 0);
do {
const charAfter = range.endContainer.textContent[range.endOffset];
if (charAfter.match(/\s/)) break;
range.setEnd(range.endContainer, range.endOffset + 1);
} while (range.endOffset !== range.endContainer.textContent.length);
const span = document.createElement('span');
span.style.fontWeight = 'bold';
range.surroundContents(span);
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>The fox jumps over the fence.</p>
No need of jQuery, also no need of IDs for each <span>.
The idea is to add a class to the span once it is clicked and later you can retrieve all elements with that bolded class.
Here is a solution with pure Javascript:
// comments inline
var spans = document.getElementsByTagName('span'); // get all <span> elements
for(var i=0, l = spans.length; i<l; i++){
spans[i].addEventListener('click', function(){ // add 'click' event listener to all spans
this.classList.toggle('strong'); // add class 'strong' when clicked and remove it when clicked again
});
}
.strong {
font-weight: bold;
}
<span>The</span>
<span>fox</span>
<span>jumps</span>
<span>over</span>
<span>the</span>
<span>fence</span>
Read up: Element.getElementsByTagName() - Web APIs | MDN
$("p").mouseup(function() {
var selection = getSelected().toString();
$(this).html(function(){
return $(this).text().replace(selection, '<strong>' + selection +'</strong>');
});
});
var getSelected = function(){
var t = '';
if(window.getSelection) {
t = window.getSelection();
} else if(document.getSelection) {
t = document.getSelection();
} else if(document.selection) {
t = document.selection.createRange().text;
}
return t;
}
strong{ font-weight: bold; }
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<p>The fox jumps over the fence.</p>
I have the following HTML code on my page. There is no containing element, it's just in the body.
<b>SqFt per Carton: </b>24.30<br>
Using script, I want to wrap 24.30 in a span tag with a class so the result will look like this:
<b>SqFt per Carton: </b><span class="sqft_cart">24.30</span><br>
How can I do this?
Here is jQuery way to achieve what you asked for by iterating over all text nodes (i.e. text without any tag) in the document, and in case they come right after <b> tag replace them with <span> having proper class:
$(document).ready(function() {
$("body").contents().filter(textNodeFilter).each(function(index) {
var textNode = $(this);
if (this.previousSibling && this.previousSibling.tagName.toLowerCase() === "b") {
var value = textNode.text();
var oSpan = $("<span>").html(value).addClass("sqft_cart");
textNode.replaceWith(oSpan);
}
});
});
function textNodeFilter() {
return this.nodeType == 3;
}
Live test case.
$("b").parent().contents().filter(function() {
return this.nodeType != 1;
}).wrap("<span class='sqft_cart'></span>");
http://jsfiddle.net/FW8Ct/4/
Since you don't have a containing element you can use .nextSibling() to get the text node for 24.30. Then .insertAdjacentHTML() inserts the new span after deleting the old text node. Since I don't know what the rest of your page looks like, I'll assume there could be multiple <b> elements, but the code will work either way.
Demo: http://jsfiddle.net/ThinkingStiff/6ArzV/
Script:
var bs = document.getElementsByTagName( 'b' );
for( var index = 0; index < bs.length; index++ ) {
if( bs[index].innerHTML.indexOf( 'SqFt per Carton:' ) > -1 ) {
var text = bs[index].nextSibling,
span = '<span class="sqft_cart">' + text.nodeValue + '</span>';
text.parentNode.removeChild( text );
bs[index].insertAdjacentHTML('afterEnd', span);
};
};
HTML:
<b>SqFt per Carton: </b>24.30<br>
<b>Price per Carton: </b>193.00<br>
<b>SqFt per Carton: </b>12.90<br>
<b>Price per Carton: </b>147.00<br>
<b>SqFt per Carton: </b>14<br>
CSS:
.sqft_cart {
color: red;
}
Output:
you could get the value of SqFt per Carton: 24.30 by using
var takentext = $("class Or ID of SqFt per Carton").text();
$("class of span").text(takentext);