Splitting node content in JavaScript DOM - - javascript

This existing answer is an excellent piece of code that very nearly does what I want. Like the OP in that questions I want HTML tags to be split, but based on a tag rather than an offset, and bounded by an item that should not be split.
That is, I want to turn this:
<p>
<strong>hi there, how <em>are <span>y<!--break-->ou</span> doing</em> today?</strong>
</p>
into this:
<p>
<strong>hi there, how <em>are <span>y</span></em></strong>
<!--break-->
<strong><em><span>ou</span> doing</em> today?</strong>
</p>
I'm still getting my head around javascript so while I had a play with the jsbin provided by #Hemlock I couldn't get it to do what I intended.
The given answer was:
function splitNode(node, offset, limit) {
var parent = limit.parentNode;
var parentOffset = getNodeIndex(parent, limit);
var doc = node.ownerDocument;
var leftRange = doc.createRange();
leftRange.setStart(parent, parentOffset);
leftRange.setEnd(node, offset);
var left = leftRange.extractContents();
parent.insertBefore(left, limit);
}
function getNodeIndex(parent, node) {
var index = parent.childNodes.length;
while (index--) {
if (node === parent.childNodes[index]) {
break;
}
}
return index;
}

No ranges required, you just need to duplicate all the cut elements and move their children around:
function splitOn(bound, cutElement) {
// will divide the DOM tree rooted at bound to the left and right of cutElement
// cutElement must be a descendant of bound
for (var parent = cutElement.parentNode; bound != parent; parent = grandparent) {
var right = parent.cloneNode(false);
while (cutElement.nextSibling)
right.appendChild(cutElement.nextSibling);
var grandparent = parent.parentNode;
grandparent.insertBefore(right, parent.nextSibling);
grandparent.insertBefore(cutElement, right);
}
}
(jsfiddle demo)

You could build your own split function by thinking how to split the content into an array and later concatinate the string together.
the problem with this answer is that it does not start/nor finish any split tag, like in your situation, is the SPAN element.
<script>
var content = document.getElementById('content');
var elements = document.getElementsByTagName('strong');
var array = element.split("<!--break-->");
var string = '';
for(var i = 0; i < array.length; i++) {
string += '<strong>' + sarray[i] + "</strong>';
}
content.innerHTML = string;
</script>
<div id="content">
<strong>hi there, how <em>are <span>y<!--break-->ou</span> doing</em> today?</strong>
</div>

Related

Compare and remove array elements

i have a bug in this code that i cannot seem to solve. if there is only 1 instance of Act, it works as it should. But when there is more than 1 instance of Act, it breaks. Not sure what I am missing here.
//Find all instances of italics
var findItalics = new RegExp(/(<em>.*?<\/em>)/g);
var italicsArray = [];
var italicCount;
while (italicCount = findItalics.exec(searchInput)) {
italicsArray.push(italicCount[0]);
}
//Find the italics containing the word 'Act'
var keywordItalics = new RegExp(/<em>.*?(Act).*?<\/em>/g);
var keywordItalicArray = [];
var italicCountKeyword;
while (italicCountKeyword = keywordItalics.exec(italicsArray)) {
keywordItalicArray.push(italicCountKeyword[0]);
}
//Remove all instances of the keyword(s)
for (var tlcs = italicsArray.length - 1; tlcs >= 0; tlcs--) {
if(italicsArray[tlcs] == keywordItalicArray) {
italicsArray.splice(tlcs, 1);
}
}
Thanks to #artgb who helped me rethink this.
//Find all instances of italics
var findItalics = new RegExp(/(<em>.*?<\/em>)/g);
var italicsArray = [];
var italicCount;
while (italicCount = findItalics.exec(searchInput)) {
italicsArray.push(italicCount[0]);
}
//Find the italics containing the word 'Act'
var keywordItalics = new RegExp(/<em>.*?(Act).*?<\/em>/g);
var keywordItalicArray = [];
var italicCountKeyword;
while (italicCountKeyword = keywordItalics.exec(searchInput)) {
keywordItalicArray.push(italicCountKeyword[0]);
}
//Remove all instances of the keyword(s)
for(var xXx = 0; xXx < keywordItalicArray.length; xXx++){
for (var tlcs = italicsArray.length - 1; tlcs >= 0; tlcs--) {
if(italicsArray[tlcs] == keywordItalicArray[xXx]) {
italicsArray.splice(tlcs, 1);
}
}
}
var keywordItalics = new RegExp(/<em>.*?(Act).*?<\/em>/g);
Should usually be shortened to:
var keywordItalics = /<em>.*?(Act).*?<\/em>/g;
Where your () are, this would only get a capture of "Act", so to capture whole string in the em, it should be:
var keywordItalics = /<em>(.*?Act.*?)<\/em>/g;
However, a faster way (without regexp) you could get an array of all the emphasized tags just by:
var keywordItalics = document.getElementsByTagName('em');
If you're just trying to get rid of all em's containing "Act", all you should need is:
document.body.innerHTML = document.body.innerHTML.replace(
/<em>.*?Act.*?<\/em>/g,
''
);
This should remove all traces of em's containing "Act" in the document (effectively replacing those strings with an empty string, aka nothing). It will cause a reflow, however. If they are inside a containing element besides body, would be better to get containing element first (instead of using body). There are "better" ways of doing this, but this is probably simplest coding-wise.
Update: an easy way to remove em's with "Act" from the array would be:
italicsArray = italicsArray
.join('_SEP_') // Convert to string
.replace(/<em>.*?Act.*?<\/em>/g,'') // Delete matched entries
.replace(/(_SEP_)+/g,'_SEP_') // Collapse multiple seperators
.split('_SEP_') // Convert back to array
;
This basically uses a seperator _SEP_ (to avoid collisions with strings containing ',') and turns the array into a string, deletes all matches to your regexp, removes what would become undefined entries, and recreates the array in the same name.

Dynamically loading multiple <li>'s with a javascript for loop - nothing loading yet

I'm trying to load X amount of <li>'s into a <ul> via a for loop in a jquery function, and while I think I've got the syntax about right I'm not getting anything loading. (no problem with loading a single <li>, but none for multiples with the method I've tried)
Initially I attempted to pass a variable into the loop to determine the amount of increments: var peekListAmount = 5;
That didn't work so I went for a bog-standard loop incrementer. That doesn't work either so, after searching here and getting close, I have put together a fiddle to see if someone can point out what I'm doing wrong: http://jsfiddle.net/janowicz/hEjxP/8/
Ultimately I want to use Knockout.js to dynamically input a number to pass to the loop amount variable, but 1st things 1st.
Many thanks in advance.
When you do:
var peekListItem = $('<li>...</li>');
you're creating a single instance of an <li> node, encapsulated in a jQuery object.
Appending an already-present node to the DOM just removes it from its current place in the DOM tree, and moves it to the new place.
You need to create the node inside the loop, not outside, otherwise you're just re-appending the same node each time, not a copy of that node.
In fact, given you're not manipulating that node, you can just put the required HTML directly inside the .append() call without wrapping it in $(...) at all:
$(function() {
var peekList = $('<ul class="peekaboo-list">').appendTo('div.peekaboo-wrap');
function addLiAnchorNodes(nodeAmount) {
var html = '<li>' +
'<p class="peekaboo-text"></p></li>';
for (var i = 0; i < nodeAmount; ++i) {
peekList.append(html);
}
}
addLiAnchorNodes(5);
});
See http://jsfiddle.net/alnitak/8xvbY/
Here is you updated code
$(function(){
var peekList = $('<ul class="peekaboo-list"></ul>');
var peekListItem = '<li><p class="peekaboo-text"></p></li>';
//var peekListAmount = 5;
var tmp = '';
var addLiAnchorNodes = function (nodeAmount){
//var nodeAmount = peekListAmount;
for (var i = 0; i < 10; i++){
tmp += peekListItem;
}
peekList.append(tmp);
$('div.peekaboo-wrap').append(peekList); // This bit works fine
}
addLiAnchorNodes();
});
This should work. Instead of appending the list item in each loop, append the list only once at the end.
$(function(){
var peekList = $('<ul class="peekaboo-list"></ul>');
peekList.appendTo('div.peekaboo-wrap');
var addLiAnchorNodes = function (nodeAmount){
var list = "";
for (var i = 0; i < 10; i++){
list += '<li>Sample<p class="peekaboo-text"></p></li>';
}
peekList.append(list);
}
addLiAnchorNodes();
});
Here is the updated fiddle
Try this:
$(function(){
var peekList = $('<ul class="peekaboo-list"></ul>');
$(peekList).appendTo('div.peekaboo-wrap'); // This bit works fine
var addLiAnchorNodes = function (nodeAmount){
//var nodeAmount = peekListAmount;
for (var i = 0; i < 10; i++){
var peekListItem = $('<li><p class="peekaboo-text"></p></li>');
peekListItem.appendTo(peekList);
}
}
addLiAnchorNodes();
});

How could I wrap an area of a string with an html element?

If I had something like the following:
$400/week
$800/week
With jQuery being available, what would be an ideal method of wrapping the "/week" part in an html element like <span class='small'>/week</span>?
You could do something like this:
var data = $("#text").html();
$("#text").html(data.replace(/\/\s*week/g, '<span class="small">/week</span>'));
Working example: http://jsfiddle.net/jfriend00/TKa8D/
Note: Replacing HTML will undo any event handlers assigned within that HTML so you should generally replace the HTML at the lowest level possible so you aren't replacing elements that already have event handlers on them. If you share your specific HTML, we could make more concrete recommendations for the easiest way.
It's not pretty, but the only safe way to do it (preserving event handlers and such) is to walk the tree, splitting text nodes where necessary. Unfortunately, jQuery does not like working with text nodes; it is very element-centric.
You can start by defining a helper function to do the recursion:
function recurse(node, fn) {
if(node.nodeType === Node.TEXT_NODE) {
fn(node);
}else if(node.nodeType === Node.ELEMENT_NODE) {
var children = Array.prototype.slice.call(node.childNodes);
for(var i = 0, l = children.length; i < l; i++) {
recurse(children[i], fn);
}
}
}
It walks every node in the tree, calling a given function for every text node. Then we can use that function to split the text nodes and insert a span in the middle:
recurse(document.documentElement, function(node) {
var re = /(\$\d+)(\/week)/;
var match;
while(match = re.exec(node.nodeValue)) {
var before = document.createTextNode(node.nodeValue.substring(0, match.index + match[1].length));
node.parentNode.insertBefore(before, node);
var span = document.createElement('span');
span.className = 'week';
span.textContent = node.nodeValue.substring(match.index + match[1].length, match.index + match[0].length);
node.parentNode.insertBefore(span, node);
node.nodeValue = node.nodeValue.substring(match.index + match[0].length);
}
});
Try it.

get SINGLE text node from DOM object

Need to get all direct nodes from DOM element and don't actually know, how it many and what kind they are.
.contents()?
Ok, let's see..
$('<div />').html('<p>p</p>').contents() ->
[<p>ā€‹pā€‹</p>ā€‹]
Ok.
$('<div />').html('textNode').contents() -> []
WTF?
$('<div />').html('textNode').append('another').contents() ->
["textNode", "another"]
Ok, so what about single text node?
I don't know if this is helpful. A while ago I built a Document Fragment generator using JSON styled input. I also wrote a (somewhat working) reverse function for it so you could turn your nodeList into a JSON string.
https://gist.github.com/2313580
var reverseFunction = function(DOM /* DOM tree or nodeList */) {
var tree = [];[].forEach.call(DOM, function(obj) {
if (obj instanceof Text) {
tree.push({
'textContent': obj.textContent
});
} else {
var tmp = {};
tmp['tagName'] = obj.nodeName;
for( var data in obj.dataset ) {
tmp['data-' + data] = obj.dataset[data];
}
for (var i = 0, l = obj.attributes.length; i < l; i++) {
var key = obj.attributes[i].name,
val;
if (key.indexOf('data-') === -1) {
switch (key) {
case ('class'):
key = 'className';
break;
case ('style'):
val = {};
obj.attributes[i].value.split(';').forEach(function(rule) {
var parts = rule.split(':');
val[parts[0]] = parts[1];
});
break;
};
tmp[key] = val || obj.attributes[i].value;
}
}
if (obj.childNodes.length > 0) {
tmp['childNodes'] = reverseFunction(obj.childNodes);
}
tree.push(tmp);
}
});
return tree;
};
This does find textNodes and separates them... You may be able to extract something from it.
Update: to answer a comment in your question above...
var div = document.createElement('div');
div.appendChild(document.createTextNode('dsf'));
console.log( div.childNodes.length, div.childNodes, div.childNodes[0].textContent);ā€‹
I hope this makes a bit more sense to you know. The array appears empty in the console but it is not. check the length and attempt to access it and you will see.
.contents() is concerned with DOM nodes. That string in the 2nd example is not a DOM element.

Remove All Nodes with (nodeName = "script") from a Document Fragment *before placing it in dom*

My goal is to remove all <[script]> nodes from a document fragment (leaving the rest of the fragment intact) before inserting the fragment into the dom.
My fragment is created by and looks something like this:
range = document.createRange();
range.selectNode(document.getElementsByTagName("body").item(0));
documentFragment = range.cloneContents();
sasDom.insertBefore(documentFragment, credit);
document.body.appendChild(documentFragment);
I got good range walker suggestions in a separate post, but realized I asked the wrong question. I got an answer about ranges, but what I meant to ask about was a document fragment (or perhaps there's a way to set a range of the fragment? hrmmm). The walker provided was:
function actOnElementsInRange(range, func) {
function isContainedInRange(el, range) {
var elRange = range.cloneRange();
elRange.selectNode(el);
return range.compareBoundaryPoints(Range.START_TO_START, elRange) <= 0
&& range.compareBoundaryPoints(Range.END_TO_END, elRange) >= 0;
}
var rangeStartElement = range.startContainer;
if (rangeStartElement.nodeType == 3) {
rangeStartElement = rangeStartElement.parentNode;
}
var rangeEndElement = range.endContainer;
if (rangeEndElement.nodeType == 3) {
rangeEndElement = rangeEndElement.parentNode;
}
var isInRange = function(el) {
return (el === rangeStartElement || el === rangeEndElement ||
isContainedInRange(el, range))
? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
};
var container = range.commonAncestorContainer;
if (container.nodeType != 1) {
container = container.parentNode;
}
var walker = document.createTreeWalker(document,
NodeFilter.SHOW_ELEMENT, isInRange, false);
while (walker.nextNode()) {
func(walker.currentNode);
}
}
actOnElementsInRange(range, function(el) {
el.removeAttribute("id");
});
That walker code is lifted from: Remove All id Attributes from nodes in a Range of Fragment
PLEASE No libraries (ie jQuery). I want to do this the raw way. Thanks in advance for your help
The easiest way to gather all <script> nodes would be to use getElementsByTagName, but unfortunately that is not implemented on DocumentFragment.
However, you could create a temporary container and append all elements within the fragment, and then go through and remove all <script> elements, like so:
var temp = document.createElement('div');
while (documentFragment.firstChild)
temp.appendChild(documentFragment.firstChild);
var scripts = temp.getElementsByTagName('script');
var length = scripts.length;
while (length--)
scripts[length].parentNode.removeChild(scripts[length]);
// Add elements back to fragment:
while (temp.firstChild)
documentFragment.appendChild(temp.firstChild);
Correct me if I'm wrong, but if the documentFragment is a real DOM Fragment, you should be able to do something like:
var scripts = documentFragment.getElementsByTagName('script');
if (scripts.length){
for (var i=0, l = scripts.length;i<l;i++){
documentFragment.removeChild(scripts[i]);
}
}
right?
Correction: you can't apply getElementsByTagName to a documentFragment, J-P is right. You can however us a child of the fragment if it is a (cloned) node supporting getElementsByTagName. Here's some (working) code I use within a larger script a few days ago:
var fragment = d.createDocumentFragment(), f;
fragment.appendChild(document.createElement('div'));
fragment.firstChild.appendChild(zoeklijst.cloneNode(true));
f = fragment.firstChild;
return f.getElementsByTagName(getList); //<==
2022, Chrome, querySelector family works on document fragments:
frag.content.querySelectorAll('script').forEach(
(s)=>s.remove()
);

Categories