JQuery menu

Status
Niet open voor verdere reacties.

RonaldGJ

Gebruiker
Lid geworden
28 apr 2007
Berichten
419
Beste mensen,

Ik ben momenteel wat aan het prutsen met een menu in JQuery. Normaal kom ik overal wel uit, maar zit eigenlijk met een vraag die mij al wel vaker dwars zit.
Je ziet vaak met JQuery als je de functie 'hover' gebruikt en je gaat er snel overheen, dat hij dan 'gek' wordt.
Misschien is hier iemand die daar een oplossing voor weet.

Code:
HTML:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
	<head>
		<title>Menu</title>
		<link href="styles/style.css" type="text/css" rel="stylesheet">
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
		<script type="text/javascript" src="http://code.jquery.com/jquery-latest.pack.js"></script>
		<script type="text/javascript">
			$(document).ready(function(){
				$("li.menu-list-item").hover(function(){
					$(this).addClass("add-borders");
					$(this).find('a').addClass("change-color");
					
					$(this).find("div.menu-sub-item").stop().delay(200).animate({'height': 'toggle'}, 200, 'linear');
				}, function(){
					$(this).stop().find("div.menu-sub-item").animate({'height' : 'toggle'}, 200, 'linear', function(){
						$(this).parent().removeClass("add-borders").find('a').removeClass("change-color");
					});
				});
			});
		</script>
	</head>
	<body>
		<div id="menu_wrapper">
			<div id="menu">
				<ul id="menu-list">
					<li class="menu-list-item">
						<div class="menu-sub-item">
							Home sub
						</div>
						<a href="#">Home</a>
					</li>
					<li class="menu-list-item">
						<div class="menu-sub-item">
							Portfolio sub
						</div>
						<a href="#">Portfolio</a>
					</li>
					<li class="menu-list-item">
						<div class="menu-sub-item">
							Contact sub
						</div>
						<a href="#">Contact</a>
					</li>
				</ul>
			</div>
		</div>
	</body>
</html>

Code:
body{
	margin: 0 auto;
	background-color: #CCC;
	font-family: "Arial";
	font-size: 12px;
	line-height: 18px;
}

*{
	margin: 0;
	padding: 0;
}

a{
	color: #FFF;
	text-decoration: none;
	display: block;
}

#menu_wrapper{
	height: 40px;
	color: #FFF;
	background-color: rgba(0,0,0,0.7);
	margin: 0 auto;
	border-bottom: 1px solid #999;
	box-shadow: 0px 0px 20px 2px rgba(0,0,0,1);
}

#menu{
	margin: 0 auto;
	width: 960px;
}

ul#menu-list{
	list-style-type: none;
	margin: auto;
	padding-top: 3px;
}

ul#menu-list li{
	float: left;
	margin-right: 30px;
	position: relative;
	border-top: 8px solid transparent;
	border-left: 12px solid transparent;
	border-right: 12px solid transparent;
	border-bottom: 12px solid transparent;
	cursor: pointer;
}

ul#menu-list li.add-borders{
	border-top: 8px solid #FFF;
	border-left: 12px solid #FFF;
	border-right: 12px solid #FFF;
	border-bottom: 12px solid #FFF;
	background-color: #FFF;
}

ul#menu-list li.menu-list-item a.change-color{
	color: #333;
	border-bottom: 1px dotted #AAA;
}

ul#menu-list li.menu-list-item div.menu-sub-item{
	width: 200px;
	background-color: #FFF;
	color: #000;
	box-shadow: 0px 4px 4px 0px rgba(0, 0, 0, 0.3);
	display: none;
	position: absolute;
	top: 30px;
	left: -12px;
}

Probleem:
Als je snel over een knop gaat en er snel weer vanaf komt het menu alsnog naar beneden. Ik zou graag willen dat als je er op gaat en er snel weer af, dat de knop weer terug gaat naar de oude staat en er geen menu naar beneden klapt. Ik heb er een delay in gedaan, juist om dit te voorkomen. Ik heb het idee dat het menu toch naar beneden gaat vanwege de 'toggle' bij de 'height'.

Iemand een idee hoe dit op te lossen?

Is het niet duidelijk hoor ik het graag.

Vast bedankt!

Gr. Ronald!:)
 
't Lijkt erop dat je de animate-functie en stop-functie van verschillende objecten aanroept. In regel 14 doe je feitelijk dit:
[JS]$(this).find("div.menu-sub-item").animate(/* arguments */);[/JS]
Je roept de animate aan van een jQuery-object dat correspondeert met een nodeList (nl. div-nodes met klasse menu-sub-item) uit this.

In regel 16, daarentegen, roep je de stop van het jQuery-object dat met this correspondeert aan:
[JS]$(this).stop().find("div.menu-sub-item")[/JS]
Als 't goed is, is het probleem verholpen als je de stop en find in regel 16 omdraait ;)
[JS]$(this).find("div.menu-sub-item").stop()[/JS]
 
Hallo Robin,

Bedankt voor je reactie!

Dit verhelpt helaas het probleem niet. Ik denk dat ik het beter uit kan leggen.

Zodra ik op de 'knop' ga staan met de muis, voert hij het eerste script van de .hover-functie uit. Zoals je ziet, staat daar een delay in. Ga ik binnen die 200 ms, die in die delay staan, er weer af, wil ik dat de knop weer naar de oude staat gaat en dus het script achter de delay niet wordt uitgevoerd. Dit om dus dat irritante geflups te voorkomen als je er vaak overheen gaat.

Graag hoor ik van je of dit duidelijker is en of je een oplossing hebt. Misschien moet het script iets uitgebreider. Maar ik weet even geen goede oplossing.

Bedankt alvast!

Gr. Ronald!:)
 
Ik snap het nu beter ja :) Dan zal je script inderdaad wat uitgebreider moeten worden. De lastigheid is nl. dat je onderscheid moet maken tussen een "collapseMenu-unhover" en een "cancelDelayedExpand-unhover". Dat gebeurt op dit moment niet - jQuery kent immers alleen de gewone unhover :P

Ik heb je code wat herschreven, nu gebruik makend van setTimeout en clearTimeout. Je moet steeds wat rommelen met de listeners om te zorgen dat de juiste event-handlers gezet zijn... Probeer maar eens of dit is wat je hebben wil :)
[JS]function onMouseEnter() {
$(this).addClass("add-borders").find('a').addClass("change-color");
setDelay.call(this);
}
function onMouseLeave() {
$(this).removeClass("add-borders").find('a').removeClass("change-color");
}

function setDelay() {
var li = $(this);
li.mouseleave(cancelDelay);

var delayer = li.data("delayer");
delayer = setTimeout(function() {
li.unbind("mouseleave", cancelDelay);
li.mouseleave(collapse);
expand.call(li);
}, 200);
li.data("delayer", delayer);
}
function cancelDelay() {
var li = $(this);
li.unbind("mouseleave", cancelDelay);

var delayer = li.data("delayer");
if (delayer) {
clearTimeout(delayer);
}
}
function expand() {
$(this).find("div.menu-sub-item").animate({
height: "toggle"
}, 200, "linear");
}
function collapse() {
var li = $(this);
li.unbind("mouseleave", collapse);

li.find("div.menu-sub-item").stop().animate({
height: "toggle"
}, 200, "linear");
}

$(document).ready(function(){
$("li.menu-list-item").hover(onMouseEnter, onMouseLeave);
});[/JS]
 
Wauw!

Dit komt inderdaad erg dicht in de buurt.

Het enige wat ik nu nog zou willen is dat als je van de knop afgaat en het uitklapmenu is al wel uitgeklapt, het eerst inklapt en dan pas het witte achter de knop verwijderd. Snap je wat ik bedoel?
En als je een beetje tijd hebt, zou je dan kunnen uitleggen wat je precies doet. Ik heb de functies als .data wel eens langs zien komen, maar snap niet helemaal wat het doet. Ook niet helemaal wat jij doet.

Ik ben er in ieder geval al ontzettend blij mee, weer een hoop geleerd!

Bedankt maar weer.

Gr. Ronald!:)
 
Tuurlijk :) Ik zal eerst even uitleggen wat data doet, dan de nieuwe versie van het script met daarachteraan wat uitgebreidere uitleg ;)

.data()
Het gebruiken van data is vergelijkbaar met het definiëren en opvragen van eigenschappen van een object zoals je die vaker in JS tegenkomt.
[JS]var data = {
height: "toggle"
};
data.height = 500;
var height = data.height;[/JS]

De jQuery-library is zo gemaakt, dat ieder jQuery-object zo'n eigen data-object heeft. Voor dat object kun je eigenschappen definiëren
[JS]data.width = "toggle"; //voor een gewoon object data
$(this).data("width", "toggle"); //voor het jQ-data-object dat bij this hoort[/JS]

...of opvragen
[JS]var width = data.width; //voor een gewoon object data
var width = $(this).data("width"); //voor het jQ-data-object dat bij this hoort[/JS]

Nieuwe versie script
[JS]function onMouseEnter() {
$(this).addClass("add-borders").find('a').addClass("change-color");
setDelay.call(this);
}
function removeClass(li) {
li.removeClass("add-borders").find('a').removeClass("change-color");
}

function setDelay() {
var li = $(this);
li.mouseleave(cancelDelay);

var delayer = setTimeout(function() {
li.unbind("mouseleave", cancelDelay);
li.mouseleave(collapse);
expand.call(li);
}, 200);
li.data("delayer", delayer);
}
function cancelDelay() {
var li = $(this);
li.unbind("mouseleave", cancelDelay);
clearTimeout(li.data("delayer"));
removeClass(li);
}
function expand() {
$(this).find("div.menu-sub-item").animate({
height: "toggle"
}, 200, "linear");
}
function collapse() {
var li = $(this);
li.unbind("mouseleave", collapse);
li.find("div.menu-sub-item").stop().animate({
height: "toggle"
}, 200, "linear", function() {
removeClass(li);
});
}

$(document).ready(function(){
$("li.menu-list-item").mouseenter(onMouseEnter);
});[/JS]

Wat gebeurt er nu precies?
- 't Begint natuurlijk met de document-ready: alle <li class="menu-list-item"> krijgen een listener voor wanneer je er met de muis overheen gaat.
- Gebeurt dat voor een bepaalde <li>, dan wordt er in onMouseEnter de stijl toegevoegd en setDelay aangeroepen
- We beginnen nu dus aan de setDelay, waar this een referentie is naar de <li> waar je met je muis overheen bent gegaan. We doen twee dingen: 1) <li> krijgt een mouseLeave-listener (cancelDelay), en 2) we zetten een timer, en slaan die op in het data-object dat bij <li> hoort.

Vanaf hier kan het script op twee verschillende manieren lopen: a/b

a) Je gaat met de muis van de <li> af vóórdat de timer afloopt
- cancelDelay wordt aangeroepen, this is een referentie naar de <li> waar je met je muis vanaf gegaan bent.
- We verwijderen de mouseLeave-listener van <li>. Deze wordt opnieuw gezet bij een mouseEnter-event in setDelay ;)
- We stoppen de timer (die ene die we hadden opgeslagen in setDelay)
- We verwijderen de stijl voor een "hovered-<li>"

b) De timer loopt eerst af
- We verwijderen ook hier de mouseLeave-listener van <li>. Deze wordt opnieuw gezet bij een mouseEnter-event in setDelay
- We maken een nieuwe mouseLeave-listener voor <li> (collapse). We willen namelijk dat 't ding weer inklapt als we er met de muis vanaf gaan ^^
- Begin uitklappen (expand)

Als 't menu uitgeklapt is, en we gaan er met de muis vanaf, wordt de handler collapse uitgevoerd.
- We verwijderen deze handler. Hij wordt opnieuw toegevoegd wanneer er een timer afloopt.
- De animatie wordt gestart. Aan het einde ervan wordt removeClass nog even aangeroepen om de stijl aan te passen

~~
Zo, wat een lap tekst :eek: Ik hoop dat je er wat aan hebt :)
Gr. Robin
 
Laatst bewerkt:
Zo he!

Echt super uitgelegd. Hier heb ik echt veel aan gehad. Deze kant van JQuery kende ik nog niet echt eigenlijk.
Ik had nog 1 vraagje.

Waarom mag en kun je precies de $(this) gebruiken bij regel 10, 21, 27 en 32. Heeft dat te maken met die .call functie en met het feit als je een functie aanroept vanaf de variabele 'li'?

Super bedankt trouwens!
Netjes gedaan.

Gr. Ronald!:)
 
Waarom mag en kun je precies de $(this) gebruiken bij regel 10, 21, 27 en 32. Heeft dat te maken met die .call functie en met het feit als je een functie aanroept vanaf de variabele 'li'
Yep :) Iedere functie heeft bij het uitvoeren een this. Die kan bij iedere aanroep naar iets anders verwijzen. Voor functies die als event handlers gebruikt worden, verwijst this naar het element waarvoor de event plaatsvond. Je kunt this ook zelf aangeven, door f.call(thisObject /*, arguments...*/) of f.apply(thisObject /*, arguments[]*/) te gebruiken. f is hier natuurlijk een functie, en bij het uitvoeren van f verwijst het keyword this naar het object thisObject.

Als je dat niet doet op regel 16 in je programma, verwijst this gewoon naar je window:
[JS]expand.call(li); //this -> li
expand(); //this -> window[/JS]

Het aanroepen van de functie $ op this (die dan dus verwijst naar een <li>), maakt gewoon een jQuery-object bij het element. Dat zorgt ervoor dat je er van die handige jQuery-functies op kunt aanroepen, zoals find en animate ;)
 
Status
Niet open voor verdere reacties.
Terug
Bovenaan Onderaan