DOM - Find text between arbitrarily nested elements - javascript

Suppose my DOM looks something like this (purely fictional example):
<div>
<img id="img1" />
Text 1
</div>
<img id="img2" />
<div>
<p>Text 2</p>
<div>
<img id="img3" />
</div>
<img id="img4" />
</div>
What I'm attempting to do is to find all text (if any) between consecutive elements independent of nesting level (so in this case, I'd find Text1 between #img1 and #img2, Text 2 between #img2 and #img3, and nothing/an empty string between #img3 and #img4)
I don't know ahead of time how the dom is going to be structured.
I've tried using JQuery's nextUntil(), but that only seems to work for sibling nodes.

You can use the contents() method which returns all child nodes including textnodes.
This way, doing $('body *').contents().addBack() returns a flattened representation of the DOM.
Now, you can iterate between img (or whatever tag you want) elements and get the textnodes (having a nodeType of 3)
function textBetweenTags(tag){
var contents = $('body *').contents().addBack(),
allOfType = contents.filter(tag),
count = allOfType.length,
map = allOfType.map(function(){return contents.index( this );}),
texts = [];
for (var i = 0, l = map.length-1; i < l; i++){
var start = map[i],
end = map[i+1],
textnodes = contents.slice(start,end).get().filter(function(item,index){return item.nodeType===3;});
texts.push( {
start: contents[start],
end: contents[end],
text: $.trim($(textnodes).text())
});
}
return texts.filter(function(item,index){return item.text !== '';});
}
var texts = textBetweenTags('img');
console.log(texts);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div>
<img id="img1" />
Text 1
</div>
<img id="img2" />
<div>
<p>Text 2</p>
<div>
<img id="img3" />
</div>
<img id="img4" />
</div>

I think my solution is more closely achieves what you specified. This is also using contents() but does the traversing/filtering too, based on the ids.
function textBetweenIds(firstId, lastId) {
var texts = [],
betweenIds = false,
recurse = function ($item) {
$item.each(function() {
var text,
$item = $(this),
itemId = $item.attr("id"),
contents = $item.contents();
if (itemId == firstId) {
betweenIds = true;
} else if (itemId == lastId) {
betweenIds = false;
}
if (contents.length == 0 && betweenIds) {
text = $item.text().trim();
if (text != undefined && text.length > 0) {
texts.push(text);
}
}
recurse(contents);
});
return texts;
};
recurse($("body"));
return texts;
}
result = textBetweenIds("img1", "img3");
// result: ["Text 1", "Text 2"]

Related

Check if elements has the same parent (JS)

I'm working on a system that puts all images inside paragraph-tags, as such:
<p>
<img src="..." />
</p>
So I wrote a function to move the images out of the paragraph tags. It looks like this:
moveImagesOutOfPtags() {
let images = document.querySelectorAll('p > img');
Object.entries(images).forEach(entry => {
let value = entry[1];
if( value.parentNode.nodeName === 'P' || value.parentNode.nodeName === 'p' ){
value.parentNode.outerHTML = value.parentNode.innerHTML;
}
});
}
But it doesn't work if there are two images inside the same p-tag, as such:
<p>
<img src="..." />
<img src="..." />
</p>
... Since the parent is removed/rewritten/overwritten with the value.parentNode.outerHTML = ...-line, for the first image. So when it gets to the second image in that p-tag, then it throws the error:
Failed to set the 'outerHTML' property on 'Element': This element has no parent node.
Any suggestions on a good way to solve this?
This is easy to achieve with DOM manipulating methods instead of setting HTML strings, like this:
function moveImagesOutOfPtags() {
let images = document.querySelectorAll('p > img');
images.forEach(img => {
const parent = img.parentElement;
// Insert the image as a previous sibling to the paragraph
parent.parentElement.insertBefore(img, parent);
if (parent.children.length === 0) {
// Remove the empty paragraph
parent.remove();
}
});
}
moveImagesOutOfPtags();
<p>
<img src="...">
<img src="...">
</p>
<p>
<img src="...">
</p>
You don't have to check the parent, since the query will select the images wrapped in a paragraph only, just insert the image as a previous sibling of the parent. Then check if the parent is empty, and remove if needed.
use id for every paragraph-tags and check the previously modified id with current one . i don't think this is the best way but this will let the job done.
html contents
<p id = "p_id_1">
<img src="..." />
<img src="..." />
</p>
<p id = "p_id_2">
<img src="..." />
<img src="..." />
</p>
script will be
moveImagesOutOfPtags() {
let images = document.querySelectorAll('p > img');
var p_id='';
Object.entries(images).forEach(entry => {
let value = entry[1];
if(p_id != value.parentNode.id && (value.parentNode.nodeName === 'P' || value.parentNode.nodeName === 'p' )){
value.parentNode.outerHTML = value.parentNode.innerHTML;
p_id = value.parentNode.id;
}
});
}
You could just catch the error and not carry on like this:
Try this:
moveImagesOutOfPtags() {
document.querySelectorAll('p > img').forEach(entry => {
const value = entry && entry[0]; // catches error if p has been replaced and doesn't continue.
if(value){ // you don't have to test if parent is p tag, you've already determined that with your direct child selector p > img
value.parentNode.outerHTML = value.parentNode.innerHTML;
}
});
}

Use hasAttribute() get TypeError: hasAttribute is not a function

I try to find out the number of <img> elements that do not have "style" attribute in a HTML file by using JavaScript.
My solution: find out numbers of <img> tags as "imgCount", then get number of <img> tags with "style" attribute as "imgStyCount". After that, use "imgCount" minus "imgStyCount" to get the final result that I wish to know.
However, something goes wrong. My browser keep told me
TypeError: document.getElementsByTagName(...)[K].hasAttribute is not a function
At the if statement.
And the weird thing is, the alert(document.getElementsByTagName("img")[k].hasAttribute("style") show the if statement result is TRUE.
How it can be like not a function and give the true value?
var imgCount = 0;
var imgStyCount = 0;
var result;
for (k in document.getElementsByTagName("img")) {
if (document.getElementsByTagName("img")[k].hasAttribute("style") == true) {
alert(document.getElementsByTagName("img")[k].hasAttribute("style"));
console.log(" <img> =: ", document.getElementsByTagName("img")[k].style);
imgStyCount++;
}
imgCount++;
}
result = imgCount - imgStyCount;
<img height="150px" src="Http://flax.nzdl.org/images/ngf.jpeg" style="vertical-align:middle;margin-right:20px;" />
<img src="Http://flax.nzdl.org/images/abc.jpg" />
<img src="Http://flax.nzdl.org/images/fbc.jpg" />
<img src="Http://flax.nzdl.org/images/agc.jpg" />
<img src="Http://flax.nzdl.org/images/abt.jpg" />
Here's a simple way without using loop.
You can use querySelectorAll with attribute selector
document.querySelectorAll('img[style]') will select all the <img> elements on the page having style attribute.
var result = document.querySelectorAll('img').length - document.querySelectorAll('img[style]').length;
alert(result);
<img height="150px" src="Http://flax.nzdl.org/images/ngf.jpeg" style="vertical-align:middle;margin-right:20px;" />
<img src="Http://flax.nzdl.org/images/abc.jpg" />
<img src="Http://flax.nzdl.org/images/fbc.jpg" />
<img src="Http://flax.nzdl.org/images/agc.jpg" />
<img src="Http://flax.nzdl.org/images/abt.jpg" />
Use for-loop to iterate image elements than for-in
var imgStyCount = 0;
var elems = document.getElementsByTagName("img");
for (var k = 0; k < elems.length; k++) {
if (elems[k].hasAttribute("style")) {
imgStyCount++;
}
}
var result = elems.length - imgStyCount;
alert(result);
<img height="150px" src="Http://flax.nzdl.org/images/ngf.jpeg" style="vertical-align:middle;margin-right:20px;" />
<img src="Http://flax.nzdl.org/images/abc.jpg" />
<img src="Http://flax.nzdl.org/images/fbc.jpg" />
<img src="Http://flax.nzdl.org/images/agc.jpg" />
<img src="Http://flax.nzdl.org/images/abt.jpg" />
Fiddle demo
You're welcome.
function hasAttr(el, attr) {
if(typeof el === 'object' && el !== null && 'getAttribute' in el && el.hasAttribute(attr)) return true
else return false
}
<span medium-img>Whatever</span>
alert($('span').is('[medium-img]')); // Returns true

jQuery hide all images except first

I have a data collection thats gets rendered from the server. This is how the HTML might looks like:
<div>
<img class="image_98" alt="..." height="..." src="..." width="..." />
</div>
<div>
<img class="image_99" alt="..." height="..." src="..." width="..." />
</div>
<div>
<img class="image_99" alt="..." height="..." src="..." width="..." />
</div>
<div>
<img class="image_100" alt="..." height="..." src="..." width="..." />
</div>
<div>
<img class="image_100" alt="..." height="..." src="..." width="..." />
</div>
<div>
<img class="image_100" alt="..." height="..." src="..." width="..." />
</div>
The image class is dynamic depending on the id of the image in the database. How can I hide all images except the first one of every class?
In this example I only want to see the first "image_98", "image_99" and "image_100" class.
You can do it like this, using attribute selector [attribute="value"] and each() method
var img=$('[class^="image_"]');
img.hide(); //hide all image
img.each(function(){
$('.'+this.className).eq(0).show(); // show first image in each class
});
Use
show() , to show the selected item
hide() , to hide the selected item.
Old school method :
var slice = Array.prototype.slice,
imgs = slice.call(document.getElementsByTagName('img')),
img, ct, cls;
while (img = imgs.shift()) {
if (cls === img.className) {
ct = img.parentNode;
ct.parentNode.removeChild(ct);
} else {
cls = img.className;
}
}
Yuk indeed...
Using jQuery, I would do quite the same :
var prevCls;
$('img').each(function () {
var cls = $(this).attr('class'); // I have a doubt about .className support
if (prevCls === cls) {
$(this.parentNode).remove(); // or .hide() if preferred
} else {
prevCls = cls;
}
});
Advantage comparing to the accepted answer : a single loop.
Here is another method using recursivity - quite close to the first one :
(function deldup(list, prevCls) {
var item = list.shift();
if (item.className !== prevCls) {
prevCls = item.className;
} else {
item = item.parentNode;
item.parentNode.removeChild(item);
}
if (list.length) {
deldup(list, prevCls);
}
})(
Array.prototype.slice.call(
document.getElementsByTagName('img')
)
);
Advantage : no global variables.
In case images are mixed :
(function deldup(list, cls) {
var item = list.shift();
if (!cls[item.className]) {
cls[item.className] = true;
} else {
item = item.parentNode;
item.parentNode.removeChild(item);
}
if (list.length) {
deldup(list, cls);
}
})(
Array.prototype.slice.call(
document.getElementsByTagName('span')
), {} // notice this empty object
);
Demo : http://jsfiddle.net/wared/QpZD5/.

How to get innerHTML of DIV without few inside DIV's?

I have some DIV, what contains HTML with images, styles e.t.c. I want to remove exact div's that contains id = 'quot' or className = 'quote', but i don't understand how i can get not only innerHTML of each tag. For example, < p > and < /p > which don't have innerHTML also should be included in final parsed HTML.
var bodytext = document.getElementById("div_text");
var NewText = "";
if (bodytext.hasChildNodes){
var children = bodytext.childNodes;
for (var i = 0; i < children.length; i++){
if (children[i].id != "quot" && children[i].className != "quote" && children[i].innerText != ""){
NewText = NewText + children[i].innerHTML;
}
}
HTML of source need to be parsed:
<div id="div_text">
<p>
Some Text</p>
<p>
Some Text</p>
<p>
<img alt="" src="localhost/i/1.png" /></p>
<div id="quot" class="quote" />
any text <div>text of inside div</div>
<table><tr><td>there can be table</td></tr></table>
</div>
<p>
</p>
</div>
Desired output:
<p>
Some Text</p>
<p>
Some Text</p>
<p>
<img alt="" src="localhost/i/1.png" /></p>
<p>
</p>
Just grab a reference to the targeted divs and remove them from their respective parents.
Perhaps something a little like this?
EDIT: Added code to perform operation on a clone, rather than the document itself.
div elements don't have .getElementById method, so we search for an element manually.
window.addEventListener('load', myInit, false);
function removeFromDocument()
{
// 1. take car of the element with id='quot'
var tgt = document.getElementById('quot');
var parentNode = tgt.parentNode;
parentNode.removeChild(tgt);
// 2. take care of elements whose class == 'quote'
var tgtList = document.getElementsByClassName('quote');
var i, n = tgtList.length;
for (i=0; i<n; i++)
{
// we really should be checking to ensure that there aren't nested instances of matching divs
// The following would present a problem - <div class='quote'>outer<div class='quote'>inner</div></div>
// since the first iteration of the loop would also remove the second element in the target list,
parentNode = tgtList[i].parentNode;
parentNode.removeChild(tgtList[i]);
}
// 3. remove the containing div
var container = document.getElementById('div_text');
container.outerHTML = container.innerHTML;
}
function cloneAndProcess()
{
var clonedCopy = document.getElementById('div_text').cloneNode(true);
var tgt;// = clonedCopy.getElementById('quot');
var i, n = clonedCopy.childNodes.length;
for (i=0; i<n; i++)
{
if (clonedCopy.childNodes[i].id == 'quot')
{
tgt = clonedCopy.childNodes[i];
var parentNode = tgt.parentNode;
parentNode.removeChild(tgt);
break; // done with for loop - can only have 1 element with any given id
}
}
// 2. take care of elements whose class == 'quote'
var tgtList = clonedCopy.getElementsByClassName('quote');
var i, n = tgtList.length;
for (i=0; i<n; i++)
{
// we really should be checking to ensure that there aren't nested instances of matching divs
// The following would present a problem - <div class='quote'>outer<div class='quote'>inner</div></div>
// since the first iteration of the loop would also remove the second element in the target list,
parentNode = tgtList[i].parentNode;
parentNode.removeChild(tgtList[i]);
}
// 3. remove the containing div
//var container = clonedCopy; //.getElementById('div_text');
//container.outerHTML = container.innerHTML;
console.log(clonedCopy.innerHTML);
}
function myInit()
{
cloneAndProcess();
//removeFromDocument();
}

Appending ascending images based on the id of parent div

Say I had the following three divs with unique ids on a page
<div id="product-id-001"></div>
<div id="product-id-002"></div>
<div id="product-id-003"></div>
What code would I need to add the following image elements based on the id of the div?
<div id="product-id-001">
<img src="images/product-id-001-1.jpg"></img>
<img src="images/product-id-001-2.jpg"></img>
<img src="images/product-id-001-3.jpg"></img>
</div>
<div id="product-id-002">
<img src="images/product-id-002-1.jpg"></img>
<img src="images/product-id-002-2.jpg"></img>
<img src="images/product-id-002-3.jpg"></img>
</div>
<div id="product-id-003">
<img src="images/product-id-003-1.jpg"></img>
<img src="images/product-id-003-2.jpg"></img>
<img src="images/product-id-003-3.jpg"></img>
</div>
Thanks for any tips.
$('div[id^=product]').each(function(index, elem) {
for(var i = 1; i < 4; i++) {
$('<img>', {
src: '/images/' + elem.id + i
}).appendTo(this);
}
});
Demo: http://www.jsfiddle.net/9S9Av/
(You need Firebug or another DOM inspector to see the result)
From the jQuery docs:
"The .append() method inserts the specified content as the last child of each element in the jQuery collection"
So:
$('#product-id-001").append('<img src="images/product-id-001-1.jpg"></img>');
etc...
// Select all elements with an id beginning with product-id:
var $productContainers = $('[id^=product-id]');
// Loop through the matched elements and append the images:
$productContainers.each(function () {
var $this = $(this),
productId = $this.attr('id'),
numImages = 3,
extension = '.jpg';
// Create and append the images based on the configuration above. Note that this
// assumes that each product have three valid images.
for (var i = 1; i <= numImages; i++)
{
var $image = $('<img alt="" />').attr('src', 'images/'+productId+'-'+i+extension);
$image.appendTo($this);
}
});

Categories

Resources