Merging Text Nodes Together After Inserting Span - javascript

I have an extension where I am storing/retrieving a section of the DOM structure (always a selection of text on the screen) the user has selected. When I am storing a selection, I enclose the section in a SPAN tag, and highlight the text in yellow. This causes the DOM structure around the selected text to split up into various text nodes. This causes a problem for me as when I try to restore this selection (without refreshing the page) it causes problems as the DOM structure has been modified.
My question is how do I prevent the DOM structure from splitting up after inserting the SPAN? If this cannot be achieved, how would I reassemble the DOM structure after removing the SPAN tag to its original state?
//Insert the span
var sel = restoreSelection(mootsOnPage[i].startXPath);
var range = sel.getRangeAt(0).cloneRange();
var newNode = document.createElement('span');
newNode.className = 'highlightYellow';
range.surroundContents(newNode);
//Original DOM structure
<p>Hello there, how are you today</p>
//What the DOM looks like after insertion of SPAN
<p>
"Hello there, "
<span class="highlightYellow">how</span
" are you today"
</p>

Use element.normalize().
After you remove the span you inserted, you can use the element.normalize() method to merge the extra text nodes that were created as a result of the insertion/removal of the span. The normalize() method puts the specified element and all of its subtree into a "normalized" form (i.e. no text nodes in the subtree are empty and there are no adjacent text nodes). Found, thanks to #tcovo's comment.
Text nodes inside of an element are broken apart if you insert nodes and then remove them. Unfortunately they don't automatically re-merge once the extra node is removed. To answer peoples' questions as to "why" this matters, it usually causes issues when working with text highlighting in your UI.

The very act of inserting a <span> tag will alter the DOM. That's, somewhat by definition, what you're doing when you call surroundContents(). You can't add a span tag without altering the DOM which includes splitting text nodes and adding new elements for the span.
Further, unless the selected text includes only whole text nodes and the selection never starts/stops in the middle of a text node, you will have to split text nodes to put the span in the right place. When you later remove the span tags, you will have extra text nodes. That shouldn't really matter to anything, but if you really think you have to get the split text nodes back to the way they were, I can think of a couple options:
1) Save the original parentNode before the span is inserted into it. Clone it, add your span to the clone, replace the original node with the clone and save the original. When you want to restore, put the original back and remove the cloned one.
2) When you remove the span, run a function that looks for neighboring text nodes and combine them.
3) Figure out why it matters that there are more text nodes afterwards than there were before because this should not matter to any code or display.

When using normalize() pay attention!
It will strip away nodes like <br/> and will alter the text and its visualisation.
normalize() is good, but it has its drawbacks.
So <p>"this is an "<br/>"example"</p> will turn into <p>this is an example</p>
Is there a way to use normalize() but keeping the <br/>s?

You can concatenate and then remove the second node
node1.textContent += node2.textContent;
node2.remove();

You can use this to unwrap your content.
$(".highlightYellow").contents().unwrap();
Demo: http://jsfiddle.net/R4hfa/

Related

Isn't text node a child when using children function?

I have the following html code:
<h1>
When
<!-- Green highlight effect -->
<span class="highlight">banking</span>
meets<br />
<span class="highlight">minimalist</span>
</h1>
And i wrote the following code in js:
const h1 = document.querySelector('h1');
// Going downwards: child
console.log(h1.childNodes); //prints text element(**When**)
console.log(h1.children); // doesn't print text element (**When**)
i understand that children function prints only direct child, but As far as i understand, When is indeed direct child of h1.
Can someone explain me if i'm wrong?
Isn't text node a child when using children function?
Yes. Text nodes are nodes and can be child nodes of an element.
doesn't print text element
There's no such thing as a text element (at least in HTML, SVG has one). Element nodes and text nodes are different kinds of nodes.
You're reading too much into the name of the property.
See the MDN documentation:
children is a read-only property that returns a live HTMLCollection which contains all of the child elements
or the specification:
The children attribute’s getter must return an HTMLCollection collection rooted at this matching only element children.
If you want to get all the nodes, including the text nodes, then use childNodes. DOM doesn't need two properties that do the same thing and it is often useful to get just the elements (especially if the only text nodes contain only white space).

When and how does firstChild select #text nodes?

Bit new to JS here, so I apologize if this is something obvious. I've read through the relevant documentation, and I'm a bit perplexed about how and when exactly firstChild selects text nodes.
I have a span and an input like so:
<span class="checkbox">
<input class="inputs" value="1">
</span>
On page load, if I call:
$(".checkbox").firstChild
I'll get back that input html element. Now, if I make an ajax call that replaces the entire span and its input with identical code, and then call:
$(".checkbox").firstChild
I get a #text node element back. Why? It may be that a more pertinent question is when are #text nodes inserted into whitespaces?
Please let me know if you need some more context and I appreciate you taking the time to help a beginner out.
Whitespace between nodes creates text nodes, so depending on whether there's any space/newline/tab between the closing > of the parent and the opening < of the child you may or may not get text nodes.
Use firstElementChild instead. Similar equivalents exist for sibling traversal.
Other options are to adjust your CSS selector to get the first child or use jquery's traversal methods.

Can execCommand or a similar function be used to set, rather than to toggle, a simple style like bold?

If a contenteditable div's contents are already bold, then execCommand("bold") will remove the bold style.
That's great in normal circumstances, but I have a situation where I'd like to loop through a bunch of contenteditable divs and set any non-bold text to bold, sort of a way to style multiple rich text elements at once, using the proper or or 'font-weight' whatevers that execCommand uses.
I can do it a hard way by scrutinizing each node in each div in isolation, but I want to make sure there isn't a simpler way first.
It depends on how complex the editor you are making is. If you don't want to allow normal text inside a bold region, it may be enough to test only the deepest containers of editable texts, so one element for each of the editable divs.
Here's an example,
function bold(node) {
var selection = window.getSelection();
var range = document.createRange();
range.selectNodeContents(node);
selection.removeAllRanges();
selection.addRange(range);
if (!isBold(getDeepestContainer(selection.anchorNode)))
document.execCommand("bold");
selection.removeAllRanges();
node.blur();
}
function getDeepestContainer(node) {
var result = node;
while (result.childNodes.length === 1) {
result = result.firstChild;
}
if (result instanceof Element)
return result;
return result.parentElement;
}
http://codepen.io/anon/pen/NqZQzR?editors=101
Okay, I couldn't find an easy way, so I did it the hard way.
I already had a function that copied the contents of a single element to a working space div with clean CSS, and then recursed through each node within that working space. For each text node, it would loop through each style I'm looking for and see if that style is set. If it's NOT, it would clear an "every node has this style" flag set before its run. So I ended up with a list of every style set for every node.
The first step for this current task was to identify which styles applied to all selected elements. So I took that function and created a modified version that first added each element's contents to the working space div. Then it ran the normal recursive walk through the working space, basically treating each separate element as if they're part of the same complex rich text string.
That gave me the status of each style, like whether everything was bold and thus the bold button should show as pressed.
I also added an array for each style. As I recursed through the elements, tracking their individual "every node has this style" value, I would come up with a list of elements where every node had the style, and thus execcommand("bold") would toggle that element.
So now, for each style (like for the bold button), I knew whether it would toggle any elements, or all elements. If it would toggle all elements, or no elements, then that's fine because the behavior's consistent. But if it toggled a subset of elements (if "elements_that_would_toggle" array length for a style was greater than zero but less than the total_elements count), then I would ignore those toggle-able elements on the first button click.
At the end of that click processing, I then blank out those arrays for each style, thus reverting the behavior across the board to toggling, because now every element has been set to the same status.
It's sort of a disgusting approach, but it's controlled and consistent, and it works really well.
And it only took a day to get working.

text in HTML becomes split / malformed in the DOM when elements are inserted around text and then removed (preserving the inner text, though)

So, here is normal text: just your standard paragraph.
I have javascript that will insert a span around selected text (for highlighting purposes).
The problem is that when I remove the span, the nice text block becomes chunky, and malformed:
How do I restore the block of text to its original state?
I think you must to keep the original node, and replace it cloning the innerHTML with your span-wraper. Then just replace the nodes.
Instead of removing the span, try replacing the content of the standard paragraph with its original data.

Javascript/jQuery substring highlighting problem

I am using Javascript to calculate the offset and length of a substring selected on my page. This gets stored somewhere and later, when I hover over the text I want to highlight certain words in it using that offset and length in jQuery. Here is the basic code used for the highlight:
content = $("#reader").html();
newContent = content.substring(0,offset)+'<font style="color: red;">'+content.substring(offset,offset+length)+'</font>'+content.substring(offset+length,content.length);
content = $("#reader").html(newContent);
Now my problem is this: the offset and length are calculated over what is displayed on the screen. The actual HTML code, however, may also contain <p> or other tags. As a result, the text that I highlight gets "shifted", depending on the amount of HTML code present.
What is the easiest way to solve this?
This is not easily solvable. You shouldn’t calculate offset and length over the text, but over the HTML, if you want to work on the HTML afterwards. However, inserting HTML tags somewhere into an existing HTML string may lead to invalid nesting. And finally, you lose state like attached event handlers if you re-insert nodes as HTML code.
use
$("#reader").text()
instead of $("#reader").html() to get the text out of your content
I'd strip tags or use DOM methods - like. .nodeType, .nodeValue.

Categories

Resources