Find if HTML data attribute exists in any ancestor elements - javascript

I have reference to an element, baseElement, and I want to find if some HTML data attribute exists somewhere on one of its ancestors, and to get the value of it if it is found. If the attribute exists on multiple ancestors, I just want the attribute on the one that is most closely related.
I've designed a loop that goes up by parents and checks if the attribute exists. Assume the first parent of baseElement is not null.
let currentElement = baseElement.parentElement;
let foundAttribute = false;
let valueOfAttribute = null;
do {
foundAttribute = currentElement.hasAttribute('html-attribute');
if (foundAttribute) {
valueOfAttribute = currentElement.getAttribute('html-attribute');
} else {
currentElement = currentElement.parentElement;
}
} while (!foundAttribute && currentElement);
I want to see if there is a more efficient, more eloquent, or easier-to-code way of doing this. Please, only pure JavaScript answers.

I want to see if there is a more efficient, more eloquent, or easier-to-code way of doing this.
There certainly is. Meet closest()
let ancestor = baseElement.closest('[html-attribute]');
let attr_val = ancestor ? ancestor.getAttribute('html-attribute') : null;
closest() takes a selector, just like querySelector and similar methods.

Related

Check if element contains #shadow-root

Is it possible to see if a Shadow DOM element exists? I'm not too concerned with manipulating it, or even really targeting it per-say. I understand the reasoning of the encapsulation. But I'd like to be able to style other elements in the regular DOM, based on whether or not the Shadow DOM element is present.
Sort of like:
if ( $('#element-id #shadow-root').length ) {
// true
}
Or if not for the shadow-root, at least a specific element within, like the id of a div. So if that div exists, then clearly that Shadow DOM element is on the page.
I know it wouldn't be that simple... From some research I've done, there are things like >>> and /deep/ but their support seems to be low/none/deprecated. Buy maybe there's another way, however inelegant it may be?
If you want to check whether or not a specific element is hosting an open Shadow DOM element, you can do the following:
var el = document.querySelector('#some-element');
if (!!el.shadowRoot) {
// Then it is hosting an OPEN Shadow DOM element
}
You can also get the Shadow DOM element, and then operate on it like a normal node:
var shadowEl = el.shadowRoot;
// And for example:
console.log(shadowEl.innerHTML);
Here is an example that works in the latest version of Chrome:
const div = document.querySelector('div');
const p = document.querySelector('p');
const shadowRoot = p.attachShadow({mode: 'open'})
shadowRoot.textContent = 'A Shadow DOM Paragraph. I overrode the content specified!';
console.log('Paragraph has Shadow DOM:', !!p.shadowRoot); // true
console.log('Div has Shadow DOM:', !!div.shadowRoot); // false
<div>A Normal Div</div>
<p>A Normal Paragraph</p>
You can access the shadowRoot of an element with the property shadowRoot, so you could traverse all the nodes and check if the property is null or not.
You can select all nodes in a document with document.getElementsByTagName('*').
So all in all, we would have something like this:
var allNodes = document.getElementsByTagName('*');
for (var i = 0; i < allNodes.length; i++) {
if(allNodes[i].shadowRoot) {
// Do some CSS styling
}
}
With the additions of ES6, we could do something simpler like this:
document.getElementsByTagName('*')
.filter(element => element.shadowRoot)
.forEach(element => {
// Do some CSS styling
});
The other answers by KevBot and Marko Kajzer only work for ShadowRoot created with mode: 'open'. Here's a way to detect if an element has a ShadowRoot, even if the root is closed. Make sure this runs before other code (before any calls to attachShadow) or it will fail to catch any elements that already have a ShadowRoot by the time this code is set up:
const shadowHosts = new WeakSet()
const original = Element.prototype.attachShadow
Element.prototype.attachShadow = function attachShadow(...args) {
const result = original.apply(this, args)
shadowHosts.add(this)
return result
}
export function hasShadow(el) {
return shadowHosts.has(el)
}
then use hasShadow on any element
if (hasShadow(someElement)) {...}

Filter out all elements in a jQuery result set that have a parent (at any level) with a certain class

I'm trying to apply some jQueryUI functionality to elements all throughout my SPA, however I'm having trouble omitting elements contained in a certain parent. I'm not actually sure if jQuery can do what I'm asking right now.
The bit of relevant code:
function initializeControls($container) {
var $context;
if ($container != undefined)
$context = $container;
else
$context = $("html");
$context = //$context, but ignoring all .no-controls elements and ALL children
//do stuff...
}
Simply put, I want all elements in $context where no parent element, first parent or otherwise, has the .no-controls class.
It sounds simple, but I cannot seem to get this to work. I've tried different combinations of .find(), .filter(), .remove()... but it never seems to do exactly what I need.
Any tips?
Can use closest() in a filter()
var $filteredCollection = $context.filter(function(){
return !$(this).closest('.no-controls').length;
});
closest() inspects the element itself and all ancestors
My proposal is:
jQuery.expr[':'].parents = function(a,i,m){
return jQuery(a).parents(m[3]).length < 1 && !jQuery(a).is(m[3]);
};
$context.filter(':parents(.no-controls)')

Pure JavaScript alternative to jQuerys .find

I have this in jQuery:
$(document).bind("click touchstart", function(e) {
if (e.target.class != searchControls && !searchControls.find(e.target).length) {
//do something
});
});
What this does is checks the click and if it's not the search element a child of that element, then I can run something. What I'm trying to do is find a pure JavaScript alternative to this.
So far I have:
var searchTrigger = document.querySelector(".header__search-btn"),
productSearch = document.querySelector(".product-search"),
searchControls = document.querySelectorAll(".product-search__positioner");
document.onclick = function (e) {
if (e.target.class != searchControls && e.parentNode != searchControls)
// do something
}
}
However this still fires my function inside the if statement, so clearly I'm going wrong—not sure how wrong. Help appreciated.
document.querySelectorAll returns a collection of DOM elements. e.target.class != searchControls doesn't make sense (ignoring that target.class doesn't even exist): Assuming you mean className, you are trying to compare a string with a list of DOM elements. That will always be false.
So lets ignore that part and look at e.parentNode != searchControls. The event object doesn't have a property parentNode. Again, even if you meant e.target.parentNode, it would not make much sense, since you are testing whether a single element is identical to a list of elements.
Based in your use of .find, you basically want to know whether a node is contained in another node. Every DOM node has a method .contains, so all you have to do is iterate over the collection of DOM elements and call that method:
var contained = false;
for (var i = 0; i < searchControls.length; i++) {
if (searchControls[i].contains(e.target)) {
contained = true;
break;
}
}
This can easily be moved into its own reusable function.
There is no e.target.class. If you are checking for class name then it should be
e.target.className
Then to check for parent elements class name
e.target.parentNode.className
if you are looking to search inside the parent node then use
e.target.parentNode.querySelector("pass your selector here")
if you want to search for just child elements:
e.target.querySelector("pass your selector here")

How to know if an element, created dynamically, exist?

I have this simple function:
var x = document.createTextNode("ERROR");
document.body.appendChild(x);
So then I need to create an IF to verify if this message exist [If this message has been created]. This is the problem, I don't know how to do that.
GetElementByID seems to don't work with element created by dynamically.
Any help? Thanks.
You can use document.contains to check if a element is in the DOM
Just a quick example of how it works
document.contains($('<div>')[0]); // FALSE
And
document.contains($('<div>').appendTo('body')[0]); // TRUE
jQuery only used for a shorthand to element creation
This also works for text nodes and you can use contains on any node.
document.body.contains(Node); // Example
The browser support is somewhat very good
Documentation: https://developer.mozilla.org/en-US/docs/Web/API/Node.contains
Question specifics:
var x = document.createTextNode("ERROR");
document.body.appendChild(x);
document.contains(x); // Should be TRUE
You are creating a text node, not an element. You need to create an element and give it an id to be able to use getElementById.
I don't know of any reasonable way to search for a text node, although you could always check the text nodes of the element you attached it to and see if it's there.
var message = "ERROR";
var t = document.createTextNode(message);
var node = document.getElementById('content').appendChild(t);
if (document.getElementById('content').innerHTML !== message) {
console.log('element not added');
} else {
console.log('element added');
}
Here is a fiddle:
http://jsfiddle.net/btipling/rBg4w/
I believe this would work:
var x = document.createTextNode("ERROR");
var element = document.body.appendChild(x); //returns text node
if(!element){
//element was not added
}
although if I were you I might create a span element with an id or a class called error. This way you can apply any css styles to it.
Try this:
var x = document.createTextNode("ERROR");
document.body.appendChild(x);
if (document.body.innerText.indexOf('ERROR')>=0){
alert('"ERROR" found');
}
indexOf doesn't work in all browsers.
As #slowpython said I'd rather create a DOM element with ID or NAME.

Unique element ID, even if element doesn't have one

I'm writing a GreaseMonkey script where I'm iterating through a bunch of elements. For each element, I need a string ID that I can use to reference that element later. The element itself doesn't have an id attribute, and I can't modify the original document to give it one (although I can make DOM changes in my script). I can't store the references in my script because when I need them, the GreaseMonkey script itself will have gone out of scope. Is there some way to get at an "internal" ID that the browser uses, for example? A Firefox-only solution is fine; a cross-browser solution that could be applied in other scenarios would be awesome.
Edit:
If the GreaseMonkey script is out of scope, how are you referencing the elements later? They GreaseMonkey script is adding events to DOM objects. I can't store the references in an array or some other similar mechanism because when the event fires, the array will be gone because the GreaseMonkey script will have gone out of scope. So the event needs some way to know about the element reference that the script had when the event was attached. And the element in question is not the one to which it is attached.
Can't you just use a custom property on the element? Yes, but the problem is on the lookup. I'd have to resort to iterating through all the elements looking for the one that has that custom property set to the desired id. That would work, sure, but in large documents it could be very time consuming. I'm looking for something where the browser can do the lookup grunt work.
Wait, can you or can you not modify the document? I can't modify the source document, but I can make DOM changes in the script. I'll clarify in the question.
Can you not use closures? Closuses did turn out to work, although I initially thought they wouldn't. See my later post.
It sounds like the answer to the question: "Is there some internal browser ID I could use?" is "No."
The answer is no, there isn't an internal id you can access. Opera and IE (maybe Safari?) support .sourceIndex (which changes if DOM does) but Firefox has nothing of this sort.
You can simulate source-index by generating Xpath to a given node or finding the index of the node from document.getElementsByTagName('*') which will always return elements in source order.
All of this requires a completely static file of course. Changes to DOM will break the lookup.
What I don't understand is how you can loose references to nodes but not to (theoretical) internal id's? Either closures and assignments work or they don't. Or am I missing something?
Closure is the way to go. This way you'll have exact reference to the element that even will survive some shuffling of DOM.
Example for those who don't know closures:
var saved_element = findThatDOMNode();
document.body.onclick = function()
{
alert(saved_element); // it's still there!
}
If you had to store it in a cookie, then I recommend computing XPath for it (e.g. walk up the DOM counting previous siblings until you find element with an ID and you'll end up with something like [#id=foo]/div[4]/p[2]/a).
XPointer is W3C's solution to that problem.
A bit confused by the wording of your question - you say that you "need a string ID that [you] can use to reference that element later, " but that you "can't store the references in [your] script because when [you] need them, the GreaseMonkey script itself will have gone out of scope."
If the script will have gone out of scope, then how are you referencing them later?!
I am going to ignore the fact that I am confused by what you are getting at and tell you that I write Greasemonkey scripts quite often and can modify the DOM elements I access to give them an ID property. This is code you can use to get a pseudo-unique value for temporary use:
var PseudoGuid = new (function() {
this.empty = "00000000-0000-0000-0000-000000000000";
this.GetNew = function() {
var fourChars = function() {
return (((1 + Math.random()) * 0x10000)|0).toString(16).substring(1).toUpperCase();
}
return (fourChars() + fourChars() + "-" + fourChars() + "-" + fourChars() + "-" + fourChars() + "-" + fourChars() + fourChars() + fourChars());
};
})();
// usage example:
var tempId = PseudoGuid.GetNew();
someDomElement.id = tempId;
That works for me, I just tested it in a Greasemonkey script myself.
UPDATE: Closures are the way to go - personally, as a hard-core JavaScript developer, I don't know how you didn't think of those immediately. :)
myDomElement; // some DOM element we want later reference to
someOtherDomElement.addEventListener("click", function(e) {
// because of the closure, here we have a reference to myDomElement
doSomething(myDomElement);
}, false);
Now, myDomElement is one of the elements you apparently, from your description, already have around (since you were thinking of adding an ID to it, or whatever).
Maybe if you post an example of what you are trying to do, it would be easier to help you, assuming this doesn't.
UPDATE: Closures are indeed the answer. So after fiddling with it some more, I figured out why closures were initially problematic and how to fix it. The tricky thing with a closure is you have to be careful when iterating through the elements not to end up with all of your closures referencing the same element. For example, this doesn't work:
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var button = document.createElement("button");
button.addEventListener("click", function(ev) {
// do something with element here
}, false)
}
But this does:
var buildListener = function(element) {
return function(ev) {
// do something with event here
};
};
for (var i = 0; i < elements.length; i++) {
var element = elements[i];
var button = document.createElement("button");
button.addEventListener("click", buildListener(element), false)
}
Anyway, I decided not to select one answer because the question had two answers: 1) No, there are no internal IDs you can use; 2) you should use closures for this. So I simply upvoted the first people to say whether there were internal IDs or who recommended generating IDs, plus anyone who mentioned closures. Thanks for the help!
If you can write to the DOM (I'm sure you can). I would solve this like this:
Have a function return or generate an ID:
//(function () {
var idCounter = new Date().getTime();
function getId( node ) {
return (node.id) ? node.id : (node.id = 'tempIdPrefix_' + idCounter++ );
}
//})();
Use this to get ID's as needed:
var n = document.getElementById('someid');
getId(n); // returns "someid"
var n = document.getElementsByTagName('div')[1];
getId(n); // returns "tempIdPrefix_1224697942198"
This way you don't need to worry about what the HTML looks like when the server hands it to you.
If you're not modifying the DOM you can get them all by indexed order:
(Prototype example)
myNodes = document.body.descendants()
alert(document.body.descendants()[1].innerHTML)
You could loop through all of the nodes and give them a unique className that you could later select easily.
You can set the id attribute to a computed value. There is a function in the prototype library that can do this for you.
http://www.prototypejs.org/api/element/identify
My favorite javascript library is jQuery. Unfortunately jQuery does not have a function like identify. However, you can still set the id attribute to a value that you generate on your own.
http://docs.jquery.com/Attributes/attr#keyfn
Here is a partial snippet from jQuery docs that sets id for divs based on the position in the page:
$(document).ready(function(){
$("div").attr("id", function (arr) {
return "div-id" + arr;
});
});
You can generate a stable, unique identifier for any given node in a DOM with the following function:
function getUniqueKeyForNode (targetNode) {
const pieces = ['doc'];
let node = targetNode;
while (node && node.parentNode) {
pieces.push(Array.prototype.indexOf.call(node.parentNode.childNodes, node));
node = node.parentNode
}
return pieces.reverse().join('/');
}
This will create identifiers such as doc/0, doc/0/0, doc/0/1, doc/0/1/0, doc/0/1/1 for a structure like this one:
<div>
<div />
<div>
<div />
<div />
</div>
</div>
There are also a few optimisations and changes you can make, for example:
In the while loop, break when that node has an attribute you know to be unique, for example #id
Not reverse() the pieces, currently it is just there to look more like the DOM structure the ID's are generated from
Not include the first piece doc if you don't need an identifier for the document node
Save the identifier on the node in some way, and reuse that value for child nodes to avoid having to traverse all the way up the tree again.
If you're writing these identifiers back to XML, use another concatenation character if the attribute you're writing is restricted.
Use mouse and/or positional properties of the element to generate a unique ID.
In javascript, you could attach a custom ID field to the node
if(node.id) {
node.myId = node.id;
} else {
node.myId = createId();
}
// store myId
It's a bit of hack, but it'll give each and every node an id you can use. Of course, document.getElementById() won't pay attention to it.
You can also use pguid (page-unique identifier) for unique identifier generation:
pguid = b9j.pguid.next() // A unique id (suitable for a DOM element)
// is generated
// Something like "b9j-pguid-20a9ff-0"
...
pguid = b9j.pguid.next() // Another unique one... "b9j-pguid-20a9ff-1"
// Build a custom generator
var sequence = new b9j.pguid.Sequence({ namespace: "frobozz" })
pguid = sequence.next() "frobozz-c861e1-0"
http://appengine.bravo9.com/b9j/documentation/pguid.html
I 'think' I've just solved a problem similar to this. However, I'm using jQuery in a browser DOM environment.
var objA = $("selector to some dom element");
var objB = $("selector to some other dom element");
if( objA[0] === objB[0]) {
//GREAT! the two objects point to exactly the same dom node
}
OK, there is no ID associated to DOM element automatically.
DOM has a hierarchycal structure of elements which is the main information.
From this perspective, you can associate data to DOM elements with jQuery or jQLite. It can solve some issues when you have to bind custom data to elements.

Categories

Resources