Javascript: Recursion, jQuery Error - javascript

Working on a navigation menu script with jQuery. The script is being designed with recursion so that there is no hard coded limit to the number of levels the menu has.
I'll start with the code:
navigationMenu.prototype.reset = function ( ulElement, colorIndex, colors ) { //Color index should always be 1 when directly calling this function
var listItems = $(ulElement.children);
var numItems = listItems.length;
var targetWidth = (100 / numItems) + '%';
listItems.each( function ( x ) {
var children = $(listItems[x].children);
var xT = $(listItems[x]).prop('tagName');
var subMenu = null;
children.each( function ( y ) {
var yT = $(children[y]).prop('tagName');
if (yT == 'UL') {
subMenu = $(children[y]);
} else if (yT == 'A') {
$(children[y]).css('background-color', colors[colorIndex-1]); //Offset by 1 to facilitate for 0 indexed arrays
$(children[y]).hover( function () { //Set hover color to the opposite
$(children[y]).css('background-color',colors[(3-colorIndex)-1]); //3-1 = 2 and 3-2 = 1, subtract 1 to facilitate 0 indexed arrays
}, function() {
$(children[y]).css('background-color',colors[colorIndex-1]); //3-1 = 2 and 3-2 = 1, subtract 1 to facilitate 0 indexed arrays
}); //Rest of style SHOULD be handled by css (width 100%, text color, text align)
}
});
if (subMenu !== null) { //Recurse
navigationMenu.prototype.reset(subMenu, (3 - colorIndex), colors); //Not defined?
}
if (xT == 'LI') { //Format the element
$(listItems[x]).css('width',targetWidth);
$(listItems[x]).css('background-color', colors[colorIndex]);
}
});
};
Next, The error:
Uncaught TypeError: Cannot read property 'firstChild' of null <-whitespace-> jquery-1.11.1.min.js:2
What concerns me is that the error does not seem to come directly from my code, rather, a function within the jQuery library; however, I'm placing good money on the fact that it is because of something I did wrong.
A live demo can be found here:
http://proofoftheilluminati.com/test/test.html
For an idea of the final look of the menu you can see the top level with hover effect and a simple JS script that maths the link widths here:
http://proofoftheilluminati.com/test/index.html
Script:
http://proofoftheilluminati.com/test/scripts/menu.js
I'm hosting a freshly downloaded copy of jQuery version 1.11.1:
http://proofoftheilluminati.com/test/scripts/jquery-1.11.1.min.js
What it should be doing:
Top level list should be orange with black over effect
second level list should be black with orange hover effect
third level list should be same as first, etc.
Positioning is handled by external css file
What it is doing:
Handles top level list correctly, seems to error before style second level list.
Please let me know if I left anything out. I try to be thorough.
Edit: The supplied code has a comment on the line that calls itself:
//Not defined?
This was left over from a previous error, I was having trouble getting it to recognize the recursive function call. I tried the following lines here and they would not allow the function to progress:
this.reset(subMenu, (3 - colorIndex), colors);
reset(subMenu, (3 - colorIndex), colors);
navigationMenu.reset(subMenu, (3 - colorIndex), colors);
Additionally, this function is called when the document is ready:
$(document).ready(function() {
s = new navigationMenu('#NavMenu', '#884106', '#000000', -1);
});
Edit: modified code to use x/y instead of index and xT/yT instead of tag (removed nested variables with same name)

When you first call navigationMenu.prototype.reset, I'm guessing ulElement is a DOM element, but when you call it recursively, you are passing it subMenu, which is a jQuery object. That will be a problem for the following line:
var listItems = $(ulElement.children);
Try changing the following line of code:
navigationMenu.prototype.reset(subMenu, (3 - colorIndex), colors);
To:
navigationMenu.prototype.reset(subMenu[0], (3 - colorIndex), colors);
I prefer to always prefix variables that refer to jQuery objects with "$" to keep them straight.
You could also use this inside the functions given to .each(). So instead of:
children.each(function(index) {
var tag = $(children[index]).prop('tagName');
You could have:
children.each(function() {
var $child = $(this),
tag = $child.prop('tagName');
You could also consider using the jQuery .children() method, instead of the children DOM element property

Related

Finding only visible elements using JavaScript

I'm working on coverting a JQuery project to pure JavaScript and I'm stuck with the following bit of code.
$(".element-has-class:visible")
I thought perhaps something along these lines might work to catch all visible element (in the case of my project list items) but I've had no luck:
function functionName (){
var elementsOnShow = document.getElementsByClassName('element-has-class').find(isVisible);
}
function isVisible(element) {
return element.style.display === 'block';
}
(block has been set in the CSS). Is there anyway to get all visible elements within one variable?
You can convert your nodeList to an Array (read more about it here), which will allow you to use Array.prototype.filter() to get the visible elements:
function functionName (){
var myNodeList = document.getElementsByClassName('element-has-class'),
myArray = [].slice.call(myNodeList),
elementsOnShow = myArray.filter(isVisible);
}
function isVisible(element) {
return element.offsetWidth > 0
|| element.offsetHeight > 0
|| element.getClientRects().length > 0;
}
The isVisible function you see above is extracted from jQuery 2.2.4's source code (version 3.X.X is not compatible with IE 8 and below).

How to use a data identifier on an array of divs?

Hi there I'm currently building a slider and I got pretty much all nailed down except one last thing.
My bullet points are identifying what image you are currently looking at and can also be used for navigation as illustrated here (2 sliders since this is where im running into issues):
http://puu.sh/hAYp5/15477cd325.png
For the actual dots I've been able to use this code:
$(this.dots).click(function() {
var selected = $(this).data("img");
sliderEle.goTo(selected);
$(dots).removeClass("active");
$(this).addClass("active")
})
The prev and next buttons however I'm having an issue figuring out, this is what I currently got and it affects both sliders as it is none specific to the individual slider like I want it to be:
goTo: function(index) {
if (index < 0)
return;
// move <ul> left
this.ul.style.left = '-' + (100 * index) + '%';
this.currentIndex = index
},
goToPrev: function() {
this.goTo(this.currentIndex - 1)
$(this.dots).removeClass("active");
$('.slider-dot[data-img="'+this.currentIndex+'"]').addClass("active");
},
goToNext: function() {
if(this.currentIndex + 1 > this.li.length - 1) {
this.goTo(0)
}else {
this.goTo(this.currentIndex + 1)
}
$(this.dots).removeClass("active");
$('.slider-dot[data-img="'+this.currentIndex+'"]').addClass("active")
}
How do I change this line:
$('.slider-dot[data-img="'+this.currentIndex+'"]').addClass("active")
To work with something like this:
$(this.dots)
So it's targeted on the individual slider rather than all sliders.
Full js source: http://pastebin.com/NaDw0jib
Solved by using .filter from jquery
var currentDot = $(this.dots).filter('[data-img="'+this.currentIndex+'"]');
$(currentDot).addClass("active");
add a function to your prototype
getNav: function(index) {
return $(this.nav).filter(function() {
return $(this).data('img') === index;
});
});
ten use it like this:
this.getNav(3).addClass('active');
As an aside, I'd probably either expand the initialize method to actually create the DOM representation, or failing that, at least use classes rather than hard coding to children[i] references. It'll save you a headache later.

jQuery selection via class generating error

I am attempting to get some divs that all have the same class via the following jquery code:
var divs = $('.divClass');
And then loop through and get each height and left css property of the each div. I have tried 2 methods, both unsuccesfully.
First:
divs.each( function(d) {
var height = d.height();
// also tried:
var height2 = d.css("height");
});
Second:
var divArray = divs.toArray();
for (var i = 0; i < divArray.length; i++) {
var height = divArray[i].height();
}
Both these throw the error: "Uncaught TypeError: Undefined is not a function.". What is really strange is divs.first().height() returns the correct value. Also, the array has the correct number of members. Is there something wrong I am doing with either iteration scheme? This is incredibly annoying. Today I found out why everyone complains about JS. Thanks
each() has two arguments, index and the DOM element (which would also be the value of this)
divs.each( function(index, element) {
var height = $(element).height();
});
You are doing it wrong because d will not be a DOM element but the zero-based index of the current element in the matched set.
Instead of this, within the .each callback refer to the current element with $(this):
divs.each(function() {
var height = $(this).height();
});
You can also get the current element from the second (not first) argument passed to the callback, but there's no need to.

Displaying a Random div on Click

I am using the Colorbox Lightbox script to call a hidden div on a page. It works great but there is a catch. I have 15 hidden divs. When a link is clicked I would like a new lightbox to show each time without repeating until all have been shown. I do not know how to do this.
Here is my code to call the lightbox:
$(".colorbox").colorbox({
inline:true
});
Here is the HTML of the hidden divs
<div class="hide">
<div id="lightbox1">
<!-- Content -->
</div>
<div id="lightbox2">
<!-- Content -->
</div>
<!-- etc -->
</div>
How would I call each div at random until all have been shown then start over?
Also is there a way that once divs 1 - 15 have been shown to then show one last div (id="last-div") before restarting?
Note: All divs would be called on a click and I am using jQuery 1.8.2.
I do not know where to start, I have seen scripts using Math.random() but I do not understand enough to make that work.
UPDATE
I have tried Ian's answer but the lightbox is not showing (but I can see in the console log that the script is working)
Originally he has this in his script:
$(selector).show();
which I changed to this:
$(selector).colorbox({inline:true });
What do I need to do to call the lightbox?
Note: No errors are thrown.
So my idea was similar to Eric's, but I wanted to make it work "completely". So instead of storing references to all the divs in an array, I just decided to store an array of ints representing each div. The way I eventually select them with jQuery is "#lightbox + i", so if you don't have this exact structure (where the divs have an id like "lightbox" and an int - from 1 to the last count), then you can use .eq() or nth-child. It won't be the exact same results, but it will have the same random effect, just done in a different way. I found a function that "randomizes" an array - I'm guessing like what Eric's Shuffle does. But here's where I got it from - How to randomize (shuffle) a JavaScript array? . I had to modify it to return a new array instead of modify the one passed to the function. Also, I kept everything in the document.ready scope, instead of the global scope, so things are passed/returned a lot. It worked fine before when I had all and randomed declared globally and didn't pass them around, I just thought this would be "better" since they weren't global.
Here's the fiddle:
http://jsfiddle.net/6qYCL/1/
And here's the Javascript:
$(document).ready(function () {
var all,
randomed;
all = generateAll();
randomed = generateRandomed(all);
$("#generator").on("click", function (evt) {
evt.preventDefault();
randomed = doNext(all, randomed);
});
});
function generateAll() {
// Generates the array of "all" divs to work on
var a = [];
var divs = $(".hide > div.lightbox");
for (var i = 1; i <= divs.length; i++) {
a.push(i);
}
console.log("List of divs available to toggle: " + a);
return a;
}
function generateRandomed(all) {
// Randomizes the original array
randomed = fisherYates(all);
console.log("Setting randomized array: " + randomed);
return randomed;
}
function doNext(all, randomed) {
$(".lightbox, #last-div").hide();
if (randomed.length < 1) {
console.log("All lightboxes toggled, showing last, then starting over");
$("#last-div").show();
randomed = generateRandomed(all);
} else {
var next = randomed.shift();
var selector = "#lightbox" + next;
console.log("Showing " + selector);
$(selector).show();
console.log("What's left: " + randomed);
}
return randomed;
}
// Randomizes an array and returns the new one (doesn't modify original)
function fisherYates ( myArray ) {
var return_arr = myArray.slice(0);
var i = return_arr.length;
if ( i == 0 ) return false;
while ( --i ) {
var j = Math.floor( Math.random() * ( i + 1 ) );
var tempi = return_arr[i];
var tempj = return_arr[j];
return_arr[i] = tempj;
return_arr[j] = tempi;
}
return return_arr;
}
It accounts for getting to the end of the list and display #new-div like you mentioned, then starting the process over. If you look in your browser's console, you can "watch" what's happening during initialization and when clicking the link.
I think this is close to what you were looking for. I'm not sure which is a better solution - storing references to the elements or just an array of ints to loop through and eventually find. I know there are many variations on how to do this - when/how to store the counting stuff, when/how to randomize the array or retrieve a random value (and how to keep track of which has been used), where to store all references, and plenty more. I hope this at least helps!
Create an array of all of them, then shuffle that array, then pull the next one each time you get a click. When you run out, you can repopulate the array if necessary.
Something like this (using the Shuffle method from this source):
Fiddle
var array = $(".hide div").toArray(); // Add all divs within the hide div to an array
var randomArray = Shuffle(array); // Shuffle the array
$("a").click(function() {
if (randomArray.length > 0)
alert(randomArray.shift().innerHTML); // Show contents of div, as an example
else
alert("None left!");
return false;
});
The solution below works by passing an array of elements to a function. As each div is displayed it is taken out of the array. Then from the divs left in the array the next one is picked at random.
// call this function on page load
function begin( ) {
var arr = $(".hide div").toArray();
// further elements can be added to arr i.e. last-div
showDivs( arr.length, arr );
}
// begin displaying divs
function showDivs( numberOfDivs, divArray ) {
var i, lastDiv;
function nextDiv( ) {
// depending on number of slides left get random number
i = randomInt( numberOfDivs );
if( lastDiv ) { $(lastDiv).hide() };
$( divArray[ i ] ).fadeIn( 3000 );
// now that this div has been displayed
// remove from array and cache
lastDiv = divArray.splice( i, 1 );
numberOfDivs--;
// no more divs to display
if( numberOfDivs == 0 ) { return };
setTimeout( nextDiv, 4000);
}
setTimeout( nextDiv, 1000);
}
// calculate next random index
function randomInt( divsLeft ) {
var i = Math.random() * divsLeft;
return Math.round( i );
}
Fiddle here

Why does this function work for literals but not for more complex expressions?

I am having some insidious JavaScript problem that I need help with. I am generating HTML from a JSON structure. The idea is that I should be able to pass a list like:
['b',{'class':'${class_name}'}, ['i', {}, 'Some text goes here']]
...and get (if class_name = 'foo')...
<b class='foo'><i>Some text goes here.</i></b>
I use the following functions:
function replaceVariableSequences(str, vars) {
/* #TODO Compiling two regexes is probably suboptimal. */
var patIdent = /(\$\{\w+\})/; // For identification.
var patExtr = /\$\{(\w+)\}/; // For extraction.
var pieces = str.split(patIdent);
for(var i = 0; i < pieces.length; i++) {
if (matches = pieces[i].match(patExtr)) {
pieces[i] = vars[matches[1]];
}
}
return pieces.join('');
}
function renderLogicalElement(vars, doc) {
if (typeof(doc[0]) == 'string') {
/* Arg represents an element. */
/* First, perform variable substitution on the attribute values. */
if (doc[1] != {}) {
for(var i in doc[1]) {
doc[1][i] = replaceVariableSequences(doc[1][i], vars);
}
}
/* Create element and store in a placeholder variable so you can
append text or nodes later. */
var elementToReturn = createDOM(doc[0], doc[1]);
} else if (isArrayLike(doc[0])) {
/* Arg is a list of elements. */
return map(partial(renderLogicalElement, vars), doc);
}
if (typeof(doc[2]) == 'string') {
/* Arg is literal text used as innerHTML. */
elementToReturn.innerHTML = doc[2];
} else if (isArrayLike(doc[2])) {
/* Arg either (a) represents an element
or (b) represents a list of elements. */
appendChildNodes(elementToReturn, renderLogicalElement(vars, doc[2]));
}
return elementToReturn;
}
This works beautifully sometimes, but not others. Example from the calling code:
/* Correct; Works as expected. */
var siblings = findChildElements($('kv_body'), ['tr']);
var new_id = 4;
appendChildNodes($('kv_body'),
renderLogicalElement({'id': new_id},
templates['kveKeyValue']));
/* Incorrect; Substitutes "0" for the expression instead of the value of
`siblings.length` . */
var siblings = findChildElements($('kv_body'), ['tr']);
var new_id = siblings.length; // Notice change here!
appendChildNodes($('kv_body'),
renderLogicalElement({'id': new_id},
templates['kveKeyValue']));
When I trap out the first argument of renderLogicalElement() using alert(), I see a zero. Why is this?? I feel like it's some JavaScript type thing, possibly having to do with object literals, that I'm not aware of.
Edit: I have this code hooked up to the click event for a button on my page. Each click adds a new row to the <tbody> element whose ID is kv_body. The first time this function is called, siblings is indeed zero. However, once we add a <tr> to the mix, siblings.length evaluates to the proper count, increasing each time we add a <tr>. Sorry for not being clearer!! :)
Thanks in advance for any advice you can give.
If new_id is 0, doesn't it mean that siblings.length is 0? Maybe there is really no sibling.
Perhaps siblings.length is actually 0? Try debugging further (e.g. with Firebug)
OK, I fixed it. As it turns out, I was modifying my source JSON object with the first function call (because in JS you are basically just passing pointers around). I needed to write a copy function that would make a new copy of the relevant data.
http://my.opera.com/GreyWyvern/blog/show.dml/1725165
I ended up removing this as a prototype function and just making a regular old function.

Categories

Resources