Why is my code not affecting entire content? - javascript

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>

Related

Remove span elements with the same Ids and only let one remain

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>

getSelection add html, but prevent nesting of html

I have a content editable field, in which I can enter and html format som text (select text and click a button to add <span class="word"></span> around it).
I use the following function:
function highlightSelection() {
if (window.getSelection) {
let sel = window.getSelection();
if (sel.rangeCount > 0) {
if(sel.anchorNode.parentElement.classList.value) {
let range = sel.getRangeAt(0).cloneRange();
let newParent = document.createElement('span');
newParent.classList.add("word");
range.surroundContents(newParent);
sel.removeAllRanges();
sel.addRange(range);
} else {
console.log("Already in span!");
// end span - start new.
}
}
}
}
But in the case where I have:
Hello my
<span class="word">name is</span>
Benny
AND I select "is" and click my button I need to prevent the html from nesting, so instead of
Hello my
<span class="word">name <span class="word">is</span></span> Benny
I need:
Hello my
<span class="word">name</span>
<span class="word">is</span>
Benny
I try to check the parent class, to see if it is set, but how do I prevent nested html - close span tag at caret start position, add and at the end of caret position add so the html will not nest?
It should also take into account if there are elements after selection which are included in the span:
So:
Hello my
<span class="word">name is Benny</span>
selecting IS again and clicking my button gives:
Hello my
<span class="word">name</span>
<span class="word">is</span>
<span class="word">Benny</span>
Any help is appreciated!
Thanks in advance.
One way would be to do this in multiple pass.
First you wrap your content, almost blindly like you are currently doing.
Then, you check if in this content there were some .word content. If so, you extract its content inside the new wrapper you just created.
Then, you check if your new wrapper is itself in a .word container.
If so, you get the content that was before the selection and wrap it in its own new wrapper. You do the same with the content after the selection.
At this stage we may have three .word containers inside the initial one. We thus have to extract the content of the initial one, and remove it. Our three wrappers are now independent.
function highlightSelection() {
if (window.getSelection) {
const sel = window.getSelection();
if (sel.rangeCount > 0) {
const range = sel.getRangeAt(0).cloneRange();
if (range.collapsed) { // nothing to do
return;
}
// first pass, wrap (almost) carelessly
wrapRangeInWordSpan(range);
// second pass, find the nested .word
// and wrap the content before and after it in their own spans
const inner = document.querySelector(".word .word");
if (!inner) {
// there is a case I couldn't identify correctly
// when selecting two .word start to end, where the empty spans stick around
// we remove them here
range.startContainer.parentNode.querySelectorAll(".word:empty")
.forEach((node) => node.remove());
return;
}
const parent = inner.closest(".word:not(:scope)");
const extractingRange = document.createRange();
// wrap the content before
extractingRange.selectNode(parent);
extractingRange.setEndBefore(inner);
wrapRangeInWordSpan(extractingRange);
// wrap the content after
extractingRange.selectNode(parent);
extractingRange.setStartAfter(inner);
wrapRangeInWordSpan(extractingRange);
// finally, extract all the contents from parent
// to its own parent and remove it, now that it's empty
moveContentBefore(parent)
}
}
}
document.querySelector("button").onclick = highlightSelection;
function wrapRangeInWordSpan(range) {
if (
!range.toString().length && // empty text content
!range.cloneContents().querySelector("img") // and not an <img>
) {
return; // empty range, do nothing (collapsed may not work)
}
const content = range.extractContents();
const newParent = document.createElement('span');
newParent.classList.add("word");
newParent.appendChild(content);
range.insertNode(newParent);
// if our content was wrapping .word spans,
// move their content in the new parent
// and remove them now that they're empty
newParent.querySelectorAll(".word").forEach(moveContentBefore);
}
function moveContentBefore(parent) {
const iterator = document.createNodeIterator(parent);
let currentNode;
// walk through all nodes
while ((currentNode = iterator.nextNode())) {
// move them to the grand-parent
parent.before(currentNode);
}
// remove the now empty parent
parent.remove();
}
.word {
display: inline-block;
border: 1px solid red;
}
[contenteditable] {
white-space: pre; /* leading spaces will get ignored once highlighted */
}
<button>highlight</button>
<div contenteditable
>Hello my <span class="word">name is</span> Benny</div>
But beware, this is just a rough proof of concept, I didn't do any heavy testings and there may very well be odd cases where it will just fail (content-editable is a nightmare).
Also, this doesn't handle cases where one would copy-paste or drag & drop HTML content.
I modified your code to a working approach:
document.getElementById('btn').addEventListener('click', () => {
if (window.getSelection) {
var sel = window.getSelection(),
range = sel.getRangeAt(0).cloneRange();
sel.anchorNode.parentElement.className != 'word' ?
addSpan(range) : console.log('Already tagged');
}
});
const addSpan = (range) => {
var newParent = document.createElement('span');
newParent.classList.add("word");
range.surroundContents(newParent);
}
.word{color:orange}button{margin:10px 0}
<div id="text">lorem ipsum Benny</div>
<button id="btn">Add span tag to selection</button>

How do you avoid editing tags inside of tags when replacing characters inside of a tag

So lets say you have HTML that looks something like this:
<p>This text is <b>bold</b></p>
and this Javascript:
var obs = document.getElementsByTagName("p");
var obsreplace = obs[0].innerHTML.replace(/b/gi, "g");
obs[0].innerHTML = obsreplace;
This will change the HTML to this:
<p>This text is <g>gold</g></p>
But what if I want to change it to:
<p>This text is <b>gold</b></p>
and leave the bold tags unchanged?
You'll want to use childNodes, and run your replace against each of those. That will capture text nodes separately from elements. By recursively looping through each child element until there are only text nodes left, you can safely run your replace command against all text in the element.
function safeReplace(elem, replaceFn){
let nodes = elem.childNodes, i = -1
while(++i < nodes.length){
let node = nodes[i]
if(node.nodeType == 3){ //signifies a text node
node.textContent = replaceFn(node.textContent)
} else {
safeReplace(node, replaceFn)
}
}
}
document.querySelector('button').addEventListener('click', function(){
safeReplace(document.getElementById('testDiv'), function(str){
return str.replace(/e/gi, '_E_')
})
})
<div id="testDiv">
This div has outer content, as well as <p>a paragraph containing a <span>spanned text</span> in the middle</p>More text.</div>
<button>Replace Text</button>
So, two solutions, replace with regex keeping the matched groups, or use the getElementsByTagName? For sure you have to check the length of obs and bold before doing any operation on the elements, making sure there are no errors.
var obs = document.getElementsByTagName("p");
var bold = obs[0].getElementsByTagName("b")[0];
bold.innerHTML = "gold";
var obs = document.getElementsByTagName("p");
var replaceHtml = obs[1].innerHTML.replace(/(<b>).*?(<\/b>)/, "$1gold$2");
obs[1].innerHTML = replaceHtml;
<p>This text is <b>bold</b></p>
<p>This text is <b>bold</b></p>

Insert character at specific point but preserving tags?

Update #2
Okay, more testing ensues. It looks like the code works fine when I use a faux spacer, but the regex eventually fails. Specifically, the following scenarios work:
Select words above or below the a tag
You select only one line either directly above or below the a tag
You select more than one line above/below the a tag
You select more than one line specifically below any a tag
The following scenarios do not work:
You select the line/more lines above the a tag, and then the line/more lines below the a tag
What happens when it "doesn't work" is that it removes the a tag spacer from the DOM. This is probably a problem with the regex...
Basically, it fails when you select text around the a tag.
Update:
I don't need to wrap each line in a p tag, I can instead use an inline element, such as an a, span, or label tag, with display:inline-block and a height + width to act as a new line element (<br />). This should make it easier to modify the code, as the only part that should have to change is where I get the text in between the bounds. I should only have to change that part, selectedText.textContent, to retrieve the HTML that is also within the bounds instead of just the text.
I am creating a Phonegap that requires the user to select text. I need fine control over the text selection, however, and can no longer plop the entire text content in a pre styled p tag. Instead, I need represent a linebreak with something like <a class="space"></a>, so that the correct words can be highlighted precisely. When my text looks like this:
<p class="text">This is line one
Line two
Line three
</p>
And has .text{ white-space:pre-wrap }, the following code allows me to select words, then wrap the text with span elements to show that the text is highlighted:
$("p").on("copy", highlight);
function highlight() {
var text = window.getSelection().toString();
var selection = window.getSelection().getRangeAt(0);
var selectedText = selection.extractContents();
var span = $("<span class='highlight'>" + selectedText.textContent + "</span>");
selection.insertNode(span[0]);
if (selectedText.childNodes[1] != undefined) {
$(selectedText.childNodes[1]).remove();
}
var txt = $('.text').html();
$('.text').html(txt.replace(/<\/span>(?:\s)*<span class="highlight">/g, ''));
$(".output ul").html("");
$(".text .highlight").each(function () {
$(".output ul").append("<li>" + $(this).text() + "</li>");
});
clearSelection();
}
function clearSelection() {
if (document.selection) {
document.selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
}
This code works beautifully, but not when each line is separated by a spacer tag. The new text looks like this:
<p class="text">
Line one
<a class="space"></a>
Line two
<a class="space"></a>
Line three
</p>
When I modify the above code to work with have new lines represented by <a class="space"></a>, the code fails. It only retrieves the text of the selection, and not the HTML (selectedText.textContent). I'm not sure if the regex will also fail with an a element acting as a new line either. The a element could be a span or label, or any normally inline positioned element, to trick iOS into allowing me to select letters instead of block elements.
Is there anyway to change the code to preserve the new line elements?
jsFiddle: http://jsfiddle.net/charlescarver/39TZ9/3/
The desired output should look like this:
If the text "Line one" was highlighted:
<p class="text">
<span class="highlight">Line one</span>
<a class="space"></a>
Line two
<a class="space"></a>
Line three
</p>
If the text "Line one Line two" was highlighted:
<p class="text">
<span class="highlight">Line one
<a class="space"></a>
Line two</span>
<a class="space"></a>
Line three
</p>
Of course different parts and individual letters can and will be highlighted as well instead of full lines.
Here is a solution that supports all the features from your requirements:
HTML:
<p class="text">
First Line
<a class="space"></a>
<a class="space"></a>
Second Line
<span class="space"></span>
Third Line
<label class="space"></label>
Forth Line
</p>
<ul class="output"></ul>
CSS:
.space {
display: inline-block;
width: 100%;
}
.highlighting {
background-color: green;
}
JavaScript:
var text,
output,
unwrapContents,
mergeElements,
clearSelection,
clearHighlighting,
mergeHighlighting,
handleCopy;
unwrapContents = function unwrapContents(element) {
while(element.firstChild !== null) {
element.parentNode.insertBefore(element.firstChild, element);
}
element.parentNode.removeChild(element);
};
mergeElements = function mergeElements(startElement, endElement) {
var currentElement;
endElement = endElement.nextSibling;
while((currentElement = startElement.nextSibling) !== endElement) {
startElement.appendChild(currentElement);
}
};
clearSelection = function clearSelection() {
if (document.selection) {
document.selection.empty();
} else if (window.getSelection) {
window.getSelection().removeAllRanges();
}
};
clearHighlighting = function clearHighlighting(target, exception) {
$('.highlighting', target).each(function(index, highlighting) {
if(highlighting !== exception) {
unwrapContents(highlighting);
}
});
target.normalize();
};
mergeHighlighting = function mergeHighlighting() {
var i, j;
// Remove internal highlights
$('.highlighting', text).filter(function() {
return this.parentNode.className === 'highlighting';
}).each(function(index, highlighting) {
unwrapContents(highlighting);
});
text.normalize();
// Merge adjacent highlights
first:
for(i=0; i<text.childNodes.length-1; i++) {
if(text.childNodes[i].className === 'highlighting') {
for(j=i+1; j<text.childNodes.length; j++) {
if(text.childNodes[j].className === 'highlighting') {
mergeElements(text.childNodes[i], text.childNodes[j--]);
unwrapContents(text.childNodes[i].lastChild);
} else {
switch(text.childNodes[j].nodeType) {
case 1:
if(text.childNodes[j].className !== 'space') {
continue first;
}
break;
case 3:
if(text.childNodes[j].textContent.trim() !== '') {
continue first;
}
break;
}
}
}
}
}
};
handleCopy = function handleCopy() {
var range,
highlighting,
item;
// Highlighting
range = window.getSelection().getRangeAt(0);
highlighting = document.createElement('span');
highlighting.className = 'highlighting';
highlighting.appendChild(range.cloneContents());
range.deleteContents();
range.insertNode(highlighting);
// Output
item = document.createElement('li');
item.innerHTML = highlighting.innerHTML;
clearHighlighting(item);
output.appendChild(item);
// Cleanup
mergeHighlighting();
clearSelection();
};
$(function(){
text = $('.text')[0];
output = $('.output')[0];
$(text).on('copy', handleCopy);
});
Here is a working example http://jsbin.com/efohit/3/edit
Well, I came up with a solution, rather straightforward as well.
If .text looks like this:
<p class="text">Line one
<a class="space"></a>Line two
<a class="space"></a>Line three</p>
With the exact markup and line breaks as above, then I can find each \n and replace it with a spacer element.
if (textStr.indexOf("\n") >= 0) {
textStr = textStr.replace(/\n/g, "\n<a class='space'></a>");
}
This isn't versatile at all, and will fail if there are more than one line breaks, if the tags are different, etc. So, I encourage anyone who has a better method to answer the question! It can't be that hard, I figured it out.

Jquery Hide Class when no class is present

I have some text below called (16 Courses). I need to hide only this text, but I can't seem to hide it no matter what I try using jquery. Is there any help someone could provide so I can hide on this text?
<div id="programAttributes">
<div class="left" id="credits">
<h3>Credits</h3>
<h3 class="cost">48</h3>
(16 Courses)
</div>
<div class="gutter12 left"> </div>
<div class="left" id="costPer">
<h3>Cost Per Credit</h3>
<h3 class="cost">$300</h3>
</div>
</div>
I thought if I could write something like this that would do the trick, but I am so far unsuccessful.
$("#credits:not([class!=h3])").hide();
Usage
// hides in the whole document
hideText("(16 Courses)");
// only hide inside a specific element
hideText("(16 Courses)", $('#programAttributes'));
// make it visible again
showText("(16 Courses)");
[See it in action]
CSS
.hiddenText { display:none; }
Javascript
// escape by Colin Snover
RegExp.escape = function(text) {
return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&");
}
function hideText(term, base) {
base = base || document.body;
var re = new RegExp("(" + RegExp.escape(term) + ")", "gi");
var replacement = "<span class='hiddenText'>" + term + "</span>";
$("*", base).contents().each( function(i, el) {
if (el.nodeType === 3) {
var data = el.data || el.textContent || el.innerText;
if (data = data.replace(re, replacement)) {
var wrapper = $("<span>").html(data);
$(el).before(wrapper.contents()).remove();
}
}
});
}
function showText(term, base) {
var text = document.createTextNode(term);
$('span.hiddenText', base).each(function () {
this.parentNode.replaceChild(text.cloneNode(false), this);
});
}
You can check for and remove textnodes like this:
​$("#credits").contents().filter(function() {
if(this.nodeType == 3)
this.parentNode.removeChild(this);
});​​​​​​
You can test it here, this gets all the nodes (including text nodes) with .contents(), then we loop through, if it's a text node (.nodeType == 3) then we remove it.
Could you wrap it in a separate span, and then do:
$('#credits span').hide();
?
Try wrapping the text in a span as follows:
<div class="left" id="credits">
<h3>Credits</h3>
<h3 class="cost">48</h3>
<span id="toHide">(16 Courses)</span>
</div>
then you can use jquery:
$("#credits > span)").hide();
the hide() function has to be applied to a DOM element.
I would use a label tag around the text so I can handle it with jquery.
It's textnode. Loop thru all parents nodes and if it's type is textnode, hide it. See also this:
How do I select text nodes with jQuery?

Categories

Resources