PDA

View Full Version : Help with some tree menu stuff



aebstract
07-09-2009, 02:50 PM
Okay, here is a rough look of my goal:

http://www.berryequipment.net/website/misc/rough.jpg

Basically, I want these vertical tabs with a 1px border on top and bottom and a 1px space between each. This isn't a problem. I am using a javascript to show/hide divs so that I can make a "tree-like" menu navigation. I can do that with:


function ShowHide(elementId)
{
var element = document.getElementById(elementId);
if(element.style.display != "block")
{
element.style.display = "block";
}
else
{
element.style.display = "none";
}
}
function UpdateText(element)
{
if(element.innerHTML.indexOf("Show") != -1)
{
element.innerHTML = "Hide Details";
}
else
{
element.innerHTML = "Show Details";
}
}


and:


<a href="javascript: void(0);" onclick="ShowHide('equipment');"

Right now my main problem is that I need it so that whatever main tab category I click, it should change the background of that tab to the darker color (seen in the image above). I also need a way so that if I open another tab's list, it closes the other. Also it should take the 1px space away from the current and possible the tab below the current. Any information/help you guys can provide would be killer. I'm gonna keep playing around and see what I can figure out but hopefully someone here can help me quicker ;)

Jesdisciple
07-09-2009, 05:36 PM
What if your page is viewed without JavaScript enabled? Does the menu still work at all? I think each top-level link should point to something like a sitemap of that section. This would be most easily accomplished with a tree structure of your pages in PHP; this structure could serve double-duty as the menu generator to keep them consistent with each other.

The menu itself could probably be managed (mostly) with CSS, as well. Particularly, the background color could be specified as a CSS class which is only applied to the currently selected node. JS would be required for adding and removing this class.

The same CSS class could be used to determine which node's children are displayed.

I don't know what you mean about the 1px space.

aebstract
07-09-2009, 05:55 PM
What do you mean by point to a sitemap of the section? A generated list of what goes in each category? That isn't much of a problem, but what makes the menus expand out when you click on the top level? I tried my current code without javascript enabled and it's a no-go, so I definitely need to look at a new method that will work. Just confused as to all the javascript parts, I'm completely stupid at javascript.

Jesdisciple
07-09-2009, 06:15 PM
I'm assuming that you know your way around CSS; if you don't I can point to some resources. However, I'm about to leave for around three or four hours.

Yeah, the list could be generated on the same page for all the links, with a different path as a GET argument. That page could use the same script as the menu.

Use a default CSS class with display: none; for the children, then another class (I'll call it "active") with display: block; and your background color. When a category is clicked:
Get the old "active" element from wherever it is.
Split that element's class property around spaces, remove "active," join the classes with spaces again, and assign the result back to the class property. (This could be done with a RegExp instead, but this is easier to implement and maintain.)
Add " active" (with a leading space) to the selected element's class property.
Set the selected element as the "active" element to inform the next selection.
As far as I know, the major browsers will render the page according to the given CSS - but you shouldn't just assume they do. The same thing can be done with JavaScript, but CSS is faster and (I think) more portable.

aebstract
07-09-2009, 06:17 PM
How are you adding/changing anything in the classes on click without javascript?

Jesdisciple
07-09-2009, 06:28 PM
? I'm suggesting that you do use JS to modify the classes of the elements. This doesn't make the page unusable for JS-disabled users because the categories will have href attributes which send them to the mini-sitemaps. Each category's onclick listener should return false so JS-enabled users don't see the mini-sitemaps.

aebstract
07-09-2009, 06:46 PM
Could you show me a small example of how the javascript would work? I know you're gonna be gone awhile, so I'll look around and see if I can get something together working but if you do get back and can that would be great.

Jesdisciple
07-09-2009, 11:24 PM
Short-circuit operators (http://en.wikipedia.org/wiki/Short-circuit_evaluation) ( || and && ) work with everything just like with Boolean values.

default || alternative
standard || non-standard
In this case (the start of the function), we're working around some anti-standard IE behaviors. That is, IE implements some property names they made up instead of the standard names.

Functions used here... (Thanks for helping me improve the wiki, by the way.)
String.split() (http://javascript.wikia.com/wiki/String#split)
Array.indexOf() (https://developer.mozilla.org/en/Core_JavaScript_1.5_Reference/Global_Objects/Array/indexOf)
Array.splice() (http://javascript.wikia.com/wiki/Array#splice)
Array.join() (http://javascript.wikia.com/wiki/Array#join)

Also, I want to see your final JavaScript so I can comment... I didn't follow all the best practices in my code, partly because I don't want to give a full implementation and partly because I lack some information.
var active;

function onClick(event){
event = event || window.event;
// We're not using 'this' because it's not available in IE's attachEvent event-registration model. We're not using attachEvent, but compatibility is a good thing.
var that = event.target || event.srcElement;

// Get all the classes of this element.
var classes = window.active.class.split(' ');

// What's the index of the one we're interested in?
var index = classes.indexOf('active');

// Remove one element from that position.
classes = classes.splice(index, 1);

// Put them back together.
window.active.class = classes.join(' ');

that.class += ' active';

window.active = that;
}

if (!Array.prototype.indexOf){
Array.prototype.indexOf = function(elt /*, from*/){
var len = this.length >>> 0;

var from = Number(arguments[1]) || 0;
from = (from < 0)//>
? Math.ceil(from)
: Math.floor(from);
if (from < 0)//>
from += len;

for (; from < len; from++){//>
if (from in this && this[from] === elt)
return from;
}
return -1;
};
}

aebstract
07-10-2009, 12:23 PM
What is this suppose to change the class of? I assume I need to do something like this:


var classes = window.active.class.split('active, inactive');


Or leave it blank? Also should this be left blank?


window.active.class = classes.join(' ');


I think if I can figure out how to control the class of the active 'index' then I can get this working. The only thing is that I am still making my list the same way, by changing a div's display to block when the link is clicked, so javascript still has to be enabled.

vwphillips
07-10-2009, 02:23 PM
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>
<style type="text/css">
/*<![CDATA[*/
.item {
width:100px;height:20px;background-Color:#FFFFCC;
}

.active {
background-Color:#FFCC66;
}

.content {
display:none;width:100px;height:200px;background-Color:#CCFFFF;
}

/*]]>*/
</style>
<script type="text/javascript">
/*<![CDATA[*/

function Toggle(i,c){
c=document.getElementById(c);
c.style.display=zxcSV(c,'display')=='none'?'block':'none';
i.className=(zxcSV(c,'display')=='none')?i.className.replace(' active',''):i.className+' active';
}

function zxcSV(obj,par){
if (obj.currentStyle) return obj.currentStyle[par.replace(/-/g,'')];
return document.defaultView.getComputedStyle(obj,null).getPropertyValue(par.toLowerCase());
}


/*]]>*/
</script>
<title></title>
</head>

<body>
<div class="item" onclick="Toggle(this,'c1');"></div>
<div id="c1" class="content" ></div>

</body>

</html>

aebstract
07-10-2009, 02:33 PM
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">

<head>
<style type="text/css">
/*<![CDATA[*/
.item {
width:100px;height:20px;background-Color:#FFFFCC;
}

.active {
background-Color:#FFCC66;
}

.content {
display:none;width:100px;height:200px;background-Color:#CCFFFF;
}

/*]]>*/
</style>
<script type="text/javascript">
/*<![CDATA[*/

function Toggle(i,c){
c=document.getElementById(c);
c.style.display=zxcSV(c,'display')=='none'?'block':'none';
i.className=(zxcSV(c,'display')=='none')?i.className.replace(' active',''):i.className+' active';
}

function zxcSV(obj,par){
if (obj.currentStyle) return obj.currentStyle[par.replace(/-/g,'')];
return document.defaultView.getComputedStyle(obj,null).getPropertyValue(par.toLowerCase());
}


/*]]>*/
</script>
<title></title>
</head>

<body>
<div class="item" onclick="Toggle(this,'c1');"></div>
<div id="c1" class="content" ></div>

</body>

</html>

I popped this in and tried it, looks like it works except it won't work with javascript disabled. :( Also, I added another tab beneath the first, if one tab is open and you try to open another, it keeps the first open still.

Jesdisciple
07-10-2009, 04:58 PM
EDIT: vw's replace idea is good if you understand what it does.

split and join deal with delimiters, not content. So, for example, if an element has the classes "foo bar active baz" then splitting the class property around spaces will yield this array:
['foo', 'bar', 'active', 'baz']Joining the same array around spaces without modifying it will yield exactly the same string we started out with. However, we first remove 'active' to get this array:
['foo', 'bar', 'baz']And then we join this to get "foo bar baz".

EDIT: If you're familiar with PHP's standard libraries, spilt is explode and join is implode.


I think if I can figure out how to control the class of the active 'index' then I can get this working. The only thing is that I am still making my list the same way, by changing a div's display to block when the link is clicked, so javascript still has to be enabled.Unobtrusive JavaScript is simply JavaScript which is sort of an afterthought to the overall design of a page. That is, the page is built without JavaScript, then JS bells and whistles are expected to modify the page as required.

Can you link to your page please so I can see what I'm working with?

aebstract
07-10-2009, 05:23 PM
Sure: https://berryequipment.net/website/
Equipment is the only thing that is clickable atm, then it breaks down in to equipment types, then the actual equipment and then when you choose one it gives you 4 options. I'm gonna pull the lists and everything from a database after I get it working correctly.

Jesdisciple
07-10-2009, 05:46 PM
Note that I didn't intend the URL as a replacement for discussion. In fact, I was hoping to see your mini-sitemaps - but maybe you need more clarification?

Just so you know, my style of answering questions (when I don't get carried away) is to guide you in your attempt to build what you need. I hope this will result in you learning JavaScript yourself.

aebstract
07-10-2009, 05:48 PM
I just don't understand most of it at all, and need to learn it anyway so that I can do this in the future on my own.

Jesdisciple
07-10-2009, 06:23 PM
:D :D :D

Unobtrusive JavaScript is simply JavaScript which is sort of an afterthought to the overall design of a page. That is, the page is built without JavaScript, then JS bells and whistles are expected to modify the page as required. Usually such modification needs to be done after the page loads; there are two ways to wait for this, favored by different people for different reasons.

You can register an onload event-listener on the window. This is equivalent to <body onload> except that logic and layout aren't jumbled together.
window.onload = function(){
// Do your stuff.
};

Scripts placed in the body are run when the browser reaches them. For example, this script will be run after the form is loaded but before the footer exists:
<body>
<form></form>
<script type="text/javascript">
doSomething();
</script>
<div id="footer"></div>
</body>Some people (most notably Douglas Crockford) prefer to put scripts at the very bottom of the page. (My post continues below.)
A <script src="url"></script> will block the downloading of other page components until the script has been fetched, compiled, and executed. It is better to call for the script as late as possible, so that the loading of images and other components will not be delayed. This can improve the perceived and actual page loading time. So it is usually best to make all <script src="url"></script> the last features before the </body>. An in-page <script> does not have a significant impact on loading time.

If a script is defining functions or data that are used by other functions, then the defining must be done before use. So the defining scripts must come before the using scripts.

However you decide to wait for the page to load, you next need to modify the document. First you need to find what you want to modify; the most common way is document.getElementById (http://javascript.wikia.com/wiki/Document#getElementById). I think you should first get the menu this way, then use its getElementsByTagName (http://javascript.wikia.com/wiki/Element#getElementsByTagName) method to find all the LI elements inside it. (Using the Document method of the same name would yield all LI elements in the page, not just in the menu.)

Once you have all your LIs, you can loop through them and change each link (childNodes[0]) to include an onclick handler. This is the onClick method I posted, although vw's "toggle" name and generic prototype are better ideas. The first makes more sense and the second isn't bound to our specific application. But I'll show you what I mean when the page is working.

aebstract
07-13-2009, 12:57 PM
I have a better understanding of some of this stuff, I've been reading about the stuff you put and trying to figure it out. I think you're wanting me to have everything just be setup standard, and then when you click say "equipment" to open up the options under it, then use get ElementByTagName (I'm guessing you use the class of the li's?) and then change the a href to be an onclick similar to equipment? I kind of understand the plan, but am still having trouble putting it together. Gonna keep working and see if I can figure it out, any more pushes forward would be great.


I don't want you to just do it for me, like you mentioned before, I really need to learn javascript and really appreciate you taking time to teach me slowly like this :cool:



edit: I've figured out how to change the class, that's a good first step (I think). Now I'm going to try and put together the lists, and work with arrays and whatnot. Of course, a much bigger task for me. Here is what I have atm:


function onClick(event){

var element = document.getElementById(event);
if(element.style.display != "block") {
element.style.display = "block";
document.getElementById(event + 'change').className = 'main-nav-2';
} else {
element.style.display = "none";
}


and



...

<div class="main-nav">About Us</div>

<div class="main-nav" id="equipmentchange"><a href="javascript: void(0);" onclick="onClick('equipment');">Equipment</a></div>




<div id="equipment" class="sub1">
<div class="sub1-top"></div>

<ul>

...



https://berryequipment.net/website/


edit2: I'm moving along and am working on determining what is active, so that I can change it back when I click a different link, etc.

Jesdisciple
07-13-2009, 04:58 PM
I'm glad you found the className property... I just got reminded that 'class' is deprecated (shouldn't be used anymore).
document.getElementById(event + 'change').className = 'main-nav-2';What if the element already has a class before you run this function? You'll remove it unless you append the new one with a space between them:
document.getElementById(event + 'change').className += ' main-nav-2';Also, using getElementById won't work if you have more than one menu item currently collapsed (and 'event' is a bad name to use for an id). You can send the event from the HTML listener like this:
<a href="javascript: void(0);" onclick="onClick(event);">Equipment</a>You could instead send this (the link), but passing the event makes the transition to an ordinary function easier. Also, you can assign the link as 'this' within the function if you want:
<a href="javascript: void(0);" onclick="onClick.call (http://javascript.wikia.com/wiki/Function#call)(this, event);">Equipment</a>Also, this will break when the active element can be 'About Us' instead of Equipment' (you'll get two active elements, I think):
var element = document.getElementById(event);That's why I used a global in my earlier code. However, global variables are evil (http://yuiblog.com./blog/2006/06/01/global-domination/) and this is how to make your own semi-global scope:
;(function (){
var active;
function onClick(){
//...
}
})();Note that this won't work while you're still using HTML event-handlers. Also, the semicolon before the outer function prevents any previously-defined function or variable without a semicolon from being called with our function as the argument.

Finally, assigning to and checking element.style.display is unnecessary since you're using classes. I think classes make code more readable when used fully. Why do I need to know what styles are being applied to an element when reading your code? If you write the CSS classes correctly, all I need to know is the class's name; if the CSS classes seem to be misbehaving, I can check your stylesheet.

aebstract
07-14-2009, 06:36 PM
The reason I wasn't doing the

document.getElementById(event + 'change').className += ' main-nav-2';

was intentional to just replace the one style, with an "active" style. Then when it needs to be reverted back, just change it back. That was my intention.. I actually kind of had it working right, taking the style back when you close the tab, but you couldn't click another tab and have it close. I removed the "active" bit that I had in from you earlier, it wasn't working correctly. (I think) I couldn't get it to display any information, and I couldn't even run a alert(); after it, so I assumed something was flawed in it. Right now I have:


var event2;


function onClick(event){


alert(event2);

var element = document.getElementById(event);
event2 = document.getElementById(event + 'change');
if(element.style.display != "block") {
element.style.display = "block";
document.getElementById(event + 'change').className = 'main-nav-2';


} else {
element.style.display = "none";
}



alert(event);


}

Of course this isn't how the final is gonna be, but I have the alerts in there so I can try and see what is happening at what stages, and if it is working correctly. When you first click equipment with this, it pops up and says 'undefined' for event2, which I understand. I'll need to run an if statement or something to see if it is set first, before doing anything with it. Then when I click it the second time (hoping it is set at this point to equipment) it says '[object HTMLDivElement]'.. which I have no clue why, or what that would mean exactly. Obviously it is finding the entire div as the element? Don't know why it isn't telling me 'equipment'. My eventual goal was to set a variable as the ID of the element that is clicked, then when you click on another element it will change the first ID's class back to normal, and change the current one to what it needs to be. I'm still a far way away from this thing working :(


Also: Not sure how

<a href="javascript: void(0);" onclick="onClick.call(this, event);">Equipment</a>
this works, I tried it out in place of what I had, and it wasn't clickable at that point.

Jesdisciple
07-14-2009, 08:34 PM
I have a twofold reason for adding a CSS class instead of replacing the existing set... First of all, by using a base class and overlaying the active class on top of it we can reuse the common styles and overwrite the others. In your case, I think we only need to overwrite the background-color and the display.

Also, I'm trying to write this in a way that's compatible with unexpected circumstances. This way, if you later decide to add another class to these elements that has nothing to do with this module, or if someone else uses the code in a totally different page, the code "plays nice with others".

As mentioned before, I also think CSS is more readable and more efficient than JS but I haven't benchmarked it. It's probably not more portable than the JS 'style' property (at least in modern browsers), but it may be more so than other JS means of doing the same thing.

EDIT: Before I go into event-handlers, note that you were expecting the alert to show document.getElementById(event + 'change').innerHTML. The element itself, not its contents, is found by getElementById.

Now back to the function... We don't need to use document.getElementById in it because our elements are available otherwise. Finding an element in a document is fairly expensive (a recursive for-loop) and is usually not necessary after event handlers have been set. For now you're using HTML event-handlers - a poor practice but one which circumvents the need for document.getElementById.

Using onClick.call(this, event) in the handler, add this to the beginning of the function:

event = event || window.event;
var that = event.target || event.srcElement;
//These should all be true (the last one won't be yet because active is undefined).
alert(this === that);
alert(document.getElementById(event + 'change') === this);
alert(document.getElementById(event) === active);How did 'this' get in there? When any JS function is called, there is an additional implied argument called 'this' which is set to the object used as the context for the function call. Example:
var obj1 = {
foo: function (){
alert(this.bar);
},
bar: 'baz'
};
var obj2 = {};
obj2.fez = obj1.foo;
obj2.bar = 'fap';
obj2.fez();This alerts 'fap' instead of 'baz' because of a JS language feature called "late binding." The function is bound to the object on which it was called, not necessarily the one on which it was declared.

I planted a link around "onClick.call" earlier. I guess you didn't see it, so here it is (http://javascript.wikia.com/wiki/Function#call) again. As shown there, Function.prototype.call (automatically bound to all function objects - even itself) allows you to explicitly pass any value (even a non-object) as 'this':

function foo(){
alert(this);
}
foo();
foo.call(4);As shown in the first alert, functions not bound to any particular object are 'adopted' by window - even if declared within a function which is bound to an object. This behavior is quite useless; I only showed it so you would know later.

Now you might wonder why I'm using 'call' in the event handler... It gives your function the same environment as this would:
link.onclick = onClick;In fact, HTML event handlers are themselves functions; they work similar to this:
link.onclick = function (event){
// The onclick attribute value is inserted here.
};That's why 'this' and 'event' are accessible within it.