Why does the JS function stop execution after the first html element? - javascript

It is necessary to leave only the li tags.
Of the other tags, leave only the text.
My code:
let html = `
<ol>
<li><code>foo</code> link text;</li>
<li><code>bar</code> link text;</li>
</ol>
<p>Paragraph text <code>baz</code> and <code>biz</code> text.</p>
<p>Paragraph text.</p>
`;
html = `<body>${html}</body>`;
let parsed = new DOMParser().parseFromString( html, 'text/html' );
function testFn( node ) {
node.childNodes.forEach( function( e ) {
testFn( e );
if ( e.nodeType !== Node.ELEMENT_NODE ) return;
if ( e.nodeName.toLowerCase() !== 'li' ) {
e.replaceWith( ...e.childNodes );
}
});
}
testFn( parsed.body );
console.log( parsed.body.innerHTML );
Result:
<li>foo link text;</li>
<li>bar link text;</li>
<p>Paragraph text <code>baz</code> and <code>biz</code> text.</p>
<p>Paragraph text.</p>
And I need such a result:
<li>foo link text;</li>
<li>bar link text;</li>
Paragraph text baz and biz text.
Paragraph text.
Why doesn't the function handle paragraphs?

forEach() is not ideal for conditional recursion and only adds complexity in its closure over the passed array and handling of the callback. Here is your logic transposed to a for...of loop.
let html = `
<ol>
<li><code>foo</code> link text;</li>
<li><code>bar</code> link text;</li>
</ol>
<p>Paragraph text <code>baz</code> and <code>biz</code> text.</p>
<p>Paragraph text.</p>
`;
html = `<body>${html}</body>`;
let parsed = new DOMParser().parseFromString(html, 'text/html');
function testFn(node) {
for (const childNode of node.childNodes) {
testFn(childNode);
if (childNode.nodeType !== Node.ELEMENT_NODE) {
continue;
}
if (childNode.nodeName.toLowerCase() !== 'li') {
childNode.replaceWith(...childNode.childNodes);
}
}
}
testFn(parsed.body);
console.log(parsed.body.innerHTML);

You are changing the childNodes while iterating over them. Use copies and it works:
let html = `
<ol>
<li><code>foo</code> link text;</li>
<li><code>bar</code> link text;</li>
</ol>
<p>Paragraph text <code>baz</code> and <code>biz</code> text.</p>
<p>Paragraph text.</p>
`;
html = `<body>${html}</body>`;
let parsed = new DOMParser().parseFromString(html, 'text/html');
function testFn(node) {
[...node.childNodes].forEach(function(e) {
testFn(e);
});
[...node.childNodes].forEach(function(e) {
if (e.nodeType !== Node.ELEMENT_NODE) return;
if (e.nodeName.toLowerCase() !== 'li') {
e.replaceWith(...e.childNodes);
}
});
}
testFn(parsed.body);
console.log(parsed.body.innerHTML);

Related

How to find DOM elements that exist between two other "well known" DOM elements

How, using vanilla JavaScript can i most efficiently (and elegantly) select all elements between two other specified elements? For example, how could I pick all elements between:
<h2> and <hr> .. which would result in an [ p, p, ul ] ... notice the <p>this should not be found</p> should not be "found" since its there is no after it.
I know i can write a bunch of JS to check siblings, and iterate through - but im hoping there is a more elegant way im not seeing. I'd be find if there was some querySelectorAll(..) selector machination i could use as well (or like an intersection of 2 invocations) ..
<div>
<h1>Title</h1>
<h2>Subitle</h2>
<p>Para 1 link</p>
<p>Para 2</p>
<ul><li>A list</li></ul>
<hr/>
<h2>Another subtitle</h2>
<p>this should not be found</p>
</div>
Here's a couple of possible solutions:
const getChildrenInRange1 = (parent, firstChildSelector, lastChildSelector) => {
const res = [];
let current = parent.querySelector(firstChildSelector)?.nextElementSibling;
while (current) {
if (current.matches(lastChildSelector)) {
return res;
}
res.push(current);
current = current.nextElementSibling;
}
return [];
}
const getChildrenInRange2 = (parent, firstChildSelector, lastChildSelector) => {
const afterLast = parent
.querySelectorAll(`${firstChildSelector} ~ ${lastChildSelector} ~ *`);
if (afterLast.length === 0) {
return [];
}
const afterFirstWithoutLast = parent
.querySelectorAll(`${firstChildSelector} ~ *:not(${lastChildSelector})`);
return _.difference(afterFirstWithoutLast, afterLast);
}
const container = document.getElementById('container');
console.log(getChildrenInRange1(container, 'h2', 'hr'));
console.log(getChildrenInRange2(container, 'h2', 'hr'));
<script src="https://cdn.jsdelivr.net/npm/lodash#4.17.21/lodash.min.js"></script>
<div id="container">
<h1>Title</h1>
<h2>Subitle</h2>
<p>Para 1 link</p>
<p>Para 2</p>
<ul><li>A list</li></ul>
<hr/>
<h2>Another subtitle</h2>
<p>this should not be found</p>
</div>

How to validate data into paragraph's text using javascript?

I want to check if time and day is same when I click to add these paragraphs into the array, If time and day is same then also prevent adding items into an array. Can Anyone help me please.Thanks
window.addEventListener("load", function() {
var test = [];
[...document.querySelectorAll(".container p")].forEach(function(para) {
para.addEventListener("click", function(e) {
var text = e.currentTarget.textContent;
if (test.indexOf(text) === -1)
{
test.push(text);
}
console.log(test)
})
})
})
<div class="container">
<p>
<b>Group:N</b>Code:1234<br/>
<b>Session Type:</b>CS<br/>
<b>Location:</b>Main Hall<br/>
<b>Time:</b>11:00<br/>
<b>Day:</b>Tuesday<br/>
<b>Duration:</b>1hour<br/>
</p>
</div>
<div class="container">
<p>
<b>Group:M</b>Code:98743<br/>
<b>Session Type:</b>NP<br/>
<b>Location:</b>Main Hall2<br/>
<b>Time:</b>11:00<br/>
<b>Day:</b>Tuesday<br/>
<b>Duration:</b>1hour<br/>
</p>
</div>

cheerio: Get normal + text nodes

I am using cheerio to parse HTML code in different nodes. I can easily do $("*"), but this gets me only normal HTML nodes and not the separate text nodes. Lets consider 3 user inputs:
One:
text only
I need: single text node.
Two:
<div>
text 1
<div>
inner text
</div>
text 2
</div>
I need: text node + div node + text node in same sequence.
Three:
<div>
<div>
inner text 1
<div>
inner text 2
</div>
</div>
<div>
inner text 3
</div>
</div>
I need: 2 div nodes
Possible?
In hope to help someone, filter function seems to return text nodes also.
I got help from this answer: https://stackoverflow.com/a/6520267/3800042
var $ = cheerio.load(tree);
var iterate = function(node, level) {
if (typeof level === "undefined") level = "--";
var list = $(node).contents().filter(function() { return true; });
for (var i=0; i<=list.length-1; i++) {
var item = list[i];
console.log(level, "(" + i + ")", item.type, $(item).text());
iterate(item, level + "--");
}
}
iterate($.root());
HTML input
<div>
text 1
<div>
inner text
</div>
text 2
</div>
Result
-- (0) tag
text 1
inner text
text 2
---- (0) text
text 1
---- (1) tag
inner text
------ (0) text
inner text
---- (2) text
text 2
I hope the following codes can help you.
const cheerio = require("cheerio");
const htmlText = `<ul id="fruits">
<!--This is a comment.-->
<li class="apple">Apple</li>
Peach
<li class="orange">Orange</li>
<li class="pear">Pear</li>
</ul>`;
const $ = cheerio.load(htmlText);
const contents = $('ul#fruits').contents();
console.log(contents.length);// 9, since nodes like '\n' are included
console.log(new RegExp('^\\s*$').test('\n '));
function isWhitespaceTextNode(node){
if(node.type !== 'text'){
return false;
}
if(new RegExp('^\\s*$').test(node.data)){
return true;
}
return false;
}
//Note here: filter is a function provided by cheerio, not Array.filter
const nonWhitespaceTextContents = contents.filter(nodeIndex=>{
const node = contents[nodeIndex];
if(isWhitespaceTextNode(node)){
return false;
}else{
return true;
}
});
console.log(nonWhitespaceTextContents.length);// 5, since nodes like '\n ' are excluded
nonWhitespaceTextContents.each((_, node)=>console.log(node));
//[comment node]
//[li node] apple
//[text node] peach
//[li node] orange
//[li node] pear
If you want all of the immediate children of a node, both text nodes and tag nodes, use .contents() and filter out whitespace-only text nodes.
Here's the code running on your examples:
const cheerio = require("cheerio"); // 1.0.0-rc.12
const tests = [
// added a div container to make the parent selector consistent
`<div>text only</div>`,
`<div>
text 1
<div>
inner text
</div>
text 2
</div>`,
`<div>
<div>
inner text 1
<div>
inner text 2
</div>
</div>
<div>
inner text 3
</div>
</div>`
];
tests.forEach(html => {
const $ = cheerio.load(html);
const result = [...$("div").first().contents()]
.filter(e => e.type !== "text" || $(e).text().trim())
// the following is purely for display purposes
.map(e => e.type === "text" ? $(e).text().trim() : e.tagName);
console.log(result);
});
Output:
[ 'text only' ]
[ 'text 1', 'div', 'text 2' ]
[ 'div', 'div' ]
If you only want the text nodes and not the tags, see How to get a text that's separated by different HTML tags in Cheerio.

JavaScript: Basic function to go back/forward

So, I made a JavaScript function which helps me to define several parts that are hidden at first but can be shown when clicking a link (I suck at explaining, sry... you'll see what I mean when having a look at the code snippet)
The idea that I had then was to make a function that would show the next 'page' (make the id 1 higher) and another function that would go one 'page' back (reduce the id by 1, so to speak).
Here's the JS and HTML code:
function change_page (id) {
var content_raw = document.getElementById(id);
var content = content_raw.innerHTML;
var display = document.getElementById('replace');
display.innerHTML = content;
}
function page_back () {
//function to go one page back
}
function page_forward () {
//function to go one page forward
}
<div id='replace'>
This is page 1 (shown by default)
</div>
<div style='display:none;'>
<p id="1">This is page 1</p>
<p id="2">This is page 2</p>
<p id="3">This is page 3</p>
<p id="4">This is page 4</p>
<p id="5">This is page 5</p>
</div>
<p style="text-align:center; width:100%;">
Back
Page 1 |
Page 2 |
Page 3 |
Page 4 |
Page 5
Forward
</p>
I really hope that you guys can help me because although I can programm quite well in PHP, I hardly can with JS.
Create a helper variable
var currentPage = 1;
Then, in the change_page function you would always update this value
function change_page (id) {
currentPage = id;
...
And the page_back/page_forward would be calling change_page function with either increased or decreased value of currentPage. So page_back could look like this
function page_back() {
change_page(currentPage - 1);
}
Lastly, you need to make sure that the page_back and page_forward will never result in trying to load a page below 1/above the actual number of pages. To secure that, in the change_page function, you could check whether id is not less than 1 and at the same time it is smaller than max number of pages. The check could look like
function change_page (id) {
if (id < 1 || id > 5)
return;
currentPage = id;
...
Just make a global variable to store the current page you're in.
function change_page (id) {
_currentPage = id;
var content_raw = document.getElementById(_currentPage);
var content = content_raw.innerHTML;
var display = document.getElementById('replace');
display.innerHTML = content;
}
function page_back () {
change_page(_currentPage-1);
}
function page_forward () {
change_page(_currentPage+1);
}
// global variable
var _currentPage;
<div id='replace'>
This is page 1 (shown by default)
</div>
<div style='display:none;'>
<p id="1">This is page 1</p>
<p id="2">This is page 2</p>
<p id="3">This is page 3</p>
<p id="4">This is page 4</p>
<p id="5">This is page 5</p>
</div>
<p style="text-align:center; width:100%;">
Back
Page 1 |
Page 2 |
Page 3 |
Page 4 |
Page 5
Forward
</p>
Try This Code
Working JSFIDDLE Here
var curPage = 0;
function change_page (id) {
var content_raw = document.getElementById(id);
curPage = parseInt(id);
var content = content_raw.innerHTML;
var display = document.getElementById('replace');
display.innerHTML = content;
}
function page_back () {
//function to go one page back
if(curPage==1)
return;
var content_raw = document.getElementById(curPage-1);
curPage = curPage-1;
var content = content_raw.innerHTML;
var display = document.getElementById('replace');
display.innerHTML = content;
}
function page_forward () {
if(curPage==5)
return;
var content_raw = document.getElementById(curPage+1);
curPage = curPage+1;
var content = content_raw.innerHTML;
var display = document.getElementById('replace');
display.innerHTML = content;
}
<div id='replace'>
This is page 1 (shown by default)
</div>
<div style='display:none;'>
<p id="1">This is pagse 1</p>
<p id="2">This is paage 2</p>
<p id="3">This is page 3</p>
<p id="4">This is page 4</p>
<p id="5">This is page 5</p>
</div>
<p style="text-align:center; width:100%;">
Back
Page 1 |
Page 2 |
Page 3 |
Page 4 |
Page 5
Forward
</p>
Working Demo Here

Getting text of HTML elements to an array

I have the following html code.
<!DOCTYPE html>
<html>
<body>
<div>
<h1>My First Heading</h1>
<p>My first paragraph.</p>
</div>
<div id="1">
<p id="2">Paragraph First
<ol id="3">
<li id="4">alice
<ul id="31">
<li id="41">bob</li>
<li id="51">foo</li>
<li id="61">grow</li>
</ul>
</li>
<li id="5">coding</li>
<li id="6">fun</li>
</ol>
</p>
</div>
<div>
<p>Paragraph Second</p>
</div>
</body>
</html>
I want to get the text of all the elements to an array(pre order transverse preffered).
So for the following example array consists of,
[My First Heading, first paragraph, Paragraph First, alice, bob, foo, grow, coding, fun]
I can use jQuery if it needsHow. can I achieve this ?
My own non-working attempt
var root = document.documentElement;
recursivePreorder(root); // Recusively find and handle all text nodes
function recursivePreorder(node) {
// If node is a text node
if (node.type == 3) {
//add to the array
} // else recurse for each child node
else {
for(var i=0; i<node.childNodes.length; i++)
recursivePreorder(node.childNodes[i]);
}
}
UPDATE
when my tag is <li id="41">boblink textalice</li> it gives me as [bob, link text, alice] . But I want the output as [boblink textalice] means links are working correctly. This is my current solution,
var arr=$('body').find('*').contents().filter(function () { return this.nodeType === 3&&this.textContent.trim()!=""; });
how to solve this problem ?
Try this:
var arr = [];
$(document).find('li,p,h1').each(function(e){
arr.push(($(this).clone().children().remove().end().text()).trim());
})
Demo
You can use map() for that.
$(document).ready(function() {
var arr = $("#container *").contents().map(function() {
if (this.nodeType === 3 && this.textContent.trim() != "") {
return this.textContent.trim();
}
});
console.log(arr);
});
nodeType will be 3 for text contents
Demo

Categories