Related
I have adapted the following code from a tutorial to filter li elements based upon their contents:
$('#_selectSearch_' + index).keyup(function() {
var filter = $(this).val();
if(filter) {
$('#_selectDrop_' + index).find("li:not(:contains(" + filter + "))").slideUp();
$('#_selectDrop_' + index).find("li:contains(" + filter + ")").slideDown();
} else {
$('#_selectDrop_' + index).find("li").slideDown();
}
});
The code works just fine but when working with large lists is very slow bringing the browser to a grinding halt for seconds with every key-press. I have been looking around and have come to the conclusion that the way to improve this is to somehow cache the list and not operate directly on the DOM but have no idea how to implement this.
If your main concern is performance the following code:
caches element containing filter string.
caches li elements.
doesn't show or hide elements that are already in that state.
uses indexOf which is very fast.
if the user types letters under 500 milliseconds apart the showMatches will not run.
var selectSearch = $("#_selectSearch_" + index );
var li = $("#_selectDrop_" + index + " li");
var currentTimeout;
selectSearch.on( "keyup", function( ) {
if( currentTimeout ) { window.clearTimeout( currentTimeout ) };
currentTimeout = setTimeout( showMatches, 500 );
});
function showMatches( ) {
var txt = selectSearch.val();
for( var i = 0, len = li.length; i < len; i++ ) {
var content = li[i].textContent ? li[i].textContent : li[i].innerText;
if( txt && content.indexOf( txt ) > -1) {
if( li[i].style.display !== "block" ) {
li[i].style.display = "block";
}
} else {
if( li[i].style.display !== "none" ) {
li[i].style.display = "none";
}
}
}
}
Fiddle with 400 li elements here
You can cache this element $('#_selectDrop_' + index + ' li');
$('#_selectSearch_' + index).keyup(function() {
var $li = $('#_selectDrop_' + index + ' li');
var filter = $(this).val();
if (filter) {
$li.not(":contains(" + filter + ")").slideUp();
$li.contains(filter).slideDown();
} else {
$li.slideDown();
}
});
drop = $('#_selectDrop_' + index + ' li');
$('#_selectSearch_' + index).keyup(function() {
var filter = $(this).val();
if(filter) {
drop.find(":not(:contains(" + filter + "))").slideUp();
drop.find(":contains(" + filter + ")").slideDown();
} else {
drop.slideDown();
}
});
Drop will be cached just once, and then will be used at every keyup. Also this uses the minimum possible of find
$('#_selectSearch_' + index).keyup(function() {
var filter = $(this).val();
// by combining and cacheing all the way to the li
// we save a lot of time, since it seems that's where you are doing
// all your searching from
var selectDrop = $('#_selectDrop_' + index + ' li');
if (filter) {
selectDrop.not(':contains("' + filter + '")').slideUp();
selectDrop.contains(filter).slideDown();
}
else {
selectDrop.slideDown();
}
});
I'll give it a go with a somewhat modified (and untested) version:
$('#_selectSearch_' + index).on('keyup', function() {
var filter = this.value,
lis = document.getElementById('_selectDrop_' + index).getElementsByTagName('li'),
len = lis.length,
sup = 'textContent' in this;
if (filter.length) {
for (var i = len; i--) {
var text = sup ? lis[i].textContent : lis[i].innerText;
$(lis[i])[text.indexOf(filter) != -1 ? 'slideDown' : 'slideUp']();
}
} else {
$(lis).slideDown();
}
});
Is it possible to generate the most specific XPath expression automatically from the position of the cursor on the web page?
The XPath expression would change with "onMouseMove event".
If it's possible, how would you implement it? Or is it already implemented in some Javascript or Python library? I would prefer it in Python with a combination of some web library but Javascript would be good and acceptable too.
See the Get XPath thread in DZone Snippets for finding the XPath. See the How do I check if the mouse is over an element in jQuery? question here for identifying when the mouse cursor is over an element.
I have answered an almost identical question (using jQuery) at Return XPath location with jQuery? Need some feedback on a function
If you change the click event to mouseenter you would have what you ask for..
$(document).delegate('*','mouseenter',function(){
var path = $(this).parents().andSelf();
var xpath='/';
for (var i = 0; i < path.length; i++)
{
var nd = path[i].nodeName.toLowerCase();
xpath += '/';
if (nd != 'html' && nd != 'body')
{
xpath += nd;
if (path[i].id != '')
{
xpath += '#' + path[i].id;
}
else
{
xpath += '['+ ($(path[i-1]).children().index(path[i])+1) +']';
}
if (path[i].className != '')
xpath += '.' + path[i].className;
}
else
{xpath += nd;}
}
$('#xpath').html(xpath); // show the xpath in an element with id xpath..
return false;
});
Demo at http://jsfiddle.net/gaby/hsv97/25/
Update with no jQuery used.. (for modern browsers)
function getXpath(event){
var hierarchy = [],
current = event.srcElement||event.originalTarget;
while (current.parentNode){
hierarchy.unshift(current);
current = current.parentNode;
}
var xPath = hierarchy.map(function(el,i){
return el.nodeName.toLowerCase() + ((el.id !== '') ? '#'+el.id : '') + ((el.className !== '') ? '.'+el.className.split(' ').join('.') : '');
}).join('/');
document.getElementById('xpath').innerHTML = xPath;
return xPath;
}
if (document.addEventListener){
document.addEventListener('mouseover', getXpath, false);
} else {
document.onmouseover = getXpath;
}
Demo at http://jsfiddle.net/gaby/hsv97/29/
vanilla javascript (with indices) http://jsfiddle.net/nycu2/1/
function nodeindex(element, array) {
var i,
found = -1,
element_name = element.nodeName.toLowerCase(),
matched
;
for (i = 0; i != array.length; ++i) {
matched = array[i];
if (matched.nodeName.toLowerCase() === element_name) {
++found;
if (matched === element) {
return found;
}
}
}
return -1;
}
function xpath(element, suffix) {
var parent, child_index, node_name;
parent = element.parentElement;
if (parent) {
node_name = element.nodeName.toLowerCase();
child_index = nodeindex(element, parent.children) + 1;
return xpath(parent, '/' + node_name + '[' + child_index + ']' + suffix);
} else {
return '//html[1]' + suffix;
}
}
function xpathstring(event) {
var
e = event.srcElement || event.originalTarget,
path = xpath(e, '');;
document.querySelector('.xpathresult').value = path;
highlight();
}
I am having issues figuring out how to resolve the getElementsByClassName issue in IE. How would I best implement the robert nyman (can't post the link to it since my rep is only 1) resolution into my code? Or would a jquery resolution be better? my code is
function showDesc(name) {
var e = document.getElementById(name);
//Get a list of elements that have a class name of service selected
var list = document.getElementsByClassName("description show");
//Loop through those items
for (var i = 0; i < list.length; ++i) {
//Reset all class names to description
list[i].className = "description";
}
if (e.className == "description"){
//Set the css class for the clicked element
e.className += " show";
}
else{
if (e.className == "description show"){
return;
}
}}
and I am using it on this page dev.msmnet.com/services/practice-management to show/hide the description for each service (works in Chrome and FF). Any tips would be greatly appreciated.
I was curious to see what a jQuery version of your function would look like, so I came up with this:
function showDesc(name) {
var e = $("#" + name);
$(".description.show").removeClass("show");
if(e.attr("class") == "description") {
e.addClass("show");
} else if(e.hasClass("description") && e.hasClass("show")) {
return;
}
}
This should support multiple classes.
function getElementsByClassName(findClass, parent) {
parent = parent || document;
var elements = parent.getElementsByTagName('*');
var matching = [];
for(var i = 0, elementsLength = elements.length; i < elementsLength; i++){
if ((' ' + elements[i].className + ' ').indexOf(findClass) > -1) {
matching.push(elements[i]);
}
}
return matching;
}
You can pass in a parent too, to make its searching the DOM a bit faster.
If you want getElementsByClassName('a c') to match HTML <div class="a b c" /> then try changing it like so...
var elementClasses = elements[i].className.split(/\s+/),
matchClasses = findClass.split(/\s+/), // Do this out of the loop :)
found = 0;
for (var j = 0, elementClassesLength = elementClasses.length; j < elementClassesLength; j++) {
if (matchClasses.indexOf(elementClasses[j]) > -1) {
found++;
}
}
if (found == matchClasses.length) {
// Push onto matching array
}
If you want this function to only be available if it doesn't already exist, wrap its definition with
if (typeof document.getElementsByClassName != 'function') { }
Even easier jQuery solution:
$('.service').click( function() {
var id = "#" + $(this).attr('id') + 'rt';
$('.description').not(id).hide();
$( id ).show();
}
Why bother with a show class if you are using jQuery?
Heres one I put together, reliable and possibly the fastest. Should work in any situation.
function $class(className) {
var children = document.getElementsByTagName('*') || document.all;
var i = children.length, e = [];
while (i--) {
var classNames = children[i].className.split(' ');
var j = classNames.length;
while (j--) {
if (classNames[j] == className) {
e.push(children[i]);
break;
}
}
}
return e;
}
I used to implement HTMLElement.getElementByClassName(), but at least Firefox and Chrome, only find the half of the elements when those elements are a lot, instead I use something like (actually it is a larger function):
getElmByClass(clm, parent){
// clm: Array of classes
if(typeof clm == "string"){ clm = [clm] }
var i, m = [], bcl, re, rm;
if (document.evaluate) { // Non MSIE browsers
v = "";
for(i=0; i < clm.length; i++){
v += "[contains(concat(' ', #"+clc+", ' '), ' " + base[i] + " ')]";
}
c = document.evaluate("./"+"/"+"*" + v, parent, null, 5, null);
while ((node = c.iterateNext())) {
m.push(node);
}
}else{ // MSIE which doesn't understand XPATH
v = elm.getElementsByTagName('*');
bcl = "";
for(i=0; i < clm.length; i++){
bcl += (i)? "|":"";
bcl += "\\b"+clm[i]+"\\b";
}
re = new RegExp(bcl, "gi");
for(i = 0; i < v.length; i++){
if(v.className){
rm = v[i].className.match(bcl);
if(rm && rm.length){ // sometimes .match returns an empty array so you cannot use just 'if(rm)'
m.push(v[i])
}
}
}
}
return m;
}
I think there would be a faster way to iterate without XPATH, because RegExp are slow (perhaps a function with .indexOf, it shuld be tested), but it is working well
You can replace getElementsByClassName() with the following:
function getbyclass(n){
var elements = document.getElementsByTagName("*");
var result = [];
for(z=0;z<elements.length;z++){
if(elements[z].getAttribute("class") == n){
result.push(elements[z]);
}
}
return result;
}
Then you can use it like this:
getbyclass("description") // Instead of document.getElementsByClassName("description")
Let's say I have a large HTML file with different kinds of tags, similar to the StackOverflow one you're looking at right now.
Now let's say you click an element on the page, what would the Javascript function look like that calculates the most basic XPath that refers to that specific element?
I know there are an infinite ways of refering to that element in XPath, but I'm looking for something that just looks at the DOM tree, with no regard for IDs, classes, etc.
Example:
<html>
<head><title>Fruit</title></head>
<body>
<ol>
<li>Bananas</li>
<li>Apples</li>
<li>Strawberries</li>
</ol>
</body>
</html>
Let's say you click on Apples. The Javascript function would return the following:
/html/body/ol/li[2]
It would basically just work its way upward the DOM tree all the way to the HTML element.
Just to clarify, the 'on-click' event-handler isn't the problem. I can make that work. I'm just not sure how to calculate the element's position within the DOM tree and represent it as an XPath.
PS
Any answer with or without the use of the JQuery library is appreciated.
PPS
I completely new to XPath, so I might even have made a mistake in the above example, but you'll get the idea.
Edit at August 11, 2010: Looks like somebody else asked a similar question: generate/get the Xpath for a selected textnode
Firebug can do this, and it's open source (BSD) so you can reuse their implementation, which does not require any libraries.
3rd party edit
This is an extract from the linked source above. Just in case the link above will change. Please check the source to benefit from changes and updates or the full featureset provided.
Xpath.getElementXPath = function(element)
{
if (element && element.id)
return '//*[#id="' + element.id + '"]';
else
return Xpath.getElementTreeXPath(element);
};
Above code calls this function.
Attention i added some line-wrapping to avoid horizontal scroll bar
Xpath.getElementTreeXPath = function(element)
{
var paths = []; // Use nodeName (instead of localName)
// so namespace prefix is included (if any).
for (; element && element.nodeType == Node.ELEMENT_NODE;
element = element.parentNode)
{
var index = 0;
var hasFollowingSiblings = false;
for (var sibling = element.previousSibling; sibling;
sibling = sibling.previousSibling)
{
// Ignore document type declaration.
if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
continue;
if (sibling.nodeName == element.nodeName)
++index;
}
for (var sibling = element.nextSibling;
sibling && !hasFollowingSiblings;
sibling = sibling.nextSibling)
{
if (sibling.nodeName == element.nodeName)
hasFollowingSiblings = true;
}
var tagName = (element.prefix ? element.prefix + ":" : "")
+ element.localName;
var pathIndex = (index || hasFollowingSiblings ? "["
+ (index + 1) + "]" : "");
paths.splice(0, 0, tagName + pathIndex);
}
return paths.length ? "/" + paths.join("/") : null;
};
A function I use to get an XPath similar to your situation, it uses jQuery:
function getXPath( element )
{
var xpath = '';
for ( ; element && element.nodeType == 1; element = element.parentNode )
{
var id = $(element.parentNode).children(element.tagName).index(element) + 1;
id > 1 ? (id = '[' + id + ']') : (id = '');
xpath = '/' + element.tagName.toLowerCase() + id + xpath;
}
return xpath;
}
Small, powerfull and pure-js function
It returns xpath for the element and elements iterator for xpath.
https://gist.github.com/iimos/e9e96f036a3c174d0bf4
function xpath(el) {
if (typeof el == "string") return document.evaluate(el, document, null, 0, null)
if (!el || el.nodeType != 1) return ''
if (el.id) return "//*[#id='" + el.id + "']"
var sames = [].filter.call(el.parentNode.children, function (x) { return x.tagName == el.tagName })
return xpath(el.parentNode) + '/' + el.tagName.toLowerCase() + (sames.length > 1 ? '['+([].indexOf.call(sames, el)+1)+']' : '')
}
Probably you will need to add a shim for IE8 that don't support the [].filter method: this MDN page gives such code.
Usage
Getting xpath for node:
var xp = xpath(elementNode)
Executing xpath:
var iterator = xpath("//h2")
var el = iterator.iterateNext();
while (el) {
// work with element
el = iterator.iterateNext();
}
The firebug implementation can be modified slightly to check for element.id further up the dom tree:
/**
* Gets an XPath for an element which describes its hierarchical location.
*/
var getElementXPath = function(element) {
if (element && element.id)
return '//*[#id="' + element.id + '"]';
else
return getElementTreeXPath(element);
};
var getElementTreeXPath = function(element) {
var paths = [];
// Use nodeName (instead of localName) so namespace prefix is included (if any).
for (; element && element.nodeType == 1; element = element.parentNode) {
var index = 0;
// EXTRA TEST FOR ELEMENT.ID
if (element && element.id) {
paths.splice(0, 0, '/*[#id="' + element.id + '"]');
break;
}
for (var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling) {
// Ignore document type declaration.
if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
continue;
if (sibling.nodeName == element.nodeName)
++index;
}
var tagName = element.nodeName.toLowerCase();
var pathIndex = (index ? "[" + (index+1) + "]" : "");
paths.splice(0, 0, tagName + pathIndex);
}
return paths.length ? "/" + paths.join("/") : null;
};
I have just modified DanS' solution in order to use it with textNodes. Very useful to serialize HTML range object.
/**
* Gets an XPath for an node which describes its hierarchical location.
*/
var getNodeXPath = function(node) {
if (node && node.id)
return '//*[#id="' + node.id + '"]';
else
return getNodeTreeXPath(node);
};
var getNodeTreeXPath = function(node) {
var paths = [];
// Use nodeName (instead of localName) so namespace prefix is included (if any).
for (; node && (node.nodeType == 1 || node.nodeType == 3) ; node = node.parentNode) {
var index = 0;
// EXTRA TEST FOR ELEMENT.ID
if (node && node.id) {
paths.splice(0, 0, '/*[#id="' + node.id + '"]');
break;
}
for (var sibling = node.previousSibling; sibling; sibling = sibling.previousSibling) {
// Ignore document type declaration.
if (sibling.nodeType == Node.DOCUMENT_TYPE_NODE)
continue;
if (sibling.nodeName == node.nodeName)
++index;
}
var tagName = (node.nodeType == 1 ? node.nodeName.toLowerCase() : "text()");
var pathIndex = (index ? "[" + (index+1) + "]" : "");
paths.splice(0, 0, tagName + pathIndex);
}
return paths.length ? "/" + paths.join("/") : null;
};
There is nothing built in to get the xpath of an HTML element, but the reverse is common for example using the jQuery xpath selector.
If you need to determine the xpath of an HTML element you will have to provide a custom function to do this. Here are a couple of example javascript/jQuery impls to calculate the xpath.
Just for fun, an XPath 2.0 one line implementation:
string-join(ancestor-or-self::*/concat(name(),
'[',
for $x in name()
return count(preceding-sibling::*
[name() = $x])
+ 1,
']'),
'/')
The solution below is preferable if you need to reliably determine the absolute XPath of an element.
Some other answers either rely partly on the element id (which is not reliable since there can potentially be multiple elements with identical ids) or they generate XPaths that actually specify more elements than the one given (by erroneously omitting the sibling index in certain circumstances).
The code has been adapted from Firebug's source code by fixing the above-mentioned problems.
getXElementTreeXPath = function( element ) {
var paths = [];
// Use nodeName (instead of localName) so namespace prefix is included (if any).
for ( ; element && element.nodeType == Node.ELEMENT_NODE; element = element.parentNode ) {
var index = 0;
for ( var sibling = element.previousSibling; sibling; sibling = sibling.previousSibling ) {
// Ignore document type declaration.
if ( sibling.nodeType == Node.DOCUMENT_TYPE_NODE ) {
continue;
}
if ( sibling.nodeName == element.nodeName ) {
++index;
}
}
var tagName = element.nodeName.toLowerCase();
// *always* include the sibling index
var pathIndex = "[" + (index+1) + "]";
paths.unshift( tagName + pathIndex );
}
return paths.length ? "/" + paths.join( "/") : null;
};
function getPath(event) {
event = event || window.event;
var pathElements = [];
var elem = event.currentTarget;
var index = 0;
var siblings = event.currentTarget.parentNode.getElementsByTagName(event.currentTarget.tagName);
for (var i=0, imax=siblings.length; i<imax; i++) {
if (event.currentTarget === siblings[i] {
index = i+1; // add 1 for xpath 1-based
}
}
while (elem.tagName.toLowerCase() != "html") {
pathElements.unshift(elem.tagName);
elem = elem.parentNode;
}
return pathElements.join("/") + "[" + index + "]";
}
EDITED TO ADD SIBLING INDEX INFORMATION
Use https://github.com/KajeNick/jquery-get-xpath
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<script src="../src/jquery-get-xpath.js"></script>
<script>
jQuery(document).ready(function ($) {
$('body').on('click', 'ol li', function () {
let xPath = $(this).jGetXpath();
console.log(xPath);
});
});
</script>
Console will show: /html/body/ol/li[2]
I have came across this problem and found it hard to solve fully. as in my case it was giving half xpath. so i modified it a little to give full path. here is my answer.
window.onclick = (e) => {
let pathArr = e.path;
let element = pathArr[0];
var xpath = '';
if(pathArr.length<=2 && pathArr[0].nodeType!=1){
for (let i = 0; i < pathArr.length - 1 && pathArr[i].nodeType == 1; i++) {
element = pathArr[i];
var id = $(element.parentNode).children(element.tagName).index(element) + 1;
id > 1 ? (id = '[' + id + ']') : (id = '');
xpath = '/' + element.tagName.toLowerCase() + id + xpath;
}
}
else{
xpath="/html/document"
}
return xpath;
I am looking for a way to Show/ Hide some of the Radio buttons of a Radio button group using JavaScript.
How can this be achieved?
Thanks
The id attribute identifies the individual radio buttons. All of them will be related to the group by the name attribute
You can get the individual radio buttons using something like
var rbtn = document.getElementById('radioButton1');
Then set the display or visibility style to hide or show.
rbtn.style.display = 'none'; // 'block' or 'inline' if you want to show.
document.getElementById('myRadioButtonId').style.display = 'none'; //hide it
document.getElementById('myRadioButtonId').style.display = 'block'; //show it
That's a very generic question, so I can only offer a very generic answer. Using jQuery, for example:
$("#some-selector .of-some-kind .for-the-things-you-wish-to-hide").hide();
Of course, what you're selecting, and under what circumstances you want to hide it can dramatically impact the best way to go about the problem.
Apply a CSS class to the radiobuttons then use a loop to set the display = 'none' for each radiobutton.
Firstly, we need a cross-browser getElementsByClassName function
/*
Developed by Robert Nyman, http://www.robertnyman.com
Code/licensing: http://code.google.com/p/getelementsbyclassname/
*/
var getElementsByClassName = function (className, tag, elm){
if (document.getElementsByClassName) {
getElementsByClassName = function (className, tag, elm) {
elm = elm || document;
var elements = elm.getElementsByClassName(className),
nodeName = (tag)? new RegExp("\\b" + tag + "\\b", "i") : null,
returnElements = [],
current;
for(var i=0, il=elements.length; i<il; i+=1){
current = elements[i];
if(!nodeName || nodeName.test(current.nodeName)) {
returnElements.push(current);
}
}
return returnElements;
};
}
else if (document.evaluate) {
getElementsByClassName = function (className, tag, elm) {
tag = tag || "*";
elm = elm || document;
var classes = className.split(" "),
classesToCheck = "",
xhtmlNamespace = "http://www.w3.org/1999/xhtml",
namespaceResolver = (document.documentElement.namespaceURI === xhtmlNamespace)? xhtmlNamespace : null,
returnElements = [],
elements,
node;
for(var j=0, jl=classes.length; j<jl; j+=1){
classesToCheck += "[contains(concat(' ', #class, ' '), ' " + classes[j] + " ')]";
}
try {
elements = document.evaluate(".//" + tag + classesToCheck, elm, namespaceResolver, 0, null);
}
catch (e) {
elements = document.evaluate(".//" + tag + classesToCheck, elm, null, 0, null);
}
while ((node = elements.iterateNext())) {
returnElements.push(node);
}
return returnElements;
};
}
else {
getElementsByClassName = function (className, tag, elm) {
tag = tag || "*";
elm = elm || document;
var classes = className.split(" "),
classesToCheck = [],
elements = (tag === "*" && elm.all)? elm.all : elm.getElementsByTagName(tag),
current,
returnElements = [],
match;
for(var k=0, kl=classes.length; k<kl; k+=1){
classesToCheck.push(new RegExp("(^|\\s)" + classes[k] + "(\\s|$)"));
}
for(var l=0, ll=elements.length; l<ll; l+=1){
current = elements[l];
match = false;
for(var m=0, ml=classesToCheck.length; m<ml; m+=1){
match = classesToCheck[m].test(current.className);
if (!match) {
break;
}
}
if (match) {
returnElements.push(current);
}
}
return returnElements;
};
}
return getElementsByClassName(className, tag, elm);
};
then use like so
var radioButtons = getElementsByClassName('myClassName', 'input');
var len = radioButtons.length;
for (var i = 0; i < len; i++)
radioButtons[i].style.display = 'none';
Everybody else has nailed it, so I'll put in a plug for the frameworks.
Using jquery or mootools or another framework makes this super easy:
MooTools:
$$('input[name=myRadioName]).setStyle('display','none');
or
$$('input[name=myRadioName]).fade('out');
You can try something like
<span id="myspan"><input id="radioButton1" type="radio" />Some label goes here</span>
Then
document.getElementById('myspan').style.display="none"; //inline or block to display again
If you are using JQuery then
$('myspan').hide(); // show() to display again