Tijdschrift voor webwerkers » Artikel #119
Je hebt een fraai menu gemaakt, met gescheiden structuur en opmaak. Met een subtiel, vertraagd terugvallen van de menu-items kun je je navigatie in stijl afronden.
In het eerste artikel in deze serie heeft Robert Jan Verkade uiteengezet hoe je korte en krachtige CSS kunt schrijven door handig gebruik te maken van gelaagd bouwen en enkele slimmigheden in de CSS specificaties. In dit artikel voegen we de derde bouwlaag – gedrag – toe aan het voorbeeld met behulp van JavaScript. Straks zullen de menu-items na een korte vertraging langzaam terugvallen naar hun uitgangspositie, waarbij ook de kleur geleidelijk verandert van wit naar geel.
We pakken de draad op bij het eindresultaat van vorige week. Aan het menu in het voorbeeld zullen we een extern JavaScript toevoegen voor het gedrag; aan de CSS voegen we nog enkele declaraties toe met de noodzakelijke extra opmaak.
We willen dat de menu-items bij het terugvallen netjes verkleuren van wit (#fff
) naar geel
(#fc3
). Om dit te bereiken bepalen we eerst voor de zes tussenstappen welke kleur bij die
stap hoort. Dit kan online, bijvoorbeeld met de Color Blender van
Eric Meyer, maar voor dit menu wil ik een net wat fraaier verloop.
Daarom maak ik in Photoshop een klein document (bijvoorbeeld 80 bij 10 pixels groot), met het geel als achtergrondkleur. Vervolgens zet ik zeven witte vierkantjes van 10 x 10 pixels – elk in een eigen laag – naast elkaar over het geel heen. Door van elke laag de transparantie in te stellen, kan ik heel nauwkeurig de overgang van geel (geen wit vierkantje) naar wit (wit vierkantje met 100% opacity) bepalen. Je zult zien dat de resulterende kleurcodes net anders zijn dan die van de Color Blender: een visuele stap is niet hetzelfde als een berekende stap.
Mijn verloopje ziet er zo uit:
Met het pipet van Photoshop bepalen we de kleurwaarden, die we met kopieer- en plakwerk zo in de CSS kunnen zetten:
#menu li.stap0 a { color: #ffcc33; } /* Uitgangskleur: geel */
#menu li.stap1 a { color: #ffd659; } /* Stap 1 ... */
#menu li.stap2 a { color: #ffdd76; }
#menu li.stap3 a { color: #ffe38f; }
#menu li.stap4 a { color: #ffebad; }
#menu li.stap5 a { color: #fff0c2; }
#menu li.stap6 a { color: #fff7e1; } /* ... en stap 6 */
#menu li.stap7 a { color: #ffffff; } /* Hoverkleur: wit */
Je ziet dat ik de selectors alvast heb ingevuld. In het script ga ik straks telkens een bepaalde
class
koppelen aan het menu-item waar de bezoeker zijn muis overheen beweegt; de vormgeving
van die class
regel ik in de CSS. Door handig gebruik te maken van specificity voorkom ik dat mijn extra opmaakregels in conflict komen met de bestaande opmaak.
Browsers waarin het script niet werkt zullen niets merken van de extra CSS; andersom zullen de
classes
het bij een werkend script winnen van de rest van de opmaakregels voor het menu. In
het zesde voorbeeld van deze serie zie je dat dit
werkt: er is nog geen JavaScript, dus alles is exact zoals op de voorbeeldsite.
Naast het verkleuren willen we ook dat een menu-item langzaam terugvalt naar zijn uitgangspositie als de muiscursor weg wordt bewogen. Hiervoor vullen we de CSS aan met een stapsgewijs veranderende padding:
#menu li.stap0 a { color: #ffcc33; padding: .7em 1.2em .2em 1.3em; } /* Uitgangspunt: geel en beneden */
#menu li.stap1 a { color: #ffd659; padding: .6em 1.2em .3em 1.3em; } /* Stap 1 ... */
#menu li.stap2 a { color: #ffdd76; padding: .5em 1.2em .4em 1.3em; }
#menu li.stap3 a { color: #ffe38f; padding: .4em 1.2em .5em 1.3em; }
#menu li.stap4 a { color: #ffebad; padding: .3em 1.2em .6em 1.3em; }
#menu li.stap5 a { color: #fff0c2; padding: .2em 1.2em .7em 1.3em; }
#menu li.stap6 a { color: #fff7e1; padding: .1em 1.2em .8em 1.3em; } /* ... en stap 6 */
#menu li.stap7 a { color: #ffffff; padding: 0em 1.2em .9em 1.3em; } /* Hoverstatus: wit en bovenaan */
Op dit moment zul je wellicht denken: “Die CSS voor #menu li a
komt me bekend
voor!” Dit klopt: deze stond al in de CSS van Robert Jan – net als de laatste regel.
Deze herhaling is nodig om het script compact en elegant te houden. Maar daarover straks meer.
We hebben nu de visuele kant van het menu bepaald. Tijd dus voor het schrijven van de JavaScript routines
die we in de onmouseover=""
en onmouseout=""
kunnen
aanroepen. Toch?
Niet helemaal, nee. De essentie van gelaagd bouwen is dat de code voor structuur, opmaak en gedrag
gescheiden zijn. Eigenlijk willen we dus geen enkele onmouseover
in de HTML hebben staan.
Dit kan door gebruik te maken van het event model van het W3C DOM. Dit event model
beschrijft de manier waarop een actie van de bezoeker de hiërarchie van elementen in het (HTML) document
doorloopt, van startpunt (bijvoorbeeld een hyperlink) tot het root-element (in XHTML is dat <html>
). Op elk van die elementen kun je
zo’n event ‘aftappen’ en aan het optreden ervan een
stukje JavaScript hangen.
De enige XHTML die we toevoegen is dus de aanroep van het script in de <head>
van het
document:
<script type="text/javascript" src="voorbeeld_script.js"></script>
Er bestaan al verschillende JavaScript routines om functies aan events
te koppelen. Ikzelf gebruik onderstaand script, dat ik dispatch heb
genoemd vanwege zijn functie. Omdat niet alle browsers het officiële DOM volgen, zijn in het script twee
extra varianten opgenomen voor respectievelijk Internet Explorer voor MS Windows (d.m.v. IE’s eigen
methode attachEvent
), en oudere browsers.
/* ***********************************************************
*
* 0 DISPATCH: haak routines in het DOM event model
*
*/
function dispatch(targetElement,eventName,handlerName)
{
if (targetElement.addEventListener) {
targetElement.addEventListener(eventName, function() { return targetElement[handlerName](); }, false);
} else if (targetElement.attachEvent) {
targetElement.attachEvent("on" + eventName, function() { return targetElement[handlerName](); });
} else {
var originalHandler = targetElement["on" + eventName];
if (originalHandler) {
targetElement["on" + eventName] = function() { originalHandler(); return targetElement[handlerName](); }
} else {
targetElement["on" + eventName] = function() { return targetElement[handlerName](); }
}
}
}
Om ons menu te animeren, moeten we twee events afvangen:
load
event van het JavaScript window
object koppelen we de
‘opstartprocedure’ van ons script.mouseover
en mouseout
events van de links in het menu.
De opstartprocedure heet in ons script initMenu
en is hieronder weergegeven:
/* ***********************************************************
*
* 1 MENU: animeer de menu-items bij mouseover / mouseout
*
*/
// Enkele variabelen zetten.
//
var motion_top_state = 7, motion_base_state = 0; // Start en eindpunt menustand.
var motion_speed = 60; // Zet snelheid van neerwaartse beweging.
// Initieer script: hoofdroutine.
//
function initMenu()
{
var menu = document.getElementById("menu");
// Geen menu? Dan niets doen.
if (!menu) return;
// Haal de links uit het menu op en bepaal op welke pagina we zijn.
var menu_links = menu.getElementsByTagName("a");
var pagina_tak = document.getElementsByTagName("body")[0].className;
for (var i = 0; i < menu_links.length; i++)
{
// Check eerst of menu-item bij huidige pagina hoort (dan geen mouseover!).
if (menu_links[i].parentNode.id != pagina_tak)
{
menu_links[i].targetMenuOverHandler = moveUp;
menu_links[i].targetMenuOutHandler = startMoveDown;
dispatch(menu_links[i], "mouseover", "targetMenuOverHandler");
dispatch(menu_links[i], "mouseout", "targetMenuOutHandler");
}
}
}
// Hang menu handler in het DOM.
//
window.targetMenuHandler = initMenu;
dispatch(window, "load", "targetMenuHandler");
Wat gebeurt er in het opstartscript? Eerst halen we het element op dat ons menu bevat; in ons geval is
dit een ul
, maar voor het script maakt dit verder niet uit. Als dit menu niet bestaat, dan
stopt de routine – zo voorkomen we lelijke foutmeldingen in de browser. Vervolgens halen we
alle hyperlinks op die in het menu staan, en koppelen we aan elk van die links de routines die de
animatie verzorgen: moveUp
voor het omhoog zetten van een menu-item en
startMoveDown
voor het vertraagd terugzakken. Voor een van de links in het menu maken we een
uitzondering: het ge-highlighte menu-item wordt overgeslagen. Welk item dit is controleren we door het
id
van elk menu-item te vergelijken met de class
van het
<body>
element. In JavaScript benaderen we de class
van een element door
middel van de property className
– maar vraag
mij niet waarom deze benamingen verschillen.
Twee regels van het bovenstaande voorbeeld heb ik nog niet toegelicht: de declaraties van de variabelen
motion_top_state
, motion_base_state
en motion_speed
. De eerste
twee bepalen de grenzen van onze animatie: een basissituatie (0), zes animatiestappen (1-6) en een
eindsituatie (7). De derde variabele regelt de snelheid van de animatie, of beter gezegd: de pauze tussen
twee opeenvolgende stappen.
De manier waarop we nu de verschillende routines koppelen aan de hyperlinks in het menu, lijkt
omslachtig. Bedenk echter dat je voor een beetje uitgebreid menu misschien acht keer een
onmouseover
en onmouseout
in de XHTML zou hebben staan. Als je dan iets moet
wijzigen, moet je dat weer acht keer doen. In bovenstaand script pakken we in één keer alle
links in het menu, of het er nu twee zijn of vijfentwintig.
De browser weet nu dat hij een script moet uitvoeren als de bezoeker zijn muiscursor over een menu-item
beweegt. Wat er precies gebeurt bepalen we nu in de scripts voor de functies moveUp
en
startMoveDown
, aangevuld met nog een derde functie: moveDown
.
We beginnen met de eerste. Als de muiscursor de link ‘binnenkomt’, willen we dat de tekst omhoog springt:
// Hulproutine: zet menu-item omhoog.
//
function moveUp()
{
// Haal menu-item op uit document.
var current_item = this.parentNode;
// Geen menu-item? Dan niets doen.
if (!current_item) return;
// Koppel class aan item en onthoud status.
current_item.state = motion_top_state;
current_item.className = "stap" + current_item.state;
}
Hierboven hadden we deze uiterste stand
van een menu-item de class
‘stap7’ gegeven. In dit script koppelen we deze
class
aan het betreffende item. Daarnaast willen we graag onthouden hoe hoog het item staat,
zodat we straks weten wat de volgende stap is bij het terugvallen naar de beginpositie. Dit doen we door
het stapnummer te bewaren in een eigen, nieuwe property van dit menu-item:
current_item.state
. In JavaScript kun je een eigen variabele definiëren, die gekoppeld
is aan een element dat in je XHTML voorkomt. Deze variabele kun je vervolgens uitlezen en weer wijzigen
als elke andere property.
In dit script zie je de JavaScript-verwijzing this
staan. Hiermee verwijs je naar het
huidige object: als je met de muiscursor op het eerste menu-item staat, wijst this
naar (de hyperlink van) dat eerste item, als je over de derde zweeft bevat het een verwijzing naar díe
hyperlink. Dit simpele concept, waarmee je naar het actuele object kunt verwijzen dat een event heeft ‘veroorzaakt’, is één van de krachtigste onderdelen van
JavaScript.
Het menu-item springt nu omhoog (voorbeeld), maar dat is nog maar de helft van ons doel. We willen dat de tekst ook weer (langzaam) terugvalt als de muiscursor weg is:
// Hulproutine: start terugzetten menu-item met vertraging.
//
function startMoveDown()
{
// Haal huidige menu-item op.
var current_item = this.parentNode;
// Geen menu-item? Dan niets doen.
if (!current_item) return;
var in_motion = "moveDown('" + current_item.id + "')";
current_item.inMotion = setTimeout(in_motion, 2*motion_speed);
}
Het eerste deel is hetzelfde als in de functie moveUp
: we halen de directe
‘ouder’ van de actuele link op met this.parentNode
en controleren of dit
menu-item echt bestaat. Maar daarna doen we iets anders: in plaats van het direct toekennen van een
class
aan dit element, geven we het id
van dit element door aan de derde
routine, moveDown
. De aanroep van deze routine bouwen we op als een losse variabele, waarin
we de id
opnemen.
Deze variabele gebruiken we in de laatste regel van de functie om een timeout te starten. In JavaScript zit een ingebouwde stopwatch, die je met
setTimeout(functienaam, wachttijd)
aanzet. De functie geeft een verwijzing naar de net
geactiveerde stopwatch terug, die ik in bovenstaand script weer toeken aan mijn eigen property current_item.inMotion
. Hiermee kan ik later zien of deze stopwatch
nog loopt. Bij het starten van de animatie maken we de wachttijd iets langer
(2*motion_speed
), zodat een korte vertraging optreedt.
De mouseover
en mouseout
functies staan nu. Als laatste hebben we nog de
functie, die stap voor stap de juiste class
toekent aan het menu-item, zodat deze soepel
terugvalt naar de uitgangspositie:
// Recursieve hulproutine: zet menu-item stapje omlaag tot basispositie is bereikt.
//
function moveDown(item_id)
{
// Haal huidige menu-item en link uit dit menu-item op.
var current_item = document.getElementById(item_id);
// Geen menu-item? Dan niets doen.
if (!current_item) return false;
// Verplaats en 'verkleur' menu-item.
current_item.state--;
current_item.className = "stap" + current_item.state;
// Vervolg timeoutserie voor terugplaatsen totdat eindpositie weer is bereikt.
if (current_item.state > motion_base_state)
{
// We zijn er nog niet, dus volgende stap.
var in_motion = "moveDown('" + item_id + "')";
current_item.inMotion = setTimeout(in_motion, motion_speed);
}
else
{
// Klaar, dus wis de verwijzing naar de stopwatch.
current_item.inMotion = null;
}
}
Het begin ziet er vertrouwd uit: we halen weer het huidige menu-item op uit het DOM. Deze keer gebruiken
we echter niet this
, maar het als parameter aan de functie meegegeven item_id
.
Dan is het tijd voor de kern van onze animatie: we verlagen onze teller current_item.state
met één en geven het menu-item een nieuwe class
. Tot slot kijken we of we nog niet beneden
zijn en starten een nieuwe stopwatch om het menu-item weer een stapje te verplaatsen, of wissen juist de
verwijzing naar de stopwatch door current_item.state
op null
te zetten.
Als laatste moeten we nog even terug naar onze functie moveUp
. Als de bezoeker de muiscursor
kort van een menu-item haalt, maar er dan direct weer overheen beweegt, moeten we de lopende animatie
natuurlijk afbreken. Dit doen we door aan moveUp
een paar regels JavaScript toe te voegen:
// Hulproutine: zet menu-item omhoog.
//
function moveUp()
{
// Haal menu-item op uit document.
var current_item = this.parentNode;
// Geen menu-item? Dan niets doen.
if (!current_item) return false;
// Wis bestaande timeout indien aanwezig.
if (current_item.inMotion) clearTimeout(current_item.inMotion);
// Verplaats en 'verkleur' menu-item.
current_item.state = motion_top_state;
current_item.className = "stap" + current_item.state;
// Wis verwijzing naar de stopwatch.
current_item.inMotion = null;
}
Als we het menu-item hebben opgehaald controleren we eerst of er al een stopwatch loopt voor dit menu-item
en zetten deze zonodig uit met clearTimeout
. En voor het einde van de functie wissen we
ook hier de verwijzing naar de stopwatch.
Alles bij elkaar resulteert dit in het menu dat je op deze voorbeeldsite ziet. Als je snel met je muiscursor over de menu-items beweegt, zie je meteen het effect van de stopwatch en de ‘status’, die we voor elk menu-item apart bijhouden. En met de woorden van Tantek Çelik: ceci n’est pas Flash!
Door gelaagd te bouwen zoals Robert Jan en ik hebben beschreven, krijg je veel flexibiliteit cadeau. Zo laat je met een paar wijzigingen in het stylesheet en een ander achtergrondstreepje het menu horizontaal bewegen. En met het menu uit deze serie begint het pas: met de beschreven technieken kun je bijvoorbeeld ook Flash objecten elegant integreren, allerlei effecten toevoegen aan je pagina of toegankelijke drop-down menu’s bouwen.
is de ontwerpende kracht achter vizi | vorm geven aan inhoud, werkt samen met eend aan een menselijk internet en levert wekelijks zijn bijdrage aan de zijlijn van NAAR VOREN. Naast zijn ontwerpwerk is hij bezeten van fonts en films.
Publicatiedatum: 27 januari 2006
Naar Voren is op 18 juli 2010 gestopt met publiceren. De artikelen staan als een soort archief online. Het kan dus zijn dat de informatie verouderd is en dat er inmiddels veel betere of makkelijkere manieren zijn om je doel te bereiken.
Copyright © 2002-heden » NAAR VOREN en de auteurs