Multiple DOM Traverse without errors or runtime errors - javascript

I want to traverse rather complicated structure
First I tried
var textFromCLick = $0.closest('span').parentElement.querySelector('label').innerText
This works only if the user clicks on elements that has the exact structure.
If the structure is different I get 'Cannot read property 'querySelector' of null'
So I came up with this dirty code
if(0$.closest('span') !== null) {
var el2 = 0$.closest('span');
if (el2.parentElement.querySelector('label') !== null) {
textFromCLick = el2.parentElement.querySelector('label').innerText;
} else {
textFromCLick = "";
}
} else {
textFromCLick = "";
}
This works but.. as you see, the code is too long and inefficient.
How can I make it graceful?

Chaining method calls is fragile if any call might fail, as in
$0.closest('span').parentElement.querySelector('label').innerText;
If any part fails, the next part will likely throw an error. So test each part at the point that it might return a value such that the next part throws an error. In this case, various expressions might return either null or something else, so to test each bit:
let span = $0.closest('span');
let parent = span && span.parent;
let label = parent && parent.querySelector('label');
let text = label? label.textContent : '';
You can probably combine the second and third lines as:
let label = span && span.parent.querySelector('label');
on the basis that if there is a span (or any element that below the root HTML element), it will have a parent node hence span.parent is very unlikely to fail.
An alternative is to wrap the call in try..catch, but that's just lazy:
let text = '';
try {
text = $0.closest('span').parentElement.querySelector('label').textContent;
catch (e) {
text = '';
}
A better strategy is to associate elements by some property such as id or class so the relationship isn't structural, e.g.
function findLabel() {
let label = document.querySelector('label.' + this.className);
console.log(label && label.textContent);
}
window.onload = ()=>{
document.querySelectorAll('button').forEach(button =>
button.addEventListener('click', findLabel)
);
}
<p>
<span>
<button class="fooBar0">Find my label</button>
</span>
</p>
<label class="fooBar0">Here I am</label>

Related

Is there a way to test a css-selector query to an unappended element?

I have this code:
Element.prototype.queryTest = function(strQuery) {
var _r;
if (this.parentElement == null) {
_r = Array.prototype.slice.call(document.querySelectorAll(strQuery)).indexOf(this);
} else {
_r = Array.prototype.slice.call(this.parentElement.querySelectorAll(strQuery)).indexOf(this);
}
return !!(_r+1);
}
I am searching for some way to test a query to an unappended element.
I want to change the first code to make this work:
var t = document.createElement("span");
t.classList.add("asdfg");
console.log(t.queryTest("span.adsfg"));
If there is a way to detect if the element isn't appended I could create a new temporary unappended one and append the target one to the temporary one to test the css-selector query.
Is there a way to detect if the element hasn't been appended jet? Could the target element be accessible even after freeing the temporary parent one? I have tested it on Chrome and it is accessible but I don't know if that is the case for firefox.
I know I can use document.querySelectorAll("*") to get a list of nodes but... isn't too CPU-demmanding the process to turn this NodeList to an Array? This is why I prefer not to use that way.
Thanks in advance.
There is already a native Element.prototype.matches method which does that:
const el = document.createElement('span');
el.classList.add('test');
console.log(el.matches('span.test'));
Note that to check if a node is connected or not, there is the Node.prototype.isConnected getter.
I did it.
Element.prototype.querySelectorTest = function(strQuery) {
var _r;
if (this.parentElement != null) {
_r = Array.prototype.indexOf.call(this.parentElement.querySelectorAll(strQuery),this);
} else if (this == document.documentElement) {
_r = ((document.querySelector(strQuery) == this)-1);
} else {
_r = ((this == document.createElement("i").appendChild(this).parentElement.querySelector(strQuery))-1);
}
return !!(_r+1);
}
I changed the way it check the nodeList.
I renamed the function to a more proper name.
If the target element is the root one there's no need to make a querySelectorAll.
If you append the unappended element to a temporary one to test the child you don't loose the reference (variable value in case there is one).
This is not my native language so please consider that.

DOM Traversa l Question - Using mutationObserver to insertAdjacentHTML after each node, and to addEventListener to each button

So, I'm using setMutationObserver to insertAdjacentHTML input buttons in to a div class named '._5pcq'. after the end of that class. That class is located inside of a container named '._3576'.
https://greasyfork.org/scripts/12228/code/setMutationHandler.js
Once that's done, I am using the setMutationObserver to addEventListener('click', function(event) {} to each button that has been inserted in to the '._5pcq' class. Which means it's looking inside of each '._3576' class for the buttons after they've been inserted, which means that I'm pointing the second MutationObserver to look for '.TextCopyButton' inside _3576, and for each node(.TextCopyButton) that it finds, it's adding the listener.
Now, in order to copy the text in the '._3576' class. I need to traverse from the event.target which is (.TextCopyButton) up to where the text to be copied is located at. Which is 2 parentNodes up, nextSibling, nextSibling, and then up 9 parentNodes. Without losing track of the current node.
event.target.parentNode.parentNode.nextSibling.nextSibling.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.nextSibling;
I can not do getElementsByClassName(_3576), because that will require a node index number. Which means I need to use event.target to reference the current node that the button is clicked from. My question is how can I reference the current node, and get to the location I need to without using so many parentNode chains.
I need something along the lines of event.target.parentNode[2].nextSibling[2].parentNode[9]
OR
event.target.traverseToNode('.3576');
I have no idea what else to try. I have tried getElementsByClassName('_3576')[event.target]
I have tried using the THIS keyword.
My code is located here:
setMutationHandler({
target: document.querySelector('._3576'),
selector: '.copybtnP',
handler: nodes => nodes.forEach(node => {
node.setAttribute("style", "color:blue; border: 1px solid blue;");
node.addEventListener('click', function(event) {
var classCheck = document.getElementsByClassName('canvasArea');
var status_content = event.target.parentNode.parentNode.nextSibling.nextSibling.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.nextSibling;
if (classCheck.length > 0) {
status_content = event.target.previousSibling.previousSibling.previousSibling;
}
//alert(status_content + ' (Working)');
//alert(event.target.previousSibling.innerText);
getSelection().removeAllRanges();
var range = document.createRange();
range.selectNode(status_content);
window.getSelection().addRange(range);
try { var successful = document.execCommand('copy');
var msg = successful ? 'successful' : 'unsuccessful';
//console.log('Copying text command was ' + msg);
} catch (err) {console.log('Oops, unable to copy');}
return false;
})
})
});
https://pastebin.com/Et66j0uc
I think you buried your question inside a bunch of unnecessary description. From what I understand, you want to simply reduce the number of .parentNode calls when traversing up the DOM.
The most obvious answer is to create a function that takes the starting point and the number of traversals, and does them in a loop.
function up(start, num) {
while (--num >= 0 && (start = start.parentNode)) {
// nothing to do in the loop body
}
return start
}
const res = up(event.target, 2)
Then you can do the same for sibling traversal.
However, you can also use the .closest() method for going up through the ancestors, assuming you can describe the element targeted with a selector.
const res = event.target.closest(".someParentClass")
This should prove to be more resilient against changes you make to the DOM, assuming your selector is well defined.

Why is my this.class always undefined with jQuery filter?

I am trying to get all the div elements with classes having a name containing print. I am trying to achieve this like so:
const matchedContainer = [];
$('div').filter(() => {
regex = new RegExp("\w*print\w*","i");
if( this.class !== undefined && this.class.match(regex)) {
var elementContents = $(this).prop('outerHTML');
matchedContainer.push(elementContents);
}
}
Here I am never able to enter the if loop as this.class is always equated to undefined. I also tried with ".each" instead of ".filter" with the same result. I also tried giving names to each element by ".filter((ind,elem)=>{...})" and then tried using elem.class but that didn't work as well.
I tried a similar approach with this.id and that worked fine, can anyone tell me as to what I am doing wrong?
Here I am never able to enter the if loop as this.class is always
equated to undefined
You are accessing a property of an object which does't exists.
An Element doesn't have a class property (not attribute), it has a className property.
Use this.className instead of this.class.
Edit
since you are using an arrow function, this refers to the enclosing scope. Make it
$('div').filter((i, v) => { //i and v are index and value
regex = new RegExp("\w*print\w*","i");
if( v.className !== undefined && v.className.match(regex)) { //use v instead of this
var elementContents = $(this).prop('outerHTML');
matchedContainer.push(elementContents);
}
}
You have several issues with your code:
The this inside the filter gives you the reference of Window object and not div element
You should be using className instead of class in this only if this referenced the div element. But it does not reference the div element
const matchedContainer = [];
$('div').filter((index, elem) => {
regex = new RegExp("\w*print\w*","i");
if( $(elem).attr('class') !== undefined && $(elem).attr('class').match(regex)) {
var elementContents = $(elem).prop('outerHTML');
matchedContainer.push(elementContents);
}
});
console.log(matchedContainer);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class='print123'>1</div>
<div class='152print'>23</div>
<div class='print'>789</div>
<div class='pprint'>852</div>
Incorrect answer as suggested by gurvinder above:
const matchedContainer = [];
$('div').filter(() => {
regex = new RegExp("\w*print\w*","i");
if( this.className !== undefined && this.className.match(regex)) {
var elementContents = $(this).prop('outerHTML');
matchedContainer.push(elementContents);
}
});
console.log(matchedContainer);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div class='print123'>1</div>
<div class='152print'>23</div>
<div class='print'>789</div>
<div class='pprint'>852</div>
I think you can use: $('div[class*="print"]')
If we are talking about 2 or 3 classes you can use $(".class1,.class2,.class3,...")
If the class name is on the beginning of the string you can use $("div[class^='print-']")

Google places autocomplete, how to clean up pac-container?

I'm using the google places autocomplete control, and it creates an element for the drop down with a class pac-container.
I'm using the autocomplete in an ember app, and when I'm done with it the DOM element the autocomplete is bound to gets removed, but the pac-container element remains, even thought its hidden. Next time I instantiate a new autocomplete, a new pac-container is created and the old one remains. I can't seem to find anything like a dispose method on the API, so is there a way of doing this correctly? If not I guess I should just use jquery to clear up the elements.
I was having the same problem, and hopefully Google eventually provides an official means of cleanup, but for now I was able to solve the problem by manually removing the pac-container object, a reference to which can be found in the Autocomplete class returned from:
var autocomplete = new google.maps.places.Autocomplete(element, options);
The reference to the pac-container element can be found at:
autocomplete.gm_accessors_.place.Mc.gm_accessors_.input.Mc.L
Which I simply removed from the DOM in my widget destructor:
$(autocomplete.gm_accessors_.place.Mc.gm_accessors_.input.Mc.L).remove();
Hope this helps.
Update
I'm not sure how Google's obfuscation works, but parts of the above seem obfuscated, and obviously will fail if the obfuscation or internal structures of the API change. Can't do much about the latter, but for the former you could at least search the object properties by expected criteria. As we can see, some of the property names are not obfuscated, while some appear to be, such as "Mc" and "L". To make this a little more robust, I wrote the following code:
var obj = autocomplete.gm_accessors_.place;
$.each(Object.keys(obj), function(i, key) {
if(typeof(obj[key]) == "object" && obj[key].hasOwnProperty("gm_accessors_")) {
obj = obj[key].gm_accessors_.input[key];
return false;
}
});
$.each(Object.keys(obj), function(i, key) {
if($(obj[key]).hasClass("pac-container")) {
obj = obj[key];
return false;
}
});
$(obj).remove();
The code expects the general structure to remain the same, while not relying on the (possibly) obfuscated names "Mc" and "L". Ugly I know, but hopefully Google fixes this issue soon.
My implementation of code from above without jquery.
var autocomplete = new google.maps.places.Autocomplete(element, options);
export function getAutocompletePacContainer(autocomplete) {
const place: Object = autocomplete.gm_accessors_.place;
const placeKey = Object.keys(place).find((value) => (
(typeof(place[value]) === 'object') && (place[value].hasOwnProperty('gm_accessors_'))
));
const input = place[placeKey].gm_accessors_.input[placeKey];
const inputKey = Object.keys(input).find((value) => (
(input[value].classList && input[value].classList.contains('pac-container'))
));
return input[inputKey];
}
getAutocompletePacContainer(autocomplete).remove()
This works for now until Google changes the class name.
autocomplete.addListener('place_changed', function() {
$('.pac-container').remove();
});
Built this recursive function to locate element position inside autocomplete object.
Get first matching object
var elementLocator = function(prop, className, maxSearchLevel, level) {
level++;
if (level === (maxSearchLevel + 1) || !prop || !(Array.isArray(prop) || prop === Object(prop))) {
return;
}
if (prop === Object(prop) && prop.classList && prop.classList.contains && typeof prop.classList.contains === 'function' && prop.classList.contains(className)) {
return prop;
}
for (const key in prop) {
if (prop.hasOwnProperty(key)) {
var element = elementLocator(prop[key], className, maxSearchLevel, level);
if (element) {
return element;
}
}
}
};
Usage:
var elm = null;
try {
//set to search first 12 levels, pass -1 to search all levels
elm = elementLocator(this.autocomplete, 'pac-container', 12, null);
} catch(e) {
console.log(e);
}
I just encountered this issue as well. It may have something to do with my input field being inside of a flexbox but I haven't tried restructuring my page yet. Instead I added an onfocus listener to my input field as well as an onscroll listener to it's container. Inside I get the input field's position with getBoundingClientRect and then update my stylesheet with the values. I tried directly selecting and updating the .pac-container via document.querySelctor but that didn't seem to work. You may need a setTimeout to allow it to be added to the DOM first.
Here is my code:
let ruleIndex = null;
const form = document.body.querySelector('.form');
const input = document.body.querySelector('.form-input');
const positionAutoComplete = () => {
const { top, left, height } = inputField.getBoundingClientRect();
if(ruleIndex) document.styleSheets[0].deleteRule(ruleIndex);
ruleIndex = document.styleSheets[0].insertRule(`.pac-container { top: ${top + height}px !important; left: ${left}px !important; }`);
}
form.addEventListener('scroll', positionAutoComplete);
input.addEventListener('focus', positionAutoComplete);
As mentioned in an earlier answer, this breaks the minute google decides to rename .pac-container so not a perfect fix but works in the meantime.

Javascript - making form inputs appear needed through key input

I have a form that has five text inputs, a through e. I set it though so only the first two are visible through the css:display, and I set the last three to display:none initially.
I have my javascript so that it sets the last input to 'b', and the next input to 'c', and depending on whether the other texts are empty or not changes the last and next variables.
option-a, option-b, etc. is the id of the text box in the form
answer_answera, answer_answerb, etc. is the class of the form input
<script>
var last = 'b';
var next = 'c';
if (document.getElementById('option-c').value != null) {
last = 'c';
next = 'd';
}
if (document.getElementById('option-d').value != null) {
last = 'd';
next = 'e';
}
if (document.getElementById('option-e').value != null) {
last = '';
next = '';
}
$('#answer_answer'+last).keyup(function() {
console.log('beg');
var elem = document.getElementById('option-'+next);
elem.style.display="block";
console.log('hit this');
})
</script>
This works for the first input. When I type in form input b, form input c appears. However, how do I get this to continually, I suppose, refresh itself, as form input d does not appear when I type in form input c. I thought about putting a while loop around the entire last block/keyup function, but it made the entire app slow and wouldn't load properly.
Thanks for any thoughts! :)
Before we go into solving this problem, let's quickly review some Javascript concepts. When your document first loads, everything inside the <script></script> tags will execute once. This means that the following statements will all be evaluated:
var last = 'b';
var next = 'c';
if (document.getElementById('option-c').value != null) {
last = 'c';
next = 'd';
}
if (document.getElementById('option-d').value != null) {
last = 'd';
next = 'e';
}
if (document.getElementById('option-e').value != null) {
last = '';
next = '';
}
Once they are evaluated, they will never be run again until you refresh the page. Thus, when the page is done loading last = 'b' and next = 'c' and the three if statements all evaluate to false and are not executed (I assume the text fields are empty on initial load). The following code will also be executed once:
$('#answer_answer'+last).keyup(function() {
console.log('beg');
var elem = document.getElementById('option-'+next);
elem.style.display="block";
console.log('hit this');
})
However, this code binds a 'handler' (a future promise) to execute some code given a user action.
The action is a keyup event on the '#answer_answer'+last element.
Because we know that last = 'b' when this promise is made, this
really means '#answer_answerb'
The promise is to execute the following code:
console.log('beg');
var elem = document.getElementById('option-'+next);
elem.style.display="block";
console.log('hit this');
Thus, when you begin typing in #answer_answerb the #answer_answerc field is displayed (remember that next = 'c'). And if you type some more into #answer_answerb the #answer_answerc field remains visible. And now we know everything that your code does.
So, how do we fix it? Can you guess? We can make more promises. The full code:
<script>
$('#answer_answerb).keyup(function() {
var elem = document.getElementById('option-c');
elem.style.display="block";
})
$('#answer_answerc).keyup(function() {
var elem = document.getElementById('option-d');
elem.style.display="block";
})
$('#answer_answerd).keyup(function() {
var elem = document.getElementById('option-e');
elem.style.display="block";
})
</script>
This will have the desired effect, but is hard to maintain. If you decide to add more text fields, you will have to create a promise for each one. You can find a more elegant solution to your problem here http://jsfiddle.net/tppiotrowski/GkT2g/

Categories