Given an element and any selector, I need to find the closest element which matches it, not matter if it's inside the element or outside of it.
Currently jQuery doesn't provide such traversing functionality, but there is a need. Here is the scenario:
A list of many items where the <button> element reside inside <a>
<ul>
<li>
<a>
<button>click me</button>
<img src="..." />
</a>
</li>
<li>
<a>
<button>click me</button>
<img src="..." />
</a>
</li>
...
</ul>
Or the <button> element might reside outside of the <a> element
<ul>
<li>
<a>
<img src="..." />
</a>
<button>click me</button>
</li>
<li>
<a>
<img src="..." />
</a>
<button>click me</button>
</li>
...
</ul>
The very very basic code would look like this:
$('a').closest1('button'); // where `closest1` is a new custom function
// or
$('a').select('> button') // where `select` can parse any selector relative to the object, so it would also know this:
$('a').select('~ button') // where the button is a sibling to the element
the known element is <a> and anything else can change. I want to locate the nearest <button> element for a given <a> element, no matter if that button is inside or outside of <a>'s DOM tree.
It would be very logical that native jQuery function "closest" would do as the name suggests and find the closest, but it only searches upwards as you all know. (it should have been named differently IMO).
Does anyone know any custom traversing function which does the above?
Thanks. (i'm asking you people because someone must have written this for sure but I was unlucky to find a lead on the internet)
Here is another attempt using the idea I mentioned in comment:
$(this).parents(':has(button):first').find('button').css({
"border": '3px solid red'
});
JSFiddle: http://jsfiddle.net/TrueBlueAussie/z3vwk1ko/40/
It basically looks for the first ancestor that contains both the elements (clicked and target), then finds the target.
Performance:
With regard to speed, this is used at human interaction speeds, i.e. a few times per second maximum, so being a "slow selector" is irrelevant if it solves the problem, in a reasonably obvious way, with minimal code. You would have to click 100s of times per second to notice any different compared to a fast selector :)
None of the built-in selectors allow searching up and down the tree. I did create a custom findThis extension that allows you to do things like $elementClicked.('li:has(this) button') which would allow you to do something similar.
// Add findThis method to jQuery (with a custom :this check)
jQuery.fn.findThis = function (selector) {
// If we have a :this selector
if (selector.indexOf(':this') > 0) {
var ret = $();
for (var i = 0; i < this.length; i++) {
var el = this[i];
var id = el.id;
// If not id already, put in a temp (unique) id
el.id = 'id' + new Date().getTime();
var selector2 = selector.replace(':this', '#' + el.id);
ret = ret.add(jQuery(selector2, document));
// restore any original id
el.id = id;
}
ret.selector = selector;
return ret;
}
// do a normal find instead
return this.find(selector);
}
// Test case
$(function () {
$('a').click(function () {
$(this).findThis('li:has(:this) button').css({
"border": '3px solid red'
});
});
});
JSFiddle: http://jsfiddle.net/TrueBlueAussie/z3vwk1ko/38/
note: Click the images/links to test.
A while ago I wanted to do the same for a completely DOM-based text editor, and needed to find the previous (ARR LEFT) and next (ARR RIGHT) text nodes, both up and down the tree. Based on this code I have made an adaptation suiting this question. Be warned, it's quite performance-heavy, but it is adapted to any scenario. There are two functions findPrevElementNode and findNextElementNode which both return an object with properties:
match - returns the closest matching node for the search or FALSE if none is found
iterations - returns the number of iterations done to find the node. This allows you to check whether the previous node is closer than the next or vice-versa.
The parameters are as follows:
//#param {HTMLElement} referenceNode - The node from which to start the search
//#param {function} truthTest - A function that returns true for the given element
//#param {HTMLElement} [limitNode=document.body] - The limit up to which to search to
var domUtils = {
findPrevElementNode: function(referenceNode, truthTest, limitNode) {
var element = 1,
iterations = 0,
limit = limitNode || document.body,
node = referenceNode;
while (!truthTest(node) && node !== limit) {
if (node.previousSibling) {
node = node.previousSibling;
iterations++;
if (node.lastChild) {
while (node.lastChild) {
node = node.lastChild;
iterations++;
}
}
} else {
if (node.parentNode) {
node = node.parentNode;
iterations++;
} else {
return false;
}
}
}
return {match: node === limit ? false : node, iterations: iterations};
},
findNextElementNode: function(referenceNode, truthTest, limitNode) {
var element = 1,
iterations = 0,
limit = limitNode || document.body,
node = referenceNode;
while (!truthTest(node) && node !== limit) {
if (node.nextSibling) {
node = node.nextSibling;
iterations++;
if (node.firstChild) {
while (node.firstChild) {
node = node.firstChild;
iterations++;
}
}
} else {
if (node.parentNode) {
node = node.parentNode;
iterations++;
} else {
return false;
}
}
}
return {match: node === limit ? false : node, iterations: iterations};
}
};
In your case, you could do:
var a = domUtils.findNextElementNode(
document.getElementsByTagName('a')[0], // known element
function(node) { return (node.nodeName === 'BUTTON'); }
);
var b = domUtils.findPrevElementNode(
document.getElementsByTagName('a')[0], // known element
function(node) { return (node.nodeName === 'BUTTON'); }
);
var result = a.match ? (b.match ? (a.iterations < b.iterations ? a.match :
(a.iterations === b.iterations ? fnToHandleEqualDistance() : b.match)) : a.match) :
(b.match ? b.match : false);
See it in action.
DEMO PAGE / GIST
I have solved working by logic, so I would first look inside the elements, then their siblings, and last, if there are still unfound items, I would do a recursive search on the parents.
JS CODE:
jQuery.fn.findClosest = function (selector) {
// If we have a :this selector
var output = $(),
down = this.find(selector),
siblings,
recSearch,
foundCount = 0;
if(down.length) {
output = output.add(down);
foundCount += down.length;
}
// if all elements were found, return at this point
if( foundCount == this.length )
return output;
siblings = this.siblings(selector);
if( siblings.length) {
output = output.add(siblings);
foundCount += siblings.length;
}
// this is the expensive search path if there are still unfound elements
if(foundCount < this.length){
recSearch = rec(this.parent().parent());
if( recSearch )
output = output.add(recSearch);
}
function rec(elm){
var result = elm.find(selector);
if( result.length )
return result;
else
rec(elm.parent());
}
return output;
};
// Test case
var buttons = $('a').findClosest('button');
console.log(buttons);
buttons.click(function(){
this.style.outline = "1px solid red";
})
I think using sibling selector (~) or child selector (>) will solve your purpose(What ever your case is!!).
Related
I am facing an issue with the numbered list in ckeditor. When I try to bold some text in li, only the text is getting bold, without the preceding number. This is how it looks like,
One
Two
Three
It should be like this
2. Two
When I check the source, I found the code like below
<li><strong>Two</strong></li>
I would like to know is there any way to change the working of bold button, so that it will add something like below
<li style="font-weight:bold">Two</li>
<p> Hello <strong>World</strong></p>
I tried to solve your problem.
My solution isn't the best, because I guess that create a bold plugin, that takes care about list items would be the best solution.
I make it without using jQuery; however, using it the code should became simpler and more readable.
First of all, we need to define something useful for the main task:
String trim. See this.
if (!String.prototype.trim) {
String.prototype.trim = function() {
return this.replace(/^\s+|\s+$/g, '');
};
}
String contains. See this
String.prototype.contains = function(it) {
return this.indexOf(it) != -1;
};
First child element. The following function obtains the first child element, or not-empty text node, of the element passed as argument
function getFirstChildNotEmpty(el) {
var firstChild = el.firstChild;
while(firstChild) {
if(firstChild.nodeType == 3 && firstChild.nodeValue && firstChild.nodeValue.trim() != '') {
return firstChild;
} else if (firstChild.nodeType == 1) {
return firstChild;
}
firstChild = firstChild.nextSibling;
}
return firstChild;
}
Now, we can define the main two functions we need:
function removeBoldIfPresent(el) {
el = el.$;
var elStyle = el.getAttribute("style");
elStyle = (elStyle) ? elStyle : '';
if(elStyle.trim() != '' && elStyle.contains("font-weight:bold")) {
el.setAttribute("style", elStyle.replace("font-weight:bold", ''));
}
}
CKEDITOR.instances.yourinstance.on("change", function(ev) {
var liEls = ev.editor.document.find("ol li");
for(var i=0; i<liEls.count(); ++i) {
var el = liEls.getItem(i);
var nativeEl = el.$.cloneNode(true);
nativeEl.normalize();
var firstChild = getFirstChildNotEmpty(nativeEl);
if(firstChild.nodeType != 1) {
removeBoldIfPresent(el);
continue;
}
var firstChildTagName = firstChild.tagName.toLowerCase()
if(firstChildTagName == 'b' || firstChildTagName == 'strong') {
//TODO: you also need to consider the case in which the bold is done using the style property
//My suggest is to use jQuery; you can follow this question: https://stackoverflow.com/questions/10877903/check-if-text-in-cell-is-bold
var textOfFirstChild = (new CKEDITOR.dom.element(firstChild)).getText().trim();
var textOfLi = el.getText().trim();
if(textOfFirstChild == textOfLi) {
//Need to make bold
var elStyle = el.getAttribute("style");
elStyle = (elStyle) ? elStyle : '';
if(elStyle.trim() == '' || !elStyle.contains("font-weight:bold")) {
el.setAttribute("style", elStyle + ";font-weight:bold;");
}
} else {
removeBoldIfPresent(el);
}
} else {
removeBoldIfPresent(el);
}
}
});
You need to use the last release of CkEditor (version 4.3), and the onchange plugin (that is included by default in the full package).
CKEditor 4.1 remove your classes, styles, and attributes that is not specified in its rules.
If that's the problem, you might want to disable it by adding this line:
CKEDITOR.config.allowedContent = true;
Here is full code to use it:
window.onload = function() {
CKEDITOR.replace( 'txt_description' );
CKEDITOR.config.allowedContent = true; //please add this line after your CKEditor initialized
};
Please check it out here
<ul class="test">
<li><span>hello</span></li>
</ul>
.test li
{
font-weight:bold;
}
.test li span
{
font-weight:normal;
}
Say a web page has a string such as "I am a simple string" that I want to find. How would I go about this using JQuery?
jQuery has the contains method. Here's a snippet for you:
<script type="text/javascript">
$(function() {
var foundin = $('*:contains("I am a simple string")');
});
</script>
The selector above selects any element that contains the target string. The foundin will be a jQuery object that contains any matched element. See the API information at: https://api.jquery.com/contains-selector/
One thing to note with the '*' wildcard is that you'll get all elements, including your html an body elements, which you probably don't want. That's why most of the examples at jQuery and other places use $('div:contains("I am a simple string")')
Normally jQuery selectors do not search within the "text nodes" in the DOM. However if you use the .contents() function, text nodes will be included, then you can use the nodeType property to filter only the text nodes, and the nodeValue property to search the text string.
$('*', 'body')
.andSelf()
.contents()
.filter(function(){
return this.nodeType === 3;
})
.filter(function(){
// Only match when contains 'simple string' anywhere in the text
return this.nodeValue.indexOf('simple string') != -1;
})
.each(function(){
// Do something with this.nodeValue
});
This will select just the leaf elements that contain "I am a simple string".
$('*:contains("I am a simple string")').each(function(){
if($(this).children().length < 1)
$(this).css("border","solid 2px red") });
Paste the following into the address bar to test it.
javascript: $('*:contains("I am a simple string")').each(function(){ if($(this).children().length < 1) $(this).css("border","solid 2px red") }); return false;
If you want to grab just "I am a simple string". First wrap the text in an element like so.
$('*:contains("I am a simple string")').each(function(){
if($(this).children().length < 1)
$(this).html(
$(this).text().replace(
/"I am a simple string"/
,'<span containsStringImLookingFor="true">"I am a simple string"</span>'
)
)
});
and then do this.
$('*[containsStringImLookingFor]').css("border","solid 2px red");
If you just want the node closest to the text you're searching for, you could use this:
$('*:contains("my text"):last');
This will even work if your HTML looks like this:
<p> blah blah <strong>my <em>text</em></strong></p>
Using the above selector will find the <strong> tag, since that's the last tag which contains that entire string.
Take a look at highlight (jQuery plugin).
Just adding to Tony Miller's answer as this got me 90% towards what I was looking for but still didn't work. Adding .length > 0; to the end of his code got my script working.
$(function() {
var foundin = $('*:contains("I am a simple string")').length > 0;
});
this function should work. basically does a recursive lookup till we get a distinct list of leaf nodes.
function distinctNodes(search, element) {
var d, e, ef;
e = [];
ef = [];
if (element) {
d = $(":contains(\""+ search + "\"):not(script)", element);
}
else {
d = $(":contains(\""+ search + "\"):not(script)");
}
if (d.length == 1) {
e.push(d[0]);
}
else {
d.each(function () {
var i, r = distinctNodes(search, this);
if (r.length === 0) {
e.push(this);
}
else {
for (i = 0; i < r.length; ++i) {
e.push(r[i]);
}
}
});
}
$.each(e, function () {
for (var i = 0; i < ef.length; ++i) {
if (this === ef[i]) return;
}
ef.push(this);
});
return ef;
}
<div id="tab1" class="nav left">
<ul>
<li>Home</li>
......
</ul>
</div>
Now, i want to remove the class="now" or set the class value empty. If the url not on mangento, I using the following code. But the I don't know how to write the last part.
window.onload = function removeNow() {
var div = document.getElementById("tab1").getElementsByTagName("a");
if (window.location.pathname != '/magento/') {
div.removeClass();
}
}
Thank you.
In modern browsers you can use the classList API:
div.classList.remove( 'now' );
But a problem specific to your code: You must loop in order to remove the class. So try this:
for ( var i = 0; i < div.length; i++ ) {
div[i].classList.remove( 'now' );
}
If your browser doesn't support classList, use this removeClass shim:
function removeClass( elem, name ) {
var classlist = elem.className.split( /\s/ ),
newlist = [],
idx = 0;
for ( ; idx < classlist.length; idx++ ) {
if ( classlist[ idx ] !== name ) {
newlist.push( classlist[ idx ] );
}
}
elem.className = newlist.join(" ");
return true;
}
or with jQuery (with which we are not required to use classList or className):
$('a').each(function() {
if (window.location.pathname != '/magento/')
$(this).removeClass();
});
Set the className property:
div.className = '';
Note that getElementsByTagName returns a (possibly empty) NodeList, so:
var div = document.getElementById("tab1").getElementsByTagName("a");
is a collection of all the A element descendents of the element with ID "tab1" (and so 'div' is probably not a good name).
If all you want to do is remove all class values of the first such A element, then:
div[0].className = '';
will do the job. But since the NodeList might be empty, the following would be more robust:
if (div[0]) {
div[0].className = '';
}
or perhaps
div[0] && div[0].className = '';
it depends on your coding style and maintainability requirements.
I need to generate unique css selector for elements.
Particularly, I have onclick event handler that should remember what target element
was clicked and send this info to my server. Is there a way to do it without doing DOM modifications?
P.S. my javascript code supposed to be run on different
3-rd party websites so I can't make any assumptions about html.
This function creates a long, but quite practical unique selector, works quickly.
const getCssSelector = (el) => {
let path = [], parent;
while (parent = el.parentNode) {
path.unshift(`${el.tagName}:nth-child(${[].indexOf.call(parent.children, el)+1})`);
el = parent;
}
return `${path.join(' > ')}`.toLowerCase();
};
Example result:
html:nth-child(1) > body:nth-child(2) > div:nth-child(1) > div:nth-child(1) > main:nth-child(3) > div:nth-child(2) > p:nth-child(2)
The following code creates a slightly more beautiful and short selector
const getCssSelectorShort = (el) => {
let path = [], parent;
while (parent = el.parentNode) {
let tag = el.tagName, siblings;
path.unshift(
el.id ? `#${el.id}` : (
siblings = parent.children,
[].filter.call(siblings, sibling => sibling.tagName === tag).length === 1 ? tag :
`${tag}:nth-child(${1+[].indexOf.call(siblings, el)})`
)
);
el = parent;
};
return `${path.join(' > ')}`.toLowerCase();
};
Example result:
html > body > div > div > main > div:nth-child(2) > p:nth-child(2)
Check this CSS selector generator library #medv/finder
Generates shortest selectors
Unique selectors per page
Stable and robust selectors
2.9 kB gzip and minify size
Example of generated selector:
.blog > article:nth-child(3) .add-comment
Yes, you could do this. But with a few caveats. In order to be able to guarantee that selectors are unique, you'd need to use :nth-child() which isn't universally supported. If you're then wanting to put these CSS selectors into CSS files, it won't work in all browsers.
I'd do it with something like this:
function () {
if (this.id) {
return sendToServer('#' + this.id);
}
var parent = this.parentNode;
var selector = '>' + this.nodeName + ':nth-child(' + getChildNumber(this) ')';
while (!parent.id && parent.nodeName.toLowerCase() !== 'body') {
selector = '>' + this.nodeName + ':nth-child(' + getChildNumber(parent) + ')' + selector;
parent = parent.parentNode;
}
if (parent.nodeName === 'body') {
selector = 'body' + selector;
} else {
selector = '#' + parent.id + selector;
}
return sendToServer(selector);
}
Then add that to your click handler for each element you want to model. I'll leave you to implement getChildNumber().
Edit: Just seen your comment about it being 3rd party code... so you could add an event argument, replace all instances of this with event.target and then just attach the function to window's click event if that's easier.
let say you have a list of links for the sake of simplicity: you can simply pass the index of the triggering element in the collection of all elements
...
...
...
the js (jQuery 1.7+, I used .on()otherwise use bind()) function can be
var triggers = $('a');
triggers.on('click', function(e) {
e.preventDefault();
var index = triggers.index($(this));
/* ajax call passing index value */
});
so that if you click on third element index value passed will be 2. (0-based index);
of course this is valid as long as the code (the DOM) doesn't change. Later you can use that index to create a css rule to that element e.g. using :nth-child
Otherwise if each one of your elements have a different attribute (like an id) you can pass that attribute
example on JsFiddle : http://jsfiddle.net/t7J8T/
You could probably traverse the DOM tree from the node back to the body element to generate a selector.
Firebug has a feature for this, both using XPath and CSS selectors.
See this answer
"use strict";
function getSelector(_context){
var index, localName,pathSelector, that = _context, node;
if(that =='null') throw 'not an dom reference';
index = getIndex(that);
while(that.tagName){
pathSelector = that.localName+(pathSelector?'>'+pathSelector :'');
that = that.parentNode;
}
pathSelector = pathSelector+':nth-of-type('+index+')';
return pathSelector;
}
function getIndex(node){
var i=1;
var tagName = node.tagName;
while(node.previousSibling){
node = node.previousSibling;
if(node.nodeType === 1 && (tagName.toLowerCase() == node.tagName.toLowerCase())){
i++;
}
}
return i;
}
document.addEventListener('DOMContentLoaded', function(){
document.body.addEventListener('mouseover', function(e){
var c = getSelector(e.target);
var element = document.querySelector(c);
//console.log(element);
console.log(c);
//element.style.color = "red"
});
});
you can try this one. without using jquery.
The Firefox developer console uses the following heuristics to find a unique selector for an element. The algorithm returns the first suitable selector found. (Source)
If the element has a unique id attribute, return the id selector #<idname>.
If the tag is html, head, or body, return the type selector <elementname>.
For each class of the element:
If the class is unique to the element, return the class selector .<classname>.
If the tag/class combination is unique, return the selector <elementname>.<classname>.
Compute the element's index in its parent's child list. If the selector <elementname>.<classname>:nth-child(<index>) is unique, return that.
Let index be the element's index in its parent's child list.
If the parent node is the root node, return the selector <elementname>:nth-child(<index>).
Otherwise, recursively find a unique selector for the parent node and return <parentselector> > <elementname>:nth-child(<index>).
the empty selector says that: Matches all elements that have no children (including text nodes).Finds all elements that are empty - they don't have child elements or text.
What i want is to get elements which has no children but may have text inside., how?
UPDATE:
Example:
I want select these elements which has no children but may have text, with syntax like this:
$('div:empty, a:empty, span, p:empty, td:empty, img, input').mousemove(myMouseOverHandler);
Get any element that doesn't have any other element:
$('*:not(:has(*))');
If an element has only text, children() will have a length of 0:
<div id="test1">
Hello World
</div>
<div id="test2">
<b>Hey there</b>
</div>
<script>
alert($("#test1").children().length); // alerts 0
alert($("#test2").children().length); // alerts 1 (the bold tag)
</script>
EDIT: In response to your edit, jQuery is awesome enough to let you do custom filters:
$.expr[':'].emptyOrText = function(e) {
return $(e).children().length == 0;
};
So, using the above against the HTML above, you could do this:
$('div:emptyOrText'); // will select #test1
I made a pure JavaScript function for anyone that does not want to use jQuery.
const getElementsWithNoChildren = (target) => {
let candidates;
if (target && typeof target.querySelectorAll === 'function') {
candidates = target.querySelectorAll('*');
}
else if (target && typeof target.length === 'number') {
candidates = target;
}
else {
candidates = document.querySelectorAll('*');
}
return Array.from(candidates).filter((elem) => {
return elem.children.length === 0;
});
};