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();
}
});
Related
I am having difficulty in selecting only elements that contain a certain value.
if (autoCostBatches) {
if ($('#noCostRecovery').attr('checked')) {
var tester= "";
for (var i = 0; i < autoCostBatches2.length; i++) {
tester = tester + autoCostBatches2[i].items + ", ";
}
var userString = tester.substring(0, tester.length - 2);
console.log(userString);
element = $('td:not(:contains(' + userString + '))');
if (element.length) {
elementParent = element.parent();
checkBox = elementParent.children('td').children('input');
checkBox.prop('checked', true);
}
}
} else {
for (var i = 0; i < autoCostBatches.d.batches.length; i++) {
element = $('td:not(:contains(' + autoCostBatches.d.batches[i].items + '))');
found = $.inArray(element, test) > -1;
if (element.children().length && found === false) {
elementParent = element.parent();
checkBox = elementParent.children('td').children('input');
checkBox.prop('checked', false);
}
}
}
here is the printed userstring to console and appears to be right.
and here are the elements in a table. Note that there is a td element containing 630316 and 632848 split by a comma. This to me looks correct but when I run the function all checkboxes are being checked which should not happen.
I have this little javascript to add some classes to some hyperlinks to give them a nice effect. It works on any links inside an element with the class linkroll. However, when the link is nested within some elements and div's, it doesnt apply the effect. I am finding that querySelectorAll is not very consistent.
I was thinking if I rewrite this and use something like jQuery's each() function, I may have better results. Here's how it looks now:
var supports3DTransforms = document.body.style['webkitPerspective'] !== undefined || document.body.style['MozPerspective'] !== undefined;
function linkify() {
if (supports3DTransforms) {
var selector = '.linkroll a';
var nodes = document.querySelectorAll(selector);
for (var i = 0, len = nodes.length; i < len; i++) {
var node = nodes[i];
var sibling = node.nextSibling; // Do not apply to images
if (sibling.nodeName != "img") {
if (!node.className || !node.className.match(/roll/g)) {
node.className += ' roll';
node.innerHTML = '<span data-title="' + node.text + '">' + node.innerHTML + '</span>';
}
}
};
}
}
linkify();
How could I possibly rewrite document.querySelectorAll(selector); and use something like jQuery's each() instead?
Try this:
var supports3DTransforms = $('body').css('-webkit-perspective') !== undefined || $('body').css('MozPerspective') !== undefined;
function linkify() {
if (supports3DTransforms) {
$('.linkroll a').each(function (i, el) {
var $el = $(el),
$sibling = $el.next();
!$sibling.is('img') && !$el.hasClass('roll') && $el.addClass('roll').wrap('<span data-title="' + $el.text() + '"></span>');
});
}
}
linkify();
Example fiddle
I am trying to add a toolbox to a MediaWiki Chat using JavaScript. Well, with this code I do a AJAX request to another MediaWiki page (MediaWiki:Chat-herramientas-2) to get the content of the toolbox and, on the chat, print it.
The format of the toolbox content is:
- Navigation
* Page 1
* Page 2
* Page 3|Hahaha
* #|Execute script|hello()
The first line is the first < li > of the < ul > (toolbox). It has the class "active".
The second and third lines are "< a >" with href "#", and the text are "Page 1" and "Page 2", respectly.
The fourth line has a href with url ".../Page 3" and the text is "Hahaha".
But with the fifth line, I want to insert a blank href ("#"), with the text "Execute script" and after the 2nd "|" add the onclick attribute with "hello()" on it.
I can't accomplish it because I don't know how to detect the 2nd appearance of a character. The full code is this:
$(function() {
var $chatheader = $('#ChatHeader');
select = 'MediaWiki:Chat-herramientas-2';
var $menu = $('<ul class="dropdown"></ul>')
.insertAfter($chatheader)
function flatten (options, indent) {
var flattened = [];
indent = indent || '';
for (var i = 0; i < options.length; i++) {
if ($.isArray(options[i])) {
flattened = flattened.concat(flatten(options[i], '* '));
} else {
flattened.push(indent + options[i]);
}
}
return flattened;
}
function render (lines) {
var options = '', selected = ' selected',
ignore = { '(': 1, ':': 1, '<': 1 };
for (var i = 0; i < lines.length; i++, selected = '') {
if (!lines[i].length || ignore[lines[i][0]]) {
continue;
}
var contents = mw.html.escape( lines[i].substring(2) );
if (lines[i].substring(0, 2) === '* ') {
var clase = contents.replace(/[^a-z0-9\s]/gi, '').replace(/[_\s]/g, '-').toLowerCase();
var url = contents.replace(/[^a-z0-9\s]/gi, '').replace(/[_\s]/g, '-').toLowerCase();
var checksitiene = /\|/g.test(contents)
if(checksitiene) {
var wachem = contents.replace(/\|/,'">');
options += '<li class="' + clase + '"' +
selected + '><a target="_blank" href="/wiki/' + wachem + '</a></li>';
} else {
options += '<li class="' + clase + '"' +
selected + '>' + contents + '</li>';
}
} else {
options += '<li class="active">' + contents + ' <span>▼</span></li>';
}
}
$menu.append(options);
}
if (typeof select === 'string') {
$.get('/wiki/' + select + '?action=raw&ctype=text/javascript')
.done(function (data) {
render(data.split(/\r\n|\n|\r/));
});
} else if ($.isArray(select)) {
render(flatten(select));
}
});
String.prototype.indexOf accepts two arguments, the first is the string you want to match, and the second is the index from which to start your match. See MDN.
Thus, after using indexOf to find the first instance of a string, you can then search from that index+(length of the matching string) to find the next match.
A function that can be called multiple times to receive the next index in a string:
function makeIndexer(text,pattern,start){
start = start||0;
var patternLength = pattern.length
, initialStart = start
;
return function(){
var index = text.indexOf(pattern,start);
if(index === -1){ //start over from initial start index after we reach the end
start = initialStart;
}else{
start = index + patternLength;
}
return index;
}
}
Then use it like this:
var nextIndex = makeIndexer("Wow, what an awesome function!","w")
nextIndex() //2 still case sensitive like indexOf
nextIndex() //5
nextIndex() //14
nextIndex() //-1 Like indexOf, returns -1 when there are no more matches
nextIndex() //2 starting over
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")