» direct naar zoek en menu

Tijdschrift voor webwerkers » Artikel #119

Aan de slag met JavaScript - Bewijs van goed gedrag

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.

Vloeiend verloop

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:

Verloop van geel naar wit in zes stappen

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.

Zachte landing

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.

De basis

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:

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 animatie zelf

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.

Eindresultaat

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.

Meer lezen over “JavaScript”

Meer lezen over het “event model

Auteur

Jeroen Visser

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

Let op

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