You are on page 1of 269

SPIS TREŚCI

OD REDAKCJI..................................................................................................................................8

WSTĘP..............................................................................................................................................10
CO POWINIENEŚ WIEDZIEĆ.....................................................................................................................10
UŻYCIE CZCIONEK...............................................................................................................................10
UKŁAD KSIĄŻKI..................................................................................................................................11
Wymagania programu................................................................................................................11
Struktura programu....................................................................................................................11
Techniki języka JavaScript.........................................................................................................11
Kierunki rozwoju........................................................................................................................11
O KODZIE..........................................................................................................................................11
PROGRAMOWANIE I TESTOWANIE............................................................................................................11
PODZIĘKOWANIA.................................................................................................................................12
WPROWADZENIE.........................................................................................................................13
ZALETY JĘZYKA JAVASCRIPT................................................................................................................13
Prostota, szybkość i efektywność................................................................................................13
Wszechobecność.........................................................................................................................13
Redukcja obciążenia serwera.....................................................................................................14
JavaScript rozwija się................................................................................................................14
Być może nie ma wyboru............................................................................................................14
I wiele innych zalet.....................................................................................................................14
PODSTAWOWA STRATEGIA PROGRAMOWANIA W JAVASCRIPT......................................................................14
Co może aplikacja?....................................................................................................................15
Kim są nasi odbiorcy..................................................................................................................15
Jak radzić sobie z przeszkodami?..............................................................................................16
Uwzględniaj wszelkie używane przeglądarki........................................................................16
Dyskretnie obniżaj jakość.......................................................................................................16
Mierz nisko.............................................................................................................................16
Mierz wysoko.........................................................................................................................16
Udostępniaj wiele wersji jednej aplikacji...............................................................................16
UŻYCIE JĘZYKA JAVASCRIPT
W PREZENTOWANYCH APLIKACJACH........................................................................................................16
Wielokrotne użycie kodu przyszłością narodu...........................................................................17
Wydzielanie JavaScriptu............................................................................................................17
2

Deklarowanie zmiennych globalnych i tablic na początku........................................................17


Deklarowanie konstruktorów po zmiennych globalnych...........................................................17
Definiowanie funkcji zgodnie z porządkiem „chronologicznym”..............................................17
Każda funkcja realizuje jedno zadanie......................................................................................17
W miarę możliwości używaj zmiennych lokalnych.....................................................................17
NASTĘPNY KROK.................................................................................................................................18
1..........................................................................................................................................................19

WYSZUKIWANIE DANYCH
PO STRONIE KLIENTA................................................................................................................19
WYMAGANIA PROGRAMU......................................................................................................................21
STRUKTURA PROGRAMU........................................................................................................................22
Plik nav.html..............................................................................................................................22
Plik records.js............................................................................................................................25
Zmienne globalne.......................................................................................................................25
Techniki języka JavaScript:
użycie separatorów wewnątrz łańcuchowych do rozdzielania pól
rekordów.............................................................26
Funkcje.......................................................................................................................................26
validate().................................................................................................................................27
convertString()........................................................................................................................27
allowAny()..............................................................................................................................28
requireAll().............................................................................................................................29
Techniki języka JavaScript: zagnieżdżanie pętli.30
verifyManage().......................................................................................................................31
noMatch()...............................................................................................................................31
formatResults().......................................................................................................................31
Nagłówek i tytuł dokumentu HTML......................................................................................32
Wyświetlanie tytułów, opisów i adresów URL dokumentów................................................33
Dodanie przycisków „Poprzedni” i „Następny”.....................................................................33
prevNextResults()...................................................................................................................34
Tylko przycisk „Następne”.....................................................................................................34
Przyciski „Następne” i „Poprzednie”.....................................................................................34
Tylko przycisk „Poprzednie”.................................................................................................34
Techniki języka JavaScript:
rozsądne użycie metody document.write()..........35
Techniki języka JavaScript: operator trójargumentowy..........................37
Kod HTML.................................................................................................................................37
TWORZENIE BAZY DANYCH W JĘZYKU JAVASCRIPT...................................................................................37
KIERUNKI ROZWOJU.............................................................................................................................38
Zgodność z językiem JavaScript 1.0...........................................................................................38
NICTJDO...................................................................................................................................38
Odporność na błędy...................................................................................................................39
Wyświetlanie reklam..................................................................................................................39
Rozszerzenie możliwości wyszukiwania.....................................................................................39
Zapytania predefiniowane..........................................................................................................40
2..........................................................................................................................................................41

TEST SPRAWDZANY
NA BIEŻĄCO..................................................................................................................................41
3 Spis treści

WYMAGANIA PROGRAMU......................................................................................................................44
STRUKTURA PROGRAMU........................................................................................................................44
index.html – ramki......................................................................................................................45
question.js – plik źródłowy JavaScript.......................................................................................46
Techniki języka JavaScript:
oszukany atrybut SRC.........................................47
administer.html...........................................................................................................................48
Treść HTML...........................................................................................................................50
Zmienne globalne...................................................................................................................51
Funkcje...................................................................................................................................52
itemReset().........................................................................................................................52
shuffle()..............................................................................................................................52
buildQuestion()...................................................................................................................53
Techniki języka JavaScript:
mieszanie zawartości tablicy...............................53
Techniki języka JavaScript:
protokół javascript:..............................................55
gradeTest()..........................................................................................................................55
printResults()......................................................................................................................56
chickenOut().......................................................................................................................58
KIERUNKI ROZWOJU.............................................................................................................................58
Uodpornienie na oszustwa.........................................................................................................58
Usuwanie odpowiedzi z tablicy..............................................................................................58
Usuwanie gradeTest() i modyfikacja buildQuestion()...........................................................59
Modyfikacja printResults().....................................................................................................59
Przekształcenie na ankietę.........................................................................................................59
3..........................................................................................................................................................60

INTERAKTYWNA
PREZENTACJA SLAJDÓW..........................................................................................................60
WYMAGANIA PROGRAMU......................................................................................................................62
STRUKTURA PROGAMU.........................................................................................................................62
Zmienne......................................................................................................................................65
Ustawienia domyślne warstwy DHTML................................................................................66
Zmienne związane z przeglądarkami.....................................................................................66
Zmienne związane z obrazkami.............................................................................................67
Zmienne automatycznego pokazu..........................................................................................67
Funkcje aplikacji........................................................................................................................67
Funkcje związane z warstwami..............................................................................................68
genLayer()..........................................................................................................................68
slide()..................................................................................................................................68
Techniki języka JavaScript:
pierwszy krok ku DHTML działającemu w różnych przeglądarkach.....69
Techniki języka JavaScript:
poprawne konwencje nazewnicze.......................70
genScreen().........................................................................................................................70
Elementy tablicy slideShow...............................................................................................73
Funkcje związane z obsługą obrazków..................................................................................74
preLoadImages().................................................................................................................74
imageSwap().......................................................................................................................74
4

Funkcje nawigacji...................................................................................................................75
refSlide(), hideSlide(), showSlide(), menuManager()........................................................75
Techniki języka JavaScript:
siła funkcji eval().................................................75
changeSlide()......................................................................................................................76
setSlide().............................................................................................................................77
autoPilot()...........................................................................................................................77
Techniki języka JavaScript:
używanie setInterval() i clearInteval()................78
automate()...........................................................................................................................78
KIERUNKI ROZWOJU.............................................................................................................................78
Losowy dobór slajdów w trybie automatycznym........................................................................78
Animowane GIF-y i suwaki slajdów..........................................................................................79
Animacja samych slajdów..........................................................................................................79
Pod powierzchnią................................................79
4..........................................................................................................................................................80

INTERFEJS MULTIWYSZUKIWARKI......................................................................................80
WYMAGANIA PROGRAMU......................................................................................................................82
STRUKTURA PROGRAMU........................................................................................................................82
Przechadzka Aleją Pamięci........................................................................................................86
Dynamiczne ładowanie obrazków..............................................................................................86
Uruchamianie wyszukiwarek.....................................................................................................87
Techniki języka JavaScript:
wielokrotne używanie kodu................................88
engineLinks()..............................................................................................................................88
Zarządzanie warstwami..........................................................................................................88
Techniki języka JavaScript:
rezygnacja z obiektowości..................................89
Techniki języka JavaScript:
matematyka a pamięć..........................................90
Wstępne ładowanie obrazków................................................................................................90
Tworzenie łącza......................................................................................................................90
imageSwap()...............................................................................................................................91
callSearch()................................................................................................................................92
Techniki języka JavaScript:
użycie escape() i unescape()................................92
KIERUNKI ROZWOJU:
ZWIĘKSZENIE MOŻLIWOŚCI DECYDOWANIA PRZEZ UŻYTKOWNIKA.................................................................93
Krok 1.....................................................................................................................................94
Krok 2.....................................................................................................................................94
Krok 3.....................................................................................................................................94
Krok 4.....................................................................................................................................94
Krok 5.....................................................................................................................................94
5..........................................................................................................................................................96

IMAGEMACHINE..........................................................................................................................96
WYMAGANIA PROGRAMU......................................................................................................................98
STRUKTURA PROGRAMU........................................................................................................................99
5 Spis treści

Krok 1. Załadowanie strony.....................................................................................................106


Krok 2. Określenie liczby par obrazków i ustawień domyślnych.............................................106
Krok 3. Określenie nazw plików, atrybutów HREF i tak dalej................................................107
captureDefaultProfile().........................................................................................................107
generateEntryForm()............................................................................................................108
genJavaScript().....................................................................................................................109
Techniki języka JavaScript:
ostrożne kodowanie w JavaScript.....................110
Techniki języka JavaScript:
siła zmiennych globalnych................................111
Czas na decyzje....................................................................................................................111
Generowanie kodu................................................................................................................112
Techniki języka JavaScript:
podstawianie tekstu w JavaScripcie 1.1 i 1.2....112
Krok 4. Wybór Podglądu w celu obejrzenia działania kodu....................................................113
Krok 5. Wybór Zmiany danych w celu zrobienia poprawek....................................................113
KIERUNKI ROZWOJU:
DODANIE ATRYBUTÓW DO SZABLONU....................................................................................................113
Krok 1. Dodanie pól.................................................................................................................114
Krok 2. Tworzenie tablic w setArrays()...................................................................................114
Krok 3. Pobieranie nowych ustawień domyślnych...................................................................114
Krok 4. Dodanie pól tekstowych w generateEntryForm().......................................................115
Krok 5. Odwołanie się do nowych wartości w genJavaScript()
i ich użycie...............................................................................................................................115
Krok 6. Generacja dodatkowego HTML w genJavaScript()....................................................115
6........................................................................................................................................................117

REALIZACJA PLIKÓW
ŹRÓDŁOWYCH JAVASCRIPTU...............................................................................................117
ARRAYS.JS........................................................................................................................................118
COOKIES.JS.......................................................................................................................................121
DHTML.JS.........................................................................................................................................124
EVENTS.JS.........................................................................................................................................125
FRAMES.JS........................................................................................................................................129
Słów parę o konkurujących modelach zdarzeń. 130
IMAGES.JS.........................................................................................................................................131
NAVBAR.JS........................................................................................................................................132
NUMBERS.JS......................................................................................................................................134
OBJECTS.JS........................................................................................................................................135
STRINGS.JS........................................................................................................................................138
KIERUNKI ROZWOJU...........................................................................................................................142
7........................................................................................................................................................144

USTAWIENIA UŻYTKOWNIKA OPARTE NA CIASTECZKACH.....................................144


WYMAGANIA PROGRAMU....................................................................................................................145
STRUKTURA PROGRAMU......................................................................................................................146
prefs.html..................................................................................................................................147
Formularz ustawień użytkownika.........................................................................................153
Ładowanie zapisanych ustawień..........................................................................................154
6

Składanie obrazków..............................................................................................................156
Wprowadzanie zmian...............................................................................................................157
Krok 1. Iteracja formObj......................................................................................................158
Krok 2. Zapisanie danych do pliku cookies.........................................................................159
Krok 3. Pokazanie użytkownikowi nowych ustawień..........................................................160
Zerowanie formularza..............................................................................................................160
dive.html...................................................................................................................................160
Analiza ciasteczek................................................................................................................162
Twarzą w twarz z nieznanym...............................................................................................163
Techniki języka JavaScript:
konwencje nazewnicze jeszcze raz...................164
Techniki języka JavaScript:
dynamiczny DHTML........................................165
KIERUNKI ROZWOJU...........................................................................................................................166
Więcej ustawień wyglądu.........................................................................................................166
Gotowe schematy wyglądu stron..............................................................................................166
Umożliwienie użytkownikom tworzenia własnych łącz............................................................166
Marketing bezpośredni.............................................................................................................167
8........................................................................................................................................................168

SHOPPING BAG
– WÓZEK SKLEPOWY
STWORZONY W JAVASCRIPCIE............................................................................................168
SHOPPING BAG W DWÓCH SŁOWACH.....................................................................................................168
Etap 1. Ładowanie aplikacji....................................................................................................169
Etap 2. Przeglądanie towarów i wybór....................................................................................170
Etap 3: Przeglądanie zamówienia i zmiany w nim..................................................................172
Etap 4. Płacenie.......................................................................................................................174
WYMAGANIA PROGRAMU....................................................................................................................174
STRUKTURA PROGRAMU......................................................................................................................175
ETAP 1. ŁADOWANIE APLIKACJI..........................................................................................................177
Elementy najwyższego poziomu...............................................................................................178
inventory.js...............................................................................................................................179
Techniki języka JavaScript:
zarządzanie wieloma oknami i dokumentami...179
Cechy produktów..................................................................................................................182
Cechy kategorii produktów..................................................................................................183
Tworzenie produktów i kategorii.........................................................................................184
Tworzenie koszyka na zakupy..............................................................................................185
Techniki języka JavaScript:
zapisywanie stanu klienta w obiektach JavaScriptu186
ETAP 2. POKAZANIE TOWARÓW...........................................................................................................186
manager.html...........................................................................................................................186
Zmienne................................................................................................................................193
display()................................................................................................................................194
Wyjątki od reguły.................................................................................................................194
Tworzenie wyświetlanej strony............................................................................................195
ETAP 3. POKAZANIE WSZYSTKICH KATEGORII.........................................................................................196
Wyświetlenie pierwszego produktu..........................................................................................196
Gdzie tu jest DHTML?.........................................................................................................197
7 Spis treści

ETAP 4. DODAWANIE PRODUKTÓW DO KOSZYKA ....................................................................................197


Techniki języka JavaScript:
dodawanie właściwości obiektów.....................198
Wyszukiwanie produktów.........................................................................................................198
Odwzorowanie produktów i kategorii......................................................................................198
Przeszukiwanie istniejącej bazy danych...................................................................................199
Obsługa nawigacji między produktami i kategoriami.............................................................199
Techniki języka JavaScript:
Wielokrotne użycie bazy danych JavaScriptu...201
Kod w łączach..........................................................................................................................201
ETAP 5. ZMIANA ZAMÓWIENIA, PŁACENIE.............................................................................................201
Tworzenie list wyboru..............................................................................................................204
Zapisywanie rachunku.............................................................................................................205
Techniki języka JavaScript:
zaokrąglanie liczb i konwersja napisów............206
Opakowanie showBag(): pokazywanie podsumowań..............................................................206
Przycisk Do kasy..................................................................................................................207
Koniec wyświetlania.................................................................................................................208
A po stronie serwera?..............................................................................................................209
Przycisk Wyzeruj ilości............................................................................................................209
Przycisk Zmiana koszyka..........................................................................................................209
Zapomniane funkcje.................................................................................................................209
KIERUNKI ROZWOJU...........................................................................................................................210
Inteligentniejsze towary...........................................................................................................210
Zwiększenie możliwości wyszukiwania....................................................................................210
Obsługa ciasteczek...................................................................................................................211
9........................................................................................................................................................212

SZYFRY W JAVASCRIPCIE......................................................................................................212
JAK DZIAŁAJĄ SZYFRY........................................................................................................................215
Kilka słów o łamaniu kodów....................................................................................................215
Szyfr Cezara.............................................................................................................................216
Szyfr Vigenere..........................................................................................................................216
WYMAGANIA PROGRAMU....................................................................................................................216
STRUKTURA PROGRAMU......................................................................................................................217
Definiowanie szyfru..................................................................................................................221
Techniki języka JavaScript:
przypisywanie metod obiektom........................221
Techniki języka JavaScript:
jeszcze o dopasowywaniu napisów i podstawianiu222
Tworzenie szyfru z podstawianiem...........................................................................................223
Podstawowa metoda podstawiania..........................................................................................223
Różne podstawienia do różnych szyfrów..................................................................................224
Algorytm szyfru Cezara.......................................................................................................224
Algorytm szyfru Vigenere....................................................................................................225
Jak działa shiftIdx....................................................................................................................226
Obiekty SubstitutionCipher też są obiektami Cipher...............................................................227
Techniki języka JavaScript:
parę słów o dziedziczeniu w języku JavaScript 227
Tworzenie nowych obiektów SubstitutionCipher.....................................................................227
8

Techniki języka JavaScript:


składnia alternatywna........................................228
Dobór odpowiedniego szyfru...................................................................................................229
Na koniec..................................................................................................................................229
KIERUNKI ROZWOJU...........................................................................................................................230
10......................................................................................................................................................231

ELEKTRONICZNE ŻYCZENIA:
POCZTA ELEKTRONICZNA
METODĄ PRZENIEŚ I UPUŚĆ..................................................................................................231
WYMAGANIA PROGRAMU....................................................................................................................233
STRUKTURA PROGRAMU......................................................................................................................233
Pozostałe dwa dokumenty........................................................................................................236
Co już wiemy............................................................................................................................238
Proszę zająć miejsca!...............................................................................................................239
Śledzenie położenia myszy........................................................................................................241
Wywoływanie wszystkich ikon..................................................................................................241
Przenoszenie ikon.....................................................................................................................241
Kiedy dokumenty już się załadują............................................................................................242
Poznaj zmienne.........................................................................................................................246
Techniki języka JavaScript:
różnicowanie kodu obsługi sieci.......................246
Wyświetlanie życzeń.................................................................................................................247
Techniki języka JavaScript:
komunikacja między ramkami..........................248
Obrazki po kolei.......................................................................................................................249
Utrzymanie ikon na miejscu.....................................................................................................251
Sprawdzanie, co otrzymaliśmy.................................................................................................253
Ostateczne tworzenie kartki.....................................................................................................253
Wysyłanie życzeń......................................................................................................................255
Techniki języka JavaScript:
optymalizacja funkcji........................................256
Uwaga......................................................................................................................................256
PO STRONIE SERWERA........................................................................................................................256
KIERUNKI ROZWOJU...........................................................................................................................256
Dodanie łącza „wstecz”...........................................................................................................256
Dodanie obrazków tematycznych.............................................................................................257
Banery reklamowe....................................................................................................................257
Życzenia bardziej interaktywne................................................................................................257
11......................................................................................................................................................258

POMOC KONTEKSTOWA.........................................................................................................258
WYMAGANIA PROGRAMU....................................................................................................................260
STRUKTURA PROGRAMU......................................................................................................................260
Pomoc kontekstowa..................................................................................................................261
Techniki języka JavaScript:
kontrolowanie odrębnych okienek....................263
Pokazywanie i ukrywanie dodatkowych informacji.................................................................263
Tworzenie warstw.....................................................................................................................265
9 Spis treści

Techniki języka JavaScript:


Używanie łącz bez klikania...............................266
KIERUNKI ROZWOJU...........................................................................................................................267
Techniki języka JavaScript:
warstwy bez znaczników LAYER....................267
Spis treści.................................................................................................................................267
Przeszukiwanie plików pomocy................................................................................................268
Pytanie do specjalisty...............................................................................................................268
Pomoc telefoniczna..................................................................................................................268
Od redakcji

Niniejsza książka to gotowy zestaw receptur – podobnie jak książka kucharska. O ”wirtualnym koszyku na zakupy”
można myśleć jako o ”ciasteczkach cebulowych z pastą łososiową”. W każdym rozdziale podano kod i dokumentację
przydatnej aplikacji zwykle napisanej całkowicie w JavaScripcie. Można wszystkie dania przygotowywać tak, jak to
podał autor książki, ale można też sięgnąć do pewnych sugestii, aby wykorzystać je w swoich pomysłach. Na rynku
znajdują się książki zawierające proste przepisy, jak zrobić jakieś drobiazgi i jak ozdobić JavaScriptem swoją stronę
sieciową, natomiast w tej książce znajdziemy całe aplikacje sieciowe napisane w jedynym języku skryptowym,
rozumianym przez przeglądarki.
Skoro tyle już sobie wyjaśniliśmy, zastanówmy się, co należy rozumieć przez książkę z recepturami? Jej zadaniem nie
jest na pewno podawanie treści w mało elastycznej formie, ale pomoc w tworzeniu kodu. Zapewne takie pozycje
książkowe, zawierające receptury, będzie można spotkać coraz częściej.
Richard Koman, Redaktor
Wstęp

Czegoś dotychczas brakowało. Zgłębiałem stosy książek o JavaScripcie, oglądałem kolejne witryny sieciowe wprost
ociekające kodem i pomysłami. Jednak kiedy już poznałem wszelakie nowe rodzaje składni i genialne techniki,
nie wiedziałem, co z tą wiedzą mogę zrobić poza pokazywaniem przykładów. Czułem się tak, jakbym był w kuchni
pełnej wszelakich składników jadła, ale bez żadnego przepisu. Znałem rozmaite sztuczki języka JavaScriptu i miałem
różne kawałki kodu, ale nie potrafiłem tego zastosować do rozwiązania typowych problemów na stronach sieciowych.
Oczywiście niektóre książki zawierały aplikacje JavaScript, ale nie były one odpowiednie do stosowania w Sieci.
Oczko to świetna gra, arkusz kalkulacyjny to ciekawa aplikacja, ale trudno je zamieszczać na swoich stronach
sieciowych.
W tej książce znajduje się szereg przepisów. Nie tylko można się dowiedzieć, jak sprawdzić używaną przeglądarkę czy
umożliwić przewijanie obrazków, ale można również znaleźć tu kompletne aplikacje, których będziesz mógł używać
na swoich stronach. Aplikacje te nie będą tworzone krok po kroku, od razu zostaną zaprezentowane w całości. Można
je kopiować do katalogu serwera sieciowego (lub komputera lokalnego) i natychmiast uruchamiać. Rozdziały tej
książki naszpikowane są kodem JavaScript, który ma pomóc użytkownikom w realizowaniu typowych zadań, takich jak
przeszukiwanie witryny, sporządzenie spisów treści, umożliwienie przewijania obrazków, oglądanie prezentacji,
robienie zakupów i wiele innych. Oczywiście można te przykłady modyfikować tak, aby najlepiej pasowały do naszych
potrzeb, ale i tak są one mniej więcej gotowe do użycia. Oprócz tego do każdej aplikacji dołączono dokładne
objaśnienie jej działania, więc można sobie sprawdzać, jak zadziała zmiana poszczególnych fragmentów kodu.

Co powinieneś wiedzieć
Nie jest to książka dla początkujących, gdyż nikt nie będzie tu nikogo uczył JavaScriptu, ale będzie można się
dowiedzieć się, jak go używać. Nie trzeba być wiarusem JavaScriptu z trzyletnim stażem, jeśli jednak
info.replace(/</g, "&lt;"), new Image() czy var itemArray = [] są dla kogoś niejasne, to najlepiej mieć
pod ręką opis składni tego języka.

Użycie czcionek
Kursywa
używana jest do podawania nazw plików, ścieżek katalogów, adresów URL.
Pismo o stałej szerokości czcionki
używana do przedstawiania znaczników HTML, fragmentów kodu, funkcji, nazw obiektów i innych odwołań
do kodu.
Kursywa o stałej szerokości czcionki
stosowana jest do tekstu wprowadzanego przez użytkownika i tekstu podmienianego.
Pismo o stałej szerokości czcionki, pogrubione
zastosowano do tekstu wyświetlanego na ekranie.
11 Wstęp

Układ książki
Większość rozdziałów ma podobny, czteroczęściowy układ.

Wymagania programu
Ta krótka część określa środowisko wymagane do uruchomienia aplikacji. Zwykle podawana jest potrzebna wersja
przeglądarek Netscape Navigator i Microsoft Internet Explorer. Tutaj także nakreśla się tło, omawiając zagadnienia
związane ze skalowaniem i monitorowanie wydajności.

Struktura programu
Kiedy już czytelnikowi znudzi się zabawa z aplikacją i będzie chciał zobaczyć, „co tam siedzi w środku”, powinien
przeczytać tę część. Tutaj znajdzie omówienie kodu, zwykle wiersz po wierszu. Jest to część najdłuższa, więc warto
usiąść sobie wygodnie, zanim zacznie się ją studiować.

Techniki języka JavaScript


Kiedy będziemy przebijać się przez składnię, pojawią się miejsca, gdzie warto będzie zatrzymać się na chwilę
i wskazać techniki, które można dodać do arsenału swoich środków.

Kierunki rozwoju
W tej części sugerowane są metody dalszego rozwijania aplikacji. Czasami są to sugestie, a czasem gotowy kod. Zdarza
się, że nie potrafię się powstrzymać i piszę za czytelnika kod znajdujący się w archiwum, które można załadować z Sieci.
Tak czy inaczej, warto poczuć powiew twórczej weny, a nie ograniczać się tylko do upartego zapytywania: „Niezłe, jak to
wprowadzić na moją stronę?”.

O kodzie
Cała ta książka mówi o aplikacjach. Nie powinno być zatem zaskoczeniem, że znajduje się tutaj mnóstwo kodu.
Niektóre aplikacje mają kilkaset wierszy, większość z nich to kolejne strony kodu. W niektórych wypadkach kod jest
nawet powtarzany, aby czytelnik nie musiał co chwilę przerzucać kartek między kodem programu a jego omówieniem.
Jedną z wad umieszczania kodu w książce jest... no cóż, właśnie umieszczenie go w książce. Często strona jest zbyt
wąska, aby umieścić w jednym wierszu tyle, ile by się chciało. Fragment kodu często kontynuowany jest w następnym
wierszu, i jeszcze w następnym... Aby zwiększyć czytelność, pominięto komentarze, choć znajdziemy je w plikach.
Osoby odpowiedzialne za skład nieźle się napracowały, aby sformatować kod w miarę czytelnie, ale i tak czasem
wygodniej może być czytać tenże kod w edytorze.
Jako że kod powinien być używany, a nie tylko czytany, wszystkie aplikacje dostępne są w pliku ZIP, który można
załadować ze strony sieciowej wydawnictwa O’Reilly. Warto udać się pod adres
http://www.oreilly.com/catalog/jscook/index.html i odnaleźć łącze Download. W każdym rozdziale będziemy
odwoływać się do tego pliku.

Programowanie i testowanie
Poniżej – bez jakiejkolwiek szczególnej kolejności – podałem sprzęt i programy używane podczas tworzenia kodu
prezentowanego w tej książce. W większości przypadków wszystko testowano w środowisku Windows, ale
w środowiskach Unix i Mac także nie powinno być zbyt wielu problemów, a może nie będzie ich w ogóle.
Sprzęt: IBM ThinkPad 55/P75/16M, Compaq Presario/P233/100M, IBM Aptiva C23/P120/128M, DELL OptiPlex/P2-
266/128M, Sun SPARC 20.
Systemy operacyjne: Win95, WinNT Workstation 4.0, WinNT Server 4.0, Solaris 2.5.
Przeglądarki: Netscape Navigator 3.0, 3.04 Gold, 4.0, 4.04, 4.07, 4.08, 4.5; Microsoft Internet Explorer 3.0, 3.02, 4.0,
4.01, 5.00.
Rozdzielczości: 640x480, 800x600, 1024x768, 1152x900, 1280x1024.
Oczywiście nie wszystkie aplikacje były testowane we wszystkich kombinacjach, jednak starałem się kodować jak
najbardziej ostrożnie, aby w większości możliwych środowisk wszystko działało poprawnie.
12

Podziękowania
Moje nazwisko znajduje się na okładce, mam więc obowiązek i przyjemność podziękować w tym miejscu wszystkim,
którzy przyczynili się do powstania tej książki.
Kwestie techniczne pomogli mi rozstrzygnąć: Steve Quint, James Chan, Jim Esten, Bill Anderson, Roland Chow,
Rodney Myers, Matthew Mastracci, Giorgio Braga, Brock Beauchamp i inni – dlatego im właśnie chciałbym podziękować,
zwłaszcza za wspomaganie mnie swoją znajomością JavaScriptu, a także innych zagadnień programowania. Specjalne
podziękowania kieruję w stronę Patricka Clarka, którego kod był inspiracją aplikacji obsługi pomocy. Dziękuję redaktorowi
Richardowi Komanowi, który był otwarty na moje pomysły i umożliwił przelanie ich na papier, a także Tarze
McGoldrick i Robowi Romano za ich pracę ważną, choć niewidoczną.
Serdeczne słowa podziękowania składam mojej żonie, Róndine Bradenbaugh, za to, że wytrzymała ze mną, kiedy
przez miesiące wpatrywałem się w monitor i szaleńczo stukałem w klawiaturę, noc w noc. Chciałbym podziękować
wreszcie moim rodzicom za ich wsparcie i zachęcanie mnie do rozwijania umiejętności pisarskich.
Chciałbym też podziękować komuś, o kim często się zapomina – czytelnikom. To właśnie czytelnicy zostawiają
w księgarni ciężko zarobione pieniądze, aby powstanie książki było w ogóle możliwe. Mam nadzieję, że wybór tej książki
okaże się dla czytelnika inwestycją wartą wydanych pieniędzy.
Wprowadzenie

Ta książka jest niezwykła, gdyż mówi o pisaniu w JavaScripcie dużych aplikacji sieciowych. Większość ludzi inaczej
widzi zastosowania tego języka. JavaScript zwykle jest (a przynajmniej bywał) stosowany do dodawania do obrazków
suwaków przewijania, realizacji liczników gości, określania stosowanej przeglądarki i temu podobnych rzeczy.

Zalety języka JavaScript


Żaden język i żadna technologia nie uzyskały statusu najlepszego rozwiązania tworzenia aplikacji sieciowych. Każde
z tych rozwiązań ma zalety i wady. Ostatnie postępy w języku JavaScript i innych upowszechniających się technik, jak
DHTML, Java, a nawet Macromedia Flash, umożliwiają JavaScriptowi korzystanie z tych rozwiązań i tworzenie
naprawdę złożonych systemów sieciowych. Oto jeszcze kilka innych argumentów przemawiających za tworzeniem
aplikacji w JavaScripcie.

Prostota, szybkość i efektywność


Jako że JavaScriptu dość łatwo jest się nauczyć, można od razu zacząć go używać. Jest to idealne rozwiązanie, kiedy
chcemy dodać swojej witrynie nieco dodatkowej funkcjonalności. Kiedy masz już za sobą podstawy, do tworzenia
naprawdę interesujących aplikacji już niedaleko.
JavaScript jest językiem wysokiego poziomu z dużymi możliwościami. Nie sposób zrobić w tym języku niczego
na poziomie maszynowym, ale dostępne są różne właściwości przeglądarki, stron, a czasami także systemu, w którym
przeglądarka działa. JavaScript nie musi być kompilowany, jak Java™ czy C, a przeglądarka nie wymaga ładowania
maszyny wirtualnej do uruchomienia kodu. Po prostu aplikację się koduje i uruchamia.
JavaScript działa także w architekturze obiektowej, podobnie jak Java i C++. Cechy języka, takie jak konstruktory
i dziedziczenie oparte na prototypach, dodają nowy poziom abstrakcji. Dzięki temu możliwe jest wielokrotne
wykorzystanie tego kodu w naprawdę dużym stopniu.

Wszechobecność
JavaScript jest zdecydowanie najbardziej popularnym językiem skryptowym w Sieci. Nie tysiące, ale miliony witryn
sieciowych zawierają JavaScript. Język ten jest obsługiwany przez większość najpopularniejszych przeglądarek (choć
tak naprawdę mówimy o JScript w Internet Explorerze). Zarówno Netscape, jak i Microsoft zdają się stale poszukiwać
14

sposobów rozbudowania tego języka. Takie wsparcie oznacza, że mamy naprawdę duże szanse na to, że przeglądarka
naszego gościa będzie ten język obsługiwała.1

Redukcja obciążenia serwera


To właśnie była jedna z podstawowych przyczyn tak powszechnego przyjęcia JavaScriptu. Język ten pozwala
zrealizować po stronie klienta wiele funkcji, które inaczej musiałyby być wykonywane na serwerze. Jednym
z najlepszych przykładów jest sprawdzanie poprawności danych. Programiści starej szkoły mogą pamiętać, jak kilka lat
temu jedyną metodą sprawdzania poprawności danych, wprowadzonych w formularzu HTML, było przesłanie tych
danych na serwer sieciowy i przekazanie ich skryptowi CGI w celu ich sprawdzenia.
Jeśli dane nie miały żadnych błędów, skrypt CGI działał dalej normalnie. Jeśli znalezione zostały jakieś błędy, skrypt
zwracał użytkownikowi komunikat, opisując problem. Zastanówmy się teraz, jakiego obciążenia zasobów to wymaga.
Przesłanie formularza wymaga żądania HTTP z serwera. Po podróży danych przez Sieć skrypt CGI powtórnie jest
uruchamiany. Za każdym razem, kiedy użytkownik się pomyli, cały proces się powtarza. Użytkownik musi czekać
na przesłanie komunikatu o błędzie, zanim dowie się, gdzie się pomylił.
Teraz mamy do dyspozycji JavaScript, dlatego możemy sprawdzać zawartość poszczególnych elementów formularza
przed odesłaniem ich na serwer sieciowy. Dzięki temu zmniejsza się liczba transakcji HTTP i zdecydowanie zmniejsza
się prawdopodobieństwo, że użytkownik pomyli się przy wprowadzaniu danych. JavaScript może też odczytywać
i zapisywać „ciasteczka” (cookies), co dotąd wymagało odpowiedniego użycia nagłówków w serwerze sieciowym.

JavaScript rozwija się


Kiedy pojawił się JavaScript 1.1, nowe właściwości: obiekt Image oraz tablica document.images, pozwalające
przewijać obrazki – spowodowały szerokie poruszenie. Później pojawił się JavaScript 1.2. Otworzyły się nowe
możliwości: obsługa DHTML, warstwy i mnóstwo innych udoskonaleń zdumiały wielu programistów. Wszystko to
było zbyt piękne, by mogło być prawdziwe.
Na tym jednak się nie skończyło. JavaScript stał się standaryzowanym językiem skryptowym powszechnego
zastosowania, zgodnym z EMCA-262. Przynajmniej jedna firma stworzyła środowisko mogące uruchamiać JavaScript
z wiersza poleceń. Firma Macromedia wstawiła odwołania JavaScript do technologii Flash. Do ColdFusion firmy
Allaire włączono JavaScript do technologii opartej na XML Web Distributed Data Exchange (WDDX, Wymiana
Danych Rozproszonych przez Sieć). JavaScript ma coraz więcej właściwości, coraz więcej opcji... i coraz więcej
pułapek.

Być może nie ma wyboru


Czasami mamy przed sobą tylko jedną możliwą drogę. Załóżmy, że nasz dostawca usług internetowych nie pozwala
uruchamiać skryptów CGI. Co teraz, jeśli chcemy dodać do swojej strony wysyłanie poczty elektronicznej z formularza lub
pragniemy skorzystać z ciasteczek? Musimy zająć się rozwiązaniami działającymi po stronie klienta. JavaScript spośród tego
typu rozwiązań jest zdecydowanie najlepszy.

I wiele innych zalet


Istnieje jeszcze sporo innych zalet stosowania JavaScriptu, a i czytelnik z pewnością może tę listę jeszcze dalej
rozszerzyć. Najważniejsze jest to, że mimo szeregu zalet technologii realizowanych po stronie serwera aplikacje
JavaScript mają swoje miejsce w Sieci.

Podstawowa strategia programowania w JavaScript


Kiedy budujemy jakąkolwiek aplikację, czy to w JavaScript, czy nie, w naszym dobrze zrozumianym interesie jest mieć jakąś
strategię działania. Dzięki temu łatwiej będzie uporządkować swoje pomysły, szybciej także uda się wszystko zakodować
i przetestować. Istnieje mnóstwo publikacji opisujących dokładnie tworzenie aplikacji. Czytelnik musi wybrać strategię
działania najlepiej mu odpowiadającą, nie sposób zatem tutaj zbytnio w ten temat się zagłębiać. Jeśli jednak będziemy pisać
coś między znacznikami <SCRIPT> a </SCRIPT>, to pamiętanie o pewnych zasadach projektowych niejednokrotnie
zaoszczędzi nam bólu głowy. Jest to naprawdę proste – trzeba odpowiedzieć sobie po prostu na pytania: co? kto? jak?

1
Pamiętajmy jednak, że często użytkownicy wyłączają obsługę JavaScriptu. Dobrze jest od razu sprawdzić, czy użytkownik obsługę tego języka
włączył, a przynajmniej na pierwszej stronie witryny poinformować o konieczności włączenia JavaScriptu (przyp. tłum.).
15 Wprowadzenie

Co może aplikacja?
Najpierw musimy ustalić, czym aplikacja ma się zajmować. Załóżmy, że chcemy wysyłać pocztę elektroniczną
z formularza. Odpowiedzmy na takie pytania:
• Ile pól będzie zawierał formularz?
• Czy użytkownicy będą sami podawali adres, czy będą go wybierali z listy?
• Czy dane przed wysłaniem mają być sprawdzane? Jeśli tak, co zamierzamy sprawdzać? Wiadomość? Adres
e-mail? Jedno i drugie?
• Co dzieje się po wysłaniu listu? Czy następuje przekierowanie użytkownika na inną stroną, czy nic się
nie zmienia?
Ta seria pytań z pewnością będzie dłuższa. Dobrą nowiną jest to, że jeśli na tyle pytań odpowiemy, będziemy znacznie
lepiej wiedzieć, co właściwie chcemy osiągnąć.

Kim są nasi odbiorcy


Zidentyfikowanie adresatów informacji jest ogromnie ważne dla określenia wymagań wobec aplikacji. Upewnijmy się,
że dokładnie znamy odpowiedzi przynajmniej na pytania podane niżej:
• Jakich przeglądarek będą używali odbiorcy? Netscape Navigator? Jakie wersje: 2.x, 3.x, 4.x czy wyższe?
• Czy aplikacja będzie używana w Internecie, intranecie, czy lokalnie na komputerze?
• Czy możemy określić rozdzielczość używanych przez użytkowników monitorów?
• W jaki sposób użytkownicy będą przyłączeni do Sieci? Modemem 56K, przez łącze ISDN, łączem E1 lub E3?
Można by sądzić, że wszystkie pytania – poza pytaniem o przeglądarkę – nie mają nic wspólnego z JavaScriptem.
„Łączność? Kogo to obchodzi? Nie muszę konfigurować routerów ani innych tego typu rzeczy”. Tak, to prawda.
Nie trzeba być certyfikowanym inżynierem Cisco. Przejrzyjmy szybko te pytania, jedno po drugim, i zobaczmy, co
może być szczególnie ważne.
Używana przeglądarka jest jedną z najważniejszych rzeczy. W zasadzie im nowsza przeglądarka, tym nowszą wersję
JavaScriptu obsługuje. Jeśli na przykład nasi odbiorcy są wyjątkowo przywiązani do NN 2.x i MSIE 3.x (choć nie ma
żadnego po temu powodu), automatycznie możemy wykreślić przewijanie obrazków – wersje JavaScript i JScript
nie obsługują obiektów Image ani document.images.2
Jako że większość użytkowników przeszła na wersje 4.x tych przeglądarek, przewijanie obrazków jest dopuszczalne.
Teraz jednak musimy radzić sobie z konkurującymi modelami obiektów. Oznacza to, że Twoje aplikacje muszą być
przenośne między przeglądarkami lub musisz pisać osobne aplikacje dla każdej przeglądarki i jej wersji (co może być
nadaremną nauką).
Gdzie będzie znajdowała się aplikacja? W Internecie, w intranecie, czy też może na pojedynczym komputerze
przerobionym na stanowisko informacyjne? Odpowiedź na to pytanie da także szereg innych wytycznych. Jeśli
na przykład aplikacja będzie działała w Internecie, możemy być pewni, że do strony będą dobijały się wszelkie
istniejące przeglądarki i będą używały aplikacji (a przynajmniej będą próbowały to zrobić). Jeśli aplikacja działa tylko
w intranecie lub na pojedynczym komputerze, przeglądarki będą standardowe dla danego miejsca. Kiedy to piszę,
jestem konsultantem w firmie będącej jednym z dużych sklepów Microsoftu. Jeśli używany przeze mnie kod okazuje
się zbyt dużym wyzwaniem dla Netscape Navigatora i przeglądarka ta sobie z nim nie radzi, to nie muszę się tym
przejmować, gdyż użytkownicy i tak korzystają z Internet Explorera.
Ważną rzeczą jest rozdzielczość monitora. Jeśli na stronę wstawiliśmy tabelę o szerokości 900 pikseli, a użytkownicy
mają do dyspozycji rozdzielczość tylko 800x600, nie zauważą części nasz ciężkiej pracy. Czy można liczyć na to, że
wszyscy użytkownicy będą używali jakiejś określonej rozdzielczości? W przypadku Internetu odpowiedź jest
negatywna. Jeśli chodzi o intranet, można mieć szczęście. Niektóre firmy standaryzują komputery PC,
oprogramowanie, przeglądarki, monitory, a nawet stosowaną rozdzielczość.
Nie można też pominąć zagadnień związanych z łącznością. Załóżmy, że stworzyliśmy sekwencję animowaną, która
zarobi tyle, co średni film Stevena Spielberga (jeśli to się uda, to może powinniśmy... hm... współpracować). Dobrze,
ale użytkownicy modemów 56K zapewne przed ściągnięciem tego filmu zdążą pójść do kina i ten film obejrzeć, zanim
ściągną nasz film. Większość użytkowników jest w stanie pogodzić się z tym, że Sieć może się na chwilę zapchać, ale

2
Niektóre przeglądarki Internet Explorer 3.x dla Maca obsługiwały jednak przewijanie obrazków (przyp. aut.).
16

po jakiejś minucie większość z nich uda się na inne strony. Trzeba więc w swoich planach brać pod uwagę także
przepustowość łączy.

Jak radzić sobie z przeszkodami?


Żonglowanie wszystkimi tymi zagadnieniami może wydawać się dość proste, ale rzecz nie jest wcale taka łatwa. Może
się okazać, że nie będzie się w stanie obsłużyć wszystkich wersji przeglądarek, rozdzielczości ekranu lub szczegółów
związanych z łącznością. Co teraz? Jak uszczęśliwić wszystkich i nadal zachwycać ich obrazkiem o wielkości 500 kB?
Warto rozważyć jedną lub więcej z poniższych propozycji. Przeczytaj je wszystkie, aby móc podjąć decyzję, mając
komplet informacji.

Uwzględniaj wszelkie używane przeglądarki


Ta szalenie demokratyczna metoda polega na daniu możliwie najlepszych wyników jak największej liczbie odbiorców.
Takie kodowanie jest zapewne najpowszechniej stosowanym i najlepszym podejściem. Oznacza to, że starasz się
przede wszystkim obsłużyć użytkowników używających Internet Explorera 4.x i 5.x oraz Netscape Navigatora 4.x. Jeśli
zrealizujesz wykrywanie ważniejszych przeglądarek i zakodujesz aplikację tak, aby korzystała z najlepszych cech
wersji 4.x, uwzględniając przy tym różnice między przeglądarkami, będziesz mógł zrobić wrażenie na naprawdę dużej
części użytkowników.

Dyskretnie obniżaj jakość


To jest naturalny wniosek wynikający z poprzedniej strategii. Jeśli na przykład mój skrypt zostanie załadowany
do przeglądarki nieobsługującej nowych cech, otrzymam paskudne błędy JavaScriptu. Warto używać wykrywania
przeglądarki, aby w przypadku niektórych przeglądarek wyłączyć nowe cechy. Analogicznie można ładować różne
strony stosownie do różnych rozdzielczości monitora.

Mierz nisko
To podejście polega na założeniu, że wszyscy używają przeglądarki Netscape Navigator 2.0, ekranu w rozdzielczości
640x480, modemu 14,4K oraz procesora Pentium 33 MHz. Od razu zła wiadomość: nie można zastosować niczego
poza JavaScriptem 1.0. Nie ma mowy o przewijaniu, o warstwach, wyrażeniach regularnych czy technologiach
zewnętrznych (pozostaje podziękować za możliwość użycia ramek). Teraz wiadomość dobra: nasza aplikacja
zawędruje pod strzechy. Jednak wobec ostatnich zmian samego JavaScriptu nawet to ostatnie nie musi być prawdą.
Mierzę naprawdę nisko, ale rozsądnym założeniem wydaje mi się zakładanie użycia NN 3.x i MSIE 3.x. Pozostawanie
nieco z tyłu ma swoje zalety.

Mierz wysoko
Jeśli odbiorca nie ma Internet Explorera 5.0, nie zobaczy naszej aplikacji, a tym bardziej – nie będzie jej używał.
Dopiero w tej wersji można śmiało korzystać z obiektowego modelu dokumentów Internet Explorera, modelu zdarzeń,
wiązania danych i tak dalej. Nadmierne oczekiwania dotyczące wersji przeglądarek odbiorców mogą zdecydowanie
okroić krąg potencjalnej publiczności.

Udostępniaj wiele wersji jednej aplikacji


Można napisać szereg wersji swojej aplikacji, na przykład jedną dla Netscape Navigatora, inną dla Internet Explorera.
Taki sposób działania nadaje się jednak tylko dla osób dobrze znoszących monotonię, jednak może on przynieść jedną
wyraźną korzyść. Przypomnijmy sobie, co było mówione o łączności z Siecią. Jako że często nie da się sprawdzić
szerokości pasma użytkowników, można pozwolić im dokonać wyboru. Część łączy ze strony głównej umożliwi
użytkownikom z połączeniem E1 ładować pełną grafikę, natomiast użytkownicy modemów będą mogli skorzystać z wersji
okrojonej.

Użycie języka JavaScript


w prezentowanych aplikacjach
Opisane strategie są strategiami podstawowymi. W przykładach z tej książki użyto różnych strategii. Warto jeszcze
wspomnieć o konwencjach programowania w JavaScripcie. W ten sposób lepiej zrozumiesz przyjęte przeze mnie
rozwiązania oraz ustalisz, czy są one dobre w każdej sytuacji.
Pierwsze pytanie o aplikację powinno dotyczyć tego, czy przyda się ona do czegoś gościom odwiedzającym stronę.
Każda aplikacja rozwiązuje jeden lub więcej podstawowych problemów. Wyszukiwanie i wysyłanie wiadomości,
pomoc kontekstowa, sprawdzanie lub zbieranie informacji, przewijanie obrazków i tak dalej – to są rzeczy, które lubią
17 Wprowadzenie

sieciowi żeglarze. Jeśli planowana aplikacja nie znalazła dostatecznego uzasadnienia swojego zaistnienia,
nie poświęcałem jej swojego czasu.
Następną kwestią jest to, czy JavaScript pozwala osiągnąć potrzebną mi funkcjonalność. To było dość łatwe. Jeśli
odpowiedź brzmiała „tak”, stawałem do walki. Jeśli nie, to tym gorzej dla JavaScriptu.
Potem przychodziła kolej na edytor tekstowy. Oto niektóre zasady używane w prezentowanych kodach.

Wielokrotne użycie kodu przyszłością narodu


To właśnie tutaj do głosu dochodzą pliki źródłowe JavaScriptu. W aplikacjach tych używane są pliki z kodem,
ładowane za pomocą następującej składni:
<SCRIPT LANGUAGE="JavaScript1.1" SRC="jakisPlikJS.js"></SCRIPT>

Plik jakisPlikJS.js zawiera kod, który będzie używany przez różne skrypty. W wielu aplikacjach tej książki używane są
pliki źródłowe JavaScriptu. To się sprawdza. Po co wymyślać coś od nowa? Można także użyć plików źródłowych
w celu ukrycia kodu źródłowego przed resztą aplikacji. Wygodne może być umieszczenie bardzo dużej tablicy
JavaScriptu w pliku źródłowym. Użycie plików źródłowych jest tak ważne, że poświęciłem im cały rozdział 6.
Niektóre aplikacje zawierają kod po prostu wycinany i wklejany z jednego miejsca w inne. Kod taki może być dobrym
kandydatem na wydzielenie w plik źródłowy. Wklejanie stosowałem po to, aby zbyt często nie powtarzać „zajrzyj
do kodu pliku bibliotecznego trzy rozdziały wcześniej”. W ten sposób cały czas masz kod przed oczami, póki go
nie zrozumiesz. Kiedy już aplikacja na stronie będzie działała tak, jak sobie tego życzysz, zastanów się
nad wydzieleniem części kodu do plików źródłowych.

Wydzielanie JavaScriptu
Między znacznikami <HEAD> i </HEAD> należy umieszczać możliwie dużo kodu w pojedynczym zestawie
<SCRIPT></SCRIPT>.

Deklarowanie zmiennych globalnych i tablic na początku


Nawet jeśli globalne zmienne i tabele mają początkowo wartości pustych ciągów lub wartości nieokreślone,
definiowanie ich blisko początku skryptu jest dobrym sposobem poradzenia sobie z nimi, szczególnie jeśli używane są
w różnych miejscach skryptu. W ten sposób nie musisz przeglądać zbyt wiele kodu, aby zmienić wartość zmiennej:
wiesz, że znajdziesz ją gdzieś na początku.

Deklarowanie konstruktorów po zmiennych globalnych


W zasadzie funkcje tworzące obiekty definiowane przez użytkownika umieszczam blisko początku skryptu. Robię tak
po prostu dlatego, że większość obiektów musi powstać na początku działania skryptu.

Definiowanie funkcji zgodnie z porządkiem „chronologicznym”


Innymi słowy – staram się definiować funkcje w takiej kolejności, w jakiej będą wywoływane. Pierwsza funkcja
definiowana w skrypcie jest wywoływana na początku, druga funkcja jest wywoływana jako następna, i tak dalej.
Czasem może to być trudne lub wręcz niemożliwe, ale dzięki temu przynajmniej poprawia się sposób zorganizowania
aplikacji i pojawia się szansa, że funkcje wywoływane po sobie będą w kodzie obok siebie.

Każda funkcja realizuje jedno zadanie


Staram się ograniczyć funkcjonalność poszczególnych funkcji tak, aby każda z nich realizowała dokładnie jedno
zadanie: sprawdzała dane od użytkownika, odczytywała lub ustawiała ciasteczka, przeprowadzała pokaz slajdów,
pokazywała lub ukrywała warstwy i tak dalej. Teoria jest doskonała, ale czasem trudno ją wcielić w życie.
W rozdziale 5. zasadę tę bardzo mocno naruszam. Funkcje tamtejsze realizują pojedyncze zadania, ale są naprawdę
długie.

W miarę możliwości używaj zmiennych lokalnych


Stosuję tę zasadę w celu zaoszczędzenia pamięci. Jako że zmienne lokalne JavaScriptu znikają zaraz po zakończeniu
realizacji funkcji, w której się znajdują, zajmowana przez nie pamięć jest zwracana do puli systemu. Jeśli zmienna
nie musi istnieć przez cały czas działania aplikacji, nie tworzę jej jako globalnej, lecz jako lokalną.
18

Następny krok
Teraz powinieneś mieć już jakieś pojęcie o tym, jak przygotować się do tworzenia aplikacji JavaScript i jak tworzę
swoje aplikacje. Bierzmy się więc do zabawy.
Cechy aplikacji:
 Wydajne wyszukiwanie po stronie klienta
 Wiele algorytmów wyszukiwania
 Sortowanie i dzielenie wyników
wyszukiwania
 Skalowalność
 Łatwe zapewnienie zgodności
z JavaScriptem 1.0

1
Prezentowane techniki:
 Użycie separatorów wewnątrz
łańcuchowych do rozdzielania pól
rekordów
 Zagnieżdżanie pętli for
 Rozsądne użycie metody
document.write()
 Zastosowanie operatora
trójargumentowego

Wyszukiwanie
danych
po stronie
klienta

Mechanizm wyszukiwawczy może się przydać w każdej witrynie, czy jednak trzeba zmuszać serwer do przetwarzania
wszystkich zgłaszanych w ten sposób zapytań? Prezentowane tu rozwiązanie umożliwia realizację przeszukiwania stron
WWW całkowicie po stronie klienta. Zamiast przesyłać zapytania do bazy danych lub do serwera aplikacji, użytkownik
pobiera „bazę danych” wraz z żądanymi stronami. Baza ta jest zwykłą tablicą JavaScriptu, zawierającą w każdym
elemencie pojedynczy rekord.
Takie podejście daje kilka znaczących korzyści – głównie redukcję obciążenia serwera i skrócenie czasu odpowiedzi.
Nie należy zapominać, że opisywana tu aplikacja jest ograniczona zasobami klienta, szczególnie szybkością procesora
i dostępną pamięcią. Mimo to w wielu przypadkach może ona okazać się doskonałym narzędziem. Kod programu
zamieszczono w katalogu ch01 archiwum przykładów, zaś na rysunku 1.1 pokazano pierwszy ekran aplikacji.
Program udostępnia dwie metody wyszukiwania, nazwane umownie3 AND i OR. Poszukiwanie informacji może
odbywać się według tytułu i opisu lub według adresu URL dokumentu. Z punktu widzenia użytkownika obsługa
aplikacji jest całkiem prosta: wystarczy wpisać szukany termin i nacisnąć Enter. Możliwości wyszukiwania są
następujące:
• Wprowadzenie słów rozdzielonych znakami spacji zwróci wszystkie rekordy zawierające dowolny z podanych
terminów (logiczne „lub” – OR).

3
Nazwy te pochodzą od oznaczeń operatorów warunkowych (przyp. tłum.).
20

• Umieszczenie znaku plus (+) przed wyszukiwanym łańcuchem spowoduje wybranie rekordów zawierających
wszystkie podane hasła (logiczne „i” – AND).
• Wpisanie przed częścią lub całym adresem ciągu url: spowoduje wybranie rekordów odpowiadających
fragmentowi lub całości podanego adresu URL.

Rysunek 1.1. Rozpoczynamy wyszukiwanie

Pamiętaj o archiwum przykładów! Jak napisano we wstępie, wszystkie prezentowane


w książce programy można pobrać w postaci pliku ZIP z witryny internetowej
wydawcy; plik znajduje się pod adresem http://www.helion.pl/catalog/jscook/
index.html.

Na rysunku 1.2 pokazano wyniki wykonania prostego zapytania. Wykorzystano tu domyślną metodę wyszukiwania
(bez przedrostków), a szukanym słowem było javascript. Każde przeszukanie powoduje utworzenie „na bieżąco”
strony zawierającej wyniki, po których znajduje się łącze pozwalające odwołać się do strony z krótką instrukcją
obsługi.
Przydatna byłaby też możliwość wyszukiwania danych według adresów URL. Na rysunku 1.3 pokazano stronę wyników
wyszukiwania zrealizowanego przy użyciu przedrostka url:, który nakazuje wyszukiwarce sprawdzanie jedynie adresów
URL. W tym przypadku szukanym łańcuchem był tekst html, co spowodowało zwrócenie wszystkich rekordów
zawierających w adresie ten właśnie ciąg. Krótki opis dokumentu poprzedzony jest tym razem jego adresem URL.
Metoda wyszukiwania adresów URL ograniczona jest do pojedynczych dopasowań (tak jak przy wyszukiwaniu
domyślnym), nie powinno to jednak być problemem – niewielu użytkowników ma ambicje tworzenia złożonych
warunków wyszukiwania adresów.
Opisywana tu aplikacja pozwala ograniczyć liczbę wyników prezentowanych na pojedynczej stronie i umieszcza na niej
przyciski pozwalające na wyświetlenie strony następnej lub poprzedniej, co chroni użytkownika przed zagubieniem
w kilometrowym tasiemcu wyników. Liczba jednocześnie prezentowanych wyników zależy od programisty;
ustawieniem domyślnym jest 10.
21 Rozdział 1 - Wyszukiwanie danych po stronie klienta

Rysunek 1.2. Typowa strona z wynikami wyszukiwania

Rysunek 1.3. Strona z wynikami wyszukiwania według adresów URL

Wymagania programu
Nasza aplikacja wymaga przeglądarki obsługującej język JavaScript 1.1. Jest to dobra wiadomość dla osób
używających przeglądarek Netscape Navigator 3 i 4 oraz Internet Explorer 4 i 5, zła natomiast dla użytkowników IE 3.
Osoby, którym zależy na zgodności z wcześniejszymi wersjami, nie muszą się jednak martwić. Nieco dalej, w
podrozdziale Kierunki rozwoju, pokażemy jak zadowolić także użytkowników Internet Explorera 3 (choć kosztem
możliwości funkcjonalnych programu).
Wszelkie programy uruchamiane po stronie klienta zależne są od zasobów wykonującego je komputera, co w naszym
przypadku jest szczególnie widoczne. O ile można założyć, że zasoby klienta całkowicie wystarczą do uruchomienia
samego kodu, przekazanie mu dużej bazy danych (ponad 6-7 tysięcy rekordów) spowoduje spadek wydajności, a w
skrajnym przypadku może doprowadzić do zablokowania komputera.
22

Testując bazę danych zawierającą nieco mniej niż 10 tysięcy rekordów w przeglądarkach Internet Explorer 4 i Netscape
Navigator 4, autor nie doświadczył żadnych problemów. Plik źródłowy z danymi miał jednak ponad 1 MB; używany
komputer miał od 24 do 128 MB pamięci RAM. Próba wykonania tego samego zadania z użyciem przeglądarki
Netscape Navigator 3.0 Gold doprowadziła jednak do przepełnienia stosu – po prostu tablica zawierała zbyt wiele
rekordów. Z drugiej strony wersja zakodowana w języku JavaScript 1.0 i wykonywana w przeglądarce Internet Explo-
rer 3.02 na komputerze IBM ThinkPad pozwalała wykorzystać co najwyżej 215 rekordów. Nie należy jednak przerażać
się tą liczbą – używany do testowania laptop był tak stary, że słychać było, jak szczur biegając w kółko napędza
dynamo do zasilania procesora. Większość użytkowników powinna dysponować sprzętem umożliwiającym
przetworzenie większej ilości danych.

Struktura programu
Omawiana aplikacja składa się z trzech plików HTML (index.html, nav.html oraz main.html) i pliku źródłowego
zapisanego w JavaScripcie (records.js). Trzy dokumenty w języku HTML zawierają uproszczony zestaw ramek, stronę
początkową pozwalającą wprowadzać wyszukiwane hasła oraz stronę z instrukcjami, wyświetlaną domyślnie w
głównej ramce.

Plik nav.html
Najważniejsza część aplikacji znajduje się w pliku o nazwie nav.html. Okazuje się zresztą, że jedynym miejscem,
w którym jeszcze można znaleźć kod w języku JavaScript, są generowane na bieżąco strony wyników. Przyjrzyjmy się
treści przykładu 1.1.

Przykład 1.1. Zawartość pliku nav.html


1 <HTML>
2 <HEAD>
3 <TITLE>Wyszukiwanie</TITLE>
4
5 <SCRIPT LANGUAGE="JavaScript1.1" SRC="records.js"></SCRIPT>
6 <SCRIPT LANGUAGE="JavaScript1.1">

Przykład 1.1. Zawartość pliku nav.html (ciąg dalszy)


7 <!--
8
9 var SEARCHANY = 1;
10 var SEARCHALL = 2;
11 var SEARCHURL = 4;
12 var searchType = '';
13 var showMatches = 10;
14 var currentMatch = 0;
15 var copyArray = new Array();
16 var docObj = parent.frames[1].document;
17
18 function validate(entry) {
19 if (entry.charAt(0) == "+") {
20 entry = entry.substring(1,entry.length);
21 searchType = SEARCHALL;
22 }
23 else if (entry.substring(0,4) == "url:") {
24 entry = entry.substring(5,entry.length);
25 searchType = SEARCHURL;
26 }
27 else { searchType = SEARCHANY; }
28 while (entry.charAt(0) == ' ') {
29 entry = entry.substring(1,entry.length);
30 document.forms[0].query.value = entry;
31 }
32 while (entry.charAt(entry.length - 1) == ' ') {
33 entry = entry.substring(0,entry.length - 1);
34 document.forms[0].query.value = entry;
35 }
36 if (entry.length < 3) {
37 alert("Nie możesz wyszukiwać tak krótkich łańcuchów. Wysil się trochę.");
38 document.forms[0].query.focus();
39 return;
40 }
41 convertString(entry);
42 }
43
23 Rozdział 1 - Wyszukiwanie danych po stronie klienta

44 function convertString(reentry) {
45 var searchArray = reentry.split(" ");
46 if (searchType == (SEARCHANY | SEARCHALL)) { requireAll(searchArray); }
47 else { allowAny(searchArray); }
48 }
49
50 function allowAny(t) {
51 var findings = new Array(0);
52 for (i = 0; i < profiles.length; i++) {
53 var compareElement = profiles[i].toUpperCase();
54 if(searchType == SEARCHANY) {
55 var refineElement =
56 compareElement.substring(0,compareElement.indexOf('|HTTP'));
57 }
58 else {
59 var refineElement =
60 compareElement.substring(compareElement.indexOf('|HTTP'),
61 compareElement.length);
62 }
63 for (j = 0; j < t.length; j++) {
64 var compareString = t[j].toUpperCase();
65 if (refineElement.indexOf(compareString) != -1) {
66 findings[findings.length] = profiles[i];
67 break;
68 }
69 }
70 }
71 verifyManage(findings);

Przykład 1.1. Zawartość pliku nav.html (ciąg dalszy)


72 }
73
74 function requireAll(t) {
75 var findings = new Array();
76 for (i = 0; i < profiles.length; i++) {
77 var allConfirmation = true;
78 var allString = profiles[i].toUpperCase();
79 var refineAllString = allString.substring(0,
80 allString.indexOf('|HTTP'));
81 for (j = 0; j < t.length; j++) {
82 var allElement = t[j].toUpperCase();
83 if (refineAllString.indexOf(allElement) == -1) {
84 allConfirmation = false;
85 continue;
86 }
87 }
88 if (allConfirmation) {
89 findings[findings.length] = profiles[i];
90 }
91 }
92 verifyManage(findings);
93 }
94
95 function verifyManage(resultSet) {
96 if (resultSet.length == 0) { noMatch(); }
97 else {
98 copyArray = resultSet.sort();
99 formatResults(copyArray, currentMatch, showMatches);
100 }
101 }
102
103 function noMatch() {
104 docObj.open();
105 docObj.writeln('<HTML><HEAD><TITLE>Wyniki wyszukiwania</TITLE></HEAD>' +
106 '<BODY BGCOLOR=WHITE TEXT=BLACK>' +
107 '<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER><TR><TD VALIGN=TOP>' +
108 '<FONT FACE=Arial><B><DL>' +
109 '<HR NOSHADE WIDTH=100%>"' + document.forms[0].query.value +
110 '" - nic nie znaleziono.<HR NOSHADE WIDTH=100%>' +
111 '</TD></TR></TABLE></BODY></HTML>');
112 docObj.close();
113 document.forms[0].query.select();
114 }
115
116 function formatResults(results, reference, offset) {
117 var currentRecord = (results.length < reference + offset ?
24

118 results.length : reference + offset);


119 docObj.open();
120 docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki wyszukiwania</TITLE> \n</HEAD>' +
121 '<BODY BGCOLOR=WHITE TEXT=BLACK>' +
122 '<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' +
123 '<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' +
124 '<FONT FACE=Arial><B>Zapytanie: <I>' +
125 parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
126 'Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ' +
127 currentRecord + ' z ' + results.length + '</I><BR><BR></FONT>' +
128 '<FONT FACE=Arial SIZE=-1><B>' +
129 '\n\n<!-- początek zbioru wynikowego //-->\n\n\t<DL>');
130 if (searchType == SEARCHURL) {
131 for (var i = reference; i < currentRecord; i++) {
132 var divide = results[i].split("|");
133 docObj.writeln('\t<DT>' + '<A HREF="' + divide[2] + '">' +
134 divide[2] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>\n\n');
135 }

Przykład 1.1. Zawartość pliku nav.html (dokończenie)


136 }
137 else {
138 for (var i = reference; i < currentRecord; i++) {
139 var divide = results[i].split('|');
140 docObj.writeln('\n\n\t<DT>' + '<A HREF="' + divide[2] + '">' +
141 divide[0] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>');
142 }
143 }
144 docObj.writeln('\n\t</DL>\n\n<!-- Koniec wyników //-->\n\n');
145 prevNextResults(results.length, reference, offset);
146 docObj.writeln('<HR NOSHADE WIDTH=100%>' +
147 '</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
148 docObj.close();
149 document.forms[0].query.select();
150 }
151
152 function prevNextResults(ceiling, reference, offset) {
153 docObj.writeln('<CENTER><FORM>');
154 if(reference > 0) {
155 docObj.writeln('<INPUT TYPE=BUTTON VALUE="Poprzednie ' + offset +
156 ' wyników" ' +
157 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
158 (reference - offset) + ', ' + offset + ')">');
159 }
160 if(reference >= 0 && reference + offset < ceiling) {
161 var trueTop = ((ceiling - (offset + reference) < offset) ?
162 ceiling - (reference + offset) : offset);
163 var howMany = (trueTop > 1 ? "ów" : "");
164 docObj.writeln('<INPUT TYPE=BUTTON VALUE="Następne ' + trueTop +
165 ' wynik' + howMany + '" ' +
166 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
167 (reference + offset) + ', ' + offset + ')">');
168 }
169 docObj.writeln('</CENTER>');
170 }
171
172 //-->
173 </SCRIPT>
174 </HEAD>
175 <BODY BGCOLOR="WHITE">
176 <TABLE WIDTH="95%" BORDER="0" ALIGN="CENTER">
177 <TR>
178 <TD VALIGN=MIDDLE>
179 <FONT FACE="Arial">
180 <B>Wyszukiwarka pracująca po stronie klienta</B>
181 </TD>
182
183 <TD VALIGN=ABSMIDDLE>
184 <FORM NAME="search"
185 onsubmit="validate(document.forms[0].query.value); return false;">
186 <INPUT TYPE=TEXT NAME="query" SIZE="33">
187 <INPUT TYPE=HIDDEN NAME="standin" VALUE="">
188 </FORM>
189 </TD>
190
191 <TD VALIGN=ABSMIDDLE>
25 Rozdział 1 - Wyszukiwanie danych po stronie klienta

192 <FONT FACE="Arial">


193 <B><A HREF="main.html" TARGET="main">Pomoc</A></B>
194 </TD>
195 </TR>
196 </TABLE>
197 </BODY>
198 </HTML>

Tekst źródłowy jest dość obszerny. Aby zrozumieć, co się tutaj dzieje, najprościej będzie rozpocząć analizę od
początku i stopniowo posuwać się coraz dalej. Na szczęście kod zapisano tak, aby układ poszczególnych funkcji był
mniej więcej zgodny z kolejnością ich użycia.
Analizę kodu źródłowego przeprowadzimy w następującej kolejności:
• plik źródłowy records.js,
• zmienne globalne,
• funkcje,
• kod w języku HTML.

Plik records.js
Na początek zajmiemy się plikiem źródłowym records.js. Odwołanie do niego umieszczono w znaczniku <SCRIPT>
w wierszu 5.
Plik ten zawiera dość długą tablicę o nazwie profiles. Ze względu na spore rozmiary, jego zawartość została
w książce pominięta. Po rozpakowaniu pliku ZIP trzeba będzie zatem uruchomić edytor tekstów i otworzyć plik
ch01/records.js (uwaga: to baza danych, z której będziemy korzystać!). Każdy element bazy jest trzyczęściowym
łańcuchem o postaci np.:
"http://www.serve.com/hotsyte|HotSyte-Zasoby JavaScriptu|Witryna" +
"HotSyte zawiera łącza, samouczki, darmowe skrypty i inne"

Elementy rekordu rozdzielone są znakami kreski pionowej (|). Znaki te zostaną użyte w chwili wyświetlania
odszukanych rekordów bazy na ekranie. Drugą część rekordu stanowi tytuł dokumentu (nie mający jednak nic
wspólnego z zawartością znacznika TITLE), część trzecia to opis dokumentu, zaś pierwszy element rekordu to adres
URL.
Na marginesie – nie ma żadnych przeciwwskazań odnośnie rozdzielania elementów rekordu znakami (lub ciągami
znaków) innymi niż „|”. Należy tylko zapewnić, że nie będzie to żaden ze znaków, które użytkownik mógłby wpisać w
treści zapytania (mamy do dyspozycji choćby ciągi &^ czy ~[%). Nie należy także stosować lewego ukośnika (\): znak
ten interpretowany jest przez JavaScript jako początek sekwencji unikowej i jego użycie może spowodować zwrócenie
dziwacznych wyników wyszukiwania lub nawet zawieszenie aplikacji.
Dlaczego wszystkie te dane umieszczono w pliku źródłowym zapisanym w JavaScripcie? Są ku temu dwie przesłanki:
modułowość kodu i czystość zapisu. W przypadku witryn zawierających więcej niż kilkaset pojedynczych stron, plik
rekordów najwygodniej będzie generować z użyciem programu uruchamianego na serwerze; zapisanie danych w
postaci pliku źródłowego w JavaScripcie jest w tym przypadku rozwiązaniem nieco lepszym.
Opisanej tu bazy danych można też użyć w innych aplikacjach wyszukujących, po prostu wstawiając w kodzie
odwołanie do pliku records.js. Co więcej, włączenie całego kodu w JavaScripcie do pliku HTML i wyświetlenie go w
postaci źródłowej byłoby wysoce niepożądane.4

Zmienne globalne
W wierszach 9 do 16 przykładu 1.1 deklarujemy i inicjalizujemy zmienne globalne:
var SEARCHANY = 1;
var SEARCHALL = 2;
var SEARCHURL = 4;
var searchType = '';
var showMatches = 10;
var currentMatch = 0;
var copyArray = new Array();
var docObj = parent.frames[1].document;

4
I jeszcze jedno: jeśli użytkownik wyłączy obsługę JavaScriptu w przeglądarce, będzie bardzo zdziwiony, kiedy pobierając stronę z serwera
stwierdzi, że zawierający ją plik ma ponad 1 MB. Można sądzić, że szybko opuści taką stronę, aby już na nią nie wracać (przyp. tłum.).
26

Techniki języka JavaScript:


użycie separatorów wewnątrz łańcuchowych
do rozdzielania pól rekordów
Opisywana tu aplikacja bazuje na wyszukiwaniu fragmentów informacji, podobnie jak ma to
miejsce w bazie danych. Aby zrealizować podobny schemat wyszukiwania, program w Java-
Scripcie może analizować (przeszukiwać) tablicę jednolicie sformatowanych danych.
Na pierwszy rzut oka mogłoby się wydawać, że wystarczy umieścić każdy element (adres
URL lub tytuł strony) w oddzielnym elemencie tablicy. Rozwiązanie takie będzie
działało, ale może sprawiać sporo kłopotów.
Liczbę elementów tablicy globalnej można znacznie zredukować, łącząc poszczególne ła-
ńcuchy za pomocą separatora (na przykład |) w jeden element. Podczas analizowania po-
szczególnych elementów tablicy używa się następnie metody split() obiektu String w
celu utworzenia oddzielnej tablicy dla każdego z elementów. Innymi słowy, zamiast
globalnej tablicy:
var records = new Array("Czterej pancerni", "pies", "i ich wehikuł")

można wewnątrz funkcji zdefiniować tablicę lokalną, na przykład


var records = "Czterej pancerni|pies|i ich wehikuł".split('|');

Można by pomyśleć: „sześć takich, pół tuzina innych – co za różnica?”. Otóż w pierwszej
wersji deklarujemy trzy elementy globalne, które zajmują pamięć, póki się ich nie poz-
będziemy. W drugim przypadku deklarujemy tylko jeden element globalny. Trzy
elementy tworzone przez funkcję split('|') podczas przeszukiwania są tylko
tymczasowe, gdyż tworzone są lokalnie. Interpreter JavaScriptu likwiduje zmienną
records po wykonaniu funkcji wyszukiwania, zwalniając tym samym pamięć; zmniejsza
się także ilość kodu. Autor preferuje drugą możliwość. Do zagadnienia tego wrócimy
po przyjrzeniu się fragmentowi kodu odpowiedzialnemu za analizę danych.

Oto znaczenie poszczególnych zmiennych:


SEARCHANY
Nakazuje wyszukiwanie dowolnego z wprowadzonych słów.
SEARCHALL
Nakazuje wyszukiwanie wszystkich wprowadzonych słów.
SEARCHURL
Nakazuje wyszukiwanie według adresu URL (dowolnego z wprowadzonych słów).
searchType
Określa sposób szukania (zmienna ta uzyskuje wartość SEARCHANY, SEARCHALL lub SEARCHURL).
showMatches
Określa liczbę rekordów wyświetlanych na jednej stronie wyników.
currentMatch
Identyfikuje rekord wyświetlany jako pierwszy na bieżącej stronie wyników.
copyArray
Przechowuje kopię tymczasowej tablicy wyników, używaną do wyświetlania następnej lub poprzedniej grupy.
docObj
Identyfikuje dokument znajdujący się w drugiej ramce. Nie jest to zbyt istotne dla samej aplikacji, ale pomaga
utrzymać porządek w kodzie, gdyż podczas wyświetlania wyników wyszukiwania trzeba wielokrotnie odwoływać
się do wspomnianego obiektu (parent.frames[1].document). Zastąpienie tego odwołania zmienną docObj
pozwala zmniejszyć ilość kodu i tworzy centralny punkt, w którym dokonuje się wszelkich zmian.

Funkcje
Przyjrzyjmy się teraz najważniejszym funkcjom.
27 Rozdział 1 - Wyszukiwanie danych po stronie klienta

validate()
Kiedy użytkownik naciśnie klawisz Enter, funkcja validate() z wiersza 18 ustala, czego i jak należy szukać.
Pamiętajmy tu o trzech zdefiniowanych wcześniej możliwościach:
• Wyszukiwanie tytułu i opisu dokumentu; wymagane jest dopasowanie dowolnego hasła.
• Wyszukiwanie tytułu i opisu dokumentu; wymagane jest dopasowanie wszystkich haseł.
• Wyszukiwanie adresu URL lub ścieżki dokumentu; wymagane jest dopasowanie dokładnie jednego hasła.
Funkcja validate()określa przedmiot i sposób wyszukiwania na podstawie pierwszych kilku przekazanych jej
znaków. Metodę wyszukiwania ustala się za pomocą zmiennej searchType. Jeśli użytkownik chce wyszukiwać dane
według dowolnego z podanych haseł, zmienna ta ma wartość SEARCHANY. W przypadku wyszukiwania według
wszystkich podanych wyrazów, przyjmuje ona wartość SEARCHALL (jest to zresztą ustawienie domyślne). Jeśli wreszcie
użytkownik wybierze wyszukiwanie według adresów, zmienna searchType przyjmuje wartość SEARCHURL. Cały proces
przebiega następująco:
W wierszu 19 za pomocą metody charAt() obiektu String sprawdzamy, czy pierwszym znakiem napisu jest plus
(+). Jeśli zostanie on odnaleziony, należy użyć drugiej metody wyszukiwania (iloczyn logiczny).
if (entry.charAt(0) == "+") {
entry = entry.substring(1,entry.length);
searchType = SEARCHALL;
}

W wierszu 23 wykorzystujemy metodę substring() obiektu String do wyszukania ciągu url:. W przypadku jego
odnalezienia ustawiana jest odpowiednio zmienna searchType:
if (entry.substring(0,4) == "url:") {
entry = entry.substring(5,entry.length);
searchType = SEARCHURL;
}

A co robi metoda substring() w wierszach 20 i 24? Kiedy funkcja validate() ustali już, jak ma być
wykonywane wyszukiwanie, odpowiednie znaki (+ oraz url:) przestają być potrzebne. Wobec tego validate()
usuwa odpowiednią liczbę znaków z początku łańcucha i kontynuuje działanie.
Jeśli na początku napisu nie ma żadnego z powyższych ciągów, zmiennej searchType nadawana jest wartość
SEARCHANY. Przed wywołaniem funkcji convertString() wykonywane jest jeszcze drobne czyszczenie – instrukcje
while w wierszach 28 i 32 usuwają zbędne odstępy (białe znaki) z początku i końca łańcucha.

Po określeniu sposobu wyszukiwania i usunięciu odstępów należy upewnić się, że zostało jeszcze coś
do wyszukiwania. W wierszu 36 sprawdzamy, czy poszukiwany łańcuch ma przynajmniej trzy znaki. Wyniki
wyszukiwania krótszego łańcucha mogą być mało przydatne, ale ustawienie to można w razie potrzeby zmienić:
if (entry.length < 3) {
alert("Nie możesz wyszukiwać tak krótkich łańcuchów. Wysil się trochę.");
document.forms[0].query.focus();
return;

Jeśli wszystko poszło prawidłowo, validate() wywołuje funkcję convertString(), przekazując jej gotowy
łańcuch zapytania (entry).

convertString()
Funkcja convertString() realizuje dwie związane z sobą operacje: rozdziela łańcuch na elementy tablicy i
wywołuje odpowiednią funkcję wyszukiwania. Metoda split() obiektu String dzieli wprowadzony
przez użytkownika napis w miejscach wystąpienia znaków spacji, a wynik wstawia do tablicy searchArray.
Realizowane jest to w pokazanym niżej wierszu 45:
var searchArray = reentry.split(" ");

Jeśli na przykład użytkownik wpisze w polu wyszukiwania tekst „aplikacje JavaScript klient”, w tablicy searchArray
znajdą się wartości aplikacje, JavaScript i klient (odpowiednio w elementach 0, 1 i 2). Następnie, zależnie
od wartości searchType, funkcja convertString() wywołuje odpowiednią funkcję (wiersze 46 i 47):
if (searchType == (SEARCHALL)) { requireAll(searchArray); }
else { allowAny(searchArray); }

Jak widać, wywoływana jest jedna z dwóch funkcji – allowAny() lub requireAll(). Oba warianty zachowują się
podobnie, ale też nieco się różnią. Omówimy je poniżej.
28

allowAny()
Jak sugeruje sama nazwa (ang. może być dowolny), funkcja ta jest wywoływana w przypadku, gdy aplikacja ma zwrócić
rekordy pasujące do przynajmniej jednego słowa. Oto zawartość wierszy 50–68:
function allowAny(t) {
var findings = new Array(0);
for (i = 0; i < profiles.length; i++) {
var compareElement = profiles[i].toUpperCase();
if(searchType == SEARCHANY) {
var refineElement =
compareElement.substring(0,compareElement.indexOf('|HTTP'));
}
else {
var refineElement =
compareElement.substring(compareElement.indexOf('|HTTP'),
compareElement.length);
}
for (j = 0; j < t.length; j++) {
var compareString = t[j].toUpperCase();
if (refineElement.indexOf(compareString) != -1) {
findings[findings.length] = profiles[i];
break;
}

Obydwie funkcje wyszukujące działają w oparciu o porównywanie napisów w zagnieżdżonych pętlach for. Więcej
informacji na ten temat zamieszczono w ramce Zagnieżdżanie pętli. Pętle for dochodzą do głosu w wierszach 52 i 63.
Pierwsza z nich ma za zadanie przejrzenie wszystkich elementów tablicy profiles (z pliku źródłowego). Dla każdego
elementu tej tablicy druga pętla sprawdza wszystkie elementy zapytania przekazane przez funkcję convertString().
Aby zabezpieczyć się przed pominięciem któregoś z wyszukiwanych rekordów na skutek wpisania haseł z użyciem
różnej wielkości liter, w wierszach 53 i 64 zadeklarowano zmienne lokalne compareElement i compareString,
przypisując im następnie rekord i szukany łańcuch zapisane wielkimi literami. Dzięki temu nie będzie miało znaczenia,
czy użytkownik wpisze słowo „JavaScript”, „javascript”, czy nawet „jAvasCRIpt”.
W funkcji allowAny() nadal trzeba zdecydować, czy przeszukiwać bazę według tytułu i opisu dokumentu, czy
według adresu URL. Wobec tego zmienną lokalną refineElement, zawierającą napis porównywany z szukanymi
słowami, należy ustawić stosownie do wartości searchType (wiersze 55 lub 59). Jeśli searchType ma wartość
SEARCHANY, zmiennej refineElement przypisywany jest fragment tekstu zawierający tytuł i opis dokumentu pobrany
z rekordu. W przeciwnym przypadku searchType musi mieć wartość SEARCHURL, wobec czego wartością refine-
Element staje się tekst zawierający adres URL dokumentu.

Przypomnijmy sobie symbole kreski pionowej, pozwalające programowi na rozdzielenie poszczególnych części
rekordów. Metoda substring() zwraca łańcuch zaczynający się od pozycji zerowej i kończący się przed ciągiem „|
HTTP” lub napis zaczynający się od pierwszego „|HTTP” i ciągnący się aż do końca elementu tablicy. Teraz można
porównywać rekord z danymi wpisanymi przez użytkownika (wiersz 65):
if (refineElement.indexOf(compareString) != -1) {
findings[findings.length] = profiles[i];
break;
}

Znalezienie ciągu compareString w łańcuchu refineElement oznacza trafienie (najwyższy czas!). Pierwotna
zawartość rekordu (zawierająca adres URL) przepisywana jest do tablicy findings w wierszu 66. Przy dopisywaniu
nowych elementów jako indeksu można użyć wartości findings.length.
Po znalezieniu pasującego elementu nie ma już powodu dalej sprawdzać rekordu. W wierszu 67 znajduje się instrukcja
break, która przerywa działanie pętli porównującej for. Nie jest to konieczne, ale zmniejsza ilość pracy, którą trzeba
wykonać.
Po przeszukaniu wszystkich rekordów i znalezieniu wszystkich szukanych słów, w wierszach 95 do 101 funkcja
searchAny() przekazuje znalezione rekordy z tablicy findings do funkcji verifyManage().Sukces wyszukiwania
powoduje wywołanie funkcji formatResults() wyświetlającej dane. W przeciwnym przypadku funkcja
noMatch()informuje użytkownika, że nie udało się znaleźć żądanych przez niego informacji. Funkcje
formatResults() oraz noMatch() zostaną omówione w dalszej części rozdziału. Teraz zakończmy badanie metod
wyszukiwania, omawiając funkcję requireAll().
29 Rozdział 1 - Wyszukiwanie danych po stronie klienta

requireAll()
Jeśli na początku wyszukiwanego łańcucha znajdzie się znak plus (+), wywołana zostanie funkcja requireAll(). Jest
ona niemal identyczna, jak allowAny(), wyszukuje jednak wszystkie wpisane przez użytkownika słowa. W przypadku
allowAny() rekord był dodawany do zbioru wynikowego, gdy tylko dopasowano którykolwiek wyraz; tym razem
trzeba poczekać na porównanie z rekordem wszystkich słów; dopiero wtedy można (ewentualnie) dodać go do zbioru
wynikowego. Całość zaczyna się w wierszu 74.
function requireAll(t) {
var findings = new Array();
for (i = 0; i < profiles.length; i++) {
var allConfirmation = true;
var allString = profiles[i].toUpperCase();
var refineAllString = allString.substring(0,
allString.indexOf('|HTTP'));
for (j = 0; j < t.length; j++) {
var allElement = t[j].toUpperCase();
if (refineAllString.indexOf(allElement) == -1) {
allConfirmation = false;
continue;
}
}
if (allConfirmation) {
findings[findings.length] = profiles[i];
}
}
verifyManage(findings);
}

Na pierwszy rzut oka funkcja ta jest bardzo podobna do allowAny(). Zagnieżdżone pętle for, konwersja wielkości
liter, zmienna potwierdzająca – to wszystko już znamy. Różnica pojawia się w wierszach 79–80:
var refineAllString = allString.substring(0, allString.indexOf('|HTTP'));

Zwróćmy uwagę, że nie sprawdzamy zawartości zmiennej searchType, jak miało to miejsce w funkcji allowAny()
w wierszu 50. Nie ma takiej potrzeby – requireAll() wywoływana jest tylko wtedy, gdy zmienna searchType ma
wartość SEARCHALL (wiersz 46). Wyszukiwanie według adresów URL nie umożliwia użycia iloczynu wszystkich słów,
wiadomo zatem, że porównywać należy tytuł i opis dokumentu.
Funkcja requireAll() jest nieco bardziej wymagająca. Jako że w porównywanym napisie należy znaleźć wszystkie
podane przez użytkownika wyrazy, warunki wyboru będą bardziej wymagające niż w allowAny(). Przyjrzyjmy się
wierszom 83 do 86:
if (refineAllString.indexOf(allElement) == -1) {
allConfirmation = false;
continue;
}

Znacznie łatwiej będzie odrzucić rekord w momencie stwierdzenia pierwszej niezgodności, aniżeli sprawdzać, czy
liczba „trafień” odpowiada liczbie wyszukiwanych haseł. Zatem jeśli tylko rekord nie spełni któregoś z warunków,
instrukcja continue nakaże programowi pominąć go i przejść do analizy następnego rekordu.
Jeżeli po porównaniu z zawartością rekordu wszystkich szukanych słów zmienna allConfirmation ma nadal wartość
true, oznacza to spełnienie kryteriów wyszukiwania. allConfirmation przyjmuje wartość false, jeśli rekord
nie pasuje do któregokolwiek z szukanych wyrazów. Rekord bieżący dodawany jest do zbioru wynikowego zawartego
w tymczasowej tablicy findings (wiersz 89). Tym razem trudniej jest spełnić warunek, ale wyniki będą zapewne
dokładniejsze.
Po sprawdzeniu w ten sposób wszystkich rekordów, wartość zmiennej findings przekazywana jest do funkcji
verifyManage(). Jeśli stwierdzono jakieś trafienia, wywoływana jest funkcja formatResults(). W przeciwnym
przypadku verifyManage() wywołuje funkcję noMatch(), przekazującą użytkownikowi złe wieści.
30

Techniki języka JavaScript: zagnieżdżanie pętli


Obie funkcje wyszukujące – allowAny() i requireAll() – używają zagnieżdżonych
pętli for. Jest to wygodna technika obsługi tablic wielowymiarowych. W języku Java-
Script tablice są for malnie rzecz biorąc jednowymiarowe, ale możliwe jest też symulowanie
tablicy wielowymiarowej, jak opisano poniżej. Przyjrzyjmy się pięcioelementowej, jednowy-
miarowej tablicy:
var liczby = ("jeden", "dwa", "trzy", "cztery", "pięć");

Aby porównać łańcuch z kolejnymi wartościami, wystarczy wykonać pętlę for (lub
while) porównując kolejne elementy tablicy z zadanym tekstem:
for (var i = 0; i < liczby.length; i++) {
if (myString == liczby[i]) { alert("Znalazłem!");
break; }
}

Nic trudnego, możemy zatem podjąć następne wyzwanie. Tablica wielowymiarowa to po


prostu tablica tablic, na przykład:
var liczby = new Array {
new Array("jeden", "dwa", "trzy", "cztery", "pięć"),
new Array("uno", "dos", "tres", "cuatro", "cinco"),
new Array("won", "too", "tree", "for", "fife")
);

Pojedyncza pętla for tutaj nie wystarczy – trzeba się bardziej przyłożyć. Pierwsza tablica
liczby jest jednowymiarowa (1×5), ale jej nowa wersja jest już tablicą wielowymiarową
(3×5). Przeanalizowanie piętnastu elementów (3×5) oznacza, że będziemy potrzebować
dodatkowej pętli:
for (var i = 0; i < liczby.length; i++) { // pierwsza...
for (var j = 0; j < liczby[i].length; j++) { // i druga
if (myString == liczby[i][j]) {
alert("Znalazłem!");
break;
}
}
}

Tym razem kolejne odpowiedzi sprawdzamy w dwóch wymiarach. Pójdźmy teraz jeszcze
o krok dalej i załóżmy, że chcemy stworzyć tablicę z „bezpieczną” paletą 216 kolorów,
których można używać we wszystkich przeglądarkach, po jednym kolorze w komórce?
Odpowiedzią są zagnieżdżone pętle for. Tym razem jednak użyjemy tablicy zaledwie
jednowymiarowej.
W notacji szesnastkowej „bezpieczne” kolory zapisywane są w postaci sześciu cyfr (po
dwie na każdą barwę składową) przy czym wszystkie składniki muszą być parami cyfr:
33, 66, 99, AA, CC lub FF. Tablica będzie zatem wyglądała tak:
var hexPairs = new Array("33", "66", "99", "AA", "CC", "FF");

„Lipa! Tu jest tylko jedna tablica jednowymiarowa – oddawać pieniądze!”

Spokojnie, nie ma co jeszcze biec do księgarni. Będą trzy wymiary, tyle że dla każdego
użyjemy tej samej tablicy:
var str = '';

// Utworzenie tablicy
document.writeln('<H2>Bezpieczna paleta WWW</H2>' +
'<TABLE BORDER=1 CELLSPACING=0>');
for (var i = 0; i < hexPairs.length; i++) {
// tworzenie wiersza
document.writeln('<TR>');
for (var j = 0; j < hexPairs.length; j++) {
for (var k = 0; k < hexPairs.length; k++) {
// Tworzenie ciągu "pustych" komórek wiersza
// Zauważmy, że kolor tła jest tworzony z elementów hexPairs
str += '<TD BGCOLOR="' + hexPairs[i] + hexPairs[j] + hexPairs[k] +
'">' + '&nbsp;&nbsp;&nbsp</TD>';
}
// Wypisz wiersz komórek i "wyzeruj" str
31 Rozdział 1 - Wyszukiwanie danych po stronie klienta

verifyManage()
Jak można się było domyślać, funkcja ta określa, czy wyszukiwanie dało jakieś rezultaty i wywołuje jedną z
wyprowadzających je funkcji. Zaczynamy od wiersza 95:
function verifyManage(resultSet) {
if (resultSet.length == 0) { noMatch(); }
else {
copyArray = resultSet.sort();
formatResults(copyArray, currentMatch, showMatches);
}
}

Zarówno allowAny(), jak i requireAll() wywołują funkcję verifyManage() po zrealizowaniu odpowiedniego


schematu wyszukiwania, przekazując jej tablicę findings jako argument. W wierszu 96 pokazano wywołanie funkcji
noMatch() w przypadku, gdy tablica resultSet (kopia findings) jest pusta.
Jeśli resultSet zawiera co najmniej jeden rekord pasujący do szukanych haseł, globalnej zmiennej copyArray
nadawana jest wartość stanowiąca alfabetycznie uporządkowaną wersję zbioru wszystkich elementów tablicy
resultSet. Sortowanie nie jest konieczne, ale wydatnie pomaga w uporządkowaniu zbioru wynikowego, a ponadto
zwalnia od troszczenia się o kolejność dodawania nowych rekordów do tablicy profiles. Można je dodawać zawsze
na końcu – i tak zostaną w końcu posortowane, jeśli tylko będą wybrane.
Po co zatem tworzyć od nowa zestaw danych, które i tak już mamy? Pamiętajmy, że findings jest zmienną lokalną, a
więc tymczasową. Po zakończeniu wyszukiwania (czyli wykonaniu jednej z funkcji wyszukujących), tablica findings
zniknie. I bardzo dobrze: po co trzymać wszystko w pamięci, która może nam się przydać do innych celów? Dane
trzeba jednak gdzieś przechować.
Jako że przeglądarka wyświetla, dajmy na to, 10 rekordów na stronie, użytkownik widzieć będzie jedynie część
wyników. Zmienna copyArray jest globalna, więc sortowanie danych i wpisywanie ich do niej nie zaszkodzi
zawartości zbioru wynikowego. Użytkownik może oglądać wyniki w grupach po 10, 15 i tak dalej; zmienna
copyArray będzie przechowywała wszystkie znalezione rekordy aż do chwili wykonania nowego zapytania.

Ostatnią czynnością funkcji verifyManage() jest wywołanie formatResults() i przekazanie jej wartości
currentMatch, będącej indeksem rekordu, który ma być wyświetlony jako pierwszy, oraz wartości showMatches,
określającej liczbę wyświetlanych na stronie rekordów. Zarówno currentMatch, jak i showMatches to zmienne
globalne, które nie znikają po wykonaniu funkcji. Będziemy potrzebować ich podczas całego czasu pracy aplikacji.

noMatch()
Funkcja noMatch() (ang. brak dopasowania) robi to, co sugeruje jej nazwa. Jeśli zapytanie nie zwróci żadnych
wyników, ma ona przekazać użytkownikowi złe wieści. Jest krótka i prosta, zaś generowana przez nią strona wyników
(a raczej braku wyników) informuje, że wpisane przez użytkownika hasła nie odpowiadają żadnemu z rekordów bazy.
Zaczyna się od wiersza 103:
function noMatch() {
docObj.open();
docObj.writeln('<HTML><HEAD><TITLE>Wyniki wyszukiwania</TITLE></HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER><TR><TD VALIGN=TOP>' +
'<FONT FACE=Arial><B><DL>' +
'<HR NOSHADE WIDTH=100%>"' + document.forms[0].query.value +
'" - nic nie znaleziono.<HR NOSHADE WIDTH=100%>' +
'</TD></TR></TABLE></BODY></HTML>');
docObj.close();
document.forms[0].query.select();
}

formatResults()
Zadaniem tej funkcji jest eleganckie zaprezentowanie użytkownikowi znalezionych wyników. Nie jest ona nadmiernie
skomplikowana, ale realizuje sporo zadań. Oto elementy składające się na listę wyników:
• Nagłówek, tytuł i treść dokumentu HTML.
• Tytuł znalezionego dokumentu, jego opis oraz adres URL dla każdego znalezionego rekordu, wraz z łączem
do zawartego w rekordzie adresu URL.
32

• Przyciski „Poprzedni” i „Następny”, służące do wyświetlania rekordów poprzednich i następnych, o ile takowe
istnieją.

Nagłówek i tytuł dokumentu HTML


Utworzenie nagłówka i tytułu strony jest proste. Wiersze 116 do 129 drukują nagłówek, tytuł i początek treści
dokumentu HTML:
function formatResults(results, reference, offset) {
var currentRecord = (results.length < reference + offset ?
results.length : reference + offset);
docObj.open();
docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki wyszukiwania</TITLE>\n </HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' +
'<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' +
'<FONT FACE=Arial><B>Zapytanie: <I>' +
parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
'Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ' +
currentRecord + ' z ' + results.length + '</I><BR><BR></FONT>' +
'<FONT FACE=Arial SIZE=-1><B>' +
'\n\n<!-- początek zbioru wynikowego //-->\n\n\t<DL>');

Przed wydrukowaniem nagłówka i tytułu trzeba sprawdzić, od którego rekordu należy zacząć wyprowadzanie.
Wiadomo, że pierwszy wyświetlany rekord znajduje się w elemencie results [reference]. Należy wyświetlić
offset rekordów, chyba że wartość reference + offset jest większa niż liczba rekordów. Aby to sprawdzić, znów
używamy operatora trójargumentowego, zaś odpowiednią wartość umieszczamy w zmiennej currentRecord w
wierszu 117. Użyjemy jej wkrótce.
Teraz funkcja formatResults() wyprowadza nagłówek i tytuł dokumentu HTML. Treść dokumentu zaczyna się od
wyśrodkowanej tabeli i poziomej linii. Wyświetlenie treści zapytania, pobranej z odpowiedniego pola formularza, nie
jest żadnym problemem (wiersz 125):
parent.frames[0].document.forms[0].query.value

W wierszu 126 rzecz się trochę komplikuje, zaczyna się bowiem zbiór wynikowy. Najpierw wyświetlana jest
informacja o prezentowanym właśnie podzbiorze – jego wielkość i liczba wszystkich wyników, na przykład:
Wyniki wyszukiwania: 1 - 10 z 38

Potrzeba do tego trzech liczb: numeru pierwszego rekordu podzbioru, liczby wyświetlanych jednorazowo rekordów oraz
rozmiaru tablicy copyArray, w której znajdują się wybrane rekordy. Przyjrzyjmy się im po kolei, pamiętając, że
nie chodzi tutaj o pokazanie samych wyników, ale o poinformowanie użytkownika, ile jest rekordów i od którego
zaczynamy. Dzieje się to tak:
1.Numer bieżącego rekordu przypisujemy zmiennej reference i wyświetlamy ją.
2.Dodajemy do reference wartość offset, określającą liczbę rekordów wyświetlanych na stronie (tutaj 10).
3.Jeśli suma reference + offset jest większa niż liczba znalezionych odpowiedzi, wyświetlamy liczbę
odpowiedzi, w przeciwnym przypadku – obliczoną sumę (wartość ta została już określona i znalazła odbicie
w zmiennej currentRecord).
4.Wyświetlamy łączną liczbę wyników.
Kroki 1 i 2 są proste. Przypomnijmy sobie kod funkcji verifyManage(), szczególnie wiersz 99:
formatResults(copyArray, currentMatch, showMatches);

Zmienna lokalna results to kopia tablicy copyArray. Zmienna reference otrzymuje wartość currentMatch,
więc suma reference + offset jest równa sumie currentMatch i showResults. W pierwszych kilku wierszach
kodu (ściśle – 13 i 14) zmiennej showMatches nadajemy wartość 10, a zmiennej currentMatch wartość 0. Wobec
tego reference ma na początku wartość 0, zaś suma reference + offset równa jest 10. Krok 1 wykonywany jest
bezpośrednio po wyprowadzeniu zmiennej reference; opisana wyżej arytmetyka obsługuje krok 2.
W kroku 3 używamy operatora trójargumentowego (wiersze 117–118) do zdecydowania, czy suma wartości
reference i offset jest większa od całkowitej liczby wyników. Innymi słowy, należy sprawdzić, czy po dodaniu
offset rekordów do reference uzyskamy liczbę większą od całkowitej liczby wyników. Jeśli reference ma
wartość 20, a rekordów jest 38, dodanie wartości 10 do reference da 30 i na ekranie pojawi się:
Wyniki wyszukiwania: 20 - 30 z 38

Jeśli jednak reference ma wartość 30, a rekordów jest 38, dodanie do reference wartości 10 da 40, co dałoby:
33 Rozdział 1 - Wyszukiwanie danych po stronie klienta

Wyniki wyszukiwania: 30 - 40 z 38

a to jest niedopuszczalne. Wyszukiwarka nie może wyświetlić rekordów 39 i 40, bo znalazła ich tylko 38. Oznacza to
osiągnięcie końca zbioru wynikowego, a zatem zamiast sumy należy wyświetlić całkowitą liczbę rekordów. W ten
sposób dochodzimy do ostatniego, czwartego kroku:
Wyniki wyszukiwania: 30 - 38 z 38

Treść funkcji formatResults() upstrzona jest znakami specjalnymi, jak np. \n i \t. \n to
znak nowego wiersza, czyli odpowiednik naciśnięcia klawisza Enter podczas pisania
w edytorze tekstów. \t to odpowiednik naciśnięcia klawisza tabulacji. Obecność
znaków specjalnych ma poprawić wygląd kodu źródłowego strony wyników
wyszukiwania. Wstawiono je tu po to, aby pokazać sposób ich użycia, ale należy
pamiętać, że nie są one niezbędne dla programów i że nie mają wpływu na ich
działanie. Jeśli ktoś uważa, że zaśmiecają kod, wcale nie musi ich używać. W dalszej
części książki będą one stosowane sporadycznie.

Wyświetlanie tytułów, opisów i adresów URL dokumentów


Teraz, gdy podzbiór rekordów został już określony, czas go wyświetlić. Do głosu dochodzą wiersze 130 do 143:
if (searchType == SEARCHURL) {
for (var i = reference; i < currentRecord; i++) {
var divide = results[i].split("|");
docObj.writeln('\t<DT>' + '<A HREF="' + divide[2] + '">' +
divide[2] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>\n\n');
}
}
else {
for (var i = reference; i < currentRecord; i++) {
var divide = results[i].split('|');
docObj.writeln('\n\n\t<DT>' + '<A HREF="' + divide[2] + '">' +
divide[0] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>');
}
}

Wiersze 131 i 138 zawierają pętle for, wykonujące na rekordzie currentRecord tę samą operację – jedyną różnicą
jest kolejność wyprowadzania poszczególnych fragmentów. Do głosu ponownie dochodzi zmienna searchType. Jeśli
jest ona równa SEARCHURL, jako tekst łącza wyświetlany jest adres URL. W przeciwnym razie, tj. dla wartości
SEARCHANY lub SEARCHALL – tytuł dokumentu.

Sposób wyszukiwania jest już określony, ale jak ładnie wyświetlić rekordy? Wystarczy przebiec w pętli przez cały
podzbiór rekordów, rozbijając każdy z nich na tytuł, opis oraz adres URL i rozmieszczając uzyskane elementy w
odpowiednim porządku. Oto pętla for używana dla wszystkich przypadków:
for (var i = reference; i < lastRecord; i++) {

Teraz zajmijmy się elementami rekordów. Jak pamiętamy z opisu pliku records.js, każdy element tablicy profiles to
łańcuch opisujący rekord i składający się z części rozdzielonych znakami |. Rozdzielimy je tak:
var divide = results[i].split('|');

Dla każdego elementu zmiennej lokalnej divide przypisywana jest tablica łańcuchów rozdzielonych znakami |.
Pierwszy jej element (divide[0]) to adres URL, drugi (divide[1]) to tytuł dokumentu, trzeci zaś (divide[2]) to
jego opis. Każdy z elementów jest podczas wyprowadzania uzupełniany odpowiednim zestawem znaczników HTML
(w naszym przypadku <DL>, <DT> i <DD>). Jeśli wyszukiwanie odbywa się według adresów URL, będą one stanowiły
treść łączy; w przeciwnym przypadku użyte zostaną tytuły dokumentów.

Dodanie przycisków „Poprzedni” i „Następny”


Ostatnią czynnością jest dodanie przycisków, które pozwolą użytkownikowi oglądać poprzednią i następną porcję
wyników. Obsługą tych przycisków zajmuje się funkcja prevNextResults(), którą wkrótce omówimy, ale teraz
kilka ostatnich wierszy funkcji formatResults():
docObj.writeln('\n\t</DL>\n\n<!-- Koniec wyników //-->\n\n');
prevNextResults(results.length, reference, offset);
docObj.writeln('<HR NOSHADE WIDTH=100%>' +
'</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
docObj.close();
document.forms[0].query.select();
}
34

Zacytowany tu kod wywołuje funkcję prevNextResults(), dodaje kilka zamykających znaczników HTML i ustawia
kursor w polu treści zapytania.

prevNextResults()
Wszystkich, którym udało dobrnąć się aż tutaj, i ta funkcja nie powinna zanadto zmęczyć. Definicja funkcji
prevNextResults() zaczyna się w wierszu 152:
function prevNextResults(ceiling, reference, offset) {
docObj.writeln('<CENTER><FORM>');
if(reference > 0) {
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Poprzednie ' + offset +
' wyników" ' +
'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference - offset) + ', ' + offset + ')">');
}
if(reference >= 0 && reference + offset < ceiling) {
var trueTop = ((ceiling - (offset + reference) < offset) ?
ceiling - (reference + offset) : offset);
var howMany = (trueTop > 1 ? "ów" : "");
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Następne ' + trueTop +
' wynik' + howMany + '" ' +
'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference + offset) + ', ' + offset + ')">');
}
docObj.writeln('</CENTER>');
}

Funkcja ta wyświetla HTML-owy formularz zawierający dwa przyciski – „Następne” i „Poprzednie” – i wyśrodkowany
w dolnej części strony wyników. Na rysunku 1.3 pokazano stronę wyników z obydwoma przyciskami. Mogą one
pojawić się w trzech kombinacjach:
• Tylko przycisk „Następne”. Jest on wyświetlany dla pierwszego podzbioru wyników; nie ma jeszcze żadnych
poprzednich rekordów.
• Przyciski „Następne” i „Poprzednie”. Wyświetlane są one dla wszystkich stron poza pierwszą i ostatnią;
istnieją rekordy poprzednie, są też rekordy dalsze.
• Tylko przycisk „Poprzednie”. Pojawia się na ostatniej stronie wyników – dalej nie ma już żadnych rekordów
do przeglądania.
Trzy kombinacje, dwa przyciski. Oznacza to, że aplikacja musi wiedzieć, czy dany przycisk ma być wyświetlany, czy
też nie. Poniżej opisano warunki określające pojawianie się poszczególnych kombinacji:

Tylko przycisk „Następne”


Kiedy powinien pojawić się przycisk „Następne”? Na wszystkich stronach poza ostatnią, czyli zawsze wtedy, gdy
ostatni rekord na stronie (reference + offset) ma numer mniejszy od całkowitej liczby wyników.
Kiedy nie powinien pojawić się przycisk „Poprzednie”? Na pierwszej stronie wyników, czyli kiedy wartość
reference uzyskana ze zmiennej currentMatch wynosi 0.

Przyciski „Następne” i „Poprzednie”


Kiedy należy wyświetlić oba przyciski? Skoro przycisk „Następne” powinien znajdować się na wszystkich stronach
poza ostatnią, a „Poprzednie” na wszystkich poza pierwszą, przycisku „Poprzednie” będziemy potrzebować, kiedy
wartość reference będzie większa od 0, a przycisku „Następne” – gdy suma reference + offset będzie mniejsza
od całkowitej liczby wyników.

Tylko przycisk „Poprzednie”


Skoro wiemy, kiedy powinien pojawić się przycisk „Poprzednie”, to kiedy powinien zniknąć „Następne”? Kiedy
wyświetlana jest ostatnia strona wyników, czyli gdy suma reference + offset jest większa bądź równa całkowitej
liczbie wyników.
Podany tu opis jest nadal dość ogólny, ale przynajmniej wiadomo już, kiedy i które przyciski mają być wyświetlane, zaś
obsługą warunków zajmują się instrukcje if z wierszy 154 i 160. Umieszczają one na stronie jeden lub oba przyciski
w zależności od aktualnego podzbioru wynikowego i liczby pozostałych do wyprowadzenia wyników.
35 Rozdział 1 - Wyszukiwanie danych po stronie klienta

Techniki języka JavaScript:


rozsądne użycie metody document.write()
Przyjrzyj się jeszcze raz funkcji formatResults(). Kod w języku HTML zapisywany
jest przez wywołanie metody document.write() lub document.writeln(). Przeka-
zywany jej tekst jest zwykle długi i rozciąga się na kolejne wiersze, połączone operatorem
+. Można by się spierać, czy taki kod jest czytelniejszy niż wywoływanie dla każdego
wiersza metody document.writeln(), istnieje jednak ważki argument przemawiający
na korzyść pierwszego sposobu. O co chodzi? Przyjrzyjmy się fragmentowi funkcji for-
matResults():
function formatResults(results, reference, offset) {
docObj.open();
docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki
wyszukiwania</TITLE>\n</HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' +
'<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' +
'<FONT FACE=Arial><B>Zapytanie: <I>' +
parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
'Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ' +
currentRecord + ' z ' + results.length + '</I><BR><BR></FONT>' +
'<FONT FACE=Arial SIZE=-1><B>' +
'\n\n<!-- początek zbioru wynikowego //-->\n\n\t<DL>');

Jak widać, za pomocą pojedynczego wywołania metody zapisujemy tekst całej strony.
Alternatywą jest wielokrotne wywoływanie metody w celu wyprowadzenia pojedynczych
wierszy:
function formatResults(results, reference, offset) {
docObj.open();
docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki
wyszukiwania</TITLE>\n</HEAD>');
docObj.writeln('<BODY BGCOLOR=WHITE TEXT=BLACK>');
docObj.writeln('<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER ' +
'CELLPADDING=3><TR><TD>');
docObj.writeln('<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>');
docObj.writeln('<FONT FACE=Arial><B>Zapytanie: <I>' +
parent.frames[0].document.forms[0].query.value + '</I><BR>\n');
docObj.writeln('Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ');
docObj.writeln( (reference + offset > results.length ?
results.length : reference + offset) +
' z ' + results.length + '</I><BR><BR></FONT>' +
'<FONT FACE=Arial SIZE=-1><B>');
docObj.writeln('\n\n<!-- początek zbioru wynikowego //-->\n\n\t<DL>');

Wygląda to może trochę porządniej, ale każde wywołanie oznacza nieco więcej pracy dla
interpretera JavaScriptu. Pomyślmy: czy wygodniej jest pięć razy iść do sklepu, za każ-
dym razem kupując jakiś drobiazg, czy iść do sklepu raz i od razu kupić wszystko, czego
nam trzeba? Przekazanie długiego łańcucha „sklejonego” znakami + rozwiązuje problem
skutecznie i za jednym zamachem.

W wyniku kliknięcia obu przycisków wywoływana jest funkcja formatResults(); jedyna różnica to przekazywane
jej parametry, opisujące różne podzbiory wyników. Z technicznego punktu widzenia przyciski są w gruncie rzeczy takie
same, różnią się natomiast wyglądem ze względu na różnice w wartościach atrybutu VALUE. Oto początek definicji przycisku
„Poprzednie” (wiersze 155-156):
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Poprzednie ' + offset + ' Wyników" ' +

Z kolei przycisk „Następne” definiowany jest w wierszach 164–165:


docObj.writeln('<INPUT TYPE=BUTTON VALUE="Następne ' + trueTop + ' wyników' + howMany

Oba wiersze zawierają atrybuty TYPE i VALUE przycisku oraz wartość określającą liczbę rekordów poprzedzających
lub następujących po danym. Jako że liczba rekordów poprzedzających jest zawsze taka sama (wynosi offset), jest
ona wyświetlana zawsze (na przykład „Następne 10 wyników”). Jednak liczba rekordów następujących po danym może
36

się zmieniać – może ona być równa wartości offset lub liczbie rekordów pozostałych do wyprowadzenia, mniejszej
od offset. Aby obsłużyć te możliwości, używamy zmiennej trueTop.
Zwróćmy uwagę, że przycisk „Poprzednie” zawsze zawiera słowo „wyników”. Wartość zmiennej showMatches jest
stała przez cały czas pracy aplikacji i w naszym przypadku wynosi 10. Użytkownik może zawsze liczyć, że
zobaczy poprzednich 10 rekordów. Jednak w przypadku rekordów następnych nie zawsze musi tak być. Załóżmy, że
następny podzbiór wyników zawiera tylko jeden rekord – wtedy przycisk zawierałby tekst „Następne 1 wyników”. Jest
to niedopuszczalne i dlatego właśnie w funkcji prevNextResults() używamy lokalnej zmiennej howMany wraz
z operatorem trójargumentowym (wiersz 163):
var howMany = (trueTop > 1 ? "ów" : "");

Jeśli zmienna trueTop jest większa od 1, howMany przyjmuje wartość „ów”. Jeśli trueTop równa jest 1, howMany
zawiera łańcuch pusty. Jak widać w wierszu 165, zawartość zmiennej howMany jest wyprowadzana zaraz za słowem
„wynik”. Jeśli zatem do pokazania pozostał jeden rekord, użyte zostanie słowo „wynik”; jeśli jest ich więcej, ujrzymy
słowo „wyników”.5
Ostatni krok to określenie akcji wykonywanej w chwili kliknięcia przycisków. Jak już wcześniej wspomniano, zdarzenia
onClick obu przycisków obsługiwane są przez funkcję formatResults(). Jej wywołanie konstruowane jest
dynamicznie w wierszach 157–158 i 166–167. Oto pierwsze z wywołań:
'onClick="' + parent.frames[0].formatResults(parent.frames[0].copyArray, ' + (reference -
offset) + ', ' + offset + ')">');

Argumenty określane są przy użyciu operatora trójargumentowego i wypisywane „na bieżąco”. Jak widać, przycisk
„Poprzednie” zawsze przekazuje funkcji trzy argumenty: copyArray, reference – offset oraz offset. Warto też
zwrócić uwagę na zapis odwołań do funkcji formatResults() i zmiennej copyArray
parent.frames[0].formatResults(...);

oraz
parent.frames[0].copyArray

W pierwszej chwili może to się wydawać nieco dziwne, ale należy pamiętać, że odwołanie do funkcji
formatResults() nie znajduje się w dokumencie nav.html (czyli w ramce parent. frames[0]). Ma ono miejsce
w ramce wyników, parent.frames[1], która nie zawiera funkcji formatResults() ani zmiennej copyArray. Stąd
właśnie bierze się taka, a nie inna forma odwołań.
Przycisk „Następne” wykorzystuje podobną procedurę obsługi zdarzenia, ale... czy nie należałoby przypadkiem
uwzględnić faktu, że w ostatnim podzbiorze może znajdować się mniej niż offset wyników (tak jak miało to miejsce
podczas wywołania formatResults() w celu wyświetlenia wyników)? Otóż nie. To w funkcji formatResults()
podejmuje się stosowną decyzję, zatem wystarczy dodać do siebie wartości reference i offset, a sumę przekazać do
funkcji. Przyjrzyjmy się wierszom 166–167, zawierającym ostatni fragment wywołania metody document. writeln():
'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference + offset) + ', ' + offset + ')">');

5
W języku polskim, jak już Czytelnicy zapewne zauważyli, rzecz nie jest taka prosta. Po pierwsze, dla liczb 2, 3 i 4 należy użyć formy „wyniki”,
nie „wyników”. Po drugie, jeden wynik jest „następny”, nie „następne”. Stosowne poprawki proponujemy wykonać w ramach ćwiczenia (przyp.
tłum.).
37 Rozdział 1 - Wyszukiwanie danych po stronie klienta

Techniki języka JavaScript: operator trójargumentowy


Po ostatniej porcji materiału można się już było tego spodziewać. Operator trójargumen-
towy jest całkiem przydatny, a zatem czas na nieco teorii. Operatory te, używane w naszej
aplikacji jako jednowierszowe odpowiedniki instrukcji warunkowej if-else, wymagają
trzech argumentów. Składnia operatora trójargumentowego, cytowana z opublikowanego
przez firmę Netscape Communications dokumentu JavaScript Guide for Communicator
4.0 (rozdział 9) wygląda następująco:
(warunek) ? wartość1 : wartość2

Po zastąpieniu parametrów odpowiednimi wartościami, operator taki spowoduje zwróce-


nie wartości1 w przypadku prawdziwości warunku i wartości2 w przypadku jego fał-
szywości. Po co jednak ten cały wykład? Otóż omawianą tu konstrukcję niejednokrotnie
łatwiej jest czytać, niż odpowiednie instrukcje if-else (mniej też trzeba pisać). Operator
ten jest też szczególnie użyteczny, kiedy trzeba zakodować kilka zagnieżdżonych wyrażeń.
Operator trójargumentowy nie jest jednak żadnym panaceum. Jeśli w przypadku spełnie-
nia lub niespełnienia warunku należy wykonać kilka operacji, trzeba będzie odwołać się
do konstrukcji if-else. Jeśli nie – warto spróbować operatora trójargumentowego.

Kod HTML
W pliku nav.html jest bardzo niewiele statycznego kodu HTML. Cytujemy go ponownie, zaczynając od wiersza 174:
</HEAD>
<BODY BGCOLOR="WHITE">
<TABLE WIDTH="95%" BORDER="0" ALIGN="CENTER">
<TR>
<TD VALIGN=MIDDLE>
<FONT FACE="Arial">
<B>Wyszukiwarka pracująca po stronie klienta</B>
</TD>

<TD VALIGN=ABSMIDDLE>
<FORM NAME="search"
onsubmit="validate(document.forms[0].query.value); return false;">
<INPUT TYPE=TEXT NAME="query" SIZE="33">
<INPUT TYPE=HIDDEN NAME="standin" VALUE="">
</FORM>
</TD>

<TD VALIGN=ABSMIDDLE>
<FONT FACE="Arial">
<B><A HREF="main.html" TARGET="main">Pomoc</A></B>
</TD>
</TR>
</TABLE>
</BODY>
</HTML>

Nie ma tu żadnych niespodzianek. Formularz rozmieszczony został w komórkach tabeli, zaś jego przesłanie uruchamia
omawiany uprzednio kod. Można by co najwyżej zapytać, jak przesyłać dane, skoro nie ma przycisku SUBMIT?
Poczynając od specyfikacji HTML 2.0, większość przeglądarek (w tym Netscape Navigator i Internet Explorer)
umożliwia uproszczone wysłanie formularza za pomocą pojedynczego pola tekstowego.
Oczywiście wcale nie trzeba postępować właśnie w taki sposób – równie dobrze można uatrakcyjnić formularz,
wyposażając go w przycisk lub obrazek (przycisk graficzny).

Tworzenie bazy danych w języku JavaScript


Zawartość opisanej tu przykładowej bazy danych trzeba będzie prędzej czy później zastąpić własnymi danymi. Robi się
to w trzech prostych etapach:
1.Otwórz plik records.js w edytorze tekstów.
38

2.Usuń znajdujące się tam rekordy tak, by uzyskać następującą zawartość pliku:
var profiles = new Array(
);

3.Dodaj kolejne rekordy według poniższego wzoru:


"Tytuł strony|Opis strony|http://adres.url/plik.html",

Pomiędzy nawiasami można zapisać dowolną liczbę rekordów, pamiętając, by na końcu każdego poza ostatnim
umieścić przecinek. Należy też pamiętać o rozdzielaniu tytułów stron, ich opisów i adresów URL znakami kreski
pionowej. Znaków tych nie wolno używać w danych, gdyż spowoduje to błędy interpretera JavaScriptu. Na koniec nie
należy zapominać, że w przypadku konieczności użycia wewnątrz danych znaku cudzysłowu, należy zacytować go w formie
sekwencji unikowej (tj. \" zamiast samego ").

Kierunki rozwoju
Nasza wyszukiwarka już w opisanej tu postaci jest całkiem użyteczna, jednak w zmianach i udoskonaleniach można
pójść znacznie dalej. Oto wybrane możliwości:
• zapewnienie zgodności z językiem JavaScript 1.0,
• uodpornienie programu na błędy,
• wyświetlanie reklam,
• rozszerzenie możliwości wyszukiwania,
• użycie zapytań predefiniowanych.

Zgodność z językiem JavaScript 1.0


Wszyscy to wiemy: obie najpopularniejsze wśród użytkowników przeglądarki dostępne są obecnie (tj. w chwili
wydawania tej książki – przyp. red.) w późnych wersjach 4.x lub wczesnych 5.x. Obie są bezpłatne. Nadal jednak
trafiają się użytkownicy korzystający z takich staroci, jak Internet Explorer 3.02 czy Netscape Navigator 2. Statystyka
odwiedzin prowadzonej przez autora witryny HotSyte (http://www.serve.com/hotsyte/) wciąż wskazuje na to, że wersje te
są zaskakująco popularne.
Jako że wyszukiwarka jest jedną z najważniejszych usług dostępnych w witrynie, warto pokusić się o zakodowanie jej
w sposób zgodny ze specyfikacją JavaScript 1.0. Na szczęście jest to dość proste – wystarczy przejrzeć omawiany kod
wiersz po wierszu, sprawdzić, które elementy nie są w wersji 1.0 obsługiwane i wymienić je.
OK. Autor już to zrobił, ale zapewne większość czytelników kusiło, by zabrać się za to samemu (prawda?).
Zmodyfikowaną wersję kodu można znaleźć w katalogu /ch01/searchengineMSIE/. Jak miało to miejsce poprzednio,
otwórzmy w przeglądarce plik index.html i przyjrzyjmy się szybko zmianom zapewniającym zgodność aplikacji z
JavaScriptem 1.0. Jest ich trzy:
• brak pliku źródłowego skryptu (problem ten jest specyficzny dla przeglądarki),
• brak sortowania tablicy (metodą sort()),
• „obejście” braku metody split().
Netscape Navigator 2.x i Microsoft Internet Explorer 3.x nie obsługują plików .js.6 Można sobie z tym poradzić, włączając
tablicę profiles do pliku nav.html. Druga zmiana polega na rezygnacji z wywołania resultSet.sort() w wierszu
90. Oznacza to, że dane nie zostaną posortowane alfabetycznie, lecz pozostaną w kolejności, w jakiej zapisano rekordy
w tablicy profiles. Trzecia modyfikacja to usunięcie nieobsługiwanej przez JavaScript 1.0 metody split().
Zastosowane rozwiązanie jest skuteczne, ale niestety pogarsza wydajność kodu.

NICTJDO
Hasło to napisał na tablicy profesor ekonomii, kiedy autor był studentem pierwszego roku na Uniwersytecie Stanu
Floryda. Rozwinięcie tego akronimu to „Nie Istnieje Coś Takiego Jak Darmowy Obiad”. W naszym przypadku znaczy
to, że dokonanie zmian pozwoli na korzystanie z wyszukiwarki w starszych przeglądarkach, ale odbędzie się to kosztem
możliwości funkcjonalnych i struktury kodu.

6
Nie do końca; niektóre odmiany przeglądarki MSIE 3.02 już to robią.
39 Rozdział 1 - Wyszukiwanie danych po stronie klienta

Bez plików .js trzeba będzie upchnąć tablicę profiles w pliku nav.html. Rozwiązanie to jest mało eleganckie, a jeszcze
mniej wygodne, jeśli okaże się, że bazę trzeba będzie wykorzystać gdzie indziej.
Metoda sort() nie jest dla aplikacji niezbędna, ale okazuje się bardzo przydatna. Brak uporządkowania zbioru
wynikowego może zmusić użytkownika do żmudnego przeglądania wszystkich odpowiedzi. Co prawda można by
zapisywać rekordy w tablicy alfabetycznie, ale i to nie jest raczej wygodnym rozwiązaniem. Sortowanie dla JavaScriptu
w wersji 1.0 można również zaprogramować samodzielnie. Brak metody split() jest zdecydowanie najmniejszym
problemem. Wersja aplikacji przeznaczona dla JavaScriptu 1.0 zawiera zresztą rozwiązanie, więc nie ma co rozpaczać.

Odporność na błędy
W obecnej postaci aplikacja umożliwia użytkownikowi umieszczenie w zapytaniu znaku kreski pionowej. Warto
pokusić się o ulepszenie programu poprzez dodanie kodu usuwającego z łańcucha zapytania znaki wykorzystywane
jako separatory danych, co zwiększy odporność wyszukiwarki na błędy.

Wyświetlanie reklam
Jeśli w witrynie panuje ruch jak w południe na Marszałkowskiej, czemu by na tym jeszcze trochę nie zarobić? Świetny
pomysł, ale jak? Oto jedna z możliwości. Załóżmy, że chcemy wyświetlać losowo pięć reklam (bez jakiejś określonej
kolejności). Po umieszczeniu kilku adresów URL obrazków w tablicy, możemy losowo wybierać jeden z nich
do wyświetlenia. Oto przykładowa tablica:
var adImages = new Array("pcAd.gif", "modemAd.gif", "webDevAd.gif");

Losowe wybranie jednego z nich i wyświetlenie go na stronie z wynikami wyglądałoby tak:


document.writeln('<IMG SRC=' + ads[Math.floor(Math.random(ads.length))] +
'>');

Rozszerzenie możliwości wyszukiwania


Ten pomysł może dać ambitniejszym programistom niezłe pole do popisu. Załóżmy na przykład, że użytkownik
mógłby wybierać wyszukiwane elementy z tablicy, a dopiero potem zawężałby odpowiednio zakres interesujących go
danych.
Jednym z rozwiązań jest wyświetlenie pod polem tekstowym w pliku nav.html następującego zestawu opcji:
<INPUT TYPE=CHECKBOX NAME="group" VALUE="98">Dane z roku 1998<BR>
<INPUT TYPE=CHECKBOX NAME="group" VALUE="99">Dane z roku 1999<BR>
<INPUT TYPE=CHECKBOX NAME="group" VALUE="00">Dane z roku 2000<BR>

Pól tych można użyć do zdecydowania, które tablice rekordów należy przeszukać (w naszym przypadku będą to np.
tablice profiles98, profiles99 i profiles00).
Kryteria wyszukiwania można rozszerzać na wiele innych sposobów. Jednym z prostszych jest umożliwienie
wyszukiwania z uwzględnieniem wielkości liter i bez jego uwzględniania. Obecnie wielkość liter nie ma znaczenia, ale
można by dodać do formularza pole wyboru pozwalające na takie rozróżnienie.
Możliwość tworzenia bardziej złożonych warunków wyszukiwania można też zapewnić, uzupełniając operatory AND
i OR funkcjami NOT, ONLY, a nawet LIKE. Oto ogólny opis ich znaczenia:
AND
Rekord musi zawierać oba hasła połączone operatorem AND.
OR
Rekord musi zawierać co najmniej jedno z haseł połączonych operatorem OR.
NOT
Rekord nie może zawierać hasła znajdującego się za operatorem NOT.
ONLY
Wynik musi zawierać tylko zadany rekord.
LIKE
Rekord może zawierać terminy podobne (w pisowni lub wymowie) do podanego hasła.
Zaprogramowanie tych możliwości (szczególnie LIKE) wymaga sporo pracy, ale jej efekty mogą rzucić użytkowników
na kolana.
40

Zapytania predefiniowane
Inną popularną, a przy tym użyteczną techniką wyszukiwania jest tworzenie tzw. zapytań predefiniowanych (ang.
cluster set). Zapytanie takie zawiera z góry ustaloną grupę haseł, dla której zdefiniowano gotowy zestaw odpowiedzi.
Jeśli na przykład użytkownik umieści w zapytaniu tekst „kredyt obrotowy”, wyszukiwarka może od razu zwrócić
gotowy zestaw wyników opisujących wszystkie produkty finansowe firmy. Technika ta wymaga starannego
przygotowania danych, ale jej użycie może okazać się niezwykle cenne.
Cechy aplikacji:
 Interaktywny test z wyborem
wielokrotnym
 Tysiące testów z jednym zbiorem pytań
 Klarowna prezentacja wyników i ocena
 Doskonała aplikacja dla sieciowych
ankieterów
 Kontekstowe objaśnienia brakujących

2
pytań
Prezentowane techniki:
 Kombinacje z atrybutem SRC
 Mieszanie zawartości tablicy
 Protokół javascript:

Test
sprawdzany
na bieżąco

Przedstawiony w tym rozdziale interaktywny test należy traktować jako aplikację szablonową, na podstawie której
można realizować dowolne tego typu zadania działające poprzez Internet. Elastyczność tej aplikacji polega na tym, że:
• Określa się liczbę pytań zadawanych użytkownikowi.
• Pytania i odpowiedzi są dobierane losowo przy każdym załadowaniu aplikacji lub rozpoczęciu testu, w efekcie
czego użytkownik za każdym razem ma do czynienia
z nowym testem.
• Można pytania testowe dodawać i usuwać – aplikacja dostosuje wówczas do tego sposób mieszania pytań,
administrację, ocenianie odpowiedzi i ocenę badanego.
• Bez problemu można z aplikacji usunąć odpowiedzi, dzięki czemu unika się oszukiwania, a odpowiedzi
użytkownika można przesłać do działającej po stronie serwera aplikacji oceniającej.
Aplikację tę załadujemy otwierając w przeglądarce plik ch02/index.html. Na rysunku 2.1 pokazano ekran początkowy.
Któż zgadłby tutaj, że pytania działają dzięki JavaScriptowi? Spróbujemy rozwiązać test, składający się z 50 pytań,
które zwykle sprawiają użytkownikom sporo kłopotów. Pytania obejmują wiele zagadnień naszego języka: jego rdzeń,
skrypty działające po stronie klienta i po stronie serwera, LiveConnect, znane błędy i tak dalej. Zadanie nie jest łatwe,
ale o to chodzi (istnieje dokumentacja omawiająca wszystkie pytania i odpowiedzi tego testu, ale w razie odkrycia
błędów w pytaniach użytkownik może skontaktować się z autorem).
Po rozpoczęciu tekstu użytkownik na każde pytanie otrzyma cztery możliwe odpowiedzi. Kiedy, zdecyduje się na
którąś z nich, aplikacja automatycznie przejdzie do następnego pytania, zatem nie można się cofać. Każde pytanie to
jedna próba. Na rysunku 2.2 pokazano postać pytań i odpowiedzi.
42

Rysunek 2.1. Gotów do testu?

Rysunek 2.2. Pytania z kilkoma możliwymi odpowiedziami


Po odpowiedzi na ostatnie pytanie, nasze odpowiedzi będą porównane z tymi właściwymi, po czym zostaniemy ocenieni, a
wyniki wyświetlą się na ekranie, tak jak pokazano to na rysunku 2.3. Warto zwrócić uwagę, że teraz na ekranie
pokazywane są poszczególne pytania ze wszystkimi możliwymi odpowiedziami i z odpowiedzią przez nas wybraną.
Jeśli wybór był poprawny, tekst jest zielony; w przeciwnym wypadku ma kolor czerwony.
43 Rozdział 2 - Test sprawdzany na bieżąco

Rysunek 2.3. Wyniki testu


Aby lepiej zrozumieć pytania, na które udzieliło się złych odpowiedzi, można przejrzeć prawidłowe propozycje; jeśli
najedzie się kursorem myszki na czerwony tekst, na górze po prawej stronie pojawi się objaśnienie – rysunek 2.4.

Rysunek 2.4. Objaśnienie błędnie udzielonych odpowiedzi


W porządku, to był pierwszy kontakt z aplikacją. Wszystko wygląda prosto, ale diagram z rysunku 2.5 da czytelnikowi
pewne wyobrażenie o sposobie działania aplikacji z punktu widzenia użytkownika. Kreski przerywane wskazują
opcjonalne działania użytkownika lub stan oczekiwania na jego reakcję. Całość składa się z pięciu etapów.
44

Rysunek 2.5. Działanie aplikacji z punktu widzenia użytkownika


Oto poszczególne etapy:
1.Użytkownik wybiera przycisk Zaczynamy. Wypisywane jest pierwsze pytanie i aplikacja czeka na odpowiedź
użytkownika lub wciśnięcie klawisza Koniec.
2.Jeśli użytkownik wybierze odpowiedź, aplikacja zapisuje dokonany wybór, określa, czy test się już skończył,
czy też należy pokazać następne pytanie. Jeśli tekst jest skończony (użytkownik odpowiedział na ostatnie
pytanie), idziemy do etapu 4. (ocena testu). W przeciwnym wypadku pokazuje się następne pytanie.
3.Jeśli użytkownik wybierze przycisk Koniec, musi jeszcze swój wybór potwierdzić. W przypadku wybrania OK
test jest oceniany (mimo że jeszcze na to za wcześnie). Jeśli użytkownik wybierze Anuluj, test jest
kontynuowany.
4.Kiedy test się skończy (lub zostanie przerwany), odpowiedzi użytkownika są porównywane z prawidłowymi
odpowiedziami, a na ekranie pokazują się wyniki.
5.Kiedy użytkownik przegląda wyniki, może przesunąć mysz nad dowolny czerwony tekst (czyli swoją błędną
odpowiedź), a wówczas pokazane zostaną dodatkowe informacje.

Wymagania programu
Mamy tu do czynienia z aplikacją JavaScriptu 1.1, więc Navigator 3.x i MSIE 4.x sobie z nią poradzą. Jeśli chodzi
o wielkość aplikacji, obecnie jest 75 pytań. Testowanie przerwałem, kiedy było ich 400. Jako że zapewne nikt
nie będzie raczej używał naszej aplikacji do przeprowadzania egzaminu adwokackiego ani magisterskiego, uważam, że
400 to liczba wystarczająca.

Struktura programu
Na rysunku 2.5 pokazano działanie aplikacji od początku do końca. Dobrym sposobem na zrozumienie, co się
naprawdę dzieje, jest przeanalizowanie dokładniejszego schematu opisującego działanie JavaScriptu, a później
przejrzenie odpowiednich plików z kodem.
45 Rozdział 2 - Test sprawdzany na bieżąco

Na rysunku 2.6 przedstawiono działanie JavaScriptu. Prostokąty narysowane linią przerywaną wskazują przetwarzanie
zachodzące przed testem lub po nim (na przykład podczas ładowania stron). Strzałki kresek oznaczają możliwe
czynności użytkownika lub oczekiwanie na jego odpowiedź.

Rysunek 2.6. Działanie JavaScriptu


Funkcje związane z poszczególnymi akcjami (czynnościami) zaznaczono kursywą. Porównanie rysunków 2.5 i 2.6,
umożliwi szybkie zorientowanie się w problemie. W zasadzie mamy do czynienia z tym samym działaniem, przy czym
w drugim przypadku dodano pewne przetwarzanie przed testem i po nim.

index.html – ramki
Aplikacja ta składa się z trzech plików: index.html, administer.html oraz questions.js. Jako że index.html zawiera ramki,
zacznijmy od niego – obejrzyjmy przykład 2.1.

Przykład 2.1. Plik index.html


1 <HTML>
2 <HEAD>
3 <TITLE>JavaScript On-line Test</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.1">
5 <!--
6 var dummy1 = '<HTML><BODY BGCOLOR=WHITE></BODY></HTML>';
7 var dummy2 = '<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>W nauce JavaScriptu nie ma
wakacji...</BODY></HTML>';
8 //-->
9 </SCRIPT>
10
11 </HEAD>
12 <FRAMESET ROWS="90,*" FRAMEBORDER=0 BORDER=0>
13 <FRAMESET COLS="250,*">
14 <FRAME SRC="administer.html" SCROLLING=NO>
46

15 <FRAME SRC="javascript: self.dummy1">


16 </FRAMESET>

Przykład 2.1. Plik index.html (dokończenie)


17 <FRAME NAME="questions" SRC="javascript: self.dummy2">
18 </FRAMESET>
19 </HTML>

Jak można zauważyć, nie jest to zestaw ramek spotykany w Sieci. Po pierwsze ramki te są zagnieżdżone – czyli mają
postać ramek. Zewnętrzny zestaw w wierszu 12 definiuje dwa wiersze: jeden jest wysoki na 90 pikseli, drugi natomiast
zajmuje resztę wysokości okna.
90-pikselowa ramka zawiera znów ramki, tym razem składające się z dwóch kolumn: pierwszej o szerokości 250
pikseli i drugiej zajmującej resztę okna. Na rysunku 2.7 pokazano sposób podzielenia okna na ramki. Podano też
atrybuty SRC poszczególnych ramek.

Rysunek 2.7. Układ ramek zagnieżdżonych w index.html


administer.html w atrybucie SRC znacznika FRAME ma sens, ale co z pozostałymi dwiema ramkami? Dwie zmienne
dummy1 i dummy2 definiują niejawne strony HTML – czyli strony bez nazwy pliku. Obie istnieją jedynie w aplikacji.
Zmienne te zdefiniowane są w wierszach 7 i 8. Warto zwrócić uwagę, że każda z nich zawiera nieco kodu HTML –
niewiele wprawdzie, ale wystarczy. W pliku index.html używa się protokołu javascript: do interpretacji wartości
zmiennych dummy1 oraz dummy2, następnie całość jest zwracana jako zawartość adresu URL wskazanego w atrybucie
SRC. Więcej informacji można znaleźć w ramce „Techniki języka JavaScript”.
Teraz ramki są już na swoim miejscu. Wszystkie trzy wypełniliśmy, stosując tylko jedną stronę HTML
(administer.html). Teraz przejdźmy do sedna rzeczy.

question.js – plik źródłowy JavaScript


Zajmijmy się teraz plikiem źródłowym JavaScriptu questions.js wywoływanym przez administer. html, a pokazanym
jako przykład 2.2.

Przykład 2.2. Początek pliku questions.js


1 function question(answer, support, question, a, b, c, d) {
2 this.answer = answer;
3 this.support = support;
4 this.question = question;
5 this.a = a;
6 this.b = b;
7 this.c = c;
8 this.d = d;
9 return this;
10 }
11 var units = new Array(

Przykład 2.2. Początek pliku questions.js (dokończenie)


12 new question("a", "The others are external objects.",
13 "Choose the built-in JavaScript object:", "Image", "mimeType",
14 "Password", "Area"),
15 // i tak dalej...
16 }

Istnieje oczywiście wersja skrócona tego pliku. Tablica units jest znacznie dłuższa (ma 75 elementów), ale każdy jej
element opisuje obiekt question (ang. pytanie), którego strukturę pokazano w wierszach 1–10.
47 Rozdział 2 - Test sprawdzany na bieżąco

Aplikacja jest oparta na obiektach definiowanych przez użytkownika. Jeśli idea obiektów JavaScriptu nie jest do końca
jasna, można zajrzeć do dokumentacji Netscape’a pod adresem: http://
developer.netscape.com/docs/manuals/communicator/jsguide4/model.htm. Pozwoli to lepiej zrozumieć model
obiektowy JavaScriptu. Tymczasem następnych kilka akapitów warto potraktować jako krótki podręcznik typu „jak
tego użyć i sobie nie zaszkodzić”.
Obiekt to zestaw danych strukturalnych. Każdy obiekt jest związany z dwoma elementami: właściwościami i metodami.
Właściwości zawierają coś, na przykład liczbę 6, wyrażenie a * b lub napis „Jimmy”. Metody coś robią, na przykład
wyliczają długość łuku lub zmieniają kolor tła dokumentu. Przyjrzyjmy się obiektowi JavaScriptu document. Każdy
dokument coś zawiera (document.gbColor, document.fgColor i tak dalej) oraz coś robi (document.open(),
document.write(), document.close()).

Techniki języka JavaScript:


oszukany atrybut SRC
Ustawienie atrybutu SRC na wartość będącą wynikiem rozwinięcia wyrażenia JavaScript
może wyglądać nieco dziwnie, więc zastanówmy się nad tym przez chwilę. Załóżmy, że
otwieramy edytor tekstowy i wstawiamy do nowego pliku taki tekst:
<BODY BGCOLOR=WHITE>
<FONT FACE=Arial>
W nauce JavaScriptu nie ma wakacji...
</FONT>
</BODY>

Teraz nadajemy nazwę temu plikowi zdanie.html i ładujemy go do przeglądarki. Łatwo


przewidzieć rezultat. W pliku index.html mamy do czynienia właściwie z taką samą sytu-
acją, tyle tylko, że powyższy tekst jest wartością zmiennej dummy2, natomiast protokół
javascript: tę zmienną ewaluuje. Więcej informacji o tym protokole można znaleźć
w ramce dalej w tym rozdziale.
Mamy do czynienia z anonimową stroną HTML. Tę technikę nazywam oszukanym atry-
butem SRC. W dalszej części rozdziału skorzystamy jeszcze z tej techniki.

Obiekty tworzy się przez określenie najpierw funkcji konstruktora, choćby takiego:
function mojPierwszyKonstruktor(arg1, arg2, argn) {
this.wlasciwosc1 = arg1;
this.wlasciwosc2 = arg2;
this.wlasciwoscn = argn;
return this;
}

Wygląda to podobnie jak każda inna funkcja, tyle tylko, że w celu odwołania się do samego siebie obiekt używa słowa
kluczowego this. Wszelkie przekazane argumenty mogą zostać przypisane właściwościom lub być przetwarzane
inaczej. Kiedy już mamy konstruktor, nowe obiekty tworzymy stosując operator new:
var mojPierwszyObiekt= new mojPierwszyKonstruktor(6, a*b, "Jimmy");
var mojDrugiObiekt= new mojPierwszyKonstruktor(6, a*b, "Jimmy");
var mojTrzeciObiekt= new mojPierwszyKonstruktor(6, a*b, "Jimmy");

W przypadku naszego skryptu implementacja obiektów jest faktycznie taka prosta. Obiekty tworzy funkcja-konstruktor
question(), przy czym mają one tylko właściwości. W wierszach 2–8 pokazano siedem właściwości każdego
pytania: odpowiedź, wyjaśnienie, samo pytanie (tekst) oraz cztery możliwe odpowiedzi – a, b, c i d. Oto wiersze od 1
do 10:
function question(answer, support, question, a, b, c, d) {
this.answer = answer;
this.support = support;
this.question = question;
this.a = a;
this.b = b;
this.c = c;
this.d = d;
return this;
}

Właściwości i metody przypisywane są obiektowi przy użyciu takiej właśnie notacji. Wobec tego każdy element units
za pomocą operatora new tworzy nowy obiekt question(), przekazując mu siedem parametrów, które będą jego
właściwościami. W wierszu 9 mamy zapis:
48

return this;

Oznacza to zwrócenie wskazania na zmienną (w naszym wypadku każdy z elementów units), co można porównać
z przypieczętowaniem jakiegoś postanowienia. Teraz każdy element units jest pytaniem, question. Stanowi to
wygodny sposób tworzenia, usuwania i innego typu obsługi pytań testu. Nowe pytania można tworzyć, stosując tę samą
składnię, co w przypadku elementów units:
new question("litera_odpowiedzi", "objaśnienie", "treść pytania",
"opcja a", "opcja b", "opcja c", "opcja d");

Jeśli ktoś zastanawia się, dlaczego odpowiedź jest pierwszą pozycją, to powinien wiedzieć, że po prostu łatwiej jest
umieścić napis składający się z jednej litery na początku listy argumentów, niż na końcu. Niektóre pytania są przecież
dosyć długie, więc przy zaproponowanym układzie łatwiej będzie coś znaleźć i poprawić.
Tworzenie obiektu pytania dla każdego z nich może wydawać się zbyteczne, ale znacznie ułatwia to dalsze działanie,
szczególnie kiedy przyjdzie nam dalej pracować z danymi właściwości poszczególnych pytań. Zajmiemy się tym, kiedy
zbadamy jeszcze plik administer.html.

Jeśli w swoich aplikacjach nie używasz obiektów JavaScriptu, warto zastanowić się
nad zmianą stylu pisania. Obiekty mają wiele zalet. Dzięki nim kod będzie elegantszy
i łatwiejszy w utrzymaniu. Poza tym obiekty umożliwiają dziedziczenie, czyli
przenoszenie metod z obiektu pierwotnego do obiektu budowanego na jego bazie.
Można ściągnąć plik PDF lub przeczytać dokumentację o JavaScripcie i dziedziczeniu
obiektów w Sieci pod adresem http://developer.netscape.com:80/docs/manuals/
communicator/jsobj/contents.htm.

administer.html
Teraz mamy już obiekty, zacznijmy więc ich używać. Jest to kolejna aplikacja, w której cały mechanizm JavaScriptu
rezyduje w górnej ramce, a dolna ramka służy jako okno interakcji. Można rozbić aplikację na szereg operacji.
Zestawiono je w tabeli 2.1 oraz opisano, jak również podano związane z nimi zmienne i funkcje JavaScriptu.

Tabela 2.1. Operacje testu i związane z nimi funkcje JavaScriptu


Operacja Opis Elementy JavaScriptu
Przygotowanie Deklarowanie i inicjalizacja Zmienne: qIdx, correct,
środowiska zmiennych globalnych, howMany, stopOK, nextQ,
przemieszanie zestawów pytanie– results, aFrame, qFrame
odpowiedź. Tablice: keeper, rank,
questions, answers
Funkcje: itemReset(),
shuffle()
Zarządzanie testem Zapisanie zestawu pytanie– Funkcje: buildQuestion(),
odpowiedź w oknie, zapis makeButton(), ewentualnie
odpowiedzi użytkownika. chickenOut()
Ocena testu Porównanie odpowiedzi badanego Funkcja gradeTest()
z poprawnymi odpowiedziami.
Pokazanie wyników Pokazanie odpowiedzi poprawnych Funkcja printResults()
i błędne wraz z oceną.
Wyœwietlanie Wyœwietlanie i chowanie Funkcje explain() i show()
wyjaœnieñ wyjaśnień w parent.frames[1].
Czyszczenie Przywracanie zmiennym ich Zmienne: qIdx, correct,
œrodowiska pierwotnych wartości. stopOK
Tablica keeper
Funkcje cleanSlate()
i shuffle()

Za chwilę przyjrzymy się wszystkiemu po kolei. Na razie obejrzyjmy kod administer.html – przykład 2.3.

Przykład 2.3. Kod źródłowy administer.html


49 Rozdział 2 - Test sprawdzany na bieżąco

1 <HTML>
2 <HEAD><TITLE>On-line JavaScript Test</TITLE>
3 <SCRIPT LANGUAGE="JavaScript1.1" SRC="questions.js"></SCRIPT>
4 <SCRIPT LANGUAGE="JavaScript1.1">
5 var qIdx = 0;
6 var correct = 0;
7 var howMany = 50;
8 var keeper = new Array();
9 var rank = new Array('Nie obraź się, ale potrzebna Ci pomoc.',
10 'Byli i tacy, co zrobili jeszcze gorzej...',
11 'Cosik tam wiesz. Przynajmniej tego nie zapomnij.',
12 'Zdaje się, że pracujesz nad swoją wiedzą.',
13 'Lepiej od przeciętnego niedźwiedzia.',

Przykład 2.3. Kod źródłowy administer.html (ciąg dalszy)


14 'Jesteś niezłym programistą JavaScriptu.',
15 'Jesteś znawcą JavaScriptu.', 'Jesteś doskonały w JavaScripcie.',
16 'Ogłaszam Cię guru JavaScriptu.'
17 );
18 var stopOK = false;
19 var nextQ = '';
20 var results = '';
21 var aFrame = parent.frames[1];
22 var qFrame = parent.frames[2];
23 function shuffle() {
24 for (var i = 0; i < units.length; i++) {
25 var j = Math.floor(Math.random() * units.length);
26 var tempUnit = units[i];
27 units[i] = units[j];
28 units[j] = tempUnit;
29 }
30 }
31 function itemReset() {
32 qIdx = 0;
33 correct = 0;
34 stopOK = false;
35 keeper = new Array();
36 shuffle();
37 }
38 function buildQuestion() {
39 if (qIdx == howMany) {
40 gradeTest();
41 return;
42 }
43 nextQ = '<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>' +
44 '<H2>Pytanie ' + (qIdx + 1) + ' z ' + howMany + '</H2>' +
45 '<FORM>' + '<B>' + units[qIdx].question + '</B><BR><BR>' +
46 makeButton("a", units[qIdx].a) +
47 makeButton("b", units[qIdx].b) +
48 makeButton("c", units[qIdx].c) +
49 makeButton("d", units[qIdx].d) +
50 '</FORM></BODY></HTML>';
51 qFrame.location.replace("javascript: parent.frames[0].nextQ");
52 qIdx++;
53 if(qIdx >= 2 && !stopOK) { stopOK = true; }
54 }
55 function makeButton(optLtr, optAnswer) {
56 return '<INPUT TYPE=RADIO NAME="answer" VALUE="' + optLtr +
57 '" onClick="parent.frames[0].keeper[parent.frames[0].qIdx - 1] =
58 this.value; parent.frames[0].buildQuestion()">' + optAnswer + '<BR>';
59 }
60 function chickenOut() {
61 if(stopOK &&
62 confirm('Masz już dość? Tchórzysz?')) {
63 gradeTest();
64 }
65 }
66 function gradeTest() {
67 for (var i = 0; i < qIdx; i++) {
68 if (keeper[i] == units[i].answer) {
69 correct++;
70 }
71 }
72 var idx = Math.ceil((correct/howMany) * rank.length - 1) < 0 ? 0 :
73 Math.ceil((correct/howMany) * rank.length - 1);
74 printResults(rank[idx]);
50

75 itemReset();
76 }
77 function printResults(ranking) {
78 results = '<HTML><BODY BGCOLOR=WHITE LINK=RED VLINK=RED ALINK=RED>' +

Przykład 2.3. Kod źródłowy administer.html (dokończenie)


79 '<FONT FACE=Arial>' +
80 '<H2>Odpowiedzi poprawnych: ' + correct + '/' + howMany + '.</H2>' +
81 '<B>Ocena: <I>' + ranking +
82 '</I><BR>Ustaw kursor myszki nad czerwonym tekstem, a zobaczysz' +
83 ' poprawki odpowiedzi.</B>' +
84 '<BR><BR><FONT SIZE=4>Oto Twoje oceny: </FONT><BR><BR>';
85 for (var i = 0; i < howMany; i++) {
86 results += '\n\r\n\r\n\r<B>Pytanie ' + (i + 1) + '</B><BR>' +
87 units[i].question + '<BR><BR>\n\r<FONT SIZE=-1>' +
88 'a. ' + units[i].a + '<BR>' +
89 'b. ' + units[i].b + '<BR>' +
90 'c. ' + units[i].c + '<BR>' +
91 'd. ' + units[i].d + '<BR></FONT>';
92 if (keeper[i] == units[i].answer) {
93 results += '<B><I><FONT COLOR=GREEN>' +
94 'Na to odopwiedziałeś poprawnie (' +
95 keeper[i] + ').</FONT></I></B>\n\r<BR><BR><BR>';
96 }
97 else {
98 results += '<FONT FACE=Arial><B><I>' +
99 '<A HREF=" " onMouseOver="parent.frames[0].show();' +
100 parent.frames[0].explain(\'' + units[i].support + '\'); ' +
101 'return true"' + ' onMouseOut="parent.frames[0].explain(\' \');"' +
102 'onClick="return false;">' +
103 'Prawidłowa odpowiedź: ' + units[i].answer +
104 '</A></FONT></I></B>\n\r<BR><BR><BR>';
105 }
106 }
107 results += '\n\r</BODY></HTML>';
108 qFrame.location.replace("javascript: parent.frames[0].results");
109 }
110 function show() { parent.status = ''; }
111 function explain(str) {
112 with (aFrame.document) {
113 open();
114 writeln('<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>' + str +
115 '</FONT></BODY></HTML>');
116 close();
117 }
118 }
119 function cleanSlate() {
120 aFrame.location.replace('javascript: parent.dummy1');
121 qFrame.location.replace('javascript: parent.dummy2');
122 }
123 </SCRIPT>
124 </HEAD>
125 <BODY BGCOLOR=WHITE onLoad="cleanSlate();">
126 <FONT FACE="Arial">
127 <FORM>
128 <INPUT TYPE=BUTTON VALUE="Zaczynamy"
129 onClick="itemReset(); buildQuestion();">
130 &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
131 <INPUT TYPE=BUTTON VALUE="Koniec" onClick="chickenOut();">
132 </FORM>
133 </FONT>
134 </BODY>
135 </HTML>

Ten dość długi plik można by podzielić na cztery części. W pierwszej wywoływany jest plik źródłowy questions.js.
W następnej definiowane są pewne zmienne globalne, po czym przejdziemy do funkcji. W końcu mamy kilka wierszy
HTML i od nich właśnie zacznijmy.

Treść HTML
Kiedy administer.html skończy się ładować, wywoływana jest funkcja cleanSlate() z wiersza 125:
<BODY BGCOLOR=WHITE onLoad="cleanSlate();">
51 Rozdział 2 - Test sprawdzany na bieżąco

W cleanSlate() używa się metody replace() obiektu location, w ten sposób bieżący adres URL
parent.frames[1] (alias aFrame) i parent.frames[2] (alias qFrame) zamieniany jest na zawartość zmiennych
dummy1 i dummy2, zdefiniowanych wcześniej w pliku index.html. Spójrzmy na wiersze 119 do 122:
function cleanSlate() {
aFrame.location.replace('javascript: parent.dummy1');
qFrame.location.replace('javascript: parent.dummy2');
}

Właśnie to robiliśmy w pliku index.html, zgadza się? Tym razem zapewniamy, że jeśli z jakiegoś powodu
administer.html ulegnie przeładowaniu, górna ramka zawierać będzie naszą sentencję, a istniejący tam ewentualnie
tekst pytania zostanie usunięty.
Reszta pliku HTML to po prostu formularz HTML z dwoma przyciskami. To jest już proste. Każdy przycisk –
po kliknięciu go – wywołuje inną funkcje. Poniżej przedstawiono kod wierszy 127–132:
<FORM>
<INPUT TYPE=BUTTON VALUE="Zaczynamy"
onClick="itemReset(); buildQuestion();">
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
<INPUT TYPE=BUTTON VALUE="Koniec" onClick="chickenOut();">
</FORM>

Warto zauważyć, że przycisk Zaczynamy wywołuje funkcje itemReset() i buildQuestion(), natomiast Koniec
wywołuje chickenOut(). Wszystkie trzy funkcje będą omówione w sekcji im poświęconej.

Zmienne globalne
Zaraz za instrukcją powodującą włączenie pliku źródłowego JavaScript questions.js w wierszu 5 można znaleźć zmienne
globalne używane w aplikacji. Oto wiersze 5 do 22:
var qIdx = 0;
var correct = 0;
var howMany = 50;
var keeper = new Array();
var rank = new Array('Nie obraź się, ale potrzebna Ci pomoc.',
'Byli i tacy, co zrobili jeszcze gorzej...',
'Cosik tam wiesz. Przynajmniej tego nie zapomnij.',
'Zdaje się, że pracujesz nad swoją wiedzą.',
'Lepiej od przeciętnego niedźwiedzia.',
'Jesteś niezłym programistą JavaScriptu.',
'Jesteś znawcą JavaScriptu.', 'Jesteś doskonały w JavaScripcie.',
'Ogłaszam Cię guru JavaScriptu.'
);
var stopOK = false;
var nextQ = '';
var results = '';
var aFrame = parent.frames[1];
var qFrame = parent.frames[2];

Na poniższej liście opisujemy znaczenie poszczególnych zmiennych. Dokładniej przyjrzymy się im przy omawianiu
poszczególnych funkcji.
qldx
Zmienna używana do monitorowania bieżącego pytania, wyświetlanego na ekranie.
correct
Zmienna rejestrująca liczbę poprawnych odpowiedzi podczas oceny testu.
howMany
Niezmienna liczba określająca liczbę pytań, na które odpowiadać ma użytkownik.
keeper
Początkowo pusta tablica, zawiera odpowiedzi udzielone przez użytkownika.
rank
Tablica napisów określających poziom umiejętności.
stopOK
Zmienna logiczna określająca, czy przerwać test.
nextQ
Pusty napis, któremu przypisywany jest tekst kolejnych pytań.
52

results
Początkowo napis pusty, później ocena testu.
aFrame
Prosta metoda odwołania się do drugiej ramki.
qFrame
Prosta metoda odwołania się do trzeciej ramki.

Funkcje
Teraz przechodzimy do funkcji. Zaczniemy od itemReset().
itemReset()
Pierwsza z funkcji wywoływanych w aplikacji to itemReset(). Pojawia się, kiedy użytkownik wciśnie przycisk
Zaczynamy (wiersze 128-129):
<INPUT TYPE=BUTTON VALUE="Zaczynamy"
onClick="itemReset(); buildQuestion();">

itemReset() przywraca zmiennym globalnym ich pierwotne wartości i miesza zawartość tablicy obiektów pytań
(więcej o mieszaniu już wkrótce). Spójrzmy na wiersze 31–37:
function itemReset() {
qIdx = 0;
correct = 0;
stopOK = false;
keeper = new Array();
shuffle();
}

Warto zwrócić uwagę, że użytkownik nie widział jeszcze pierwszego pytania, a JavaScript już napracował się
przy ustawianiu zmiennych globalnych. Po co? Załóżmy, że już test rozwiązaliśmy i tylko na dwa pytania
odpowiedzieliśmy poprawnie wtedy wciskamy jeszcze raz przycisk Zaczynamy. Jednak wiele zmiennych globalnych
ma już różne nieoczekiwane wartości i tym właśnie zajmuje się funkcja itemReset(): odświeża wartości tych
zmiennych.
Zauważmy, że nie dotyczy to zmiennej howMany. Wartość ta pozostaje niezmienna przez cały czas działania aplikacji.
Zmienne netQ i results na początku mają ciąg pusty jako wartość, ale ich wartości nie są zerowane. Nie ma
po prostu takiej potrzeby. Zajrzyjmy do wierszy 43 i 86, a zobaczymy, jak te zmienne są ustawiane na bieżąco.
Kiedy zmienne są już odpowiednio ustawione, można wywołać w wierszu 36 funkcję shuffle().
shuffle()
Ta mała funkcja daje administratorowi testu ogromną elastyczność – zmienia ona losowo kolejność pytań, dzięki czemu
daje prawie pewność, że testowany dostanie za każdym razem inny zestaw. Aby unaocznić wynikające z tego
możliwości, przypomnijmy, że liczba możliwych kombinacji (różnych uporządkowań) pytań testu wynosi n(n-1),
przy czym n to liczba pytań. Zatem najmniejszy nawet tekst z dziesięciu pytań da 10*(10–1) kombinacji, czyli 90.
W przypadku testu z 20 pytań możliwości jest już 380. Z kolei test z 50 pytań oznacza 2 450 możliwych kombinacji. To
może być nieciekawa wiadomość dla oszustów.
Test jest także niepowtarzalny dlatego, że choć cała tablica units ma 75 pytań, zmienna howMany ustawiana jest na 50.
Kiedy skończy się mieszanie, wybieranych jest 50 pierwszych pytań. Wobec tego istnieje duża szansa, że zestaw
następnych 50 pytań jest inny, niż pierwsze 50. Oznacza to, że w teście tym istnieją tysiące możliwych kombinacji
pytań. Zdumiewające, jak prosty jest proces mieszania tychże pytań.
Oto wiersze 23 do 30:
function shuffle() {
for (var i = 0; i < units.length; i++) {
var j = Math.floor(Math.random() * units.length);
var tempUnit = units[i];
units[i] = units[j];
units[j] = tempUnit;
}
}

Dla każdego elementu tablicy units:


1.Wybierana jest przypadkowa liczba między 0 a units.length - 1.
2.Wartość zmiennej lokalnej tempUnit ustawiana jest na bieżący indeks (units[i]).
53 Rozdział 2 - Test sprawdzany na bieżąco

3.Wartość elementu bieżącego indeksu (units[i]) ustawiana jest na wartość elementu o przypadkowym
indeksie całkowitym (units[j]).
4.Wartość elementu o przypadkowym indeksie staje się równa wartości zmiennej lokalnej tempUnit.
Innymi słowy, kolejno przeglądane są wszystkie elementy tablicy i parami są zamieniane wartości z losowo wybranym
elementem.
Pytania zostały już zatem losowo wymieszane i czekają na użytkownika.
buildQuestion()
Funkcja ta pełni rolę administratora testu. Jak zapewne łatwo zauważyć na poprzednim schemacie, buildQuestion()
jest używana kilkakrotnie. Spoczywa na niej wielka odpowiedzialność. Zaczyna się w wierszu 38, a kończy w wierszu
54:
function buildQuestion() {
if (qIdx == howMany) {
gradeTest();
return;
}
nextQ = '<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>' +
'<H2>Pytanie ' + (qIdx + 1) + ' z ' + howMany + '</H2>' +
'<FORM>' + '<B>' + units[qIdx].question + '</B><BR><BR>' +
makeButton("a", units[qIdx].a) +
makeButton("b", units[qIdx].b) +
makeButton("c", units[qIdx].c) +
makeButton("d", units[qIdx].d) +
'</FORM></BODY></HTML>';
qFrame.location.replace("javascript: parent.frames[0].nextQ");
qIdx++;
if(qIdx >= 2 && !stopOK) { stopOK = true; }
}

Prześledźmy wszystko po kolei: najpierw buildQuestion() sprawdza, czy zmienna qIdx równa jest zmiennej
howMany. Jeśli tak, użytkownik odpowiedział na ostatnie pytanie i przyszedł czas na ocenę. Funkcja gradeTest()
wywoływana jest w wierszu 40.

Techniki języka JavaScript:


mieszanie zawartości tablicy
W naszym teście zmieniamy przypadkowo układ elementów tablicy. Jest to zachowanie
pożądane w przypadku tej aplikacji, ale nietrudno przychodzi też zapisywać inne, bardziej
kontrolowane metody przestawiania danych w tablicy. Poniższa funkcja jako parametry
przyjmuje kopię tablicy do uporządkowania oraz liczbę całkowitą, wskazującą, co ile jej
elementy mają być porządkowane:
function shakeUp(formObj, stepUp) {
setUp = (Math.abs(parseInt(stepUp)) > 0 ?
Math.abs(parseInt(stepUp)) : 1);
var nextRound = 1;
var idx = 0;
var tempArray = new Array();
for (var i = 0; i < formObj.length; i++) {
tempArray[i] = formObj[idx];
if (idx + stepUp >= formObj.length) {
idx = nextRound;
nextRound++;
}
else {
idx += stepUp;
}
}
formObj = tempArray;
}

Jeśli na przykład tablica ma 10 elementów i porządkujemy je co drugi (0, 2, 4, 6, 8, na-


stępnie 1, 3, 5, 7, 9), wywołujemy shakeUp(twojaTablica, 2). Jeśli przekażemy 0, do-
myślny przyrost to 1. Więcej tego typu funkcji znajdziesz w rozdziale 6.
54

Jeśli test jeszcze się nie skończył, buildQuestion() idzie dalej, co oznacza, że generowana jest treść następnego
pytania w postaci strony HTML (wiersze 43 do 50). Jeśli zbada się nextQ, można zauważyć, że strona ta zawiera
wskaźnik numeru pytania i całkowitą liczbę pytań testu. Oto wiersz 44:
'<H2>Pytanie ' + (qIdx + 1) + ' z ' + howMany + '</H2>'

Dalej znajdziemy otwierający znacznik FORM, a za nim treść pytania. Warto pamiętać, że treść znajduje
się we właściwości question poszczególnych elementów units. Nie należy być zatem zaskoczonym, że wiersz 45
wygląda następująco:
'<FORM>' + '<B>' + units[qIdx].question

W pobliżu elementu formularza FORM znajdują się też same elementy formularza. Tak naprawdę są one całą resztą
strony HTML. Ta formatka ma tylko cztery elementy, a wszystkie są opcjami radio. Zamiast kodować wszystko
w HTML, funkcja makeButton() te wszystkie opcje (niemalże identyczne) tworzy. Wystarczy jedynie przekazywać
jej literę odpowiedzi i tekst tej odpowiedzi, co widać w wierszach 46 do 49. Oto funkcja makeButton() z wierszy 55–
59:
function makeButton(optLtr, optAnswer) {
return '<INPUT TYPE=RADIO NAME="answer" VALUE="' + optLtr +
'" onClick="parent.frames[0].keeper[parent.frames[0].qIdx - 1] =
this.value; parent.frames[0].buildQuestion()">' + optAnswer + '<BR>';
}

Funkcja po prostu zwraca tekst opisujący jedną opcję radio z odpowiednio ustawionym atrybutem VALUE (a, b, c lub d)
oraz tekst odpowiedzi umieszczany na prawo od opcji. Atrybut VALUE pochodzi z optLtr, a tekst z optAnswer.
Warto pamiętać, że test reaguje na działania użytkownika, więc automatycznie przechodzi dalej, kiedy tylko
użytkownik udzieli odpowiedzi. W języku JavaScriptu oznacza to, że ze zdarzeniem onClick opcji musi być
związanych kilka kolejnych działań.
Po pierwsze – w tablicy keeper należy zarejestrować odpowiedź użytkownika. Aby sprawdzić, który element należy
przypisać wyborowi, używamy poniższego wyrażenia:
parent.frames[0].qIdx - 1

Zmienna qIdx „pamięta” numer aktualnej odpowiedzi, więc świetnie nadaje się do określenia kolejnej odpowiedzi
użytkownika.
Następnie JavaScript musi wywołać buildQuestion(), aby pokazać następne pytanie lub ocenić test, jeśli został on
zakończony. Zwróćmy uwagę, że do keeper i do buildQuestion() odwoływaliśmy się na początku
parent.frames[0]. Informacja ta zostanie zapisana w ramce parent. frames[1], więc będziemy musieli dostać się
do górnej ramki.
Kiedy już mamy gotowy formularz, trzeba jeszcze tylko (jeśli chodzi o HTML) pozamykać znaczniki i załadować treść
do okna – wiersze 50 i 51:
'</FORM></BODY></HTML>';
qFrame.location.replace("javascript: parent.frames[0].nextQ");

Wartość nextQ ładowana jest do dolnej ramki. Zauważmy, że w aplikacji używana jest metoda replace obiektu
location, nie ustawiamy więc tym razem właściwości location.href ani nie stosujemy document.write().
W tej aplikacji jest to istotne. Funkcja replace() ładuje wskazany adres URL do przeglądarki (w naszym przypadku
adres URL to napis HTML rozwijany przez użycie protokołu javascript:), przy czym nowa strona zastępuje
bieżącą. Dzięki temu użytkownik nie może wrócić do strony wcześniejszej ani zmieniać odpowiedzi. Jeśli użytkownik
wciśnie w przeglądarce przycisk Back, załaduje się strona, która była oglądana przed index.html.
Ostatnią rzeczą, którą trzeba zrobić przed opuszczeniem funkcji buildQuestion(), jest drobne uporządkowanie,
pokazane w wierszach 52–53:
qIdx++;
if(qIdx >= 2 && !stopOK) { stopOK = true; }

Zwiększenie qIdx o 1 przygotowuje sytuację do następnego wywołania buildQuestion(). Pamiętaj, że w wierszu


39 sprawdzamy, czy qIdx jest większa od liczby pytań w teście (zmienna howMany); jeśli tak, czas na ocenę. Instrukcja
if z wiersza 53 sprawdza, czy użytkownik zamierza przerwać test. Kod w obecnej jego postaci wymaga udzielenia
odpowiedzi na przynajmniej jedno pytanie. Jeśli użytkownik chce, może to już sam poprawić.
55 Rozdział 2 - Test sprawdzany na bieżąco

Techniki języka JavaScript:


protokół javascript:
Wcześniej już zetknęliśmy się z powyższym protokołem w tej książce. Protokół ten po-
zwala JavaScriptowi ewaluować dowolne wyrażenie. Jeśli na przykład chcemy, aby po
kliknięciu przez użytkownika łącza stało się coś innego niż zwykłe przeładowanie strony,
można użyć odpowiedniego wywołania w atrybucie HREF znacznika <A>:
<A HREF="javascript: alert('Znalazłeś ostrzeżenie!');">Kliknij mnie</A>

Można też ustawiać w taki sposób atrybuty SRC innych znaczników HTML. Szczegóły
znajdziemy w ramce opisującej „oszukane” atrybuty SRC, wcześniej w tym rozdziale.
Teraz jeszcze ostrzeżenie: jeśli w zdefiniowanej przez siebie funkcji używamy protokołu
javascript:, nie próbujemy ewaluować zmiennych lokalnych funkcji. To nie zadziała.
Omawiany protokół ma zasięg globalny, więc „widzi” jedynie globalne zmienne, globalne
obiekty i tak dalej. Wiersz 51 to klasyczny przykład ilustrujący tę zasadę:
qFrame.location.replace("javascript: parent.frames[0].nextQ");

Zmienna nextQ właściwie mogłaby zostać zdefiniowana jako lokalna, w końcu przecież
jest używana jedynie w funkcji buildQuestion(). Jednak, jako że w wierszu 51 znaj-
duje się odwołanie do protokołu javascript:, poniższy kod nie zadziałałby:
qFrame.location.replace("javascript: nextQ");

Jeśli zmienna nextQ byłaby lokalna, javascript: nie mógłby jej ewaluować.

gradeTest()
Funkcja gradeTest() realizuje dwa zadania. Najpierw porównuje odpowiedzi użytkownika z tymi prawidłowymi,
zapamiętując liczbę uzyskanych dotąd właściwych odpowiedzi. Po drugie gradeTest() wylicza wskaźnik oceny
i na podstawie liczby prawidłowych odpowiedzi wybiera odpowiedź. Oto całość gradeTest(), wiersze 66–76:
function gradeTest() {
for (var i = 0; i < qIdx; i++) {
if (keeper[i] == units[i].answer) {
correct++;
}
}
var idx = Math.ceil((correct/howMany) * rank.length - 1) < 0 ? 0 :
Math.ceil((correct/howMany) * rank.length - 1);
printResults(rank[idx]);
itemReset();
}

Tablica keeper zawiera litery (a, b, c lub d) związane z odpowiedzią wybraną przez użytkownika. Każdy element tablicy
units to obiekt pytania zawierający właściwość answer – też a, b, c lub d. gradeTest() przegląda kolejne elementy
keeper i porównuje wartość właściwości answer z odpowiednim elementem z units. Jeśli pasują do siebie, zmienna
correct zwiększana jest o 1.

Warto zauważyć, że zapamiętaniu nie podlega to, które odpowiedzi były prawidłowe. Funkcja jedynie określa liczbę
prawidłowych odpowiedzi i podaje ocenę na postawie tej liczby. Jeszcze raz użyjemy tablicy keeper, kiedy będziemy
omawiać funkcję printResults(). Zwróćmy uwagę też na to, że gradeTest() nie używa zmiennej howMany.
Nie ma znaczenia, ile jest pytań w teście; ważne jest tylko, na ile użytkownik udzielił odpowiedzi.
Kiedy już mamy wyniki, zmienna correct zawiera liczbę odpowiedzi poprawnych. Teraz gradeTest() musi tylko
ocenić użytkownika – wiersze 72–73:
var idx = Math.ceil((correct/howMany) * rank.length - 1) < 0 ? 0 :
Math.ceil((correct/howMany) * rank.length - 1);

Oto jak rzecz działa, gdy chcemy przypisać użytkownikowi jedną z ocen z tabeli rank z wiersza 9. Aby wybrać
element, musimy mieć liczbę między 0 a rank.length - 1. Funkcja gradeTest() wybiera liczbę całkowitą
w trzystopniowym procesie:
1.Wylicza procent odpowiedzi poprawnych (correct / howMany).
2.Mnoży uzyskane procenty przez (rank.length - 1).
56

3.Zaokrągla uzyskany iloczyn w górę.


Wynik przypisuje się zmiennej lokalnej idx, która jest (w postaci liczby całkowitej) określeniem skuteczności
użytkownika między zerem a rank.length. Innymi słowy, ile by nie było pytań, użytkownik zawsze otrzyma ocenę
opartą na procencie prawidłowo udzielonych odpowiedzi. W zrozumieniu tego powinien pomóc następujący przykład.
Załóżmy, że mamy taką oto tablicę rank:
var rank = new Array( "byli lepsi", "jako-tako", "dobrze",
"bardzo dobrze", "doskonale");

rank.length równe jest 5, jeśli więc nasz test ma 50 pytań, skala ocen jest następująca:7

Poprawne odpowiedzi Wyznaczona liczba Ocena (rank[int])


0–9 0 byli lepsi
10–19 1 jako-tako
20–29 2 dobrze
30–39 3 bardzo dobrze
40–50 4 doskonale

Mamy zatem mniej więcej howMany / rank.length odpowiedzi na jedną ocenę (poza oceną najwyższą). Nie ma
znaczenia, czy pytania są 2, czy jest ich 2008 – wszystko działa tak samo.
Zaproponowany system ocen jest skuteczny, ale jest dość zgrubny. Stosowane powszechnie systemy ocen zwykle są
bardziej złożone. Większość szkół amerykańskich używa systemu literowego, gdzie A to ponad 90%, B to 80–89%, C
to 70–79%, D to 60–69%, a F to mniej niż 60%. Być może ktoś zechce użyć jakiejś podobnej krzywej. W sekcji
opisującej możliwe rozszerzenia aplikacji znajdzie się kilka innych pomysłów.
gradeTest() w zasadzie skończyła już swoje zadanie. Zmienna rank[idx] przekazywana jest do funkcji wyświetlającej
printResults(), a później wywoływana jest funkcja czyszcząca itemReset().

printResults()
Aplikacja wie, jakie wyniki osiągnął użytkownik, czas więc poinformować go o tym. Służy do tego funkcja
printResults() wyświetlająca następujące rzeczy:

• Stosunek liczby odpowiedzi poprawnych do ilości wszystkich pytań.


• Ocenę użytkownika wyznaczoną przez gradeTest().
• Wszystkie pytania testu wraz z kompletem czterech odpowiedzi.
• Informację, czy użytkownik wybrał dobrą odpowiedź, czy też nie.
• Przyłączony tekst pozwalający użytkownikowi uzyskać dodatkowe informacje o pytaniach, na które
nie odpowiedział prawidłowo.
Pierwsze dwa punkty realizowane są w wierszach 77 do 84:
function printResults(ranking) {
results = '<HTML><BODY BGCOLOR=WHITE LINK=RED VLINK=RED ALINK=RED>' +
'<FONT FACE=Arial>' +
'<H2>Odpowiedzi poprawnych: ' + correct + '/' + howMany + '.</H2>' +
'<B>Ocena: <I>' + ranking +
'</I><BR>Ustaw kursor myszki nad czerwonym tekstem, a zobaczysz' +
' poprawki odpowiedzi.</B>' +
'<BR><BR><FONT SIZE=4>Oto Twoje oceny: </FONT><BR><BR>';

Zmienne correct i howMany oznaczają odpowiednio liczbę odpowiedzi poprawnych oraz liczbę zadanych pytań,
rank[rankIdx] to zapis zawierający ocenę użytkownika. Jeśli chodzi o wyświetlanie pytań i możliwych czterech
odpowiedzi, obejrzyjmy wiersze 85 do 91. Nie powinno nikogo zaskakiwać pojawienie się pętli for:
for (var i = 0; i < howMany; i++) {
results += '\n\r\n\r\n\r<B>Pytanie ' + (i + 1) + '</B><BR>' +

7
Jest tutaj pewna pułapka: jeśli użytkownik nie będzie znał odpowiedzi na żadne pytanie, może wybierać odpowiedzi losowo. Przy teście z czterema
dopuszczalnymi odpowiedziami średnia prawidłowych odpowiedzi wyniesie 12,5 na 50 pytań, czyli już „jako-tako”. Jeśli zamierzamy,
wykorzystać taką aplikację, powinniśmy zmienić jej skalę (żeby minimalna ocena promująca była większa od oczekiwanej wartości dobrych
odpowiedzi przy „strzelaniu”). No, chyba że chodzi o poprawienie humoru testowanemu (przyp. tłum.).
8
Nie całkiem – dopóki pytań jest mniej niż możliwych ocen, część ocen nie będzie mogła wystąpić w ogóle (przyp. tłum.).
57 Rozdział 2 - Test sprawdzany na bieżąco

units[i].question + '<BR><BR>\n\r<FONT SIZE=-1>' +


'a. ' + units[i].a + '<BR>' +
'b. ' + units[i].b + '<BR>' +
'c. ' + units[i].c + '<BR>' +
'd. ' + units[i].d + '<BR></FONT>';

W każdej iteracji od 0 do howMany - 1 podawana jest jako tekst liczba (i + 1), tekst pytania (units[i].question)
oraz cztery możliwe odpowiedzi (units[i].a, units[i].b, units[i].c, units[i].d). Trochę zamieszania
wprowadzają użyte znaczniki HTML.
Ostatni fragment wydruku to wyświetlenie dobrych odpowiedzi użytkownika na zielono i złych – z dodatkowym
objaśnieniem – na czerwono. Oto odpowiednie wiersze – 92 do 106:
if (keeper[i] == units[i].answer) {
results += '<B><I><FONT COLOR=GREEN>' +
'Na to odopwiedziałeś poprawnie (' +
keeper[i] + ').</FONT></I></B>\n\r<BR><BR><BR>';
}
else {
results += '<FONT FACE=Arial><B><I>' +
'<A HREF=" " onMouseOver="parent.frames[0].show();' +
parent.frames[0].explain(\'' + units[i].support + '\'); ' +
'return true"' + ' onMouseOut="parent.frames[0].explain(\' \');"' +
'onClick="return false;">' +
'Prawidłowa odpowiedź: ' + units[i].answer +
'</A></FONT></I></B>\n\r<BR><BR><BR>';
}
}

Przetwarzaniu ulegają wszystkie kolejne pytania, niezależnie od trafności odpowiedzi udzielonej przez użytkownika.
Nie należy być zatem zaszokowanym, gdy się widzi wewnątrz instrukcję if-else. Jeśli keeper[i] równe jest
units[i].answer, użytkownik wybrał dobrze, a odpowiedź wyświetla się na zielono. Jeśli równość nie zachodzi,
czerwony tekst wskazuje dostępność dodatkowego objaśnienia pytania w parent.frames[1]. Ramka ta nie była
dotąd właściwie używana, więc teraz przyszedł i na nią czas.
Pytania, na które użytkownik udzielił jedynej słusznej odpowiedzi, są zwykłym tekstem, tymczasem pytania ze złymi
odpowiedziami są jednak tekstem łącza. Zdarzenie onMouseOver tych łącz wywołuje przed zwróceniem true dwie
funkcje: show() oraz explain(). Funkcja show() jest bardzo prosta: pokazuje pusty napis w pasku stanu (aby
uniknąć dodatkowego zamieszania związanego z umieszczeniem myszy nad łączem) – wiersz 110:
function show() { parent.status = ''; }

Funkcja explain() jako parametr przyjmuje napis i używa go w metodzie document.write() do wyświetlenia
HTML w cierpliwie czekającej parent.frames[1]. Oto wiersze 111–118:
function explain(str) {
with (aFrame.document) {
open();
writeln('<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>' + str +
'</FONT></BODY></HTML>');
close();
}
}

Mimo że zatroszczyliśmy się o zdarzenie onMouseOver, explain(), nadal ma jeszcze nieco pracy. Zwróćmy uwagę,
że explain() wywoływana jest znów w wierszu 101 w obsłudze zdarzenia onMouseOut. Tym razem jednak funkcji
explain() przekazywany jest pusty napis, więc aFrame będzie czyszczona po każdym zdarzeniu onMouseOut.

Jedyne, co nam zostało, to zabezpieczenie się przed jakąkolwiek akcją w przypadku kliknięcia naszego łącza przez
użytkownika. W wierszu 102 znajdziesz zapis onClick="return false;". Dzięki temu dokument wskazany
w atrybucie HREF nie zostanie załadowany.
Należy pamiętać, że nadal jesteśmy w pętli for. Powyższy proces ma miejsce dla każdej odpowiedzi, od 0 do howMany
- 1. Kiedy pętla for zakończy swoje działanie, zmienna results jest długim napisem, zawierającym liczbę
prawidłowych odpowiedzi, liczbę wszystkich pytań, tekst pytań z możliwymi odpowiedziami i wyborami zrobionymi
przez użytkownika oraz informacją o prawidłowości jego odpowiedzi. W wierszach 107–109 dodajemy jeszcze końcowe
znaczniki HTML, ładujemy całość do dolnej ramki i zamykamy funkcję:
results += '\n\r</BODY></HTML>';
qFrame.location.replace("javascript: parent.frames[0].results");
}
58

chickenOut()
Istnieje jeszcze jedna drobna kwestia: co się stanie, jeśli użytkownik zawczasu zakończy test. Oczywiście można by się
tym nie przejmować, warto jednak dodać tę funkcję właśnie po to, aby aplikację rozszerzyć. Oto kod wierszy 60 do 65:
function chickenOut() {
if(stopOK &&
confirm('Masz już dość? Tchórzysz?')) {
gradeTest();
}
}

Jeśli użytkownik potwierdzi rezygnację, wywoływana jest gradeTest(). Pamiętajmy, że użytkownik może się wycofać
po odpowiedzeniu na przynajmniej jedno pytanie. Zmienna stopOK początkowo ustawiana jest na false, a na true wtedy,
kiedy qIdx ma wartość większą od 1 – wiersz 53.
Chodzi o to, że gradeTest() porównuje odpowiedzi z pytaniami, nawet jeśli użytkownik na nie nie odpowiedział.
Można by pokusić się o stwierdzenie, że czyni to straszliwe spustoszenie w morale testowanego, ale taka jest cena
tchórzostwa.

Kierunki rozwoju
Aplikację tę można modyfikować na wiele sposobów. Dwa narzucające się rozszerzenia to zabezpieczenie
przed oszustwami przez stworzenie serwera oceniającego oraz zmiana aplikacji na badanie ankietowe.

Uodpornienie na oszustwa
Jedną z pierwszych myśli po zapoznaniu się z aplikacją może być obawa, że użytkownik sprawdzi odpowiedzi.
Wyszukiwanie odpowiedzi może okazać się trudne, otwierając plik źródłowy JavaScript, ale da się to zrobić.
Możemy to zagrożenie usunąć, jeśli po prostu nie będziemy wysyłać wraz z aplikacją odpowiedzi, ale zażądamy
od użytkownika przekazania sobie jego odpowiedzi. Nie będziemy się tutaj dokładnie zajmować serwerem
oceniającym, ale rzecz nie będzie trudniejsza od funkcji gradeTest(). Może trzeba uwzględnić trochę więcej
zagadnień, ale zasady pozostają bez zmian.
Aby usunąć odpowiedzi z aplikacji i dodać przesyłanie odpowiedzi użytkownika, należy:
• Usunąć z obiektów i tablicy wszelkie dane zawierające prawidłowe odpowiedzi w questions.js.
• Usunąć funkcję gradeTest() i zamienić jej wywołanie w buildQuestion() wraz z printResults().
• Zmodyfikować printResults() tak, aby użytkownik mógł obejrzeć swoje odpowiedzi i umożliwić
przesyłanie ich w postaci HTML do oczekującego serwera.

Usuwanie odpowiedzi z tablicy


Usuń z konstruktora pytań w question.js this.answer oraz this.support. Zmień poniższy zapis:
function question(answer, support, question, a, b, c, d) {
this.answer = answer;
this.support = support;
this.question = question;
this.a = a;
this.b = b;
this.c = c;
this.d = d;
return this;
}

na następujący:
function question(question, a, b, c, d) {
this.question = question;
this.a = a;
this.b = b;
this.c = c;
this.d = d;
return this;
}
59 Rozdział 2 - Test sprawdzany na bieżąco

warto zwrócić uwagę, że usunięto też zmienne answer i support. Teraz, kiedy usunąłeś je z konstruktora, można
pozbyć się ich ze wszystkich wywołań operatora new dla każdego elementu units. Innymi słowy, z każdego elementu
units należy usunąć pierwsze dwa parametry.

Usuwanie gradeTest() i modyfikacja buildQuestion()


Jako że nie ma już odpowiedzi ani wyjaśnień, nie ma powodu lokalnie oceniać testu czy wyświetlać jego wyników.
Oznacza to, że możesz pozbyć się funkcji gradeTest(). Po prostu w pliku administer.html należy usunąć wiersze 66
do 76. Można też pozbyć się wywołania gradeTest() w buildQuestion() w wierszu 40. Warto zastąpić to
wywołaniem printResults(), aby użytkownik mógł zobaczyć swoje odpowiedzi w postaci HTML.
Wiersze 39 do 42 zmień z poniższej wartości:
if (qIdx == howMany) {
gradeTest();
return;
}

na:
if (qIdx == howMany) {
printResults();
return;
}

Modyfikacja printResults()
Największe zmiany czekają nas właśnie w funkcji printResults(). Wiersz 84 w administer.html, wyglądający obecnie
tak:
'<BR><BR><FONT SIZE=4>Oto Twoje oceny: </FONT><BR><BR>';

zmieńmy na:
'<BR><BR><FoONT SIZE=4>Oto jak odpowiadałeś: </FONT><BR><BR>' +
'FORM ACTION="URL_twojego_skryptu_serwerowego" METHOD=POST>';

Zastąpmy też wierze 92 do 105 w sposób następujący:


results += '<INPUT TYPE=HIDDEN NAME="question' + (i + 1) +
'"VALUE="' + keeper[i] + '"><B><I><FONT COLOR=GREEN>Wybrałeś ' +
keeper[i] + '</I></B></FONT><BR><BR><BR>';

W rezultacie usuwamy z funkcji decyzję, czy użytkownik odpowiedział prawidłowo, i wyświetlanie tekstu zielonego
lub czerwonego. W końcu wiersz 107 wyglądający tak:
results += '\n\r</BODY></HTML>';

zmieńmy na:
results += '<INPUT TYPE=SUBMIT VALUE="Wyślij"> </FORM></BODY></HTML>';

Te drobne zmiany spowodowały dodanie początkowego i końcowego znacznika FORM, pola ukrytego z odpowiedzią
użytkownika jako wartością oraz przycisku SUBMIT. Znaczniki FORM oraz przycisk wysyłania są statyczne, pole ukryte
zawiera coś więcej.
Każdą odpowiedź zapisują się jako wartość pola ukrytego, które nazywane jest stosownie do numeru pytania. Zmienna
iteracji i używana jest do stworzenia niepowtarzalnej nazwy każdego pola ukrytego, przypisuje też odpowiedni numer
pytania właściwej odpowiedzi użytkownika. W każdym przejściu pętli for wartość zmiennej i zwiększana jest o 1 (i+
+) i można utworzyć nowe pole ukryte. Pola te nazywają się question1, question2, question3 i tak dalej.

Po tych zmianach printResults() nadal wyświetla pytania, cztery możliwe odpowiedzi oraz odpowiedzi
użytkownika. Jednak tym razem test nie jest tu oceniany. Użytkownik wciska przycisk SUBMIT, aby wysłać odpowiedzi
do oceny.

Przekształcenie na ankietę
Jako że w ankietach teoretycznie nie ma odpowiedzi dobrych ani złych, przekształcenie naszej aplikacji na ankietę
wymaga tych samych zmian, które przed chwilą pokazano, oraz jeszcze jednego – dostosowania treści. Po prostu
należy zmienić elementy units tak, aby odpowiadały pytaniom ankiety z możliwymi opcjami wyboru, i gotowe.
Dzięki takim zmianom użytkownik może obejrzeć wyniki przed wysłaniem ankiety do analizy.
Cechy aplikacji:
 Kolejne slajdy zawierają grafikę i tekst
 Kontekstowa nawigacja po slajdach
 Tryb automatyczny
 Łatwa obsługa slajdów i skalowalność
aplikacji
Prezentowane techniki:
 Pierwszy krok ku DHTML działającemu

3
w różnych przeglądarkach
 Korzyści wynikające ze stosowania
prostych konwencji nazewniczych
 Siła funkcji eval()
 Użycie setInterval() i clearInterval()

Interaktywna
prezentacja
slajdów

Aplikacja ta pozwala użytkownikom przeglądać w kolejności grupę slajdów wyrywkowo lub w sekwencji pokazywanej
automatycznie, z ustalonym czasem prezentacji jednego slajdu. Każdy slajd to warstwa DHTML zawierająca obrazek
oraz tekst go opisujący. Slajdy mogą posiadać dowolną mieszankę grafiki, tekstu, DHTML i tak dalej. Nasz pokaz
zaprezentuje nieco fantastyczne dzikie zwierzęta. Na rysunku 3.1 pokazano ekran początkowy.
61 Rozdział 3 - Interaktywna prezentacja slajdów

Rysunek 3.1. Slajd początkowy


Zwróć uwagę na to, że sam obrazek jest pośrodku, natomiast po lewej stronie na górze znajdują się dwa elementy
graficzne: Automate i <Guide>. Strzałki w <Guide> (znaki < i >) pozwalają użytkownikowi przeglądać kolejne
slajdy, do przodu lub wstecz.
Użytkownicy mogą też przeglądać slajdy kolejno, klikając samo Guide. Pokazuje się wtedy menu slajdów
automatycznie przenoszące użytkownika do slajdu, nad którego nazwą pojawi się kursor. Ponowne kliknięcie Guide
znów ukrywa wspomniane menu, pokazane na rysunku 3.2.
W poprzednich dwóch rozdziałach aplikacje działały od początku do końca: zawsze tak samo się zaczynały
(na przykład wprowadzeniem tekstu lub odpowiedzeniem na pierwsze pytanie), podobnie również się kończyły
(pokazaniem strony z wynikami wyszukiwania lub wyświetleniem wyniku testu). W przypadku slajdów mamy
do czynienia z inną sytuacją: użytkownik może dowolnie się przemieszczać i korzystać z aplikacji. Lepiej jest zatem
opisywać kod aplikacji w kontekście pełnionych przez niego funkcji, zamiast opisywać go w kolejności zapisu. Tak
właśnie skonstruowany jest ten rozdział.
62

Rysunek 3.2. Podświetlona nazwa oznacza właśnie pokazywany slajd

Wymagania programu
Kiedy tylko zauważasz literę „D” w DHTML, to już wiesz, że mówisz o MSIE 4.x, Navigatorze 4.x i nowszych.
Wszystkie slajdy to encje oparte na DHTML. Mógłbyś umieścić w prezentacji nawet setki obrazków, ale problemem
może być wydajność systemu. Aplikacja ta zawczasu ładuje wszystkie obrazki (poza dwoma małymi), więc wątpię, czy
będziesz chciał poświęcić czas na ładowanie tylu obrazków.

Struktura progamu
Cały skrypt zawarty jest w jednym pliku, index.html. Można go znaleźć w katalogu ch03 w pliku ZIP. Kod pokazano
w przykładzie 3.1.

Przykład 3.1. index.html


1 <HTML>
2 <HEAD>
3 <TITLE>Pokaz slajdów</TITLE>
4
5 <STYLE TYPE="text/css">
6 #menuConstraint { height: 800; }
7 </STYLE>
8
9 <SCRIPT LANGUAGE="JavaScript1.2">
10 <!--
11 var dWidLyr = 450;
12 var dHgtLyr = 450;
13 var curSlide = 0;
14 var zIdx = -1;
15 var isVis = false;
16
17 var NN = (document.layers ? true : false);
18 var sWidPos = ((NN ? outerWidth : screen.availWidth) / 2) -
19 (dWidLyr / 2);
20 var sHgtPos = ((NN ? outerHeight : screen.availHeight) / 2) -
21 (dHgtLyr / 2);
22 var hideName = (NN ? 'hide' : 'hidden');
23 var showName = (NN ? 'show' : 'visible');
24
25 var img = new Array();
63 Rozdział 3 - Interaktywna prezentacja slajdów

26 var imgOut = new Array();


27 var imgOver = new Array();
28 var imgPath = 'images/';
29
30 var showSpeed = 3500;
31 var tourOn = false;
32
33 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
34 if (NN) {
35 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
36 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
37 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + (++zIdx) + '>' +
38 copy + '</LAYER>');
39 }
40 else {
41 document.writeln('<DIV ID="' + sName +
42 '" STYLE="position:absolute; overflow:none; left:' + sLeft +
43 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' + sHgt +
44 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) + '">' +
45 copy + '</DIV>');
46 }
47 }
48
49 function slide(imgStr, scientific, copy) {
50 this.name = imgStr;
51 imagePreload(imgStr);
52 this.copy = copy;
53 this.structure =
54 '<TABLE WIDTH=500 CELLPADDING=10><TR><TD WIDTH=60% VALIGN=TOP>' +
55 '<IMG SRC=' + imgPath + imgStr + '.gif></TD>' +
56 '<TD WIDTH=40% VALIGN=TOP><H2>Nazwa potoczna:</H2><H2><I>' +

Przykład 3.1. index.html (ciąg dalszy)


57 camelCap(imgStr) + '</I></H2><H3>Nazwa naukowa: </H3><H3><I>' +
58 scientific + '</I></H3>' + '<B>Krótki opis:</B><BR>' + copy +
59 '</TD></TR></TABLE>';
60
61 return this;
62 }
63
64 function imagePreLoad(imgStr) {
65 img[img.length] = new Image();
66 img[img.length - 1].src = imgPath + imgStr + '.gif';
67
68 imgOut[imgOut.length] = new Image();
69 imgOut[imgOut.length - 1].src = imgPath + imgStr + 'out.gif';
70
71 imgOver[imgOver.length] = new Image();
72 imgOver[imgOver.length - 1].src = imgPath + imgStr + 'over.gif';
73 }
74
75 var slideShow = new Array(
76 new slide('bird', 'Bomb-zis Car-zes', 'Ptak - to skrzydlate stworzenie ' +
77 znane jest z wyszukiwania i paskudzenia świeżo umytych samochodów.'),
78 new slide('walrus', 'Verius Clueless', 'Tłuścioch mors to niezły rybak, ' +
79 ale mycie zębów to już inna historia.'),
80 new slide('gator', 'Couldbeus Luggajus', 'Aligator to gadzina często będąca ' +
81 maskotką podczas lokalnych zawodów sportowych.'),
82 new slide('dog', 'Makus Messus', 'Pies to najlepszy przyjaciel człowieka? ' +
83 'No to nie dziw, że te ssaczyny mają taką złą reputację.'),
84 new slide('pig', 'Oinkus Lotsus', 'Świnia - za takowe często są uważane ' +
85 'osoby o wątpliwych manierach przy jedzeniu.'),
86 new slide('snake', 'Groovius Dudis', 'Wąż jest śliskim i podstępnym ' +
87 'stworzeniem pilnie dookoła się rozglądającym.'),
88 new slide('reindeer', 'Redius Nosius', 'Renifer - choć jego kompani ' +
89 'zeń się śmieją i go przezywają, to jednak zdobył sobie należny ' +
90 'szacunek.'),
91 new slide('turkey', 'Goosius Is Cooktis', 'Indyk w Ameryce przez cały rok ' +
92 'otaczany powszechną opieką, ale potem podawany na obiad.'),
93 new slide('cow', 'Gotius Milkus', 'Zwierzę o dość wątpliwej reputacji. ' +
94 'Wykorzystuje do cna wszelkie napotkane stworzenia. Wyjątkowo paskudna ' +
95 'postać.'),
96 new slide('crane', 'Whooping It Upus', 'Żurawia nie da się pomylić ' +
97 'z maszyną budowlaną o tej samej nazwie. Mówi się, że jest on źródłem ' +
98 'terminu <I>ptasia noga</I>.')
99 );
64

100
101 function camelCap(str) {
102 return str.substring(0, 1).toUpperCase() + str.substring(1);
103 }
104
105 function genScreen() {
106 var menuStr = '';
107 for (var i = 0; i < slideShow.length; i++) {
108 genLayer('slide' + i, sWidPos, 45, dWidLyr, dHgtLyr,
109 (i == 0 ? showName : hideName), slideShow[i].structure);
110 menuStr += '<A HREF="" onMouseOver="hideStatus(); if(!tourOn)

Przykład 3.1. index.html (ciąg dalszy)


111 { setSlide(' + i + ');' +
112 ' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};' +
113 ' return true;"' +
114 ' onMouseOut="hideStatus(); if(!tourOn) { setSlide(' + i + ');' +
115 ' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', false)}; ' +
116 ' return true;"' +
117 ' onClick="return false;"><IMG NAME="' + slideShow[i].name +
118 '" SRC="' + imgPath + slideShow[i].name +
119 'out.gif" BORDER=0></A><BR>';
120 }
121
122 genLayer('automation', sWidPos - 100, 11, 100, 200, true,
123 '<A HREF="javascript: autoPilot();" onMouseOver="hideStatus(); ' +
124 'return true;">' +
125 '<IMG SRC="images/automate.gif" BORDER=0></A>'
126 );
127
128 genLayer('guide', sWidPos - 100, 30, 100, 200, true,
129 '<A HREF="javascript: if(!tourOn) { changeSlide(-1); }" ' +
130 'onMouseOver="hideStatus(); return true;">' +
131 '<IMG SRC="images/leftout.gif" BORDER=0></A>' +
132 '<A HREF="javascript: if(!tourOn) { menuManager(); }" ' +
133 'onMouseOver="hideStatus(); return true;">' +
134 '<IMG SRC="images/guideout.gif" BORDER=0></A>' +
135 '<A HREF="javascript: if(!tourOn) { changeSlide(1); }" ' +
136 'onMouseOver="hideStatus(); return true;">' +
137 '<IMG SRC="images/rightout.gif" BORDER=0></A></DIV>'
138 );
139
140 genLayer('menu', sWidPos - 104, 43, 100, 200, false,
141 '<DIV ID="menuConstraint"><TABLE><TD>' +
142 menuStr + '</TD></TABLE></DIV>'
143 );
144 }
145
146 function refSlide(name) {
147 if (NN) { return document.layers[name]; }
148 else { return eval('document.all.' + name + '.style'); }
149 }
150
151 function hideSlide(name) {
152 refSlide(name).visibility = hideName;
153 }
154
155 function showSlide(name) {
156 refSlide(name).visibility = showName;
157 }
158
159 function menuManager() {
160 if (isVis) { hideLayer('menu'); }
161 else { showLayer('menu'); }
162 isVis = !isVis;
163 }
164
165 function changeSlide(offset) {
166 hideLayer('slide' + curSlide);
167 curSlide = (curSlide + offset < 0 ? slideShow.length - 1 :
168 (curSlide + offset == slideShow.length ? 0 : curSlide + offset));
169 showSlide('slide' + curSlide);
170 }
171
172 function setSlide(ref) {
173 if (tourOn) { return; }
65 Rozdział 3 - Interaktywna prezentacja slajdów

174 hideSlide('slide' + curSlide);

Przykład 3.1. index.html (dokończenie)


175 curSlide = ref;
176 showSlide('slide' + curSlide);
177 }
178
179 function imageSwap(imagePrefix, imageIndex, isOver) {
180 if (isOver) { document[imagePrefix].src = imgOver[imageIndex].src; }
181 else { document[imagePrefix].src = imgOut[imageIndex].src; }
182 }
183
184 function hideStatus() { window.status = ''; }
185
186 function autoPilot() {
187 if (tourOn) {
188 clearInterval(auto);
189 imageSwap(slideShow[curSlide].name, curSlide, false);
190 }
191 else {
192 auto = setInterval('automate()', showSpeed);
193 imageSwap(slideShow[curSlide].name, curSlide, true);
194 showSlide('menu');
195 visible = true;
196 }
197 tourOn = !tourOn;
198 }
199
200 function automate() {
201 imageSwap(slideShow[curSlide].name, curSlide, false);
202 changeSlide(1);
203 imageSwap(slideShow[curSlide].name, curSlide, true);
204 }
205
206 //-->
207 </SCRIPT>
208 </HEAD>
209 <BODY BGCOLOR=WHITE>
210 <CENTER>
211 <FONT FACE=Arial>
212 <H2>Prezentacja królestwa zwierząt</H2>
213 </FONT>
214 </CENTER>
215 <SCRIPT LANGUAGE="JavaScript1.2">
216 <!--
217 genScreen();
218 //-->
219 </SCRIPT>
220 </FONT>
221 </BODY>
222 </HTML>

Zmienne
Najpierw przyjrzyjmy się zmiennym i innym szczegółom, później przejdziemy do funkcji. Oto wiersze 5–7:
<STYLE TYPE="text/css">
#menuConstraint { height: 800; }
</STYLE>
W ten sposób definiuje się arkusz stylów o nazwie menuConstraint, z jedną tylko właściwością, określającą
wysokość na 800 pikseli. Arkusz ten stosowany jest do wszystkich tworzonych slajdów, dzięki czemu użytkownicy
będą mieli na pewno dość miejsca na obejrzenie slajdów. Innymi słowy, jeśli użytkownik ustawił rozdzielczość
monitora mniejszą niż 800 pikseli, ten arkusz stylów wymusi dodanie pionowych pasków przewijania. Jest to
szczególnie przydatne, gdy nasze obrazki są wysokie lub jeśli mamy ich dużo. Użytkownicy będą przynajmniej mogli
zastosować przewijanie do obejrzenia całości. Wiersze 11–31 zawierają zmienne:
var dWidLyr = 450;
var dHgtLyr = 450;
var curSlide = 0;
var zIdx = -1;
var isVis = false;

var NN = (document.layers ? true : false);


var sWidPos = ((NN ? outerWidth : screen.availWidth) / 2) -
(dWidLyr / 2);
66

var sHgtPos = ((NN ? outerHeight : screen.availHeight) / 2) -


(dHgtLyr / 2);
var hideName = (NN ? 'hide' : 'hidden');
var showName = (NN ? 'show' : 'visible');

var img = new Array();


var imgOut = new Array();
var imgOver = new Array();
var imgPath = 'images/';

var showSpeed = 3500;


var tourOn = false;
Zmienne podzielono na cztery grupy:
• właściwości warstw DHTML,
• zmienne związane z obsługą poszczególnych przeglądarek,
• zmienne związane z obrazkami,
• zmienne pokazu.

Ustawienia domyślne warstwy DHTML


Zmienne dWidLyr i dHgtLyr służą do zadeklarowania szerokości i wysokości slajdów. Zmienna curSilde zawsze
zawiera indeks bieżącego slajdu w tablicy slajdów. Zmienna zIdx określa wymiar z dla każdej tworzonej warstwy,
natomiast isVis zawiera wartość logiczną określającą, czy dana warstwa jest obecnie widoczna.

Do slajdów odwołuję się jako do warstw DHTML lub po prostu warstw. Nie należy
mylić ich ze znacznikiem LAYER Netscape Navigatora. Innymi słowy warstwa to
LAYER w Netscape Navigatorze, ale nie w Internet Explorerze.

Zmienne związane z przeglądarkami


Następnych pięć zmiennych, NN, sWidPos, sHgtPos, showName i hideName to zmienne, których ustawienie zależny
od przeglądarki, do której aplikacja jest załadowana. Zmienna NN jest w 17. wierszu ustawiana na true, jeśli istnieje
właściwość layers obiektu document – czyli gdy mamy do czynienia z przeglądarką Netscape Navigator w wersji
co najmniej 4.x, gdyż model dokumentu tej przeglądarki taką właściwość rozpoznaje:
var NN = (document.layers ? true : false);
W innym przypadku skrypt zakłada, że ma do czynienia z przeglądarką Internet Explorer 4.x i ustawia NN na false.
W modelu dokumentu Microsoftu do warstw odwoływać się należy w obiekcie styles obiektu document.all.
Zmienne sWidPos i sHgtPos mają wartości współrzędnych x i y położenia lewego górnego rogu warstwy, która
zostanie umieszczona pośrodku okna przeglądarki (nie ekranu). Zmienne te nie są ustawiane tylko na podstawie
wartości NN, ale korzysta się też ze zmiennych dWidLyr i dHgtLyr. Oto wiersze 18–21:
var sWidPos = ((NN ? outerWidth : screen.availWidth) / 2) -
(dWidLyr / 2);
var sHgtPos = ((NN ? outerHeight : screen.availHeight) / 2) -
(dHgtLyr / 2);
Jak ustala się wartości współrzędnych x i y? Łatwo byłoby określić współrzędne środka, dzieląc odpowiednio szerokość
i wysokość okna przeglądarki przez dwa.
Znamy współrzędne środka okna, tymczasem chcemy, żeby były to jednocześnie współrzędne środka poszczególnych
warstw. Wystarczy zatem odjąć od współrzędnej x środka połowę wartości dWidLyr, a od współrzędnej y połowę
wartości dHgtLyr.
Pozostałe dwie zmienne zawierają odpowiednie napisy oznaczające w danym modelu pokazanie lub ukrycie warstwy –
wiersze 22 i 23:
var hideName = (NN ? 'hide' : 'hidden');
var showName = (NN ? 'show' : 'visible');

Zgodnie z DOM Netscape warstwy ukryte mają właściwość visibility ustawioną na hide, natomiast w DOM
Microsoftu taka sama właściwość ustawiana jest na hidden. Analogicznie warstwy widoczne oznaczane są przez show
i visible.
67 Rozdział 3 - Interaktywna prezentacja slajdów

Zgodnie z DOM Netscape szerokość i wysokość okna (obiekt window) to innerWidth i innerHeight, natomiast
Microsoft umieszcza te same wielkości w obiekcie screen, we właściwościach availWidth i availHeight. Jako że
do obsłużenia tej sytuacji ustawia się zmienną NN, JavaScript rozpoznaje, do jakich właściwości ma się odwoływać.

Zmienne związane z obrazkami


Następna grupa zmiennych składa się z tablic służących do zarządzania obrazkami. Oto wiersze 25 do 28:
var img = new Array();
var imgOut = new Array();
var imgOver = new Array();
var imgPath = 'images/';

Rzecz jest dość prosta. Obrazki przechowywane w tablicy img to grafika slajdów. Umieszczone w imgOut używane są
jako elementy menu slajdów. Obrazki w imgOver stosuje się do menu przewijania obrazków. Dokładniej zajmiemy się
tym, kiedy omówimy funkcję swapImage().
Ostatnia zmienna, imgPath, określa ścieżkę dostępu do tych obrazków na serwerze sieciowym. Może to być ścieżka
względna lub bezwzględna. Ścieżka bezwzględna zawiera pełną lokalizację plików wraz z nazwą serwera lub adresem
IP (na przykład http://www.oreilly.com/), lub napęd lokalny (na przykład C:\), aż do katalogu z obrazkami. Oto dwa
przykłady:
var imgPath = 'http://www.serve.com/hotsyte/';
var imgPath = 'C:\\Winnt\\Profiles\\Administrator\\Desktop\\';

W celu wstawienia w systemie Windows lewego ukośnika trzeba używać dwóch takich ukośników (\\). Jeśli tego
nie zrobimy, JavaScript zrozumie zapis następująco:
var imgPath = 'C:\Winnt\Profiles\Administrator\Desktop\';

To nie tylko nieprawidłowy adres, ale w ogóle błąd składniowy.

Zmienne automatycznego pokazu


Ostatnie dwie zmienne, showSpeed i tourOn, określają szybkość zmieniania slajdów w przypadku autopokazu
i informują, czy w ogóle funkcja ta została włączona. Oto wiersze 30 i 31:
var showSpeed = 3500;
var tourOn = false;

Zmienna showSpeed podawana jest w milisekundach. Można zwiększyć czas pokazywania jednego slajdu na przykład
do 10 sekund, ustawiając wartość 10000. Można też zrealizować błyskawiczne przewijanie, ustawiając wartość 10.
Kiedy załadowana zostanie pierwsza strona, autopokaz nie jest domyślnie uruchamiany, zatem tourOn ustawiana jest
na false.

Funkcje aplikacji
Funkcje naszego pokazu slajdów podzielić można na trzy grupy: tworzenie warstw, obsługę obrazków oraz
nawigację/wyświetlanie. W tabeli 3.1 krótko opisano wszystkie funkcje i zaznaczono, do której grupy należą.

Tabela 3.1. Funkcje i ich opis


Nazwa funkcji Grupa Opis
genLayer() warstwy generuje slajdy
slide() warstwy konstruktor obiektu slajdu
imagePreLoad() obrazki ładuje wstępnie grafikę slajdu i pasek nawigacyjny
camelCap() warstwy zmienia pierwszą literę nazwy slajdu na wielką
genScreen() warstwy wywołuje genLayer() i pozycjonuje wszystkie
warstwy
hideSlide() warstwy ukrywa warstwy
showSlide() warstwy pokazuje warstwy
refSlide() warstwy zwraca wskaźnik warstwy w zależności
od stosowanej przeglądarki
menuManager() warstwy ukrywa i pokazuje menu slajdów
changeSlide() warstwy zmienia aktualny slajd w przypadku stosowania
strzałek polecenia <Guide> lub autopokazu
68

setSlide() warstwy zmienia bieżący slajd na podstawie zdarzeń obsługi


myszy
imageSwap() obrazki przewija obrazki na podstawie menu
hideStatus() nawigacja ustawia wartość paska stanu na ""
autoPilot() nawigacja steruje trybem autopokazu
automate() nawigacja realizuje automatyczną zmianę slajdów

Funkcje związane z warstwami


Jako że większa część pokazu realizowana jest przez funkcje warstw, warto od nich zacząć.
genLayer()
Funkcja ta stanowi podstawę obsługi DHTML w różnych przeglądarkach. Cokolwiek wyświetlamy w ramach pokazu,
niezależnie od tego, jak duże, małe, wielokolorowe czy urozmaicone są pokazywane slajdy – zawsze przechodzi
właśnie tędy. Przyjrzyjmy się dokładnie wierszom 33–47:
function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
if (NN) {
document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
' VISIBILITY="' + sVis + '"' + ' z-Index=' + (++zIdx) + '>' +
copy + '</LAYER>');
}
else {
document.writeln('<DIV ID="' + sName +
'" STYLE="position:absolute; overflow:none; left:' + sLeft +
'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' + sHgt +
'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) + '">' +
copy + '</DIV>');
}
}

Funkcja ta w zasadzie zawiera pojedynczą instrukcję if-else. Tak naprawdę genLayer() realizuje w obu gałęziach tej
instrukcji to samo, tyle że odpowiedni kod działa albo w przeglądarce Netscape Navigator, albo w Internet Explorerze.
Dopóki model dokumentu nie zostanie ujednolicony, tak będziemy musieli działać.
W wierszu 34 zmiennej NN używa się do określenia, czy przeglądarką użytkownika jest Netscape Navigator, czy
(zapewne) Microsoft Internet Explorer. Jeśli NN równa jest true, przeglądarką jest Netscape Navigator.
Zwróćmy uwagę na to, jakich argumentów oczekuje się w wierszu 33. S¹ to sName, sLeft, sTop, sWdh, sHgt, sVis
i copy. Niezależnie od stosowanej przeglądarki wszystkie one mają takie samo znaczenie. SName to nazwa, jaką
chcemy nadać warstwie. sLeft to liczba pikseli od lewego brzegu ekranu do warstwy, a sTop to odległość w pikselach
od górnego brzegu. sWdh i sHgt stanowi odpowiednio szerokość i wysokość warstwy. sVis to true lub false,
informujące, czy warstwa jest widoczna. copy zawiera napis, który ma być wyświetlony jako zawartość warstwy. Treść
jest w zasadzie kodem HTML, ale może mieć również postać zwykłego tekstu.
Niezależnie od tego, jaka przeglądarka jest używana, genLayer() wywołuje metodę document. writeln() i tworzy
w Netscape Navigatorze znacznik LAYER, a w Internet Explorerze znacznik DIV.
slide()
Funkcja slide() to konstruktor obiektu. Poszczególne wystąpienia slide() mogą zawierać ważne szczegóły
dotyczące poszczególnych slajdów: nazwę zwierzęcia, tekst opisowy i treść HTML. Przyjrzyjmy się wierszom 49
do 62:
function slide(imgStr, scientific, copy) {
this.name = imgStr;
imagePreload(imgStr);
this.copy = copy;
this.structure =
'<TABLE WIDTH=500 CELLPADDING=10><TR><TD WIDTH=60% VALIGN=TOP>' +
'<IMG SRC=' + imgPath + imgStr + '.gif></TD>' +
'<TD WIDTH=40% VALIGN=TOP><H2>Nazwa potoczna:</H2><H2><I>' +
camelCap(imgStr) + '</I></H2><H3>Nazwa naukowa: </H3><H3><I>' +
scientific + '</I></H3>' + '<B>Krótki opis:</B><BR>' + copy +
'</TD></TR></TABLE>';

return this;
}
69 Rozdział 3 - Interaktywna prezentacja slajdów

Techniki języka JavaScript:


pierwszy krok ku DHTML działającemu w różnych
przeglądarkach
Przed pojawieniem się przeglądarek w wersji 4.x i DHTML, projektanci stron sieciowych
musieli zadowolić się Internet Explorerem 3.x, mimo że ten nie był zgodny ze specyfika-
cją JavaScript 1.1. Oznaczało to między innymi brak możliwości przewijania obrazków,
kiepską obsługę plików źródłowych JavaScriptu i konieczność realizacji obejść.
Teraz jednak sytuacja jest lepsza, można bowiem realizować strony zgodne z obiema
przeglądarkami. Jedną z najpotężniejszych broni jest document.all.
W celu uproszczenia aplikacji stosuje się jedynie instrukcję warunkową:
if (document.all) { // Mamy do czynienia z MSIE
// Użyj odpowiedników Jscript
// np. document.all.styles, itd.
}
else { { // teraz NN
// trzymamy się JavaScriptu
// np. document.layers, itd.
}

Funkcja slide() pobiera trzy argumenty: imgStr, scientific i copy. imgStr to nazwa „dzikiego” zwierzęcia
opisanego na slajdzie, jest to zarazem z wielu punktów widzenia rdzeń slajdu.9 Teraz jest właśnie dobry moment
na omówienie konwencji nazewniczych stosowanych w tej aplikacji. Nazwę związaną z obiektem slide określa się
w wierszu 50:
this.name = imgStr;
imgStr pojawia się jeszcze w kilku miejscach. Zajrzyjmy do wierszy 53–59, gdzie ustawiana jest właściwość
structure slajdu:
this.structure =
'<TABLE WIDTH=500 CELLPADDING=10><TR><TD WIDTH=60% VALIGN=TOP>' +
'<IMG SRC=' + imgPath + imgStr + '.gif></TD>' +
'<TD WIDTH=40% VALIGN=TOP><H2>Nazwa potoczna:</H2><H2><I>' +
camelCap(imgStr) + '</I></H2><H3>Nazwa naukowa: </H3><H3><I>' +
scientific + '</I></H3>' + '<B>Krótki opis:</B><BR>' + copy +
'</TD></TR></TABLE>';
W celu dynamicznego tworzenia obrazków slajdów łączony jest znacznik HTML <IMG> ze zmiennymi imgPath
i imgStr, po czym dopisuje się gif. Jeśli imgStr miała wartość pig, zapis obrazka będzie wyglądał następująco:
<IMG SRC='images/pig.gif'>
Właściwość structure określa treść slajdu jako tabelę HTML z jednym wierszem i dwiema komórkami. Lewa
komórka zawiera obrazek, prawa natomiast opis. W wierszu 57 imgStr używana jest znów do określenia potocznej
nazwy angielskiej zwierzęcia:
camelCap(imgStr)

Funkcja camelCap() z wierszy 88–90 po prostu zwraca przekazany jej napis z pierwszą literą zmienioną na wielką.
Jest to związane z formatowaniem i po prostu poprawia wygląd całości. Warto zwrócić uwagę, że argument
scientific zawiera nazwę naukową zwierzęcia. Oczywiście po przeczytaniu tych nazw naukowych możesz dojść
do wniosku, że naukowcy nie są zanadto poważnymi ludźmi...
Kiedy wydaje się, że imgStr została już zupełnie wykorzystana, slide() przekazuje ją do funkcji
preLoadImages() (wiersz 51). Ta funkcja z kolei ładuje wstępnie wszystkie obrazki slajdów i wkrótce się nią
zajmiemy.

9
Dlatego też nazwy te musiały pozostać w wersji angielskiej. Więcej na ten temat dalej, przy omawianiu konwencji nazewniczych. (przyp.
tłum.).
70

Techniki języka JavaScript:


poprawne konwencje nazewnicze
Temat konwencji nazewniczych przewija się w całej książce. Przyjrzyjmy się, ile aplika-
cja pokazująca slajdy może uzyskać dzięki użyciu prostych słów: cow, bird i dog. Oczy-
wiście nasza aplikacja nie jest potężną aplikacją korporacyjną, obsługującą niesamowite
ilości danych, ale i tak można uzyskać bardzo dobre wyniki. Nie jest to przy tym kwestia
techniki JavaScriptu, nazewnictwo to technika, której można stosować niezależnie od sto-
sowanego języka. Przyjrzyjmy się, jak prosta konwencja nazewnicza użyta została w odnie-
sieniu do parametru imgStr.
imgStr zawiera nazwę zwierzęcia – niech to będzie np. pig (świnia). Napis wygląda
niepozornie, ale jest to nazwa zwierzęcia, nazwa pliku z obrazkiem slajdu i dwa obrazki
do menu. Cztery obiekty JavaScriptu i nazwa zwierzęcia – wszystkie one wynikają
z jednego tylko napisu. Gra zaczyna być warta świeczki. W poniższej tabelce pokazano,
jak przykładowe wartości imgStr odwzorowywane są na poszczególne obiekty.
imgStr nazwa zwierza obrazek slajdu obrazek menu obrazek menu
pod kursorem
pig pig pig.gif pigout.gif pigover.gif
cow cow cow.gif cowout.gif cowover.gif
snake snake snake.gif snakeout.gif snakeover.gif

genScreen()
Funkcja genScreen() korzysta z możliwości tworzenia warstw przez aplikację do pokazywania treści na ekranie. Jest
to funkcja ze zdecydowanie największą ilością kodu. genScreen() nie tylko decyduje o tworzeniu slajdów i ich
pozycjonowaniu, ale definiuje też nawigację. Oto zawierające ją wiersze 105 do 144:
function genScreen() {
var menuStr = '';
for (var i = 0; i < slideShow.length; i++) {
genLayer('slide' + i, sWidPos, 45, dWidLyr, dHgtLyr,
(i == 0 ? showName : hideName), slideShow[i].structure);
menuStr += '<A HREF="" onMouseOver="hideStatus(); if(!tourOn)
{ setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};' +
' return true;"' +
' onMouseOut="hideStatus(); if(!tourOn) { setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', false)}; ' +
' return true;"' +
' onClick="return false;"><IMG NAME="' + slideShow[i].name +
'" SRC="' + imgPath + slideShow[i].name +
'out.gif" BORDER=0></A><BR>';
}

genLayer('automation', sWidPos - 100, 11, 100, 200, true,


'<A HREF="javascript: autoPilot();" onMouseOver="hideStatus(); ' +
'return true;">' +
'<IMG SRC="images/automate.gif" BORDER=0></A>'
);

genLayer('guide', sWidPos - 100, 30, 100, 200, true,


'<A HREF="javascript: if(!tourOn) { changeSlide(-1); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/leftout.gif" BORDER=0></A>' +
'<A HREF="javascript: if(!tourOn) { menuManager(); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/guideout.gif" BORDER=0></A>' +
'<A HREF="javascript: if(!tourOn) { changeSlide(1); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/rightout.gif" BORDER=0></A></DIV>'
);

genLayer('menu', sWidPos - 104, 43, 100, 200, false,


'<DIV ID="menuConstraint"><TABLE><TD>' +
menuStr + '</TD></TABLE></DIV>'
);
}
71 Rozdział 3 - Interaktywna prezentacja slajdów

Właśnie ta funkcja jest odpowiedzialna za tworzenie wszystkich warstw slajdów i trzech warstw dodatkowych,
obsługujących nawigację (jedną dla menu slajdów, jedną dla obrazków <Guide> i jedną dla obrazka Automate). Pętla
for z wierszy 106–120 tworzy warstwy i generuje treść warstwy menu:
var menuStr = '';
for (var i = 0; i < slideShow.length; i++) {
genLayer('slide' + i, sWidPos, 45, dWidLyr, dHgtLyr,
(i == 0 ? showName : hideName), slideShow[i].structure);
menuStr += '<A HREF="" onMouseOver="hideStatus(); if(!tourOn)
{ setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};' +
' return true;"' +
' onMouseOut="hideStatus(); if(!tourOn) { setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', false)}; ' +
' return true;"' +
' onClick="return false;"><IMG NAME="' + slideShow[i].name +
'" SRC="' + imgPath + slideShow[i].name +
'out.gif" BORDER=0></A><BR>';
}

Pętla przechodzi kolejno po wszystkich elementach tablicy slideShow, tworząc za każdym razem warstwę slajdu
przez wywołanie genLayer(). Przyjrzyjmy się temu dokładniej:
genLayer('slide' + i, sWidPos, 45, dWidLyr, dHgtLyr,
(i == 0 ? showName : hideName), slideShow[i].structure);
Konieczne jest przekazanie całego pakietu parametrów. W tabeli 3.2 zestawiono i opisano wszystkie te parametry.

Można mieć wątpliwości do proponowanego przez autora podejścia. O ile słuszne jest
stosowanie jednego identyfikatora do odwoływania się do wszystkich spokrewnionych
obiektów, to nie najlepszym pomysłem jest prezentowanie użytkownikowi tego
identyfikatora jako nazwy opisowej.
Dobrym przykładem jest omawiana aplikacja – zmiana angielskich nazw zwierząt
na polskie oznaczałaby konieczność zmienienia nazw wszystkich plików graficznych
lub przebudowę aplikacji (dodanie funkcji kodującej polską nazwę zwierzęcia
na nazwę angielską, na podstawie której dopiero można określać nazwy plików).
Oczywiście najprostszym rozwiązaniem byłoby dodanie do obiektu zwierzęcia
dodatkowej nazwy i pokazywanie jej zamiast nazwy angielskiej.
Nawet przy pisaniu nowej aplikacji stosowanie od razu polskich nazw zwierząt jako
identyfikatorów jest nieciekawym pomysłem. Nazwa polska musiałby także być
zawarta w nazwie pliku graficznego – na przykład świnia.gif. Pułapka polega na tym,
że w systemie Windows używane jest kodowanie polskich liter Windows 1250,
we wszelkich Uniksach (łącznie z Linuksem) stosowany jest standard ISO 8859-2,
zatem nazwy plików nie są przenośne między tymi systemami (świnia.gif zmieni się
na przykład w œwinia.gif). Pamiętajmy, że większość serwerów sieciowych to serwery
uniksowe.
Jeśli nawet nie przeszkadza komuś taka dziwna nazwa pliku, to i tak jeszcze nie koniec
problemów. W Internecie obowiązuje kodowanie polskich znaków zgodnie
z ISO 8859-2 (obecnie upowszechniający się standard XML w ogóle nie obsługuje
Windows 1250), jeśli zatem chcemy wyświetlić użytkownikowi nazwę zakodowaną
inaczej, na ekranie pojawić się mogą „krzaczki”. Jeśli zmienimy kodowanie nazwy, to
z kolei nie będzie ona zgodna z nazwami plików graficznych!
Nasuwają się więc dwa wnioski:
1. Nie używaj identyfikatorów jako opisów pokazywanych użytkownikowi (uprości
to także tworzenie wersji językowych aplikacji czy stron HTML).
2. W identyfikatorach stosuj jedynie znaki ASCII (w przypadku liter tylko alfabet
łaciński), przynajmniej dopóki nie upowszechni się standard Unicode, a to jeszcze
potrwa.

Tabela 3.2. Parametry genLayer()


Wartość Opis
'slide' + i Tworzy niepowtarzalną, indeksowaną nazwę każdego slajdu,
na przykład slide0, slide1 i tak dalej.
sWidPos Odległość od lewego brzegu okna w pikselach.
sHgtPos Odległość w pikselach od górnego brzegu okna.
dWidLyr Domyślna szerokość slajdu, w tym wypadku 450.
72

dHgtLyr Domyślna wysokość slajdu, w tym wypadku 450.


(i == 0 ? true : Sprawdza, czy slajd jest pokazany (true), czy schowany
false) (false). Początkowo schowane są wszystkie slajdy
poza pierwszym (kiedy i równe jest 0).
slideshow[i].structure Treść slajdu, tekst i grafika, wstawione do tabeli. Pochodzi
z konstruktora slajdu (wiersze 54–59).

Funkcja genLayer() wywoływana jest tyle razy, ile wynosi wartość wyrażenia slideShow.length – warstwa tworzona
jest dla każdego slajdu. Nie ma znaczenia, czy slajdów jest 6, czy 106 – wszystkie obsługiwane są tak samo, w tym jednym
wierszu. Co ciekawe, cała reszta kodu genScreen() służy do uzyskania trzech dodatkowych warstw. Zanim jednak
przejdziemy dalej, przypatrzmy się jeszcze pętli for:
menuStr += '<A HREF="" onMouseOver="hideStatus(); if(!tourOn)
{ setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};' +
' return true;"' +
' onMouseOut="hideStatus(); if(!tourOn) { setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', false)}; ' +
' return true;"' +
' onClick="return false;"><IMG NAME="' + slideShow[i].name +
'" SRC="' + imgPath + slideShow[i].name +
'out.gif" BORDER=0></A><BR>';

Tutaj zaczęliśmy od wiersza 110, ale zmienna menuStr wcześniej była zainicjalizowana jako ciąg pusty, a teraz jej
wartością będzie HTML z kodem służącym do wyświetlania par obrazków uaktywniających się, kiedy znajduje się
nad nimi wskaźnik myszki. Spojrzyjmy na rysunek 3.2, aby sprawdzić, jak działa to menu.
W przypadku każdego slajdu menuStr rozszerza swoją wartość o obrazek odpowiadający danemu slajdowi. Zanim
zaczniemy wyszukiwać pojedyncze i podwójne cudzysłowy, zastanówmy się, co jest potrzebne do każdej pary
obrazków menu:
1. Otwierający znacznik <A HREF>.
2. Kod obsługi zdarzenia onMouseOver, reagujący na najechanie przez użytkownika kursorem myszy
nad obrazek.
3. Kod obsługi zdarzenia onMouseOut, reagujący na opuszczenie przez kursor myszy obrazka.
4. Kod obsługi zdarzenia onClick, mający zapobiec reakcji programu na kliknięcie obrazka z menu przez
użytkownika.
5. Znacznik <IMG> z niepowtarzalnymi wartościami atrybutów NAME i SRC.
6. Zamykający znacznik </A>.
Pozycja 1. jest prosta: po prostu należy ją wpisać.
Pozycja 2. jest troszkę trudniejsza. Aby usunąć uciążliwy tekst w pasku stanu, najpierw przypisywana jest mu wartość pusta
przez wywołanie funkcji hideStatus(). Tę jednowierszową funkcję znajdziemy w wierszu 184.
Następnie – jeśli użytkownik nie ogląda slajdów w trakcie automatycznego pokazu – wywoływana jest funkcja
setSlide() (wkrótce będzie omawiana). Warto zapamiętać, że dostaje ona jako parametr wartość i.
Pozycja 3. wymaga tego, co obsługa zdarzenia onMouseOver, tyle tylko, że nie jest konieczne wywoływanie
hideStatus(), gdyż pasek stanu jest już pusty. W końcu do imageSwap() –zamiast true – przekazywana jest
wartość false.
Pozycja 4. to rzecz łatwa: po prostu należy dodać onClick="false". W ten sposób unika się wszelkich akcji, które
mogłyby wynikać z klikania przez użytkownika.
Oto sposób na zrealizowanie pozycji 5.:
<IMG NAME="' + slideShow[i].name + '" SRC="' + imgPath +
slideShow[i].name + 'out.gif" BORDER=0>

Znacznik <IMG> uzyska niepowtarzalną nazwę z slideShow[i].name. slideShow[i].name jest też używane wraz
ze zmienną imgPath i napisem out.gif do określenia nazwy źródła obrazka <IMG>.
Pozycja 6. to znów prosta sprawa: należy dodać na końcu znacznik <BR> – i gotowe.
Do wartości zmiennej menuStr dodawany jest napis pochodzący ze wspomnianego wcześniej kodu pętli for.
73 Rozdział 3 - Interaktywna prezentacja slajdów

Co się teraz dzieje z menuStr? Jako że menuStr zawiera kod HTML i JavaScript opisujący menu slajdów,
przekazywana jest jako argument funkcji genLayer() w wierszach 140–143:
genLayer('menu', sWidPos - 104, 43, 100, 200, false,
'<DIV ID="menuConstraint"><TABLE><TD>' +
menuStr + '</TD></TABLE></DIV>'
);

To wywołanie zostawiłem na koniec, gdyż pozostałe dwie warstwy nawigacyjne umieszczone są nad menu i wydawało
mi się, że kodowanie w takiej kolejności będzie sensowniejsze. Zwróćmy uwagę na sposób użycia znacznika <DIV>
z atrybutem ID ustawionym na menuConstraint. Dzięki temu zagwarantowana jest wysokość pokazu slajdów
wynosząca 800 pikseli.
Musimy jeszcze dwa razy odwołać się do genLayer(): pierwszy raz po to, aby wywołać rysunek pozwalający
uruchomić i zatrzymać autopokaz, drugi raz w celu umożliwienia przesuwania się strzałkami po slajdach do przodu
i do tyłu. Niewiele potrzeba do stworzenia warstwy autopilota – wiersze 122–126:
genLayer('automation', sWidPos - 100, 11, 100, 200, true,
'<A HREF="javascript: autoPilot();" onMouseOver="hideStatus(); '+
'return true;">' +
'<IMG SRC="images/automate.gif" BORDER=0></A>'
);

Właściwie widzieliśmy już niemalże wszystko, co było do pokazania. Do wywołania funkcji autoPilot() w atrybucie
HREF użyty zostanie protokół javascript:, procedura obsługi zdarzenia onMouseOver wywołuje hideStatus().
Przyszedł czas na to, aby stanąć przed jakimś trudniejszym wyzwaniem, więc przyjrzyjmy się kodowi ostatniej
warstwy. W celu jej utworzenia w wierszach 128–138 wywoływana jest funkcja genLayer(). Zawiera trzy obrazki:
dwie strzałki oraz słowo Guide, co daje razem <Guide>:
genLayer('guide', sWidPos - 100, 30, 100, 200, true,
'<A HREF="javascript: if(!tourOn) { changeSlide(-1); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/leftout.gif" BORDER=0></A>' +
'<A HREF="javascript: if(!tourOn) { menuManager(); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/guideout.gif" BORDER=0></A>' +
'<A HREF="javascript: if(!tourOn) { changeSlide(1); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/rightout.gif" BORDER=0></A></DIV>'
);

Kod obsługi wszystkich obrazków jest niemalże identyczny. Znów w obsłudze obrazków jest mnóstwo kodu, ale tym
razem kliknięcie na lewą i prawą strzałkę warunkowo wywołuje changeSlide(). Przekazanie -1 powoduje
przesunięcie do slajdu poprzedniego, tymczasem wskazanie 1 powoduje przesunięcie do slajdu następnego. Samą
changeSlide() omówimy wkrótce. Wszystko, co robi obrazek <Guide>, to pokazanie lub ukrycie menu slajdów, czym
zajmuje się funkcja menuManager().
Zanim skończymy omawianie genScreen(), zwróćmy uwagę, że całość jest wywoływana między znacznikami
<BODY> przed załadowaniem strony. Internet Explorer nie potrafi tworzyć warstw po załadowaniu dokumentu, więc
musimy ją uruchomić wcześniej. Oto wiersze 215 do 219:
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genScreen();
//-->
</SCRIPT>

Elementy tablicy slideShow


Być może zauważyłeś już zmienną tablicową slideShow. Każdy jej element zawiera właściwości jednego obiektu
slide. Oto tablica slideShow z wierszy 75–99. Mamy tutaj 10 elementów, co odpowiada 10 slajdom zwierząt:
var slideShow = new Array(
new slide('bird', 'Bomb-zis Car-zes', 'Ptak - to skrzydlate stworzenie ' +
znane jest z wyszukiwania i paskudzenia świeżo umytych samochodów.'),
new slide('walrus', 'Verius Clueless', 'Tłuścioch mors to niezły rybak, ' +
ale mycie zębów to już inna historia.'),
new slide('gator', 'Couldbeus Luggajus', 'Aligator to gadzina często będąca ' +
maskotką podczas lokalnych zawodów sportowych.'),
new slide('dog', 'Makus Messus', 'Pies to najlepszy przyjaciel człowieka? ' +
'No to nie dziw, że te ssaczyny mają taką złą reputację.'),
new slide('pig', 'Oinkus Lotsus', 'Świnia - za takowe często są uważane ' +
'osoby o wątpliwych manierach przy jedzeniu.'),
new slide('snake', 'Groovius Dudis', 'Wąż jest śliskim i podstępnym ' +
74

'stworzeniem pilnie dookoła się rozglądającym.'),


new slide('reindeer', 'Redius Nosius', 'Renifer - choć jego kompani ' +
'zeń się śmieją i go przezywają, to jednak zdobył sobie należny ' +
'szacunek.'),
new slide('turkey', 'Goosius Is Cooktis', 'Indyk w Ameryce przez cały rok ' +
'otaczany powszechną opieką, wkrótce po tym roku podawany na obiad.'),
new slide('cow', 'Gotius Milkus', 'Zwierzę o dość wątpliwej reputacji. ' +
'Wykorzystuje do cna wszelkie napotkane stworzenia. Wyjątkowo paskudna ' +
'postać.'),
new slide('crane', 'Whooping It Upus', 'Żurawia nie da się pomylić ' +
'z maszyną budowlaną o tej samej nazwie. Mówi się, że jest on źródłem ' +
'terminu <I>ptasia noga</I>.')
);

Porównajmy wartości przekazywane przy każdym wywołaniu konstruktora slide z oczekiwanymi argumentami.
Pierwszy to nazwa zwierzęcia (i obrazka), drugi to nazwa formalna, a pojawi się w końcu dodatkowy opis. Zwróćmy
uwagę na to, że w wierszu 98 do tego opisu dodano znaczniki HTML. Nie ma oczywiście żadnych przeciwwskazań
do korzystania z takiego rozwiązania w slajdach (więcej informacji na ten temat znajdzie się w sekcji o ewentualnej
rozbudowie aplikacji).
Jeśli nasza lista jest zbyt długa, dobrym pomysłem może być utworzenie osobnego pliku źródłowego JavaScriptu. W moim
przykładzie tablica ma tylko 10 pozycji, więc zostawiłem wszystko razem.

Funkcje związane z obsługą obrazków


Teraz, kiedy mamy już gotowe funkcje obsługujące slajdy, zabierzmy się za analizę sposobu obsługi obrazków.
preLoadImages()
Ta funkcja umożliwia dokładnie to, co sugeruje jej nazwa: wstępnie ładuje obrazki. W kodzie znajduje się w wierszach
od 64 do 73:
function imagePreLoad(imgStr) {
img[img.length] = new Image();
img[img.length - 1].src = imgPath + imgStr + '.gif';

imgOut[imgOut.length] = new Image();


imgOut[imgOut.length - 1].src = imgPath + imgStr + 'out.gif';
imgOver[imgOver.length] = new Image();
imgOver[imgOver.length - 1].src = imgPath + imgStr + 'over.gif';
}

Funkcja ta tworzy nowe obiekty Image i ładuje ich pliki źródłowe, po trzy naraz. Wprawdzie dzięki temu pokaz
slajdów działa szybciej, ale użytkownicy będą za to musieli chwilę poczekać przed jego uruchomieniem.
Zmienne imgPath i imgStr łączone są ze sobą i z końcówkami .gif, out.gif i over.gif, dzięki czemu
uzyskujemy pliki potrzebne nam do wybrania obrazków slajdów. Na przykład ładując slajd o nazwie cow, załadujemy
obrazki cow.gif, cowout.gif i cowover.gif.
imageSwap()
Ta funkcja realizuje przewijanie obrazków, niezależnie od tego, czy użytkownik wywołuje je poprzez wskazanie
myszką, czy dzieje się to podczas automatycznego pokazu. Nie jest to funkcja skomplikowana, a można ją znaleźć
w wierszach 179–182:
function imageSwap(imagePrefix, imageIndex, isOver) {
if (isOver) { document[imagePrefix].src = imgOver[imageIndex].src; }
else { document[imagePrefix].src = imgOut[imageIndex].src; }
}

Wiele skryptów obsługujących tego typu przewijanie, ze skryptem na stronie autora włącznie, realizuje swoje zadanie
w dwóch funkcjach: jednej – obsługującej zdarzenie onMouseOver, drugiej zdarzenie onMouseOut. Można obie
operacje połączyć w jedną funkcję, trzeba tylko użyć dodatkowych parametrów.
Parametry imagePrefix, imageIndex i isOver oznaczają nazwę podstawową (znów imgStr), wskaźnik żądanego
obrazka (wartość i z pętli for w funkcji genScreen()) oraz wartość logiczną wskazującą, czy użyć obrazków
z tablicy imgOver, czy imgOut.
Aby rzecz nieco wyjaśnić, spójrzmy jeszcze raz na wiersze 105–120 w funkcji genScreen(). Zwróć uwagę, jak
dynamiczny skrypt JavaScript generowany jest w wierszu 112:
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};'

Kiedy wynik zostanie zapisany w dokumencie, a i równe jest 0, rzecz będzie wyglądać tak:
75 Rozdział 3 - Interaktywna prezentacja slajdów

imageSwap('bird', 0, true);

Kiedy funkcja jest już raz wywołana, widzimy, co dzieje się dalej: jako że isOver równe jest true, to:
document[bird].src = imgOver[0].src;

A imgOver[0].src to images/birdover.gif. Jeœli isOver równe jest false, obrazkiem jest imgOut[0].src,
czyli images/birdout.gif.

Funkcje nawigacji
Funkcje obsługi slajdów utworzyły slajdy i kontrolki do ich oglądania. Funkcje obsługi obrazków umożliwiły
ładowanie ich i przewijanie. Teraz przyjrzyjmy się, co właściwie czyni pokaz slajdów pokazem – czyli obejrzyjmy
funkcje nawigacyjne.
refSlide(), hideSlide(), showSlide(), menuManager()
Mamy już slajdy zawierające obrazki, teraz chcemy zacząć z tymi slajdami pracować – pokazywać ten wybrany
i ukrywać pozostałe. Zanim będziemy mogli to zrobić, musimy umożliwić odwoływanie się do nich. Wydaje się to
proste: należy się odwołać do nazwy warstwy. Cóż, w rzeczywistości sprawa nie jest tak prosta. Należy użyć nazwy
warstwy, ale też pamiętać, że odwołania do warstwy w Netscape Navigatorze i w Internet Explorerze są różne, bo różne
są obiektowe modele dokumentów. Zajmuje się tym wszystkim funkcja refSlide() z wierszy 146–149:
function refSlide(name) {
if (NN) { return document.layers[name]; }
else { return eval('document.all.' + name + '.style'); }
}

Jeśli użytkownik używa Netscape Navigatora, refSlide() zwraca odwołanie do document. layers[name]. Jeśli
użytkownik ma przeglądarkę Internet Explorer, refSlide() zwraca odwołanie do eval('document.all.' +
name + '.style'). Dzięki temu możemy zmieniać widoczność poszczególnych warstw niezależnie od tego, z jaką
przeglądarką mamy do czynienia. Nie powinna być więc zaskoczeniem postać dwóch funkcji w wierszach 151–157.
Jest to nie tylko proste, ale pomaga później w bardzo łatwym odwoływaniu się do wszystkich elementów.
function hideSlide(name) {
refSlide(name).visibility = hideName;
}

function showSlide(name) {
refSlide(name).visibility = showName;
}

Techniki języka JavaScript:


siła funkcji eval()
Jak to ujmuje Netscape, funkcja eval() „ewaluuje napis zawierający kod JavaScriptu
bez odwoływania się do konkretnego obiektu”. Być może to nie brzmi zbyt interesująco,
ale funkcja dotyczyć może dowolnych obiektów i jest darem niebios dla nas, programi-
stów. Załóżmy, że chcielibyśmy odwołać się do obiektu, ale nie wiemy, jaki jest jego in-
deks (jeśli mamy do czynienia z tablicą) – wtedy właśnie czas użyć tej funkcji:
eval("document.all.styles." + nazwa + ".visibility");

Oto jeszcze inny przykład:


eval("document.forms[0]." + nazwaElementu + ".value");

Będzie to przydatne w wielu sytuacjach, między innymi podczas tworzenia obiektów for-
mularzy i przy przewijaniu obrazków, a także do realizacji obliczeń matematycznych,
przy czym zawsze jako danych wejściowych używamy zwykłych napisów. Zdecydowanie
należy do swojego arsenału narzędzi dołączyć funkcję eval(). Warto odwiedzić stronę
DevEdge Online firmy Netscape, gdzie można znaleźć więcej informacji na temat tej
funkcji: http://developer.netscape.com/docs/manuals/communicator/jsref/glob8.htm.

Obie funkcje wywołują refSlide() i przekazują jej jako parametr otrzymaną wcześniej nazwę. Kod może w pierwszej
chwili wyglądać nieco dziwnie. W jaki sposób refSlide() może mieć właściwość visibility? Faktycznie jej
nie ma. Pamiętajmy jednak, że refSlide() zwraca wskaźnik warstwy, która już zawiera potrzebną właściwość. Jeśli
chcemy dany slajd ukryć, odwołujmy się do niego przez refSlide() i ustawiamy właściwość visibility tak
76

zwróconego obiektu na hideName, czego wartością, jak już wspomniano, jest hide lub hidden (wiersz 22),
w zależności od stosowanej przeglądarki. To samo dotyczy pokazywania slajdu – poza tym, że wartością jest wartość
zmiennej showName ustawianej w wierszu 23.
hideSlide() i showSlide() używane są do ukrywania i pokazywania nie tylko slajdów, ale też menu. Funkcje nie są
wywoływane wtedy bezpośrednio, ale za pośrednictwem funkcji menuManager():
function menuManager() {
if (isVis) { hideLayer('menu'); }
else { showLayer('menu'); }
isVis = !isVis;
}

Kiedy menu slajdów jest widoczne, wartością zmiennej isVis jest true; w przeciwnym wypadku false. Wobec tego
menuManager() pokazuje menu, jeśli isVis ma wartość false, a ukrywa je, gdy isVis równa jest true, przy czym
isVis za każdym razem zmienia swój stan na przeciwny.

changeSlide()
Teraz możemy odwoływać się poprawnie do slajdów niezależnie od używanej przeglądarki. Mamy funkcje pozwalające
pokazywać i ukrywać slajdy (a także pokazywać i ukrywać menu), teraz potrzebujemy funkcji, która będzie potrafiła
zmienić pokazywany slajd. Tak naprawdę mamy dwie funkcje: changeSlide() i setSlide().
Mam nadzieję, że nie doprowadziłem jeszcze czytelnika do rozpaczy całym tym chowaniem i pokazywaniem. Otóż,
zmiana jednego slajdu na inny wymaga zrobienia trzech kroków:
1.Ukrycia bieżącego slajdu.
2.Zdecydowania, jaki slajd ma być następny.
3.Pokazania wybranego w poprzednim kroku slajdu.
Kroki 1. i 3. są niewątpliwie oczywiste, ale krok 2. jest bardziej złożony, niż mogłoby się to wydawać. Istnieją dwie
okoliczności, w których można zmienić slajd. Pierwsza sytuacja dotyczy zmiany slajdów w ustalonej kolejności
(naprzód lub wstecz), do czego służą strzałki < i >. Natomiast drugi przypadek wiąże się z automatycznym przerzucaniem
slajdów. Funkcja changeSlide() została tak napisana, aby potrafiła obsłużyć obie te sytuacje, a znajdziemy ją w wierszach
165–170:
function changeSlide(offset) {
hideLayer('slide' + curSlide);
curSlide = (curSlide + offset < 0 ? slideShow.length - 1 :
(curSlide + offset == slideShow.length ? 0 : curSlide + offset));
showSlide('slide' + curSlide);
}

Najpierw wywoływana jest hideSlide() z wyrażeniem 'slide' + curSlide jako parametrem. curSlide
początkowo, w wierszu 13, ustawiona była na 0. Jako że obecnie jest to oglądany slajd, funkcja hideSlide ukryje
slide0, czyli ptaka. Wydaje się to jasne. Który slajd ma być teraz pokazany?

Pamiętajmy, że changeSlide() jako parametru oczekuje przesunięcia, czyli liczby 1 lub -1. 1 oznacza przesunięcie
wprzód, -1 cofnięcie się o jeden slajd, a w naszym wypadku wyświetlenie z kolei slajdu ostatniego. Ponieważ
curSlide to liczba całkowita oznaczająca indeks bieżącego slajdu, dodanie jedynki zmieniać będzie tę wartość na 1, 2
i tak dalej. -1 powoduje pokazanie poprzedniego slajdu ze slideShow. Jeśli był nim slajd czwarty, o indeksie 3,
wynikiem będą kolejno 2, 1, potem 0.
Wszystko jest dobrze, póki nie próbujemy ukryć slajdu 'slide' + -1 lub 'slide' + slideShow.length. Takie
slajdy nie istnieją i można być pewnym pojawienia się błędów składniowych. Jak zatem uchronić się przed wartością
curSlide mniejszą od zera lub większą od slideShow.length-1?

Odpowiedzią są wiersze 167 i 168:


curSlide = (curSlide + offset < 0 ? slideShow.length - 1 :
(curSlide + offset == slideShow.length ? 0 : curSlide + offset));
Wartość curSlide określana jest przy użyciu zagnieżdżonego operatora trójargumentowego. Oto pseudokod:
JEŚLI curSlide + przesunięcie JEST MNIEJSZE OD 0, TO
curSlide STAJE SIĘ RÓWNE slideShow.length - 1
W PRZECIWNYM WYPADKU
JEŚLI curSlide + przesunięcie JEST RÓWNE slideShow.length, TO
curSlide STAJE SIĘ RÓWNE 0
W PRZECIWNYM WYPADKU
curSlide STAJE SIĘ RÓWNE curSlide + przesunięcie

Kiedy wartość curSlide zostanie już określona, można spokojnie wywołać showSlide() w 169 wierszu.
77 Rozdział 3 - Interaktywna prezentacja slajdów

setSlide()
changeSlide() jest jedną z dwóch funkcji używanych do zmiany slajdów. O ile changeSlide() zmienia slajdy
na następny i poprzedni, to setSlide() ukrywa slajd bieżący i pokazuje dowolny slajd o przekazanym jej indeksie.
Oto ta funkcja, znajdująca się w wierszach 172–177:
function setSlide(ref) {
if (tourOn) { return; }
hideSlide('slide' + curSlide);

curSlide = ref;
showSlide('slide' + curSlide);
}

W pierwszym wierszu sprawdza się wartość zmiennej tourOn, aby nic nie było wykonywane, jeśli jesteśmy w trakcie
automatycznego pokazu, gdyż w takiej sytuacji zmiany slajdów następują automatycznie.
Podobnie jak changeSlide(), i setSlide() ukrywa bieżący slajd, ale tym razem nie ma znaczenia, jaki to był slajd
(curSlide). Wartość parametru ref przypisywana jest zmiennej curSlide, a następnie jako bieżący jest pokazywany
slajd z takim właśnie numerem.
autoPilot()
Jak zapewne łatwo się domyślić, funkcja autoPilot() steruje automatycznym pokazem. Funkcja ta jest włączana
i wyłączana tym samym łączem na ekranie. Zakodowano ją w wierszach 186–198:
function autoPilot() {
if (tourOn) {
clearInterval(auto);
imageSwap(slideShow[curSlide].name, curSlide, false);
}
else {
auto = setInterval('automate()', showSpeed);
imageSwap(slideShow[curSlide].name, curSlide, true);
showSlide('menu');
visible = true;
}
tourOn = !tourOn;
}

Funkcja autoPilot() „wie”, czy automatyczny pokaz został włączony, czy nie, dzięki zmiennej tourOn. Jeśli
wartością tej zmiennej jest false, pokaz jest wyłączony, więc funkcja używa metody setInterval() obiektu
window do wywołania funkcji automate() (omówionej dalej) co showSpeed milisekund.

Dobrze byłoby widzieć przesuwanie się wskaźnika menu podczas zmiany kolejnych slajdów. Jako że użytkownik kliknął
obrazek Automate, autoPilot() pokazuje menu slajdów (jeśli nie było go widać wcześniej) i podświetla pierwszy
pokazywany slajd. Funkcja automate() zajmuje się już resztą.
Jeśli jednak automatyczny pokaz właśnie trwa (zmienna tourOn ma wartość false), autoPilot() za pomocą
metody clearInterval() obiektu window odwoła wywołanie setInterval(), związane ze zmienną auto. Aby
oczyścić sytuację, ostatnie wywołanie imageSwap() znów przywraca niepodświetlony obrazek w menu.
Ostatnie zadanie funkcji autoPilot() to zmiana bieżącej wartości tourOn na wartość przeciwną. Jasne przecież, że
jeśli automatyczny pokaz był włączony, to następnym kliknięciem chce go wyłączyć, i odwrotnie.
78

Techniki języka JavaScript:


używanie setInterval() i clearInteval()
Metody obiektu window o nazwach setInterval() i clearInterval() są nowymi
wersjami – dostępnych w JavaScripcie 1.0 – setTimeout() i clearTimeout(). O ile
setTimeOut() uruchamia kod ze swojego pierwszego parametru tylko raz, setInte-
rval() uruchamia ten kod stale. Aby uzyskać taki sam efekt, trzeba było rekursywnie
wywołać setTimeout() i funkcją zawierającą setTimeout(), ot tak:
y = 50;

function overAndOver() {
//coś robimy
y = Math.log(y)

//i powtórnie wywołanie


setTimeout("overAndOver()", 250);
}

Funkcja overAndOver() mogła z kolei być wywołana tak:


<BODY onLoad="overAndOver()";>

setInterval() sama realizuje rekursję i pozwala użyć jednego tylko wywołania:


y = 50;
function overAndOver() {
//coś robimy
y = Math.log(y);
}

Obsługa zdarzenia onLoad może także zrealizować ten kod. Należy się tylko upewnić, że
operacja wyłączająca zawiera clearInterval().

automate()
automate() to mała funkcja uruchamiająca pokaz przez wykonanie następujących trzech czynności.

1.Symulacja zdarzenia onMouseOut powoduje usunięcie podświetlenia bieżącego slajdu w menu. Realizuje to
funkcja imageSwap().
2.Wywołanie changeSlide() zmienia slajd na następny.
3.Symulacja zdarzenia onMouseOver powoduje podświetlenie w menu następnego slajdu. Realizuje to funkcja
imageSwap().

Oto wiersze 200–204, w których wszystko to się dzieje:


function automate() {
imageSwap(slideShow[curSlide].name, curSlide, false);
changeSlide(1);
imageSwap(slideShow[curSlide].name, curSlide, true);
}

Jeszcze jedna uwaga na koniec. Oba wywołania imageSwap() przekazują wartość curSlide, co może sprawiać
wrażenie, że ten sam slajd jest zaznaczany i odznaczany. Pamiętajmy jednak, że wywołanie changeSlide() zmienia
wartość curSlide. Drugie wywołanie imageSwap() powoduje poprawienie bieżącego slajdu w menu.

Kierunki rozwoju
Tak jak w przypadku niemalże każdej aplikacji DHTML, można do pokazu dodać dziesiątki poprawek. Postaram się
maksymalnie skrócić tę listę.

Losowy dobór slajdów w trybie automatycznym


Dlaczego by trochę nie zamieszać? Wygenerujmy przypadkową liczbę od 0 do slideShow.length-1, a następnie
wywołajmy setSlide(). Oto jak mogłaby wyglądać odpowiednia funkcja:
function randomSlide() {
var randIdx = Math.floor(Math.rand() * slideShow.length);
79 Rozdział 3 - Interaktywna prezentacja slajdów

setSlide(randIdx);
}

Zamiast wywoływać w automate() funkcję changeSlide(), wywołajmy randomSlide():


function automate() {
imageSwap(slideShow[curSlide].name, curSlide, false);
randomSlide();
imageSwap(slideShow[curSlide].name, curSlide, true);
}

Animowane GIF-y i suwaki slajdów


Ta sugestia może wydać się oczywista, ale również przydatna. Użytkownicy lubią interaktywne aplikacje: to, co się
rusza i miga na stronie sieciowej kolorami (pomijając nieszczęsny znacznik BLINK), zrobi dobre wrażenie
na oglądającym.

Animacja samych slajdów


Każdy slajd tworzony w tej aplikacji pozostaje w jednym miejscu. Czasem zdarzają się slajdy pojawiające się
i znikające. Jednak warstwy podczas całego pokazu muszą pozostać na swoim miejscu. Może by się pokusić o
przesuwanie slajdów w lewo i prawo albo z góry na dół?
Otwieramy w ten sposób furtkę do całej nowej aplikacji w aplikacji omawianej, więc nie będę wdawał się tutaj
w szczegóły kodowania, ale warto dodać, gdzie można znaleźć odpowiednie narzędzia JavaScriptu pozwalające
wykonywać na warstwach efekty specjalne. Netscape ma już gotową bibliotekę, czekającą tylko na załadowanie.
Można znaleźć ją pod adresem http://developer. netscape.com/docs/technote/dynhtml/csspapi/xbdhtml.txt.
Warto zwrócić uwagę na rozszerzenie .txt: kiedy już dokument zapiszesz lokalnie, zmień je na .js.

Pod powierzchnią
W tej książce nie będziemy się zagłębiać w DHTML. Istnieje mnóstwo źródeł, które będą
pomocne, jeśli ktoś zechce dalej rozwijać pokaz slajdów. Oto kilka ciekawych adresów:
Dynamic HTML w programie Netscape Communicator:
http://developer.netscape.com/docs/manuals/communicator/dynhtml/index.
htm
Specyfikacja DHTML Microsoftu:
http://msdn.microsoft.com/developer/sdk/inetsdk/help/dhtml/references/dht
mlrefs.htm
Specyfikacja HTML 4.0 w World Wide Web Consortium (W3C):
http://www.w3.org/TR/REC-html40/
Strefa DHTML firmy Macromedia:
http://www.dhtmlzone.com/
Dynamic Drive:
http://dynamicdrive.com/
Cechy aplikacji:
 Oparta na ramkach multiwyszukiwarka
 Wyszukiwanie po jednym kliknięciu
 Proste zarządzanie wyszukiwarkami
Prezentowane techniki:
 Wielokrotne użycie kodu
 Rezygnacja z obiektowości

4
 Matematyka a pamięć
 Użycie funkcji escape()

Interfejs
multiwyszukiwa
rki

W Sieci znajduje się dużo multiwyszukiwarek opartych na JavaScripcie. Tego typu aplikacje są jednymi
z najefektowniejszych i tak naprawdę najprostszych do zaprogramowania w JavaScripcie. Dlaczego zatem
nie spróbować? Możemy użyć danych innych ludzi w celu przerobienia swojej witryny na portal do sieciowego
wszechświata. To jest oczywiście moja wersja. Istnieją także gotowe solidne rozwiązania, ale opisywana aplikacja i tak
umożliwia zdobycie przewagi nad konkurencją niewielkim kosztem. Na rysunku 4.1 pokazano pierwszy ekran
pokazujący się po otworzeniu w przeglądarce pliku ch04/index.html.
81 Rozdział 4 - Interfejs multiwyszukiwarki

Rysunek 4.1. Interfejs multiwyszukiwarki


Użycie aplikacji nie jest skomplikowane. Użytkownik wprowadza tekst zapytania w lewym dolnym rogu, a następnie,
używając strzałek, wybiera ze skonstruowanego za pomocą warstw menu jedną z dostępnych wyszukiwarek. Wszystko,
co użytkownik musi zrobić, to kliknąć przycisk wyszukiwarki, której chce wysłać tekst zapytania, a wyniki pokażą się
w środkowej ramce. Poszukiwanie w bazie danych Lycos terminu „andromeda” zaowocowało wynikami pokazanymi
na rysunku 4.2.

Rysunek 4.2. Odpowiedź wyszukiwarki Lycos na pytanie o hasło „andromeda”


I to już naprawdę wszystko. Zauważmy, że ramka wyników wyszukiwania otoczona jest czarnym obramowaniem. To
mój wkład w określanie postaci stron sieciowych. Wybór takiego rozwiązania jest kwestią gustu, można to łatwo
zmienić, stosując typowy układ dwóch ramek (górna i dolna).
82

Czytelnik śledzący rozdziały po kolei, może zauważyć, że tym razem postępujemy nieco inaczej niż dotąd, gdyż
nie cały kod jest zupełnie nowy. Pokażę tym razem, jak skorzystać z kodu omówionego w rozdziale 3. Przykład ten
doskonale pokaże, jak oszczędzać swój czas, wielokrotnie wykorzystując ten sam kod.

Wymagania programu
W tej aplikacji używamy DHTML, więc będą potrzebne przeglądarki Netscape Navigator lub Internet Explorer w wersji
co najmniej 4.x. Włączyłem do gry 20 wyszukiwarek, ale można tę liczbę zwiększyć nawet do setek. Jednak typowego
użytkownika zadowoli już zapewne liczba 20 wyszukiwarek. Pamiętajmy też, że ta aplikacja może działać na maszynie
lokalnej, ale – jak pokaz slajdów – zbyt duża ilość grafiki zwiększy czas potrzebny do jej załadowania w przypadku
użytkowników działających za pośrednictwem Internetu.

Struktura programu
Aplikacja ta zawiera dwa pliki: index.html i multi.html. Pierwszy z nich, pokazany jako przykład 4.1, używa
zagnieżdżonych ramek w celu uzyskania efektu otaczającego obramowania.

Przykład 4.1. Index.html


1 <HTML>
2 <HEAD>
3 <TITLE>Witryna multiwyszukiwarki</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5 <!--
6 var black = '<BODY BGCOLOR=BLACK></BODY>';
7 var white = '<BODY BGCOLOR=WHITE></BODY>';
8 //-->
9 </SCRIPT>
10 </HEAD>
11 <FRAMESET ROWS="15,*,50" FRAMEBORDER=0 BORDER=0>
12 <FRAME SRC="javascript: parent.black;" SCROLLING=NO>
13 <FRAMESET COLS="15,*,15" FRAMEBORDER=0 BORDER=0>
14 <FRAME SRC="javascript: parent.black;" SCROLLING=NO>
15 <FRAME SRC="javascript: parent.white;" BORDER=0>
16 <FRAME SRC="javascript: parent.black;"
17 SCROLLING=NO>
18 </FRAMESET>
19 <FRAME SRC="multi.html" SCROLLING=NO>
20 </FRAMESET>
21 </HTML>

Dwie zmienne JavaScriptu black i white zdefiniowane w wierszach 6 i 7 zawierają kod HTML, który może zostać
użyty jako wartość atrybutu SRC ramek. Zmienne te użyte są w wierszach 12 i 14–16. Omawialiśmy to w ramce
opisującej techniki JavaScriptu w rozdziale 2. („Oszukany atrybut SRC”). Jeśli czytelnik śledzi rozdziały po kolei,
powinien być teraz kopalnią wiedzy na ten temat. Jedyne ramki realizujące jakieś funkcje, to frames[2], która
wyświetla wyniki wyszukiwania, oraz frames[4], która zawiera interfejs wyszukiwarek. Reszta służy tylko
do pokazywania. Przejdźmy teraz do pliku multi.html, pokazanego w przykładzie 4.2.

Przykład 4.2. multi.html


1 <HTML>
2 <HEAD>
3 <TITLE>Multi-Engine Menu</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5 <!--
6
7 parent.frames[2].location.href = 'javascript: parent.white';
8
9 var NN = (document.layers ? true : false);
10 var curSlide = 0;
11 var hideName = (NN ? 'hide' : 'hidden');
12 var showName = (NN ? 'show' : 'visible');
13 var perLyr = 4;
14 var engWdh = 90;
15 var engHgt = 20;
16 var left = 375;
17 var top = 10;
18 var zIdx = -1;
19 var imgPath = 'images/';
20 var arrayHandles = new Array('out', 'over');
83 Rozdział 4 - Interfejs multiwyszukiwarki

21

Przykład 4.2. multi.html (ciąg dalszy)


22 for (var i = 0; i < arrayHandles.length; i++) {
23 eval('var ' + arrayHandles[i] + ' = new Array()');
24 }
25
26 var engines = new Array(
27 newArray('HotBot',
28 'http://www.hotbot.com/?MT=',
29 'http://www.hotbot.com/'),
30 newArray('InfoSeek',
31 'http://www.infoseek.com/Titles?col=WW&sv=IS&lk=noframes&qt=',
32 'http://www.infoseek.com/'),
33 newArray('Yahoo',
34 'http://search.yahoo.com/bin/search?p=',
35 'http://www.yahoo.com/'),
36 newArray('AltaVista',
37 'http://www.altavista.com/cgi-bin/query?pg=q&kl=XX&q=',
38 'http://www.altavista.digital.com/'),
39 newArray('Lycos',
40 'http://www.lycos.com/cgi-bin/pursuit?matchmode=and&cat=lycos' +
41 '&query=',
42 'http://www.lycos.com/'),
43 newArray('Money.com',
44 'http://jcgi.pathfinder.com/money/plus/news/searchResults.oft?' +
45 'vcs_sortby=DATE&search=',
46 'http://www.money.com/'),
47 newArray('DejaNews',
48 'http://www.dejanews.com/dnquery.xp?QRY=',
49 'http://www.dejanews.com/'),
50 newArray('Insight',
51 'http://www.insight.com/cgi-bin/bp/870762397/web/result.html?' +
52 'a=s&f=p&t=A&d=',
53 'http://www.insight.com/'),
54 newArray('Scientific American',
55 'http://www.sciam.com/cgi-bin/search.cgi?' +
56 'searchby=strict&groupby=confidence&docs=100&query=',
57 'http://www.sciam.com/cgi-bin/search.cgi'),
58 newArray('Image Surfer',
59 'http://isurf.interpix.com/cgi-bin/isurf/keyword_search.cgi?q=',
60 'http://www.interpix.com/'),
61 newArray('MovieFinder.com',
62 'http://www.moviefinder.com/search/results/1,10,,00.html?' +
63 'simple=true&type=movie&mpos=begin&spat=',
64 'http://www.moviefinder.com/'),
65 newArray('Monster Board',
66 'http://www.monsterboard.com/pf/search/USresult.htm?' +
67 'loc=&EmploymentType=F&KEYWORDS=',
68 'http://www.monsterboard.com/'),
69 newArray('MusicSearch.com',
70 'http://www.musicsearch.com/global/search/search.cgi?QUERY=',
71 'http://www.musicsearch.com/'),
72 newArray('ZD Net',
73 'http://xlink.zdnet.com/cgi-bin/texis/xlink/xlink/search.html?' +
74 'Utext=',
75 'http://www.zdnet.com/'),
76 newArray('Biography.com',
77 'http://www.biography.com/cgi-bin/biomain.cgi?search=FIND&field=',
78 'http://www.biography.com/'),
79 newArray('Entertainment Weekly',
80 'http://cgi.pathfinder.com/cgi-bin/ew/cg/pshell? venue= pathfinder&q=',
81 'http://www.entertainmentweekly.com/'),
82 newArray('SavvySearch',
83 'http://numan.cs.colostate.edu:1969/nph-search?' +
84 'classic=on&Boolean=OR&Hits=10&Mode=MakePlan&df=normal&' +
85 'AutoStep=on&KW=',

Przykład 4.2. multi.html (ciąg dalszy)


86 'http://www.savvysearch.com/'),
87 newArray('Discovery Online',
88 'http://www.discovery.com/cgi-bin/searcher/-?' +
89 'output=title&exclude=/search&search=',
90 'http://www.discovery.com/'),
91 newArray('Borders.com',
84

92 'http://www.borders.com:8080/fcgi-bin/db2www/search/' +
93 'search.d2w/QResults?doingQuickSearch=1&srchPage=QResults&' +
94 'mediaType=Book&keyword=',
95 'http://www.borders.com/'),
96 newArray('Life Magazine',
97 'http://cgi.pathfinder.com/cgi-bin/life/cg/pshell?' +
98 'venue=life&pg=q&date=all&x=15&y=16&q=',
99 'http://www.life.com/')
100 );
101
102 engines = engines.sort();
103
104 function imagePreLoad(imgName, idx) {
105 for(var j = 0; j < arrayHandles.length; j++) {
106 eval(arrayHandles[j] + "[" + idx + "] = new Image()");
107 eval(arrayHandles[j] + "[" + idx + "].src = '" + imgPath +
108 imgName + arrayHandles[j] + ".jpg'");
109 }
110 }
111
112 function engineLinks() {
113 genLayer('sliderule', left - 20, top + 2, 25, engHgt, showName,
114 '<A HREF="javascript: changeSlide(1);" ' +
115 'onMouseOver="hideStatus(); return true;">' +
116 '<IMG SRC="images/ahead.gif" BORDER=0></A><BR>' +
117 '<A HREF="javascript: changeSlide(-1);" ' +
118 'onMouseOver="hideStatus(); return true;">' +
119 '<IMG SRC="images/back.gif" BORDER=0></A>');
120 lyrCount = (engines.length % perLyr == 0 ?
121 engines.length / perLyr : Math.ceil(engines.length / perLyr));
122 for (var i = 0; i < lyrCount; i++) {
123 var engLinkStr = '<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=0><TR>';
124 for (var j = 0; j < perLyr; j++) {
125 var imgIdx = (i * perLyr) + j;
126 if (imgIdx >= engines.length) { break; }
127 var imgName = nameFormat(engines[imgIdx][0]);
128 imagePreLoad(imgName, imgIdx);
129 engLinkStr += '<TD><A HREF="javascript: ' +
130 'callSearch(document.forms[0].elements[0].value, ' +
131 imgIdx + ');" ' + 'onMouseOver="hideStatus(); imageSwap(\'' +
132 imgName + '\', ' + imgIdx + ', 1); return true" ' +
133 'onMouseOut="imageSwap(\'' + imgName + '\', ' + imgIdx +
134 ', 0);">' + '<IMG NAME="' + imgName + '" SRC="' + imgPath +
135 imgName + "out.jpg" + '" BORDER=0></A></TD>';
136 }
137 engLinkStr += '</TR></TABLE>';
138 genLayer('slide' + i, left, top, engWdh, engHgt, hideName, engLinkStr);
139 }
140 }
141
142 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
143 if (NN) {
144 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
145 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
146 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + zIdx + '>' +
147 copy + '</LAYER>');
148 }

Przykład 4.2. multi.html (ciąg dalszy)


149 else {
150 document.writeln('<DIV ID="' + sName +
151 '" STYLE="position:absolute; overflow:none; left:' +
152 sLeft + 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' +
153 sHgt + 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) +
154 '">' + copy + '</DIV>');
155 }
156 }
157
158 function nameFormat(str) {
159 var tempArray = str.split(' ');
160 return tempArray.join('').toLowerCase();
161 }
162
163 function hideSlide(name) { refSlide(name).visibility = hideName; }
164
165 function showSlide(name) { refSlide(name).visibility = showName; }
85 Rozdział 4 - Interfejs multiwyszukiwarki

166
167 function refSlide(name) {
168 if (NN) { return document.layers[name]; }
169 else { return eval('document.all.' + name + '.style'); }
170 }
171
172 function changeSlide(offset) {
173 hideSlide('slide' + curSlide);
174 curSlide = (curSlide + offset < 0 || curSlide + offset >= lyrCount ?
175 (curSlide + offset < 0 ? lyrCount - 1 : 0) : curSlide + offset);
176 showSlide('slide' + curSlide);
177 }
178
179 function imageSwap(imagePrefix, imageIndex, arrayIdx) {
180 document[imagePrefix].src = eval(arrayHandles[arrayIdx] +
181 "[" + imageIndex + "].src");
182 }
183
184 function callSearch(searchTxt, idx) {
185 if (searchTxt == "") {
186 parent.frames[2].location.href = engines[idx][2] +
187 escape(searchTxt);
188 }
189 else {
190 parent.frames[2].location.href = engines[idx][1] +
191 escape(searchTxt);
192 }
193 }
194
195 function hideStatus() { window.status = ''; }
196
197 //-->
198 </SCRIPT>
199
200 </HEAD>
201 <BODY BGCOLOR="BLACK" onLoad="showSlide('slide0');">
202 <SCRIPT LANGUAGE="JavaScript1.2">
203 <!--
204 engineLinks();
205 //-->
206 </SCRIPT>
207 <FORM onSubmit="return false;">
208 <TABLE CELLPADDING=0>
209 <TR>
210 <TD>
211 <FONT FACE=Arial>
212 <IMG SRC="images/searchtext.jpg">
213 </TD>

Przykład 4.2. multi.html (dokończenie)


214 <TD>
215 <INPUT TYPE=TEXT SIZE=25>
216 </TD>
217 </TR>
218 </TABLE>
219 </FORM>
220 </BODY>
221 </HTML>

Mamy tu ponad 200 wierszy kodu, ale większość tego kodu jest już czytelnikowi znana, nie powinno być więc z
niczym problemu. Zacznijmy od wiersza 7:
parent.frames[2].location.href = 'javascript: parent.white';

Jeśli policzymy ramki w index.html, stwierdzimy, że frames[2] znajduje się tam, gdzie mają być wyniki
wyszukiwania. Ustawienie w tej ramce właściwości location.href nieco upraszcza obsługę, jeśli zdecydujemy się
powtórnie załadować aplikację. Jako dokument wynikowy załadowana zostanie jakaś lokalna strona HTML, więc
nie będzie trzeba czekać na ponowne uzyskanie poprzednich wyników wyszukiwania.
Przy okazji warto zaznaczyć, choć we frames[2] masz ładnie pokazane wyniki wyszukiwania, to kiedy klikniesz
któreś z łącz wynikowych, jesteś zdany na łaskę i niełaskę projektantów danej wyszukiwarki. Niektóre wyszukiwarki
pokażą odpowiednią stronę w tej samej ramce, inne, wśród nich niestety InfoSeek, wymusi otwarcie dokumentu
w głównym oknie przeglądarki.
86

Przechadzka Aleją Pamięci


Przejdźmy się teraz Aleją Pamięci (chodzi o RAM, jak łatwo się domyślić). Jeśli przyjrzymy się poniższym zmiennym,
stwierdzimy, że niektóre z nich są nowe, ale część jest uderzająco podobna do tych, z którymi pracowaliśmy
w rozdziale 3. Spójrzmy, mamy NN i curSlide! Są też hideName i showName, jak również imagePath i zIdx:
var NN = (document.layers ? true : false);
var curSlide = 0;
var hideName = (NN ? 'hide' : 'hidden');
var showName = (NN ? 'show' : 'visible');
var perLyr = 4;
var engWdh = 90;
var engHgt = 20;
var left = 375;
var top = 10;
var zIdx = -1;
var imgPath = 'images/';
var arrayHandles = new Array('out', 'over');

Zmienne te pełnią taką samą funkcję, jak w rozdziale 3. Jeśli chodzi o nowe zmienne, perLyr określa na przykład liczbę
wyszukiwarek, które mają być wyświetlane na warstwie. Zmienne engWdh i engHgt opisują domyślną szerokość
i wysokość poszczególnych warstw. Zmienne left i top służą do pozycjonowania warstw. Zmienna arrayHandles
zawiera tablicę używaną do wstępnego ładowania obrazków. Będzie jeszcze o tym mowa w dalszej części rozdziału.
Przyjrzyjmy się funkcjom z wierszy 142–156:
function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
if (NN) {
document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
' VISIBILITY="' + sVis + '"' + ' z-Index=' + zIdx + '>' +
copy + '</LAYER>');
}
else {
document.writeln('<DIV ID="' + sName +
'" STYLE="position:absolute; overflow:none; left:' +
sLeft + 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' +
sHgt + 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) +
'">' + copy + '</DIV>');
}
}

oraz z wierszy 163–177:


function hideSlide(name) { refSlide(name).visibility = hideName; }

function showSlide(name) { refSlide(name).visibility = showName; }

function refSlide(name) {
if (NN) { return document.layers[name]; }
else { return eval('document.all.' + name + '.style'); }
}

function changeSlide(offset) {
hideSlide('slide' + curSlide);
curSlide = (curSlide + offset < 0 || curSlide + offset >= lyrCount ?
(curSlide + offset < 0 ? lyrCount - 1 : 0) : curSlide + offset);
showSlide('slide' + curSlide);
}

Jest tu pięć funkcji: genSlide(), refSlide(), hideSlide(), showSlide() i changeSlide(). Wszystkie działają
podobnie jak w rozdziale 3.; jeśli czegoś nie pamiętasz, po prostu wróć do tego rozdziału. Istnieją też dwie nowe
funkcje: imagePreLoad() i imageSwap(), które mają te same zadania, ale zostały zmodyfikowane na tyle, że
zasadne jest ich ponowne omówienie.

Dynamiczne ładowanie obrazków


Jednym z wielkich paradygmatów Sieci jest dynamiczne przeprowadzenie operacji zasadniczo statycznych. Po co robić
coś statycznie, skoro można znacznie wygodniej dokonać „w biegu”? Tak właśnie dzieje się zazwyczaj ze wstępnym
ładowaniem obrazków. Jak tego wygląda wstępne ładowanie obrazków, których chcesz użyć do przewijanego menu?
Może to być kod mniej więcej taki:
var myImage1On = new Image();
myImage1On.src = 'images/myImgOn1.gif'
87 Rozdział 4 - Interfejs multiwyszukiwarki

var myImage1Off = new Image();


myImage1Off.src = 'images/myImgOff1.gif';

Wydaje się to całkiem proste, jednak do opisania jednej pary obrazków potrzebne nam były cztery wiersze kodu, a co
się stanie, jeśli par będzie pięć czy dziesięć? Potrzebowalibyśmy 20 lub 40 wierszy. Jeśli tylko zaczniemy robić jakieś
zmiany, natychmiast zrobi się kompletny bałagan. W tej aplikacji przedstawimy sposób poradzenia sobie z ładowaniem
dowolnej (teoretycznie) liczby par obrazków. Będziemy potrzebować trzech rzeczy:
1.Tablicy obiektów Image dla każdego zestawu obrazków. W aplikacji tej użyta zostanie jedna tablica obrazków,
nad którymi znajduje się wskaźnik myszy, i jedna dla obrazków bez wskaźnika.
2.Prostej konwencji nazewniczej dla obrazków. Doskonale nada nam się nazewnictwo typu myImg1On.gif /
myImg1Off.gif. W rozdziale 3. można znaleźć ramkę omawiającą kwestie nazewnictwa znacznie dokładniej.
Nasze nazewnictwo obejmować musi nazwy tablic z punktu 1.
3.Metody eval().
Jeśli chodzi o punkt 1., w aplikacji użyjemy dwóch tablic. Jedna nazwana zostanie out i zawierać będzie obiekty
Image odpowiadające obrazkom, nad którymi nie ma wskaźnika myszy. Druga tablica – o nazwie – over i będzie
zawierać obiekty Image z obrazkami, nad którymi akurat jest wskaźnik myszy. Zmienne te będą od teraz
reprezentowane przez wartości tablicy arrayHandles z wiersza 20:
var arrayHandles = new Array('out', 'over');

Kwestię konwencji nazewnictwa rozwiążemy bardzo prosto. Pary obrazków będą miały ten sam początek, za którym
znajdzie się out.jpg lub over.jpg, w zależności od tego, o który obrazek z pary chodzi. Na przykład obrazki związane
z InfoSeek będą nazywały się infoseekout.jpg i infoseekover.jpg.
Jeśli chodzi o punkt 3., najpierw przejdziemy po wszystkich elementach tablicy arrayHandles i używając funkcji
eval(), utworzymy tablice na obiekty Image – oto wiersze 22 do 24:
for (var i = 0; i < arrayHandles.length; i++) {
eval('var ' + arrayHandles[i] + ' = new Array()');
}

Wykonanie powyższej pętli for odpowiada następującym instrukcjom:


var out = new Array();
var over = new Array();

Aby ładowanie obrazków jeszcze trochę dopracować, użyjemy znów funkcji eval() w imagePreLoad()
do dynamicznego utworzenia obiektów Image i przypisania im właściwości SRC. Oto funkcja z wierszy 104–110:
function imagePreLoad(imgName, idx) {
for(var j = 0; j < arrayHandles.length; j++) {
eval(arrayHandles[j] + "[" + idx + "] = new Image()");
eval(arrayHandles[j] + "[" + idx + "].src = '" + imgPath +
imgName + arrayHandles[j] + ".jpg'");
}
}

Funkcja imagePreLoad() pobiera dwa parametry, początek nazwy (na przykład Infoseek) oraz liczbę całkowitą
będącą indeksem obiektu w tablicy. Znów w pętli for przeglądamy tablicę arrayHandles, za każdym razem
używając napisu do uzyskania dostępu do jednej z właśnie utworzonych tablic i przypisania jej niepowtarzalnego
identyfikatora. Na przykład wywołanie imagePreLoad('infoseek',0) równoważne jest następującym instrukcjom:
out[0] = new Image();
out[0].src = 'images/infoseekout.jpg';
over[0] = new Image();
over[0].src = 'images/infoseekover.jpg';

Jednak w tym przypadku potrzebowalibyśmy dla każdej pary czterech wierszy kodu, a tego właśnie chcieliśmy
uniknąć. Za każdym razem, kiedy chcemy dodać nową parę obrazków, wystarczy wywołać preLoadImages(), więc
jest to czysty zysk.

Uruchamianie wyszukiwarek
Zmienna engines z wierszy 26–100 to tablica, której elementy zawierają tablice elementów opisujących poszczególne
wyszukiwarki. Zmienna engines ma 20 całkiem długich elementów, więc przyjrzyjmy się tylko pierwszemu
przykładowi – z wierszy 27–29:
newArray('HotBot',
'http://www.hotbot.com/?MT=',
88

'http://www.hotbot.com/'),

Element 0 zawiera nazwę przeglądarki, w tym wypadku HotBot. Element 1 zawiera adres URL wraz z treścią zapytania
– przez ten adres będziemy wywoływać wyszukiwarkę, jeśli użytkownik poda jakieś zapytanie. Element 2 zawiera
adres URL strony głównej wyszukiwarki, używany zamiast poprzedniego adresu, kiedy użytkownik nie poda żadnego
zapytania.

Techniki języka JavaScript:


wielokrotne używanie kodu
Nie jest to technika samego języka JavaScript, można ją stosować niezależnie od tego,
w czym programujemy. Jeśli zaczniesz kodować poważniej, tworząc obiekty i funkcje,
okaże się, że w wielu sytuacjach używasz tego samego kodu. Przyjrzyjmy się funkcjom
genSlide(), refSlide(), hideSlide() i showSlide(). Realizują podstawowe, ale
konieczne zadania:
• Aby tworzyć warstwy DHTML działające na różnych przeglądarkach, użyj
genSlide().

• Aby odwoływać się do warstw DHTML niezależnie od przeglądarki, użyj refSlide().


• Aby ukrywać warstwy DHTML niezależnie od przeglądarki, użyj hideSlide().
• Aby pokazać warstwę DHTML niezależnie od stosowanej przeglądarki, użyj
showSlide().

Warto zauważyć, ile dały nam te funkcje w poprzednim rozdziale, spotkamy je również
dalej. Jeśli jeszcze nie stworzyłeś dla nich specjalnego bibliotecznego pliku źródłowego,
to zastanów się, czy nie warto tego zrobić teraz. W rozdziale 6. dowiemy się na ten temat
wszystkiego, co trzeba. Jeśli zdarzy się nam wymyślić genialną funkcję lub obiekt, któ-
rego na pewno jeszcze nie raz użyjemy, to warto umieścić go w pliku .js o starannie do-
branej nazwie.

engineLinks()
Funkcja engineLinks() podobna jest do funkcji genScreen() z rozdziału 3., gdyż odpowiada za zarządzanie
tworzeniem warstw. Istnieją jednak między tymi funkcjami różnice.

Zarządzanie warstwami
Omawiana funkcja zajmuje się przede wszystkim utworzeniem warstwy z łączami nawigacyjnymi:
genLayer('sliderule', left - 20, top + 2, 25, engHgt, showName,
'<A HREF="javascript: changeSlide(1);" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/ahead.gif" BORDER=0></A><BR>' +
'<A HREF="javascript: changeSlide(-1);" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/back.gif" BORDER=0></A>');

Wszystko odbywa się dzięki jednemu odwołanie do genLayer(). Nie ma tu nic zaskakującego. Warstwa zawiera dwa
aktywne obrazki: strzałkę do przodu i do tyłu. Zwróćmy uwagę na to, że położenie lewego górnego piksela
przekazywane jest względem lewego górnego rogu warstwy łącz wyszukiwarek (którą to warstwę utworzymy wkrótce):
left - 20 oraz top + 2.

Dalej zmienna lyrCount określa liczbę warstw przycisków wyszukiwarek, które mają być tworzone, a także liczbę
wyszukiwarek umieszczonych w tablicy engines. To jest już proste: należy podzielić liczbę wyszukiwarek
(engines.length) przez liczbę wyszukiwarek, które mają być umieszczane na jednej warstwie (perLyr). Jeśli reszta
jest różna od zera, potrzebna będzie jeszcze jedna warstwa dodatkowa.
89 Rozdział 4 - Interfejs multiwyszukiwarki

Techniki języka JavaScript:


rezygnacja z obiektowości
Przyjrzawszy się dobrze tablicy engines można by zacząć się zastanawiać, dlaczego nie
mamy konstruktora searchEngine. Może właśnie jest na to doskonałe miejsce?:
function searchEngine(name, searchURL, homePage) {
this.name = name;
this.searchURL = searchURL;
this.homePage = homePage;
return this;
}
Wtedy engines wyglądałaby tak:
var engines = new Array(
new searchEngine('HotBot',
'http://www.hotbot.com/?MT=',
'http://www.hotbot.com/')
// itp., itd.
Tak właśnie można bym postąpić, gdyby nie jeden szczegół techniczny w wierszu 102:
engines = engines.sort();
Chodzi o możliwość prezentowania wyszukiwarek w porządku alfabetycznym. Użytkow-
nicy będą wdzięczni, jeśli szybko znajdą swoją przeglądarkę. Jeśli zaczęliby wszystko re-
alizować zgodnie z metodologią obiektową, metoda sort() nie zmieniałaby kolejności
elementów. Jeśli jednak mowa o tablicy tablic, którą mamy w wierszach 26–100, to da się
ją posortować zgodnie z pierwszym elementem. Obiekty nie mają pierwszego elementu.
Spotykamy się tu z tym, z czym mieliśmy do czynienia w rozdziale 1. Wyniki
wyszukiwania wyświetlane są w kolejności alfabetycznej, więc wszystkie rekordy są
kodowane tak samo. Powyższe uwagi nie oznaczają, że autor jest przeciwnikiem technik
obiektowych – wręcz przeciwnie, jednak po prostu jest taka aplikacja, w której jest to
niepotrzebne.

Podstawmy teraz wartości z naszej aplikacji. engines.length wynosi 20, perLyr równe jest 4. Wobec tego zmienna
lyrCount będzie miała wartość 5. Jeśli użyłbym 21 wyszukiwarek, 21 / 4 = 5 i 1 reszty. Reszta 1 wskazuje, że
potrzebujemy dodatkowej warstwy, więc wartością lyrCount będzie 6. Oto odpowiedni kod:
lyrCount = Math.ceil(engines.length / perLyr);

Operator warunkowy realizuje dokładnie opisaną wyżej funkcjonalność. Jeśli reszta wynosi 0, lyrCount równe jest
engines.length/perLyr; w przeciwnym wypadku lyrCount równe jest Math.
ceil(engines.length/perLyr). Poprawne określenie wartości lyrCount jest istotne, gdyż dalej
w engineLinks() utworzymy lyrCount warstw – wiersze 122–136:
for (var i = 0; i < lyrCount; i++) {
var engLinkStr = '<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=0><TR>';
for (var j = 0; j < perLyr; j++) {
var imgIdx = (i * perLyr) + j;
if (imgIdx >= engines.length) { break; }
var imgName = nameFormat(engines[imgIdx][0]);
imagePreLoad(imgName, imgIdx);
engLinkStr += '<TD><A HREF="javascript: ' +
'callSearch(document.forms[0].elements[0].value, ' +
imgIdx + ');" ' + 'onMouseOver="hideStatus(); imageSwap(\'' +
imgName + '\', ' + imgIdx + ', 1); return true" ' +
'onMouseOut="imageSwap(\'' + imgName + '\', ' + imgIdx +
', 0);">' + '<IMG NAME="' + imgName + '" SRC="' + imgPath +
imgName + "out.jpg" + '" BORDER=0></A></TD>';
}

Dla każdej warstwy engineLinks() deklaruje lokalną zmienną engLinkStr, która będzie zawierać kod
poszczególnych slajdów. Po stworzeniu engLinkStr, która – jak widać w wierszu 123 – zawiera otwarcie tabeli,
w zagnieżdżonej pętli for, sterowanej zmienną perLyr, tworzymy komórki tabeli, które będą zawierały poszczególne
obrazki.
W każdej iteracji perLyr zmiennej lokalnej imgIdx przypisywana jest wartość (i * perLyr) + j. Wyrażenie to
jest po prostu liczbą całkowitą, która w stosunku do zera jest w każdym przejściu pętli zwiększana o jeden. imgIdx
zostanie użyta do identyfikacji początku nazwy obrazka (czyli nazwy wyszukiwarki umieszczonej w elemencie 0
90

każdej tablicy zawartej w engines), następnie ładowany jest obrazek, co omówiono wcześniej. W tabeli 4.1 pokazano
sposób działania powyższego mnożenia, jeśli perLyr równa jest 4.

Tabela 4.1. Wyliczanie wyświetlania warstw (perLayer równa jest 4)


Kiedy i równe jest... A j równe jest... to (i*perLyr)+j zwiększa się o 1
0 0, 1, 2, 3 0, 1, 2, 3
1 0, 1, 2, 3 4, 5, 6, 7
2 0, 1, 2, 3 8, 9, 10, 11
3 0, 1, 2, 3 12, 13, 14, 15
4 0, 1, 2, 3 16, 17, 18, 19

Mamy tu 20 liczb całkowitych, od 0 do 19.


Teraz, kiedy znamy wartości imgIdx, musimy upewnić się, że nie pójdziemy za daleko. Odpowiedni kod jest
w wierszu 126:
if (imgIdx >= engines.length) { break; }

Jako że wartość imgIdx zwiększa się stale bezwarunkowo, kiedy osiągnięta zostanie liczba engines. length, nie ma już
więcej wyszukiwarek do wyświetlenia, więc funkcja w tym momencie przerwie pętlę for.

Techniki języka JavaScript:


matematyka a pamięć
Dlaczego zamiast wyrażenia (i * perLyr) + j nie użyć zmiennej, na przykład count
– ustawić ją na 0 i zwiększać za każdym razem o jeden, na przykład ++count? Cóż, z pe-
wnością można byłoby. Po co jednak alokować dodatkową pamięć na deklarację dodat-
kowej zmiennej, nawet lokalnej?
JavaScript ma już konieczne wartości w i, perLyr i j, które pozwalają wykonać po-
trzebne obliczenia. Tutaj jest to niewielka rzecz, ale w ten sposób można zaoszczędzić
cenną pamięć w przypadku większych aplikacji.

Wstępne ładowanie obrazków


Teraz nadszedł czas na wstępne ładowanie obrazków poszczególnych wyszukiwarek. Zanim to się stanie, musimy znać
początek nazwy obrazka. Jest to po prostu zapisana małymi literami nazwa wyszukiwarki, na przykład dla „InfoSeek”
będzie to „infoseek”, a dla „HotBot” – „hotbot”. Zmienna imgIdx identyfikuje odpowiedni obrazek – wiersz 127:
var imgName = nameFormat(engines[imgIdx][0]);

Element 0 każdej tablicy w engines zawiera nazwę wyszukiwarki. Zmienna imgIdx wybiera odpowiedni element
engines, a wtedy zwracana jest nazwa wyszukiwarki. Wszystko, co trzeba jeszcze zrobić, to tylko zmienić nazwę
na małe litery, czym zajmuje się funkcja nameFormat() w wierszach 158–161:
function nameFormat(str) {
var tempArray = str.split(' ');
return tempArray.join('').toLowerCase();
}

Usuwane są wszystkie spacje przez podzielenie napisu na każdej spacji i umieszczenie fragmentów w tablicy, następnie
fragmenty te są łączone. Teraz imgName zawiera wartość zapisaną samymi małymi literami, bez spacji. Wynik można wraz
z imgIdx przekazać imagePreLoad() w wierszu 128.

Tworzenie łącza
Przyszedł czas na stworzenie obrazka z łączem z odpowiednim kodem obsługującym zdarzenia myszy – wiersze 129–
135:
engLinkStr += '<TD><A HREF="javascript: ' +
'callSearch(document.forms[0].elements[0].value, ' +
imgIdx + ');" ' + 'onMouseOver="hideStatus(); imageSwap(\'' +
imgName + '\', ' + imgIdx + ', 1); return true" ' +
91 Rozdział 4 - Interfejs multiwyszukiwarki

'onMouseOut="imageSwap(\'' + imgName + '\', ' + imgIdx +


', 0);">' + '<IMG NAME="' + imgName + '" SRC="' + imgPath +
imgName + "out.jpg" + '" BORDER=0></A></TD>';

Przyjrzyjmy się teraz temu. Każde łącze wyszukiwarki musi spełniać cztery warunki:
1.zawierać kod wywołujący odpowiednią przeglądarkę przy kliknięciu na obrazku,
2.zawierać kod obsługi zdarzenia onMouseOver,
3.zawierać kod obsługi zdarzenia onMouseOut,
4.zawierać znacznik IMG z niepowtarzalną wartością atrybutu NAME i atrybutem SRC wskazującym odpowiedni
plik.
Rozbicie napisu zawartego w engLinkStr pokaże sposób spełnienia powyższych warunków.
Punkt pierwszy:
HREF="javascript: callSearch(document.forms[0].elements[0].value, ' +
imgIdx + ');"

Utworzone i kliknięte łącze wywoła funkcję callSearch(), której będzie przekazana wartość docu-
ment.forms[0].elements[0].value wraz z odpowiednim imgIdx. Więcej informacji o callSearch() pojawi się
wkrótce. Teraz można spokojnie stwierdzić, że wymaganie pierwsze mamy z głowy.
Punkt drugi:
'onMouseOver="hideStatus(); imageSwap(\'' + imgName + '\', ' +
imgIdx + ', 1); return true" ' +

Kod ten umożliwia utworzenie wywołania hideStatus() w celu wyczyszczenia paska stanu, a później wywołanie
imageSwap(), która dostaje trzy parametry: imgName, imgIdx oraz liczbę całkowitą 1, odpowiadającą elementowi
w arrayHandles.
Punkt trzeci:
'onMouseOut="imageSwap(\'' + imgName + '\', ' + imgIdx + ', 0);">' +

Niewiele się tutaj zmienia. Jedyne, co warto zauważyć, to przekazanie 0 zamiast 1.


I wreszcie punkt czwarty:
'<IMG NAME="' + imgName + '" SRC="' + imgPath +
imgName + "out.jpg" + '" BORDER=0></A></TD>';

Nazwa każdego obrazka ustawiana jest na wartość imgName. W ten sposób będziemy odwoływać się do obrazków
w funkcji imageSwap(). Dla atrybutu SRC ustala się z kolei wartość będącą złączeniem imgPath, imgName
i out.jpg. Jako że obrazki najpierw będą pokazywane jako nieaktywne, SRC ma początkowo końcówkę out.jpg.
Na przykład początkowy obrazek wyszukiwarki HotBot znajduje się w pliku images/hotbotout.jpg.
W wierszach 137 i 138 kończymy:
engLinkStr += '</TR></TABLE>';
genLayer('slide' + i, left, top, engWdh, engHgt, hideName, engLinkStr);

Zatem do engLinkStr dołączamy domknięcie znacznika tabeli HTML i pozostaje tylko wywołać genLayer(), aby
utworzyć nową warstwę. Warto zwrócić uwagę na to, że genLayer()jest wywoływana z parametrem false, aby
warstwa była niewidoczna – póki strona nie zostanie załadowana. Następnie w obsłudze zdarzenia onLoad w wierszu
201 pokazywany jest slajd slide0.

imageSwap()
Tę funkcję widzieliśmy w rozdziale 3., ale ta wersja jest nieco inna. Obejrzyjmy wiersze 179 do 182:
function imageSwap(imagePrefix, imageIndex, arrayIdx) {
document[imagePrefix].src = eval(arrayHandles[arrayIdx] +
"[" + imageIndex + "].src");
}
Funkcja ta realizuje przewijanie obrazków. Parametr imagePrefix wskazuje, który obrazek ma być włączony.
Parametry imageIndex i arrayIdx to liczby całkowite służące do odwołania się do odpowiedniego obiektu Image
w tablicy arrayHandles.
92

callSearch()
Kiedy formularze HTML i warstwy są już na swoim miejscu, użytkownik musi tylko wprowadzić wyszukiwany tekst
i kliknąć wybraną wyszukiwarkę. Jeśli użytkownik kliknie jeden z obrazków, wywoływana jest funkcja
callSearch(). Przyjrzyjmy się jej w wierszach 184–193:
function callSearch(searchTxt, idx) {
if (searchTxt == "") {
parent.frames[2].location.href = engines[idx][2] +
escape(searchTxt);
}
else {
parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt);
}
}

callSearch() oczekuje dwóch następujących argumentów: searchTxt to tekst wprowadzony przez


użytkownika, idx to liczba oznaczająca wyszukiwarkę w tablicy. Aplikacja ładuje jeden z dwóch dokumentów
do frames[2]. Jeśli użytkownik nie wprowadzi żadnego tekstu, do frames[2] ładowana jest domyślna strona domowa
przeglądarki. Ten adres URL znajduje się w elemencie 2. poszczególnych tablic. Jeśli jednak użytkownik wprowadzi
wyszukiwany tekst, aplikacja załaduje do frames[2] adres URL z pytaniem – wraz z zacytowaną postacią zapytania
użytkownika.

Techniki języka JavaScript:


użycie escape() i unescape()
escape() to wbudowana funkcja JavaScriptu konwertująca niealfanumeryczne znaki
w napisie na ich szesnastkowe odpowiedniki. Dzięki temu zabronione znaki nie przeszko-
dzą w przetwarzaniu napisu. Na przykład symbol & jest używany do rozdzielania par:
pole formularza – wartość. Wobec tego każdy znak &, wprowadzony przez użytkownika,
powinien zostać zamieniony na kod %26. Funkcja escape() jest szeroko używana do
formatowania napisów, które mają być przesłane jako część zapytania URL. Kiedy prze-
syłamy formularz, kodowaniem zajmuje się przeglądarka. Jako że ta aplikacja nie przewi-
duje przesyłania danych z formularza, konieczne jest zrobienie konwersji znaków.
Funkcja unescape() jest pożyteczna w przypadku obsługi ciasteczek (cookies). Znak
plus (+) oraz znak równości (=) są zarezerwowane dla przypisywania wartości atrybutów
ciasteczek, takich jak name, domain i expires. Metoda unescape(), jak już zapewne
można było zgadnąć, zamienia szesnastkową reprezentację znaków na ich odpowiedniki
ASCII.

Być może czytelnik zastanawia się, skąd się wzięły te długie napisy z elementu 1. poszczególnych tablic w engines.
Skąd właściwie pochodzą te wartości?
Sprawdziłem po prostu kod źródło wszystkich omawianych wyszukiwarek i stworzyłem odpowiedni napis
na podstawie formularza HTML, używanego na poszczególnych stronach do przesyłania zapytania. Zacznijmy
od prostego przykładu. MusicSearch.com ma zwykłe pojedyncze pole do wyszukiwania. Atrybut ACTION formularza
zawiera adres http://www.musicsearch.com/global/ search/search.cgi. Nazwa pola to QUERY, wobec tego adres URL
z zapytaniem powinien wyglądać tak:
http://www.musicsearch.com/global/search/search.cgi?QUERY= +
escape(searchTxt);

To było proste ze względu na jedną parę nazwa–wartość. Wyszukiwarki mogą jednak mieć mnóstwo opcji. Pomyśl
o multiwyszukiwarce (jest to wyszukiwarka, która zamiast własnej bazy danych przeszukuje bazy cudze) SavvySearch.
W tym wypadku wprowadza się szukany tekst i można zaznaczyć opcje wyszukiwania: jakie wyszukiwarki mają być
użyte, czy wyszukiwać w grupach dyskusyjnych, i tak dalej. Można też utworzyć warunki logiczne wyszukiwania,
określić liczbę wyników przekazywanych z poszczególnych baz danych oraz wybrać ilość informacji wyświetlanych
jednocześnie.
Atrybut ACTION w formularzu SavvySearch to http://numan.cs.colostate.edu:1969/nph-search. Oto lista potrzebnych
elementów formularza.
• Nazwa listy wyboru przy wyszukiwaniach z warunkami logicznymi: Boolean.
93 Rozdział 4 - Interfejs multiwyszukiwarki

• Nazwa listy z oczekiwaną liczbą wyników z poszczególnych wyszukiwarek: Hits.


• Nazwa przycisków radio z liczbą wyników: df.
• Nazwa pola tekstowego: KW.
Listę funkcji logicznych ustawiamy na OR, Hits na 10, df na normal, natomiast KW ma oczywiście wartość
escape(searchTxt). Wszystkich tych wartości nie ustalono bynajmniej przypadkowo. Są to ustawienia pochodzące
z oryginalnego formularza, wartości obecne w kodzie źródłowym HTML.
Formularz zawiera też dwa pola ukryte, jedno z nich nazywa się Mode, drugie to AutoStep. Mode ma wartość
MakePlan, a AutoStep – on. Można mieć wątpliwości, do czego te pola służą, ale to nie ma znaczenia. Należy teraz
po prostu dodać je do tekstu zapytania. Wysłanie zapytania do SavvySearch wymaga zatem następującego adresu URL:
http://numan.cs.colostate.edu:1969/nph-search? +
classic=on&Boolean=OR&Hits=10&Mode=MakePlan&df=normal& +
AutoStep=on&KW=escape(searchTxt)

Inna przyjemna rzecz związana z „odszyfrowywaniem” tekstów zapytań to fakt, że kolejność poszczególnych par pole–
wartość nie ma znaczenia. O ile tylko w napisie znajdują się niezbędne elementy, wszystko działa dobrze.

Kierunki rozwoju:
Zwiększenie możliwości decydowania przez użytkownika
Jak wspomniano wcześniej, aplikacja ta zostawia użytkownika na łasce ustawień domyślnych wyszukiwarki. Oznacza to, że
użytkownik ma niewielki lub zgoła żaden wpływ na sposób wyszukiwania. Właściwie wprowadza tylko tekst zapytania.
Można doprowadzić również do takiej sytuacji, aby użytkownik mógł wpływać na liczbę wyników na jednej stronie,
liczbę informacji wyświetlanych wraz z wynikami, a może nawet tworzyć reguły zapytań z użyciem operatorów AND, OR,
LIKE i NOT LIKE. W tej sekcji sprawa ta zostanie omówiona na przykładzie wyszukiwarki HotBot.
Zdaje się, że najprostszym usprawnieniem będzie zwiększenie liczby wyników pokazywanych na jednej stronie. Należy
odpowiednią wielkość określić jako parę wartości dla każdej przeglądarki. W tabeli 4.2 podano kilka nazw
wyszukiwarek i dopuszczalne w ich wypadku wartości.

Tabela 4.2. Wyszukiwarki i zmienne określające liczbę wyników


Wyszukiwarka Nazwa pola Dopuszczalne Przykład
wartości
HotBot DC 10, 25, 50, 100 DC=10
InfoSeek Advanced Search Numberresults 10, 20, 25, 50 Numberresults=10
Scientific American Docs 10, 25, 50, 100 Docs=10
Yahoo! N 10, 20, 50, 100 n=10

Wartości te pobierałem z kodu źródłowego stron poszczególnych witryn. Niektóre pola


dostępne są tylko w zaawansowanych wersjach wyszukiwania, więc adresy URL podane
w tablicy engines mogą nie działać. Także programiści tworzący wyszukiwarkę mogli ustalić
stały limit. Jeśli nie widać na stronie żadnej możliwości określenia liczby wyników,
można skontaktować się z właścicielami i spytać kogoś, jak zmienić parametry (o ile
w ogóle jakieś są dostępne). Jeśli nie, należy u siebie dodać jakieś ustawienie domyślne,
które do niektórych wyszukiwarek w ogóle nie będzie przesyłało informacji o oczekiwanej
liczbie wyników.

Zwróćmy też uwagę na to, że w różnych wyszukiwarkach mogą być dopuszczalne inne wartości. Należy wówczas
dodać odpowiedni kod. Nie jest to trudne; użyj procedury opisanej niżej, a później w analogiczny sposób możesz
do swojej aplikacji dodawać nowe funkcje.
1.Dodaj do ramki zawierającej pole tekstowe listę wyboru.
2.Dodaj do każdej tablicy zawierającej opis przeglądarki dodatkowy element.
3.Dodaj instrukcję new Array(), tworzącą tablicę z dopuszczalnymi wartościami w danej przeglądarce; tablice
te będą nowymi elementami tablic znajdujących się w tablicy engines.
4.Usuń z tekstu zapytania odpowiednią parę wartości (jeśli para taka jest tam umieszczona).
94

5.Dostosuj kod funkcji callSearch(), aby prawidłowo łączone było zapytanie dla poszczególnych
wyszukiwarek.
Przejdźmy teraz do przykładowej wyszukiwarki HotBot.

Krok 1.
Dodanie listy wyboru nie powinno stanowić problemu. Rozsądne może być wybranie wartości najczęściej używanych
w przeglądarkach, które uwzględnia nasza aplikacja. W przykładzie zdecydowano się na liczby 10, 25, 50 i 100:
<SELECT NAME="docs">
<OPTION VALUE="10">10
<OPTION VALUE="25">25
<OPTION VALUE="50">50

<OPTION VALUE="100">100
</SELECT>

Krok 2.
Każde wywołanie new Array() w tablicy engines opisuje wyszukiwarkę z trzema elementami: nazwą
wyszukiwarki, tekstem przekazywanym do wyszukiwania i stroną domową wyszukiwarki. Oto znów kod opisujący
HotBot:
newArray('HotBot',
'http://www.hotbot.com/?MT=',
'http://www.hotbot.com/')

Teraz mamy element 3., którego wartością będzie nazwa pola określającego liczbę wyników. Pole to nazywa się –
w przypadku HotBot – DC, więc nowy rekord będzie wyglądał tak:
newArray('HotBot',
'http://www.hotbot.com/?MT=',
'http://www.hotbot.com/',
'DC')

Jeśli co najmniej jedna z wyszukiwarek nie ma potrzebnego pola, niech ta wartość pozostanie pusta (null).

Krok 3.
Teraz, kiedy określiliśmy już potrzebną nazwę, dodaliśmy kolejną tablicę zawierającą dostępne wartości. Nowa tablica
ma być elementem 4. Teraz opis HotBot będzie wyglądał następująco:
newArray('HotBot',
'http://www.hotbot.com/?MT=',
'http://www.hotbot.com/',
'DC',
new Array(10, 25, 50, 100) )

Krok 4.
Ten krok obowiązuje tylko wtedy, gdy domyślny napis zapytania w elemencie 2. zawiera parę nazwa –wartość, opisującą
ustawienia wyniku. Oto odpowiedni zapis HotBot:
http://www.hotbot.com/?MT=

Jako że DC tu nie występuje, możemy krok 4. pominąć. Jednak w ramach przykładu pokażę obsługę wyszukiwarki
Scientific American, która zawiera zapis docs=100. Spójrz:
'http://www.sciam.com/cgi-bin/search.cgi?' +
'searchby=strict&groupby=confidence&docs=100&query=',

Musielibyśmy odpowiedni fragment wyciąć, otrzymując następujący zapis:


'http://www.sciam.com/cgi-bin/search.cgi?' +
'searchby=strict&groupby=confidence&query=',

Jeśli co najmniej jedna z przeglądarek nie zawiera liczby wyników, którą można by ustawiać, po prostu nie twórz
wartości elementu 4.

Krok 5.
Ostatnią czynnością jest stworzenie zapytania przed przekazaniem go wyszukiwarce. Robi się to w funkcji
callSearch(). Oto kod oryginalny:
function callSearch(searchTxt, idx) {
95 Rozdział 4 - Interfejs multiwyszukiwarki

if (searchTxt == "") {
parent.frames[2].location.href = engines[idx][2] +
escape(searchTxt);
}
else {
parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt);
}
}

Jeśli użytkownik nic nie wprowadzi w polu tekstu, aplikacja nadal może przekierować użytkownika na stronę główną
wybranej wyszukiwarki, więc blok po if pozostanie bez zmian. Zmienimy tylko blok po else:
else {
if(engines[idx][3] != null) {
for (var i = 0; i < engines[idx][4].length; i++) {
var selRef = parent.frames[4].document.forms[0].docs;
if (selRef.options[selRef.selectedIndex].value =
engines[idx][4][i].toString()) {
parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt) + '&' + engines[idx][3] + '=' +
engines[idx][4][i];
return;
}
}
parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt);
}

Oto wiersz, który dodaje odpowiednią parę nazwa–wartość do tekstu:


parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt) + '&' + engines[idx][3] + '=' +
engines[idx][4][i];

Mamy tutaj adres URL wyszukiwarki z zacytowanym tekstem searchTxt i nazwą pola (engines[idx] [3]l) oraz jego
wartością wybraną przez użytkownika. Jednak tak się stanie tylko wtedy, gdy spełnione zostaną dwa warunki. Jeśli nie,
wybierana jest strona domyślna, zapisana w engines[idx] [1]. Po pierwsze, wyszukiwarka musi umożliwiać
zmianę liczby wyników – wtedy nazwa tego pola podana jest w engines[idx][3]. Jeśli pola takiego brakuje,
wartością odpowiedniej komórki tablicy jest null, co było ustawiane w kroku 3. Poniższa instrukcja if sprawdza, czy
engines [idx][3] nie jest puste:
if(engines[idx][3] != null) {
Jeśli wspomniana wartość okaże się pusta, nie zostaje spełniony pierwszy warunek i używany jest domyślny adres.
Jeżeli engines[idx][3] nie jest pusta, przeglądane są dopuszczalne wartości zapisane w tablicy engines[idx][4].
Gdy wybrana liczba na liście wyboru wartości jest dopuszczalna dla danej wyszukiwarki, JavaScript łączy adres URL
i tekst zapytania z odpowiednią parą nazwa–wartość, następnie do ramki frames[2] ładuje wyniki i kończy swoje
działanie.
Jeśli pętla przejdzie po wszystkich dopuszczalnych wartościach, nie znajdując odpowiednika wybranej wartości, a
nie jest spełniony drugi warunek i znów używany jest domyślny adres.
Cechy aplikacji:
 Dynamiczny generator kodu przewijania
i przeglądarka
 Kod dostosowany do słabszych
przeglądarek
 Elastyczny i rozszerzalny sposób
stosowania atrybutów HTML
 Przewijanie obrazków w odpowiedzi

5
na zdarzenie MouseDown
Prezentowane techniki:
 Ostrożne kodowanie JavaScriptu
 Siła zmiennych globalnych
 Wyszukiwanie i podstawianie fragmentów
tekstu w JavaScripcie 1.1 i 1.2

ImageMachine

Gdzie tylko nie spojrzeć, wszystkie aplikacje w tej książce tworzone są z myślą o jednej tylko osobie: użytkowniku.
Cóż, użytkowników można chyba najprościej określić jako tych, którzy przychodzą na naszą witrynę niczym lemingi,
zapychają łącza, biorą nasz towar, a w końcu ściągają nasze oprogramowanie. Omawiana tutaj aplikacja wyłamuje się
z tej konwencji: jest przeznaczona dla: programisty, administratora czy projektanta witryny.
Choć DHTML zwiększa możliwości decydowania, co ma się stać, kiedy umieścimy wskaźnik myszy nad ramką,
przyciskiem czy arkuszem stylów, to i tak przewijanie obrazków jest nadal najpowszechniej stosowaną w Sieci
techniką.
Pisanie kodu JavaScript, który będzie w stanie realizować to zadanie, nie wymaga wyższych studiów politechnicznych,
ale życie byłoby oczywiście łatwiejsze, gdyby mieć aplikację, która mogłaby sama taki kod wygenerować. Wtedy my,
programiści, moglibyśmy po prostu gotowe funkcje wstawić bezpośrednio na strony. Zapraszamy zatem
do przeglądarki obrazków. Na rysunku 5.1 pokazano, co można zobaczyć otwierając w swojej przeglądarce plik
ch05/index.html.
Aplikacja jest prosta w użyciu. Należy podjąć tylko kilka decyzji dotyczących obrazków. Jak to widać na rysunku 5.1,
kolejno:
1.Zdecyduj, ile par obrazków chcesz mieć do dyspozycji.
2.Ustaw domyślną szerokość, wysokość i obramowanie wszystkich obrazków (później można zmieniać te
ustawienia dla każdego obrazka z osobna).
3.Jeśli chcesz, aby obrazki miały trzeci stan w czasie wciśnięcia myszy, zaznacz kwadracik MouseDown; jeśli
nie, zostaw to pole puste.
4.Wciśnij przycisk Dalej, aby przejść dalej, lub Reset, aby zacząć od nowa.
Kiedy już dojdziesz do tego miejsca, aplikacja wygeneruje szablon taki, jak pokazano na rysunku 5.2.
97 Rozdział 5 - ImageMachine

Rysunek 5.1. ImageMachine gotowa do działania

Rysunek 5.2. Wygenerowany szablon o postaci określonej przez wybrane uprzednio opcje

Jeśli nie zaznaczyłeś pola „MouseDown”, otrzymasz dwa pola na nazwy plików dla każdej grupy: obrazek podstawowy
i obrazek ze wskaźnikiem myszki. Jeśli zaznaczyłeś tę opcję, pojawi się dodatkowe pole na nazwę pliku. Każdy
obrazek ma atrybut HREF dla każdego łącza – w pasku stanu może być wyświetlany jakiś komunikat, kiedy dany
obrazek znajduje się pod wskaźnikiem myszy. W końcu trzy małe pola tekstowe zawierają wartości domyślnej
szerokości, wysokości i obramowania każdej grupy – możesz te ustawienia dowolnie zmieniać. Aby otrzymać w końcu
gotowy kod, musisz jeszcze zrobić kilka rzeczy:
1.Podaj nazwy plików obrazków głównych, aktywnych, kiedy nie ma nad nimi wskaźnika myszy. Są to pola
plików, a nie zwykłe pola tekstowe, co ułatwi wybieranie plików z maszyny lokalnej. Kiedy już otrzymany kod
będzie zadawalający, można zmienić adres URL. To samo dotyczy pozostałego jednego lub dwóch obrazków.
2.Wprowadź względny lub bezwzględny adres URL w polu tekstowym, związanym z atrybutem HREF każdej
grupy obrazków.
98

3.W polu Pasek stanu wprowadź tekst, który ma być wyświetlany, kiedy użytkownik przesunie wskaźnik myszki
nad łączem.
4.Dostosuj ewentualnie ustawienia wysokości, szerokości i obramowań poszczególnych obrazków
w odpowiednich polach tekstowych.
5.Wybierz Generuj, aby zobaczyć kod, lub Podgląd, aby sprawdzić, jak kod działa w przeglądarce.
Na rysunku 5.3 pokazano ImageMachine po nakazaniu generacji. Należy sprawdzić kod JavaScript i HTML wszystko
jest skomentowane. Warto zwrócić uwagę, że dla każdej zdefiniowanej grupy wygenerowane zostaną funkcje wstępnie
obrazki ładujące i przygotowujące je do przewijania. Kod HTML zawiera znaczniki A HREF i IMG, z całkowicie
obsłużonymi procedurami obsługi zdarzeń i atrybutami obrazków.
Na dole ekranu znajdują się dwa dodatkowe przyciski. Podgląd pozwala zobaczyć, jak działa kod. Zmiana pozwala
cofnąć się i wprowadzić zmiany w ustawieniach. Na rysunku 5.4 pokazano kod wyświetlający obrazki i ich wstępnie
załadowane odpowiedniki pod kursorem myszki.
Jedną z najsilniejszych cech takiego generowanego kodu jest to, że wydajność dostosowuje się do możliwości używanej
aktualnie przeglądarki. Innymi słowy – przeglądarki obsługujące JavaScript 1.2 i wyższy mogą w pełni korzystać
z przewijania w wyniku zdarzeń onMouseOver, onMouseOut i onMouseDown. Przeglądarki obsługujące JavaScript 1.1
będą w stanie uruchomić jedynie zdarzenia onMouseOut i onMouseOver. W końcu przeglądarki obsługujące JavaScript
1.0 uruchomią jedynie kod związany ze zmianą zawartości paska stanu.
A jak wygląda sprawa wykorzystania dostępnych zasobów? Ładowane są tylko obrazki aktualnie używane. Jeśli
przeglądarka nie może użyć obrazka, to nie będzie go w ogóle odczytywać. Przeglądarki JavaScript 1.1 załadują zatem w
ogóle obrazków związanych ze zdarzeniem onMouseDown, natomiast przeglądarki zgodne jedynie z JavaScriptem 1.0
nie będą ładowały żadnych obrazków!

Wymagania programu
Choć wygenerowany kod zadziała w dowolnej przeglądarce z JavaScriptem, to należy używać przeglądarki z wersją
1.2. Niektóre funkcje zastępowania tekstu i inne fragmenty kodu wymagają stosowania tej właśnie wersji. Jeśli chodzi
o skalowalność, można tworzyć kod dla dowolnej liczby

Rysunek 5.3.Obejrzyj wygenerowany kod

obrazków, o ile tylko wytrzyma nasz system. Obecnie np. ustawiłem maksimum na 50 grup, co, jak sądzę, znacznie
przewyższa czyjekolwiek potrzeby.
Przy okazji warto dodać, że: interfejs został zaprojektowany tak, że najlepiej oglądać go przy rozdzielczości 1024x768
pikseli. Dotyczy to zarówno szablonu obrazków, jak i założonej szerokości strony.
99 Rozdział 5 - ImageMachine

Struktura programu
Zanim zaczniemy zastanawiać się nad jakimkolwiek kodem, dobrze byłoby z grubsza obejrzeć sposób działania
programu. Na rysunku 5.5 pokazano ten sposób od początku do końca. W zasadzie zaczynamy od stworzenia formularza
obrazków i ustawienia właściwości, a później można podglądać wyniki, zmieniać ustawienia i znów generować kod.
ImageMachine składa się z trzech plików: zestawu z ramkami i dwóch plików z zawartością tych ramek. Plik główny to
index.html, który zawiera nav.html i base.html. Sam index.html nie zawiera żadnego kodu JavaScript ani innych
niespodzianek. Poniżej przedstawiono dziewięć wierszy kodu – przykład 5.1.

Przykład 5.1. indeks.html


1 <HTML>
2 <HEAD>
3 <TITLE>ImageMachine</TITLE>
4 </HEAD>
5 <FRAMESET ROWS="105, *" FRAMEBORDER="0" BORDER="0">
6 <FRAME SRC="nav.html" NAME="nav" SCROLLING=NO>

Rysunek 5.4. Wybierz „Podgląd” lub „Zmiana danych”

Przykład 5.1. indeks.html (dokończenie)


7 <FRAME SRC="base.html" NAME="base">
8 </FRAMESET>
9 </HTML>
Jeśli zajrzymy do base.html, znajdziemy tam znów statyczny kod HTML. Zanim przejdziemy do nav.html z przykładu
5.2, warto zrozumieć kilka rzeczy dotyczących tego kodu. Jest on długi (ponad 400 wierszy) i dość trudny do czytania,
ale nie tak znów skomplikowany.

Przykład 5.2. nav.html


1 <HTML>
2 <HEAD>
3 <TITLE>ImageMachine</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5
6 var platform = navigator.platform;
7 var lb = (platform.indexOf("Win" != -1) ? "\n\r" :
8 (platform.indexOf("Mac" != -1) ? "\r" : "\n"));
9 var fontOpen = '<FONT COLOR=BLUE>';
10 var fontClose = '</FONT>';
11
12 function genSelect(name, count, start, select) {
13 var optStr = "";
14 for (var h = start; h <= count; h++) {
100

15 optStr += "<OPTION VALUE=" + h +


16 (h == select ? " SELECTED" : "") + ">" + h;
17 }
18 document.write("<SELECT NAME=" + name + ">" + optStr + "</SELECT>");
19 }
20

Rysunek 5.5.Logika ImageMachine

Przykład 5.2. nav.html (ciąg dalszy)


21 function captureDefaultProfile(formObj) {
22 setArrays();
23 imgDefaults = formObj;
24 var imgQty = (imgDefaults.imgnumber.selectedIndex + 1);
25 var imgHeight = (imgDefaults.pxlheight.selectedIndex);
26 var imgWidth = (imgDefaults.pxlwidth.selectedIndex);
27 var imgBorder = (imgDefaults.defbdr.selectedIndex);
28 for (var i = 0; i < imgQty; i++) {
29 imgPrim[i] = "";
30 imgRoll[i] = "";
31 imgDown[i] = "";
32 imgLink[i] = "";
33 imgText[i] = "";
34 imgWdh[i] = imgWidth;
Przykład 5.2. nav.html (ciąg dalszy)
35 imgHgt[i] = imgHeight;
36 imgBdr[i] = imgBorder;
37 }
38 generateEntryForm();
39 }
101 Rozdział 5 - ImageMachine

40
41 function setArrays() {
42 imgPrim = new Array();
43 imgRoll = new Array();
44 imgDown = new Array();
45 imgLink = new Array();
46 imgText = new Array();
47 imgWdh = new Array();
48 imgHgt = new Array();
49 imgBdr = new Array();
50 }
51
52
53 function generateEntryForm() {
54 with(parent.frames[1].document) {
55 open();
56 writeln("<HTML><BODY BGCOLOR=FFFFEE><FONT FACE=Arial SIZE=2><BLOCKQUOTE>" +
57 "Wybierz lub wpisz nazwy plików z obrazkami. Dodaj plik łącza " +
58 "(np., <FONT FACE=Courier>web_page.html</FONT>) lub tekst protokołu" +
59 " (np., <FONT FACE=Courier>javascript:</FONT>) dla wszystkich " +
60 " atrybutów HREF oraz wpisz komunikat, który chcesz wyświetlać " +
61 " w pasku stanu podczas zdarzenia <FONT FACE=\"Courier\">MouseOver" +
62 " </FONT>. Następnie wybierz <B>Generuj</B>, aby powstał potrzebny" +
63 " Ci kod, lub <B>Podgląd</B>, aby zobaczyć działanie tego kodu." +
64 "</BLOCKQUOTE> <FORM NAME='imgProfile' onSubmit='return false;'>" +
65 "<CENTER><TABLE BORDER=0 ALIGN=CENTER CELLSPACING=5 CELLPADDING=5>" +
66 "<TH ALIGN=LEFT><FONT FACE=Arial>#" +
67 "<TH ALIGN=LEFT><FONT FACE=Arial>Obrazek główny" +
68 "<TH ALIGN=LEFT><FONT FACE=Arial>Obrazek aktywny" +
69 (imgDefaults.mousedown.checked ? "<TH ALIGN=LEFT>" +
70 "<FONT FACE=Arial>MouseDown Path" : "") +
71 "<TR><TD><BR></TD></TR>");
72 }
73
74 for (i = 0; i < imgPrim.length; i++) {
75 with(parent.frames[1].document) {
76 writeln("<TR>" +
77 "<TD><FONT FACE=Arial SIZE=2><CENTER><B>" + (i + 1) +
78 "</B></CENTER><TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2>" +
79 "<INPUT TYPE=FILE NAME='prim" + i + "' VALUE='" + imgPrim[i] +
80 "'><TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2><INPUT TYPE=FILE " +
81 "NAME='seci" + i + "' VALUE='" + imgRoll[i] + "'>" +
82 (imgDefaults.mousedown.checked ? "<TD VALIGN=BOTTOM><FONT " +
83 "FACE=Arial SIZE=2><INPUT TYPE=FILE NAME='down" + i + "' VALUE='" +
84 imgDown[i] + "'>" : "") + "<TR>" +
85 "<TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2> &nbsp;&nbsp;</TD>" +
86 "<TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2><INPUT TYPE=TEXT " +
87 "NAME='href" + i + "' VALUE='" + imgLink[i] + "'> " +
88 "<IMG SRC='images/href.jpg'>" + "<TD VALIGN=BOTTOM><FONT FACE=Arial" +
89 " SIZE=2><INPUT TYPE=TEXT NAME='stat" + i + "' VALUE='" +
90 imgText[i] + "'> <IMG SRC='images/statusbar.jpg'> " +
91 (!imgDefaults.mousedown.checked ?"<TR>" : "") +
92 "<TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2>" +
93 (!imgDefaults.mousedown.checked ?
94 "</TD><TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2>" : "") +
95 "<INPUT TYPE=TEXT NAME='wdh" + i + "' VALUE='" +
96 imgWdh[i] + "' SIZE=3> <IMG SRC='images/wdh.jpg'> " +
Przykład 5.2. nav.html (ciąg dalszy)
97 "&nbsp; <INPUT TYPE=TEXT NAME='hgt" + i + "' VALUE='" +
98 imgHgt[i] + "' SIZE=3> <IMG SRC='images/hgt.jpg'> &nbsp;" +
99 (!imgDefaults.mousedown.checked ?
100 "<TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2>" : "") +
101 "<INPUT TYPE=TEXT NAME='bdr" + i + "' VALUE='" + imgBdr[i] +
102 "' SIZE=3> <IMG SRC='images/bdr.jpg'>" +
103 "<TR><TD VALIGN=BOTTOM COLSPAN=" +
104 (!imgDefaults.mousedown.checked ? "3" : "4") +
105 "><BR><HR NOSHADE><BR></TD></TR>");
106 }
107 }
108
109 with(parent.frames[1].document) {
110 writeln("</TABLE><CENTER><INPUT TYPE=BUTTON " +
111 "onClick='parent.frames[0].imgValid8(this.form, true);'" +
112 " VALUE='Generuj'><INPUT TYPE=BUTTON " +
113 "onClick='parent.frames[0].imgValid8(this.form, false);' " +
114 "VALUE='Podgląd'> <INPUT TYPE=RESET VALUE=' Wyczyść '>" +
102

115 "</FORM></BODY></HTML>");
116 close();
117 }
118 }
119
120 function imgValid8(imgTemplate, mimeType) {
121 for (var i = 0; i < imgPrim.length; i++) {
122 if (imgTemplate['prim' + i].value == "" ||
123 imgTemplate['seci' + i].value == "" ||
124 imgTemplate['href' + i].value == "") {
125 alert("Wszystkie obrazki i atrybuty HREF muszą mieć adresy URL");
126 return;
127 }
128 if (imgDefaults.mousedown.checked) {
129 if(imgTemplate['down' + i].value == "") {
130 alert("Wszystkie obrazki i atrybuty HREF muszą mieć adresy URL");
131 return;
132 }
133 }
134 }
135 genJavaScript(imgTemplate, mimeType);
136 }
137
138 function genJavaScript(imgTemplate, mimeType) {
139 imageLinks = '';
140
141 if (mimeType) {
142 lt = "&lt;";
143 gt = "&gt;";
144 br = "<BR>";
145 HTML = true;
146 nbsp = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
147 }
148 else {
149 lt = "<";
150 gt = ">";
151 br = lb;
152 HTML = false;
153 nbsp = " ";
154 }
155
156 if(imgTemplate != null) {
157 setArrays();
158 for (var i = 0; i < (imgDefaults.imgnumber.selectedIndex + 1); i++) {
159 imgPrim[i] = purify(imgTemplate['prim' + i].value);
160 imgRoll[i] = purify(imgTemplate['seci' + i].value);
Przykład 5.2. nav.html (ciąg dalszy)
161 if (imgDefaults.mousedown.checked) {
162 imgDown[i] = purify(imgTemplate['down' + i].value);
163 }
164 imgLink[i] = purify(imgTemplate['href' + i].value);
165 imgText[i] = purify(imgTemplate['stat' + i].value);
166 imgWdh[i] = purify(imgTemplate['wdh' + i].value);
167 imgHgt[i] = purify(imgTemplate['hgt' + i].value);
168 imgBdr[i] = purify(imgTemplate['bdr' + i].value);
169 }
170 }
171
172 if (HTML) {
173 primJavaScript = "<HTML><HEAD><TITLE>Image Machine Code</TITLE>" +
174 "</HEAD><BODY BGCOLOR=FFFFEE><FONT FACE=Arial>" +
175 "<I>Wytnij i wklejaj poniższy kod do pliku HTML. Kod niebieski " +
176 "to podane przez Ciebie informacje.</I>" +
177 "<BR><BR></FONT><FONT SIZE=2 FACE=Arial>" +
178 lt + "HTML" + gt + "<BR>" + lt + "HEAD" + gt + "<BR>" +
179 lt + "TITLE" + gt + "Kod z Image Machine" + lt + "/TITLE" + gt;
180 }
181 else {
182 primJavaScript = "<HTML><HEAD><TITLE>Kod z Image Machine</TITLE>";
183 }
184
185 primJavaScript += br + br + lt + "SCRIPT LANGUAGE=\"JavaScript\"" +
186 gt + br + br + "// Definicja zmiennych globalnych w JavaScript 1.0" + br +
187 "var canRollOver = false;" + br + "var canClickDown = false;" + br +
188 br + lt + "/SCR" + "IPT" + gt + br + br + lt +
189 "SCRIPT LANGUAGE =\"JavaScript1.1\"" + gt + br + br +
103 Rozdział 5 - ImageMachine

190 "// W JavaScript 1.1 zmiana canRollOver na true" + br +


191 "canRollOver = true;" + br + br;
192
193 secJavaScript = lt + "SCRIPT LANGUAGE=\"JavaScript1.2\"" + gt + br +
194 br + "// Zmiana w JavaScript 1.2 canClickDown na true" + br +
195 "canClickDown = true;" + br + br;
196
197 for (var j = 0; j < imgPrim.length; j++) {
198 primJavaScript += "// Obrazki podstawowe i aktywne #" +
199 (j + 1) + br +"switch" + (j + 1) + "out = new Image(" +
200 (HTML ? fontOpen : "") + imgWdh[j] +
201 (HTML ? "</FONT>," : ", ") +
202 (HTML ? fontOpen : "") + imgHgt[j] +
203 (HTML ? fontClose : "") + "); " + br + "switch" + (j + 1) +
204 "out.src = '" +
205 (HTML ? fontOpen : "") +
206 (imgPrim[j].indexOf(":\\") != -1 ? pathPrep(imgPrim[j]) :
207 imgPrim[j]) +
208 (HTML ? fontClose : "") + "';" + br + "switch" + (j + 1) +
209 "over = new Image(" +
210 (HTML ? fontOpen : "") + imgWdh[j] +
211 (HTML ? "</FONT>," : ", ") +
212 (HTML ? fontOpen : "") + imgHgt[j] +
213 (HTML ? fontClose : "") + "); " + br + "switch" + (j + 1) +
214 "over.src = '" +
215 (HTML ? fontOpen : "") +
216 (imgRoll[j].indexOf(":\\") != -1 ? pathPrep(imgRoll[j]) :
217 imgRoll[j]) +
218 (HTML ? fontClose : "") + "';" + br + br;
219
220 if (imgDefaults.mousedown.checked) {
221 secJavaScript += "// obrazek MouseDown #" + (j + 1) + br +
222 "switch" + (j + 1) + "down = new Image(" +
223 (HTML ? fontOpen : "") + imgWdh[j] +
224 (HTML ? "</FONT>," : ", ") +
Przykład 5.2. nav.html (ciąg dalszy)
225 (HTML ? fontOpen : "") + imgHgt[j] +
226 (HTML ? fontClose : "") + "); " + br + "switch" +
227 (j + 1) + "down.src = '" +
228 (HTML ? fontOpen : "") +
229 (imgPrim[j].indexOf(":\\") != -1 ? pathPrep(imgDown[j]) :
230 imgDown[j]) +
231 (HTML ? fontClose : "") + "';" + br + br;
232 }
233
234 imageLinks += lt + "!-- <I> Image Link #" + (j + 1) +
235 " </I>//--" + gt + br + lt + "A HREF=\"" +
236 (HTML ? fontOpen : "") + imgLink[j] +
237 (HTML ? fontClose : "") + "\" " + br + nbsp +
238 "onMouseOver=\"imageSwap('switch" + (j + 1) +
239 "', 'over', false); display('" +
240 (HTML ? fontOpen : "") + imgText[j] +
241 (HTML ? fontClose : "") + "'); return true;\"" + br +
242 nbsp + "onMouseOut=\"imageSwap('switch" +
243 (j + 1) + "', 'out', false); display('');\"" +
244 (imgDefaults.mousedown.checked ?
245 br + nbsp + "onMouseDown=\"isDown=!isDown; imageSwap('switch" +
246 (j + 1) + "', 'down', true);\"" : "") +
247 gt + br + lt + "IMG SRC=\"" +
248 (HTML ? fontOpen : "") + pathPrep(imgPrim[j]) +
249 (HTML ? fontClose : "") + "\"" + br + nbsp +
250 "NAME=switch" + (j + 1) + br + nbsp + "WIDTH=" +
251 (HTML ? fontOpen : "") + imgWdh[j] +
252 (HTML ? fontClose : "") + br + nbsp + "HEIGHT=" +
253 (HTML ? fontOpen : "") + imgHgt[j] +
254 (HTML ? fontClose : "") + br + nbsp + "BORDER=" +
255 (HTML ? fontOpen : "") + imgBdr[j] +
256 (HTML ? fontClose : "") +
257 gt + "" + lt + "/A" + gt + br + br + br;
258 }
259
260 scriptClose = br + lt + "/SCR" + "IPT" + gt + br + br;
261
262 swapCode = br + lt + "/SCR" + "IPT" + gt + br + br +
263 lt + "SCRIPT LANGUAGE =\"JavaScript\"" + gt + br + br +
264 (imgDefaults.mousedown.checked ?
104

265 "var isDown = false;" + br + br : "") +


266 "// Warunkowe wykonanie przewijania w JavaScript 1.0" + br +
267 "function imageSwap(imageName, imageSuffix) {" + br +
268 nbsp + "if (!canRollOver) { return; }" + br + nbsp +
269 (imgDefaults.mousedown.checked ?
270 "if (!isDown) { " + br + nbsp + nbsp : "") +
271 "document[imageName].src = " +
272 "eval(imageName + imageSuffix + \".src\");" + br + nbsp +
273 (imgDefaults.mousedown.checked ? nbsp + "}" + br + nbsp +
274 "else if (canClickDown) {" + br +
275 nbsp + nbsp + "document[imageName].src = " +
276 "eval(imageName + imageSuffix + \".src\");" + br +
277 nbsp + nbsp + "}" + br + nbsp : "") + "}" + br + br +
278 "function display(stuff) { window.status = stuff; }" +
279 br + br + lt + "/SCR" + "IPT" + gt + br;
280
281 primHTML = br + lt + "/HEAD" + gt + br +
282 lt + "BODY BGCOLOR=FFFFEE" +
283 gt + br + br + (HTML ? "<FONT COLOR=RED>" : "") + lt +
284 "!-- <I> Zaczyna się kodowanie obrazków</I> //--" + gt + br +
285 (HTML ? fontClose : "") + br + br;
286
287 secHTML = (HTML ? "<FONT COLOR=RED>" : "") +
288 lt + "!-- <I> Koniec kodowania obrazków</I> //--" + gt +
Przykład 5.2. nav.html (ciąg dalszy)
289 (HTML ? fontClose : "") + br + br +
290 (HTML ? lt + "/BODY" + gt + br + lt + "/HTML" + gt : "") +
291 br + br + "<CENTER><FORM>" + br +
292 "<INPUT TYPE=BUTTON onClick='parent.frames[0].genJavaScript(null, " +
293 (HTML ? "false" : "true") + ");' VALUE='" +
294 (HTML ? 'Podgląd' : 'Generuj') + "'>&nbsp;&nbsp;&nbsp;" +
295 "<INPUT TYPE=BUTTON " +
296 "onClick='parent.frames[0].generateEntryForm();' " +
297 "VALUE='Zmień dane'>" + br + "</FORM></CENTER>" + br + br +
298 "</BODY></HTML>";
299
300 agregate = primJavaScript +
301 (imgDefaults.mousedown.checked ? scriptClose + secJavaScript : "") +
302 swapCode + primHTML + imageLinks + secHTML;
303
304 parent.frames[1].location.href =
305 "javascript: parent.frames[0].agregate";
306 }
307
308 function purify(txt) { return txt.replace(/\'|\"/g, ""); }
309
310 function pathPrep(path) {
311 if (path.indexOf(":\\") != -1) {
312 path = path.replace(/\\/g, "/");
313 path = path.replace(/:\//, "|/");
314 return "file:///" + path;
315 }
316 else { return path; }
317 }
318
319 </SCRIPT>
320 </HEAD>
321 <BODY BGCOLOR=FFFFEE>
322 <FORM>
323 <TABLE BORDER="0">
324 <TR>
325 <TD VALIGN=MIDDLE>
326 <IMG SRC="images/image_machine.gif" WIDTH=275 HEIGHT=56 HSPACE=25>
327 </TD>
328 <TD>
329 <!-- Tworzenie warunków domyślnych //-->
330 <TABLE BORDER="0" ALIGN="CENTER">
331 <TR>
332 <TD VALIGN="TOP">
333 <FONT FACE="Arial" SIZE=2>
334 Par obrazków
335 </TD>
336 <TD VALIGN="TOP">
337 <FONT FACE="Arial" SIZE=2>
338 <SCRIPT LANGUAGE="JavaScript1.2">
339 <!--
105 Rozdział 5 - ImageMachine

340 genSelect("imgnumber", 50, 1, 1);


341 //-->
342 </SCRIPT>
343 </TD>
344 <TD VALIGN="TOP">
345 <FONT FACE="Arial" SIZE=2>
346 Szerokość
347 </TD>
348 <TD VALIGN="TOP">
349 <FONT FACE="Arial" SIZE=2>
350 <SCRIPT LANGUAGE="JavaScript1.2">
351 <!--
352 genSelect("pxlwidth", 250, 0, 90);
Przykład 5.2. nav.html (dokończenie)
353 //-->
354 </SCRIPT>
355 </TD>
356 <TD VALIGN="TOP">
357 <FONT FACE="Arial" SIZE=2>
358 MouseDown
359 </TD>
360 <TD VALIGN="TOP">
361 <FONT FACE="Arial" SIZE=2>
362 <INPUT TYPE=CHECKBOX NAME="mousedown">
363 </TD>
364 </TR>
365 <TR>
366 <TD VALIGN="TOP">
367 <FONT FACE="Arial" SIZE=2>
368 Obramowanie
369 </TD>
370 <TD VALIGN="TOP">
371 <FONT FACE="Arial" SIZE=2>
372 <SCRIPT LANGUAGE="JavaScript1.2">
373 <!--
374 genSelect("defbdr", 10, 0, 0);
375 //-->
376 </SCRIPT>
377 </TD>
378 <TD VALIGN="TOP">
379 <FONT FACE="Arial" SIZE=2>
380 Wysokość
381 </TD>
382 <TD VALIGN="TOP">
383 <FONT FACE="Arial" SIZE=2>
384 <SCRIPT LANGUAGE="JavaScript1.2">
385 <!--
386 genSelect("pxlheight", 250, 0, 50);
387 //-->
388 </SCRIPT>
389 </TD>
390 <TD VALIGN="TOP">
391 <FONT FACE="Arial" SIZE=2>
392 <INPUT TYPE=BUTTON VALUE="Generuj"
393 onClick="captureDefaultProfile(this.form);">
394 </TD>
395 <TD VALIGN="TOP">
396 <FONT FACE="Arial" SIZE=2>
397 <INPUT TYPE=RESET VALUE=" Reset ">
398 </TD>
399 </TR>
400 </TABLE>
401 </TD>
402 </TR>
403 </TABLE>
404 </CENTER>
405 </FORM>
406 </BODY>
407 </HTML>

To jest jak dotąd największy fragment kodu aplikacji. Niektóre części mogą wyglądać dość onieśmielająco, ale nie jest
aż tak źle, jak można by sądzić. Aby lepiej zrozumieć działanie aplikacji, zastanówmy się nad czynnościami typowego
użytkownika. Rozważmy pięcioetapowy scenariusz:
1.załadowanie strony,
106

2.użytkownik wprowadza liczbę par obrazków i ustawienia domyślne, następnie wybiera Generuj,
3.użytkownik wypełnia pola na nazwy plików, atrybuty HREF i tak dalej, a w końcu wybiera Generuj, aby
obejrzeć kod,
4.użytkownik wybiera Podgląd, aby zobaczyć działanie kodu,
5.użytkownik wybiera Zmień dane, aby wprowadzić jakieś poprawki.

Krok 1. Załadowanie strony


Wszystko wygląda całkiem normalnie. Zestaw ramek nazywa się index.html, a mamy w nim dwie ramki: nav.html oraz
base.html. Jednak JavaScript wykona pewną pracę jeszcze zanim użytkownik będzie miał szansę cokolwiek zrobić.
Zwróćmy uwagę na wiersze 323–403. Znajduje się tam kod tabeli z kilkoma wywołaniami funkcji JavaScriptu
w komórkach, na przykład:
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
Pary obrazków
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genSelect("imgnumber", 50, 1, 1);
//-->
</SCRIPT>
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
Szerokość
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genSelect("pxlwidth", 250, 0, 90);
//-->
</SCRIPT>
</TD>

W wywołaniach funkcji genSelect() język JavaScript używany jest do dynamicznego tworzenia list wyboru. Każda
lista umożliwia ustawienie domyślnych wartości atrybutów obrazków. Lista wyboru działa lepiej niż pole tekstowe,
gdyż nie trzeba się tak martwić o sprawdzanie zawartości pola. Użytkownik nie może wprowadzić nieprawidłowej
wartości (na przykład innej niż liczba), opisującej ramkę czy szerokość obrazka. Ale kto chce wpisywać listę z 250 czy
300 pozycjami, po jednej dla każdej liczby z takiego zakresu? Załóżmy, że musimy zmienić liczbę opcji. JavaScript jest
tu bardzo przydatny, gdyż umożliwia wywołanie po prostu genSelect() dla każdej tworzonej listy – spójrzmy
na wiersze 12–19:
function genSelect(name, count, start, select) {
var optStr = "";
for (var h = start; h <= count; h++) {
optStr += "<OPTION VALUE=" + h +
(h == select ? " SELECTED" : "") + ">" + h;
}
document.write("<SELECT NAME=" + name + ">" + optStr + "</SELECT>");
}

Funkcja genSelect() oczekuje czterech parametrów: napisu z nazwą listy wyboru, liczby oznaczającej największą
wartość na liście, liczby początkowej (całkowitej) dalej zwiększanej o 1 i liczby określającej wybraną opcję. genSelect()
po prostu tworzy w pętli kolejne znaczniki <OPTION>. Kiedy pętla zakończy swoje działanie, JavaScript zapisuje wynik
między znacznikami <SELECT> do dokumentu. Teraz strony są ładowane i gotowe do działania. Zobaczmy, co się dzieje,
kiedy użytkownik już wprowadzi ustawienia domyślne.

Krok 2. Określenie liczby par obrazków i ustawień domyślnych


Zwróćmy uwagę na rysunek 5.1, gdzie użytkownik ustawił wartości domyślne w czterech listach wyboru i jednym polu
opcji. Najważniejsze jest ustawienie liczby par obrazków. ImageMachine pozwala użyć od 1 do 50 takich par. Wątpię,
czy owe 50 będzie kiedykolwiek potrzebne, ale mała nadwyżka mocy nie zaszkodzi.
107 Rozdział 5 - ImageMachine

Użytkownik może wybierać domyślną szerokość i wysokość obrazków w pikselach, przy czym dopuszczalne rozmiary
mogą sięgać od 1 do 250. Być może ktoś będzie chciał to w przyszłości zmienić, ale na razie ustawienia są właśnie
takie. Domyślna szerokość i wysokość to odpowiednio 90 i 50 – otrzymuje się w ten sposób bardzo ładny prostokąt
o rozmiarach typowego przycisku.
Ostatnia lista wyboru pozwala zdefiniować obramowanie, które może mieć grubość od 0 do 10 pikseli. Zwykle ustawia
się ją na 0, ale spotyka się też przewijane obrazki z widocznymi ramkami.
Zaznaczenie pola opcji powoduje dodanie obsługi zdarzenia onMouseDown, obsługiwanego w JavaScripcie w wersji 1.2
i w odpowiednich modelach dokumentu w NN i MSIE. Teraz użytkownik musi jeszcze tylko wybrać Generuj,
a pojawi się szablon strony z obrazkami, bazujący na wpisanych właśnie danych.

Krok 3. Określenie nazw plików, atrybutów HREF i tak dalej


Kiedy użytkownik kliknie Generuj, ImageMachine wygeneruje odpowiedni szablon obrazków, pokazany na rysunku 5.2.
Do generowania tego szablonu używa się trzech funkcji: captureDefaultProfile(), setArrays() oraz
generateEntryForm().

captureDefaultProfile()
Funkcja ta wywoływana jest jako pierwsza, a znajduje się w wierszach 21–39:
function captureDefaultProfile(formObj) {
setArrays();
imgDefaults = formObj;
var imgQty = (imgDefaults.imgnumber.selectedIndex + 1);
var imgHeight = (imgDefaults.pxlheight.selectedIndex);
var imgWidth = (imgDefaults.pxlwidth.selectedIndex);
var imgBorder = (imgDefaults.defbdr.selectedIndex);
for (var i = 0; i < imgQty; i++) {
imgPrim[i] = "";
imgRoll[i] = "";
imgDown[i] = "";
imgLink[i] = "";
imgText[i] = "";
imgWdh[i] = imgWidth;
imgHgt[i] = imgHeight;
imgBdr[i] = imgBorder;
}
generateEntryForm();
}

Pierwsze jej zadanie to wywołanie funkcji setArrays(). Ta funkcja z kolei, pokazana poniżej (wiersze 41–50),
deklaruje i inicjalizuje 8 tablic. Każda z tych tablic odpowiada jakiemuś atrybutowi wartości poszczególnych grup
obrazków. Na przykład imgPrim zawiera nazwy plików z obrazkami podstawowymi, imgRoll nazwy plików
z obrazkami pod wskaźnikiem myszy, i tak dalej. Jeśli tablice nie były jeszcze zadeklarowane, zajmie się tym właśnie
funkcja setArrays(). Jeżeli tablice już zadeklarowano (użytkownik wygenerował kod), tablice zostaną wyzerowane.
function setArrays() {
imgPrim = new Array();
imgRoll = new Array();
imgDown = new Array();
imgLink = new Array();
imgText = new Array();
imgWdh = new Array();
imgHgt = new Array();
imgBdr = new Array();
}

Następnie captureDefaultProfile() kopiuje obiekt formularza formObj do zmiennej imgDefaults. To ważne:


imgDefaults jest globalna, więc nie znika po zakończeniu działania funkcji i pozwala obsłużyć przełączanie się
między generacją kodu a poprawianiem ustawień. Jest to jedyne miejsce w całej aplikacji, gdzie imgDefaults jest
ustawiana. Oznacza to, że użytkownik może zmienić wartości domyślne, jedynie uruchamiając znów przycisk Generuj.
Kiedy ImageMachine ma już ustawienia domyślne użytkownika, captureDefaultProfile() deklaruje cztery
następujące zmienne lokalne:
var imgQty = (imgDefaults.imgnumber.selectedIndex + 1);
var imgHeight = (imgDefaults.pxlheight.selectedIndex);
var imgWidth = (imgDefaults.pxlwidth.selectedIndex);
var imgBorder = (imgDefaults.defbdr.selectedIndex);
108

Pętla for robi tyle iteracji, ile użytkownik chce grup obrazków, przy czym każda grupa jest opisywana
w zadeklarowanych właśnie tablicach. imgPrim zawiera obrazki zdarzenia MouseOut, imgRoll obrazki zdarzenia
MouseOver, a imgDown zawiera obrazki zdarzenia MouseDown. imgLink i imgText to wartości atrybutu HREF i treść
paska stanu. Jako że użytkownik określi wartości pierwszych pięciu elementów bezpośrednio w poszczególnych
okienkach szablonu, odpowiednie wartości początkowo ustawiane są jako puste.
Wszystkie elementy pozostałych trzech tablic ustalane są podobnie. Domyślna szerokość, wysokość i obramowanie
dla wszystkich obrazków są takie same, a użytkownik może je później zmieniać.

generateEntryForm()
Ostatnie zadanie naszej funkcji to wywołanie generateEntryForm(). Tutaj naprawdę zaczyna się coś dziać. Funkcja
jest odpowiedzialna za utworzenie szablonu HTML, w którym użytkownik będzie mógł wprowadzać dane opisujące
poszczególne grupy obrazków. Przyjrzyjmy się wierszom od 53 do 118.
Sześćdziesiąt sześć wierszy na opisanie jednej tylko funkcji! To narusza wszelkie zasady dotyczące wielkości funkcji,
ale generateEntryForm() i tak realizuje jedną tylko operację: tworzy szablon obrazków. Jest ona prosta, jeśli tylko
podzieli się ją na trzy części HTML: nagłówek tabeli (TH), pola tekstowe formularza oraz przyciski. Cała funkcja tak
naprawdę jest szeregiem wywołań document. writeln(). Oto kod zapisujący nagłówki – z wierszy 54–72:
with(parent.frames[1].document) {
open();
writeln("<HTML><BODY BGCOLOR=FFFFEE><FONT FACE=Arial SIZE=2><BLOCKQUOTE>" +
"Wybierz lub wpisz nazwy plików z obrazkami. Dodaj plik łącza " +
"(np., <FONT FACE=Courier>web_page.html</FONT>) lub tekst protokołu" +
" (np., <FONT FACE=Courier>javascript:</FONT>) dla wszystkich " +
" atrybutów HREF oraz wpisz komunikat, który chcesz wyświetlać " +
" w pasku stanu podczas zdarzenia <FONT FACE=\"Courier\">MouseOver" +
" </FONT>. Następnie wybierz <B>Generuj</B>, aby powstał potrzebny" +
" Ci kod, lub <B>Podgląd</B>, aby zobaczyć działanie tego kodu." +
"</BLOCKQUOTE> <FORM NAME='imgProfile' onSubmit='return false;'>" +
"<CENTER><TABLE BORDER=0 ALIGN=CENTER CELLSPACING=5 CELLPADDING=5>" +
"<TH ALIGN=LEFT><FONT FACE=Arial>#" +
"<TH ALIGN=LEFT><FONT FACE=Arial>Obrazek główny" +
"<TH ALIGN=LEFT><FONT FACE=Arial>Obrazek aktywny" +
(imgDefaults.mousedown.checked ? "<TH ALIGN=LEFT>" +
"<FONT FACE=Arial>MouseDown Path" : "") +
"<TR><TD><BR></TD></TR>");
}

Warto zwrócić uwagę na to, że formularz szablonu obrazków jest zawarty w tabeli. Wszystko w tym bloku jest
statyczne poza wierszami 69 i 70. Używając operatora trójargumentowego, JavaScript dołącza dodatkowy nagłówek,
jeśli użytkownik zaznaczył opcję MouseDown. Przyjrzyjmy się poniższym wierszom:
(imgDefaults.mousedown.checked ? "<TH ALIGN=LEFT>" +
"<FONT FACE=Arial>MouseDown Path" : "") +

O to właśnie tutaj chodzi: jeśli zrozumie się to, to cała funkcja nie będzie miała przed nami żadnych tajemnic, albowiem
generateEntryForm() podejmuje wszystkie swoje decyzje na podstawie zaznaczenia bądź niezaznaczenia opcji
MouseDown. Przyjrzyjmy się polom tekstowym w wierszach 74–107.
Dla tylu elementów, ile znajduje się w imgPrim (a więc ile jest grup obrazków) dodawany jest nowy znacznik TR,
obejmujący TD z dwoma lub trzema polami TD, w których są pola FILE, tekst atrybutu HREF, tekst paska stanu oraz
szerokość, wysokość i obramowanie. Jeśli dokładniej przyjrzymy się, zauważymy, że każdy element o indeksie i,
deklarowany w setArrays() ma wartość odpowiedniego pola tekstowego.
imgPrim, imgRoll, imgDown, imgLink i imgText pierwotnie były pustymi ciągami. Aż do teraz użytkownik
nie miał szans wprowadzenia tam jakiejkolwiek wartości. Jednak szerokość, wysokość i obramowanie miały wartości
domyślne, więc dobrze będzie przypisać odpowiednim polom wartości z tablic imgWidth, imgHeight i imgBorder.
Oto powtarzający się fragment kodu:
(imgDefaults.mousedown.checked ?

Za każdym razem dochodzimy do punktu, kiedy na podstawie zaznaczenia bądź niezaznaczenia przez użytkownika
opcji MouseDown musimy ewentualnie wygenerować kod obsługi dodatkowego obrazka.
Nagłówki i pola tekstowe formularza już mamy. Pozostało nam wygenerować przyciski Generuj,
Podgląd i Wyczyść:
with(parent.frames[1].document) {
writeln("</TABLE><CENTER><INPUT TYPE=BUTTON " +
109 Rozdział 5 - ImageMachine

"onClick='parent.frames[0].imgValid8(this.form, true);'" +
" VALUE='Generuj'><INPUT TYPE=BUTTON " +
"onClick='parent.frames[0].imgValid8(this.form, false);' " +
"VALUE='Podgląd'> <INPUT TYPE=RESET VALUE=' Wyczyść '>" +
"</FORM></BODY></HTML>");
close();
}

Wyczyść to typowy przycisk RESET, więc nie będziemy się tu nim zajmować. Zwróćmy natomiast uwagę na pozostałe
dwa przyciski: kliknięcie obu wywołuje tę samą funkcję, imgValid8(). W obydwóch przypadkach przekazywany jest
formularz, ale raz z wartością true, a raz z false. Ta właśnie wartość decyduje, czy generowany kod jest pokazywany,
czy interpretowany. Teraz tym właśnie się zajmiemy.
Być może zechcesz przejrzeć kolejne wiersze generateEntryForm(), aby zobaczyć, jak wygląda HTML formularza.
Można wtedy zobaczyć, jak tworzony jest długi napis z całym formularzem, który po wypełnieniu przez użytkownika
pozwala przejść dalej. Funkcja generateEntryForm() wymusza wypełnienie formularza na użytkowniku, który
zgodnie z naszą czteroetapową procedurą, omówioną na początku tego rozdziału, wybierze Generuj. Wtedy
wywoływane są właśnie funkcje imgValid8() z wierszy 120–136:
function imgValid8(imgTemplate, mimeType) {
for (var i = 0; i < imgPrim.length; i++) {
if (imgTemplate['prim' + i].value == "" ||
imgTemplate['seci' + i].value == "" ||
imgTemplate['href' + i].value == "") {
alert("Wszystkie obrazki i atrybuty HREF muszą mieć adresy URL");
return;
}
if (imgDefaults.mousedown.checked) {
if(imgTemplate['down' + i].value == "") {
alert("Wszystkie obrazki i atrybuty HREF muszą mieć adresy URL");
return;
}
}
}
genJavaScript(imgTemplate, mimeType);
}

Ta funkcja zapewnia, że użytkownik wprowadził odpowiednie wartości we wszystkich polach przeznaczonych


do wstawienia informacji o plikach. Wartości przypisywane są imgTemplate. Znów – używając długości imgPrim
jako ograniczenia pętli – ImageMachine przechodzi przez wszystkie pola tekstowe opisujące obrazki. Pola zawierające
nazwy plików głównych nazwane są prim + i, gdzie i jest liczbą w zakresie od 0 do imgPrim.length - 1. Pola
zawierające obrazki ze wskaźnikiem myszy są nazywane podobnie, przy czym zamiast prim używane jest seci. Jeśli
użytkownik załącza też obrazki zdarzenia MouseDown, odpowiednie pola tekstowe mają w nazwie down.

genJavaScript()
Jeśli którekolwiek sprawdzane pole okaże się puste, fakt ten jest zgłaszany użytkownikowi, a funkcja kończy swoje
działanie. Jeżeli każde pole zawiera jakikolwiek tekst, ImageMachine wywołuje funkcję genJavaScript(),
przekazując imgTemplate i niesprawdzoną jeszcze wartość logiczną mimeType. Jak nietrudno zgadnąć,
genJavaScript() odpowiedzialna jest za stworzenie kodu JavaScript strony wynikowej. Funkcja ta jest bardzo długa,
ale działa podobnie jak generateEntryForm() (można ją znaleźć w wierszach 138–306).
Mogło by się wydawać, że generateEntryForm() jest długa! To nadal jednak działanie tego samego typu –
genJavaScript() ma jedno tylko zadanie: wygenerować kod obsługi przewijania obrazków pod wskaźnikiem
myszy, czyli głównie kod JavaScript.
genJavaScript() najpierw wyzeruje zmienne globalne imageLinks. Więcej o tych zmiennych globalnych
zawierających kod dowiemy się za chwilę. Następnie ustawiane są pomocnicze zmienne globalne – zgodnie
z mimeType. Oto one w wierszach 141 do 154:
if (mimeType) {
lt = "&lt;";
gt = "&gt;";
br = "<BR>";
HTML = true;
nbsp = "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;";
}
else {
lt = "<";
gt = ">";
br = lb;
110

HTML = false;
nbsp = " ";
}

Techniki języka JavaScript:


ostrożne kodowanie w JavaScript
Nie jest przypadkiem, że znacznik SCRIPT w generowanym kodzie ma kilka różnych
wartości atrybutu LANGUAGE. Nieco kodu znajdziemy między znacznikami <SCRIPT
LANGUAGE="JavaScript"> a </SCRIPT> w wierszu 185. Inny kod mieści się między
<SCRIPT LANGUAGE="JavaScript1.1"> a </SCRIPT> w wierszu 189 i w końcu jesz-
cze inny między <SCRIPT LANGUAGE="JavaScript1.2"> a </SCRIPT> w wierszu 193.
W ten sposób przeglądarki, obsługujące różne wersje JavaScriptu, nie wykonują kodu
przez nie nierozumianego, więc unikamy błędów wykonania. Na przykład w wersji 1.0
obiekt Image() nie jest obsługiwany, dlatego w kodzie między znacznikami <SCRIPT
LANGUAGE="JavaScript"> a </SCRIPT> żadnych takich obiektów nie znajdziemy,
chyba że będą one zagnieżdżone między znacznikami <SCRIPT> z atrybutem LANGUAGE
odpowiadającym wersji 1.1 lub wyższej.
Stosując kontrolne zmienne globalne, których wartości będą ustawiane w sekcjach
<SCRIPT>, można realizować takie ostrożne kodowanie. Kiedy przychodzi do wywołania
funkcji, która raczej nie jest w danej wersji obsługiwana, wywołuje się ją tylko wtedy,
gdy spełnione są odpowiednie warunki dotyczące wartości zmiennych kontrolnych.
Można to sobie obejrzeć na zmiennych canRollOver dla obiektu Image
i canClickDown dla obsługi zdarzenia onMouseDown.
Takie kodowanie pozwala zmniejszyć obciążenie. Na przykład przeglądarki nie obsługujące
JavaScriptu 1.2 nie wykonają żadnego kodu między <SCRIPT LANGUAGE= "JavaScript-
1.2"> a </SCRIPT>, dzięki czemu przeglądarka nie będzie w ogóle próbowała ściągać
obrazków związanych z obsługą zdarzenia onMouseDown.

Jeśli mimeType ma wartość true, zmienne globalne będą miały wartości wymuszające drukowanie kodu. Zmienne lt
i gt będą ustawione na &lt; i &gt;. Zmienna br ma wtedy wartość <BR>. HTML to zmienna logiczna, wskazująca, czy
użytkownik chce kod interpretować (zamiast pokazywać na ekranie). To zacznie mieć znaczenie zaraz
po wygenerowaniu kodu. Zmiennej nbsp przypisywana jest wartość spacji niedzielących w HTML. nbsp jest
odpowiednikiem użycia klawisza tabulacji w HTML.
Jeśli mimeType ma wartość false, zmienne globalne będą ustawione dla potrzeb kodu interpretowanego. Zmienne lt
i gt przyjmą wartości < i >, a zmienna br będzie miała wartość zmiennej lb. lb z kolei ustawiana jest w wierszach 6–
8:

var platform = navigator.platform;


var lb = (platform.indexOf("Win" != -1) ? "\n\r" :
(platform.indexOf("Mac" != -1) ? "\r" : "\n"));

Jak widać, zmienne lb i platform współpracują ze sobą. platform zawiera napis określający system operacyjny,
dla którego przeglądarka została skompilowana. lb ustawiana jest stosownie do wartości platform. W Windows
(i DOS) jest to koniec wiersza, czyli klawisz ENTER, znaki \n\r. W przypadku Macintosha jest to \r, a w Uniksie \n.
Takie potraktowanie kodu zapewni, że kod ten nie będzie miał pięciu kilometrów długości. Nie jest to specjalnie istotne
dla samej aplikacji, ale kiedy ImageMachine wygeneruje kod HTML, można będzie go znacznie łatwiej później oglądać.
111 Rozdział 5 - ImageMachine

Techniki języka JavaScript:


siła zmiennych globalnych
W tej aplikacji korzystamy z możliwości oferowanych przez zmienne globalne.
ImageMachine generuje kod interpretowany lub drukowany. Jeden z nich oglądamy,
drugi wykonujemy. Oba jego rodzaje są niemalże takie same – poza tym, że w kodzie
wykonywanym używa się znaków < i > –zamiast &lt; i &gt;. Zmienne globalne lt i gt
ustawiane są stosownie do tego, czy wybierze się przycisk Generuj, czy Podgląd.
Analogicznie określa się wartość zmiennych br i nbsp. Na tym właśnie polega siła
zmiennych globalnych: po prostu zmieniasz ich wartość i otrzymujesz napisy, które mogą
pełnić inne funkcje, będąc nadal równie użytecznymi.

Przejdźmy dalej – HTML ustawiane jest na false. Jest to zmienna globalna informującą, czy użytkownik chce
interpretować otrzymany kod. Nabierze to znaczenia zaraz po wygenerowaniu tego kodu. Zmienna nbsp to napis
składający się z białych znaków.
Teraz ImageMachine ma już informacje z pól formularza, wie też, co użytkownik pragnie zrobić z otrzymanym kodem.
Generowanie JavaScriptu zaczyna się tak naprawdę od wiersza 185 i trwa już do końca funkcji.
Przeglądając kod, kilka razy natkniesz się na wywołanie funkcji pathPrep(). Funkcja ta zmienia formatowanie napisu
z nazwą pliku, jeśli nazwa ta wygląda na lokalną w systemie Windows (o ścieżkach nazw więcej powiedziano
w rozdziale 3.). Po co całe to zamieszanie? Pamiętajmy, że Windows do rozdzielania poszczególnych katalogów używa
lewego ukośnika (\), natomiast przeglądarki (jak i unix) używają ukośnika zwykłego (/). Wobec tego konieczna będzie
zmiana wszystkich lewych ukośników na zwykłe, choć niektóre przeglądarki są w stanie taką konwersję zrealizować
w biegu.
Problem w tym, że JavaScript interpretuje lewy ukośnik jako część cytowanego znaku, zatem ścieżkę
C:\My_Dir\My.File JavaScript odczyta jako C:My_DirMy.File.10 Funkcja pathPrep(), pokazana w wierszach 310–
317, zajmuje się wymaganą konwersją:
function pathPrep(path) {
if (path.indexOf(":\\") != -1) {
path = path.replace(/\\/g, "/");
path = path.replace(/:\//, "|/");
return "file:///" + path;
}
else { return path; }
}

Przeglądarki otwierają też dokumenty lokalne za pośrednictwem protokołu file, zatem będziemy musieli przed URL
umieścić file:///, a zamiast dwukropka po nazwie dysku – znak potoku (|).

Czas na decyzje
Wszystko jest już gotowe do generowania kodu, czy to do wydruku, czy do interpretacji. Zanim kod ten zostanie
utworzony, aplikacja musi jeszcze wiedzieć, czy ma generować kod na podstawie nowych danych z szablonu
obrazków, czy użyć informacji już znajdujących się w tablicach. Zgodnie z naszym cyklem obsługi użytkownika,
opisanym wcześniej, dane zostały wprowadzone już z formularza do tablic. Inaczej jest, kiedy kod już został
wygenerowany i użytkownik cofnął się w celu poprawienia danych, a następnie znów wcisnął Generuj lub Podgląd.
Wkrótce zajmiemy się tym.
Jeśli informacje pochodzą z szablonu obrazków (o tym wypadku na razie mówimy), genJavaScript() czyści
wszystkie tablice img, wywołując setArrays(), dzięki czemu informacje z szablonu mogą zostać przypisane.
ImageMachine decyduje, czy wywołać setArrays() i ponownie przypisać wartości, określając wartość
imgTemplate. genJavaScript() może być wywołana trzema sposobami: przez przycisk Generuj, przycisk
Podgląd i z funkcji imgValid8(). Wywołanie funkcji genJavaScript() za pomocą przycisków przekazuje jako
imgTemplate wartość pustą (null). Jeśli zatem imgTemplate nie jest pusta, genJavaScript()przyjmuje, że należy
wyczyścić tablice i przygotować miejsce na nowe dane. W przeciwnym wypadku elementy tablic img nie będą
modyfikowane. Warto uważnie przeanalizować wiersze 156 do 170, aby zobaczyć, jak to działa:
if(imgTemplate != null) {
setArrays();

10
Jeszcze gorzej będzie, jeśli za ukośnikiem znajdzie się n czy r, np. C:\network czy C:\reg.exp, gdyż \n i \r zostaną zinterpretowane jako
odpowiednio nowy wiersz (ASCII 10) i powrót karetki (ASCII 13) (przyp.tłum.).
112

for (var i = 0; i < (imgDefaults.imgnumber.selectedIndex + 1); i++) {


imgPrim[i] = purify(imgTemplate['prim' + i].value);
imgRoll[i] = purify(imgTemplate['seci' + i].value);
if (imgDefaults.mousedown.checked) {
imgDown[i] = purify(imgTemplate['down' + i].value);
}
imgLink[i] = purify(imgTemplate['href' + i].value);
imgText[i] = purify(imgTemplate['stat' + i].value);
imgWdh[i] = purify(imgTemplate['wdh' + i].value);
imgHgt[i] = purify(imgTemplate['hgt' + i].value);
imgBdr[i] = purify(imgTemplate['bdr' + i].value);
}
}

Jeśli elementy tablicy zostały zmodyfikowane, wartości są im przypisywane przez szybki, aczkolwiek niebezpieczny
proces usuwania znaków funkcją purify() w wierszu 308:
function purify(txt) { return txt.replace(/\'|\"/g, ""); }

W ten sposób z wartości usuwa się wszystkie pojedyncze i podwójne cudzysłowy. Znaki te nie są zabronione, ale
JavaScript musi użyć obu tych znaków przy generowaniu kodu. Jeśli nie są one odpowiednio zacytowane lewymi
ukośnikami, to przy generowaniu muszą się pojawić problemy. purify() usuwa je z przekazanych napisów,
a następnie zwraca nowy wiersz.

Generowanie kodu
Kiedy już wszystko jest gotowe, czas na długo oczekiwane generowanie kodu. Dzieje się to w wierszach 185 do 305
przez przypisanie całego generowanego kodu kilku pomocniczym zmiennym. Zmienne są następujące:
primJavaScript
Zawiera znaczniki HTML, jak HTML, HEAD i TITLE. Posiada też wstępny kod JavaScript związany ze zdarzeniami
MouseOver i MouseOut.
secJavaScript
Zawiera kod przewijania związany ze zdarzeniem MouseDown w JavaScripcie 1.2.

Techniki języka JavaScript:


podstawianie tekstu w JavaScripcie 1.1 i 1.2
JavaScript 1.2 zawiera szereg nowych, użytecznych funkcji. Jedną z najważniejszych jest
możliwość używania wyrażeń regularnych do dopasowywania i podstawiania napisów.
Funkcje pathPrep() i purify() dają proste, ale potężne możliwości podstawiania tek-
stu w JavaScripcie 1.2. Jest to cecha świetna, ale przeglądarka Netscape 3.x nadal po-
wszechnie jest używana. Oto funkcja realizująca podmianę tekstu w JavaScripcie 1.1 przy
użyciu metod obiektu Array:
function replacev11(str, oldSubStr, newSubStr) {
var newStr = str.split(oldSubStr).join(newSubStr);
return newStr;
}

Funkcja ta pobiera napis, tworzy tablicę elementów, stosując split() z użyciem pod-
ciągu, który chcemy usunąć (oldSubStr), następnie zwraca używając funkcji – join()
– napis składający się z elementów tablicy z nowym podciągiem (newSubStr). Nie jest
to rozwiązanie najefektowniejsze, ale działa.

imageLinks
Zawiera kod HTML wyświetlający łącza.
scriptClose
Zawiera zamykający znacznik SCRIPT.
swapCode
Zawiera funkcje JavaScriptu realizujące przewijanie obrazków.
primHTML
Zawiera znacznik BODY i nieco komentarzy HTML.
113 Rozdział 5 - ImageMachine

secHTML
Zawiera zamykające znaczniki HTML oraz przyciski formularza, wyświetlane po wygenerowanym kodzie
(Generuj i Zmień dane lub Podgląd i Zmień dane).
aggregate
Zmienna łączy wszystkie inne wymienione tu zmienne.
Pętla for w wierszu 197 jeszcze raz korzysta z licznika imgPrim.length. Za każdym razem zmienne
primJavaScript, secJavaScript (jeśli użytkownik zaznaczył opcję MouseDown) i imageLinks dodawane są
do kodu odpowiedniej grupy obrazków.
Zmienne scriptClose, swapCode, primHTML i secHTML nie należą do części for. Ich zawartość może być
ustawiona tylko raz w operatorze trójargumentowym w połączeniu ze zmienną HTML
i imgDefaults.mousedown.checked.
Kiedy pętla for zakończy swoje działanie i ustawione są stosowne zmienne, ostatnią czynnością jest pobranie
zawartości na stronę. Dzieje się to w wierszach 300 do 305:
agregate = primJavaScript +
(imgDefaults.mousedown.checked ? scriptClose + secJavaScript : "") +
swapCode + primHTML + imageLinks + secHTML;

parent.frames[1].location.href =
"javascript: parent.frames[0].agregate";

Krok 4. Wybór Podglądu w celu obejrzenia działania kodu


Trudno się dziwić, jeśli ktoś jest już zmęczony. Na szczęście następne dwa kroki są całkiem szybkie i proste. Załóżmy,
że użytkownik oglądał wygenerowany kod i teraz chce zobaczyć, jak on działa. Wystarczy kliknąć Podgląd.
Pamiętajmy, że wywołana zostanie wtedy funkcja genJavaScript(), ale mimeType ma wartość false, a nie true,
jak w przypadku wybrania Generuj. To jest jedyna różnica: zmienne z wierszy 141–154 są po prostu ustawiane tak,
aby generowany był kod do interpretacji, a nie do drukowania, jak poprzednio. Wszystko inne działa tak samo – jak
po kliknięciu przycisku Generuj.

Krok 5. Wybór Zmiany danych w celu zrobienia poprawek


Widzieliśmy już kod wygenerowany i tenże kod w akcji. Załóżmy, że użytkownik chce coś zmienić. Wybieramy więc
przycisk Zmień dane, więc znów pokazuje się szablon obrazków – szerokości, wysokości, tekst paska stanu –
wszystko, co wprowadzałeś oprócz adresów URL obrazków. Zaraz – ale dlaczego bez?
Dzieje się tak dlatego, że adresy URL obrazków znajdują się w obiektach FileUpload (czyli <INPUT TYPE=FILE>).
Ze względów bezpieczeństwa obiekty takie są przeznaczone tylko do odczytu. Innymi słowy – trzeba te pola ręcznie
wypełnić z klawiatury lub wybierając plik myszką w stosownym dialogu. Łatwo jest to zmienić: po prostu trzeba
zmienić w generateEntryForm() TYPE=FILE na TYPE=TEXT. Znajdziemy trzy takie ustawienia. Jedyny problem
polega na tym, że tracimy możliwość wybierania plików lokalnych myszką przez dialog. Można użyć takiego usta-
wienia, jeśli bardziej nam odpowiada. Kiedy już wprowadziliśmy potrzebne zmiany, znów możesz kliknąć Generuj lub
Podgląd i obejrzeć nowy kod.

Kierunki rozwoju:
dodanie atrybutów do szablonu
Duże aplikacje zawsze można powiększyć. W tej sekcji zobaczymy, jak dodać do szablonu obrazków atrybuty, które
zapewnią większą kontrolę nad generowanym kodem. Aby rzecz uprościć, pokażę, jak dodać do znacznika IMG atrybuty
HSPACE i VSPACE. Procedura ta składa się z sześciu kroków:

1.dodania do domyślnego szablonu nowych pól,


2.utworzenia tablic na nowe wartości w setArrays(),
3.pobrania nowych wartości domyślnych,
4.dodania pól tekstowych w szablonie obrazków, w generateEntryForm(),
5.odwołania się i przypisania nowych wartości atrybutów w genJavaScript(),
6.generowanie kodu HTML, potrzebnego do wyświetlenia atrybutów w genJavaScript().
114

Krok 1. Dodanie pól


<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
HSpace
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genSelect("hspace", 25, 0, 0);
//-->
</SCRIPT>
</TD>
<TD VALIGN="TOP">
FONT FACE="Arial" SIZE=2>
VSpace
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genSelect("vspace", 25, 0, 0);
//-->
</SCRIPT>
</TD>

Krok 2. Tworzenie tablic w setArrays()


function setArrays() {
imgPrim = new Array();
imgRoll = new Array();
imgDown = new Array();
imgLink = new Array();
imgText = new Array();
imgWdh = new Array();
imgHgt = new Array();
imgBdr = new Array();
imgHSpace = new Array(); // na HSPACE
imgVSpace = new Array(); // na VSPACE
}

W ten sposób można przygotować sobie miejsce na nowe wartości domyślne, a teraz dochodzimy do następnego kroku
– wypełnienia tych tablic.

Krok 3. Pobieranie nowych ustawień domyślnych


W funkcji captureDefaultProfile() dodamy dwie zmienne lokalne imgHspace i imgVspace, a następnie
przypiszemy im wartości z szablonu. Teraz captureDefaultProfile() wygląda tak:
function captureDefaultProfile(formObj) {
setArrays();
imgDefaults = formObj;
var imgQty = (imgDefaults.imgnumber.selectedIndex + 1);
var imgHeight = (imgDefaults.pxlheight.selectedIndex);
var imgWidth = (imgDefaults.pxlwidth.selectedIndex);
var imgBorder = (imgDefaults.defbdr.selectedIndex);
var imgHspace = (imgDefaults.hspace.selectedIndex);
var imgVspace = (imgDefaults.vspace.selectedIndex);
for (var i = 0; i < imgQty; i++) {
imgPrim[i] = "";
imgRoll[i] = "";
imgDown[i] = "";
imgLink[i] = "";
imgText[i] = "";
imgWdh[i] = imgWidth;
imgHgt[i] = imgHeight;
imgBdr[i] = imgBorder;
imgHSpace[i] = imgHspace; // HSPACE
imgVSpace[i] = imgVspace; // VSPACE
}
generateEntryForm();
}

Aktualnie ImageMachine może włączyć do szablonu obrazków wartości domyślne HSPACE i VSPACE.
115 Rozdział 5 - ImageMachine

Krok 4. Dodanie pól tekstowych w generateEntryForm()


W generateEntryForm() możesz teraz dodać treści HTML do obsługi dwóch nowych pól tekstowych. Wstawmy je
do osobnego wiersza TR, pod istniejącymi dotąd. Później będzie można wszystko sobie poukładać, aby poprawić wygląd.
Wiersze 103–106 teraz będą wyglądały następująco:
"<TR><TD VALIGN=BOTTOM COLSPAN=" +
(!imgDefaults.mousedown.checked ? "3" : "4") +
"><BR><HR NOSHADE><BR></TD></TR>");

Dodanie na koniec dwóch pól tekstowych da taki oto wynik:


"<TR><TD VALIGN=BOTTOM><INPUT TYPE=TEXT NAME='hsp " + i +
"' VALUE='" + imgHspace[i] + "' SIZE=3> HSPACE </TD>" +
"<TR><TD VALIGN=BOTTOM><INPUT TYPE=TEXT NAME='vsp" + i +
"' VALUE='" + imgVspace[i] + "' SIZE=3> VSPACE </TD></TR>" +
"<TR><TD VALIGN=BOTTOM COLSPAN=" +
(!imgDefaults.mousedown.checked ? "3" : "4") + ">" +
"<BR><HR NOSHADE><BR></TD></TR>");

Kod ten powoduje dodanie dwóch pól tekstowych do każdej grupy obrazków i wyświetlenie ich wartości domyślnych.
Użytkownik będzie mógł później te wartości zmienić, jak i w przypadku innych pól.

Krok 5. Odwołanie się do nowych wartości w genJavaScript()


i ich użycie
Kiedy użytkownik zdecyduje się już wygenerować kod, ImageMachine musi pobrać dane z nowych pól tekstowych
w szablonie obrazków. Po prostu dodajmy kod do wierszy 158–169, a uzyskamy następujący wynik:
for (var i = 0; i < (imgDefaults.imgnumber.selectedIndex + 1); i++) {
imgPrim[i] = purify(imgTemplate['prim' + i].value);
imgRoll[i] = purify(imgTemplate['seci' + i].value);
if (imgDefaults.mousedown.checked) {
imgDown[i] = purify(imgTemplate['down' + i].value);
}
imgLink[i] = purify(imgTemplate['href' + i].value);
imgText[i] = purify(imgTemplate['stat' + i].value);
imgWdh[i] = purify(imgTemplate['wdh' + i].value);
imgHgt[i] = purify(imgTemplate['hgt' + i].value);
imgBdr[i] = purify(imgTemplate['bdr' + i].value);
imgHSpace[i] = purify(imgTemplate['hsp' + i].value);
imgVSpace[i] = purify(imgTemplate['vsp' + i].value);
}

Ostatnie dwa wiersze w tym bloku pokazują przypisanie wartości z formularza elementom tablicy imgHSpace
i imgVSpace. Już prawie gotowe. Jedyne, co zostało, to upewnić się, że nowe atrybuty zostały dołączone w procesie
generacji kodu, czy to drukowanego, czy interpretowanego.

Krok 6. Generacja dodatkowego HTML w genJavaScript()


Do zmiennej imageLinks dodany zostanie nowy kod, którego ostatnich kilka wierszy pokazano niżej:
(HTML ? fontClose : "") + br + nbsp + "HEIGHT=" +
(HTML ? fontOpen : ") + imgHgt[j] +
(HTML ? fontClose : ") + br + nbsp + "BORDER=" +
(HTML ? fontOpen : "") + imgBdr[j] +
(HTML ? fontClose : "") +
gt + "" + lt + "/A" + gt + br + br + br;

Pozostało jedynie skopiowanie kilku wierszy i zmiana HEIGHT na HSPACE, imgHgt na imgHSpace, BORDER na VSPACE
oraz imgBdr na imgVSpace. Oto nowa wersja:
(HTML ? fontClose : "") + br + nbsp + "HEIGHT=" +
(HTML ? fontOpen : ") + imgHgt[j] +
(HTML ? fontClose : ") + br + nbsp + "BORDER=" +
(HTML ? fontOpen : "") + imgBdr[j] +
(HTML ? fontClose : "") + br + nbsp + "HSPACE=" +
(HTML ? fontOpen : ") + imgHSpace[j] +
(HTML ? fontClose : ") + br + nbsp + "VSPACE=" +
(HTML ? fontOpen : "") + imgVSpace[j] +
(HTML ? fontClose : "") +
gt + "" + lt + "/A" + gt + br + br + br;
116

W ten sposób do naszych obrazków dodane zostaną dwa nowe atrybuty. Można też zastanowić się nad dodaniem
atrybutu ALT. Nie trzeba się też ograniczać tylko do znacznika <IMG>; świetnie do rozbudowy nadaje się też <A> –
można tworzyć mapy obrazkowe, i tak dalej.
Cechy bibliotek:
 Działania na tablicach
 Obsługa cookies
 Użycie DHTML
 Obsługa myszy i klawiatury
 Powiązania ramek
 Tworzenie paska nawigacyjnego

6
 Formatowanie i poprawianie liczb
 Tworzenie i badanie obiektów
 Działania na napisach

Realizacja plików
źródłowych
JavaScriptu

Jak na razie od początku przedzieramy się przez kod aplikacji, próbując zrozumieć, jak współdziałają ze sobą funkcje
i zmienne, aby stworzyć razem funkcjonalną aplikację. Chyba miło będzie na chwilę przerwać i ułatwić sobie dalsze
programowanie.
W tym rozdziale nie znajdzie się już żadna aplikacja. Zamiast tego pojawi się tutaj kilkadziesiąt funkcji z plików
źródłowych JavaScriptu. Choć niektóre mogą wydać się niezbyt przydatne, to jest tu zapewne też garść takich, których
zechcesz używać, a także wiele innych, które przydadzą się po drobnych poprawkach.
Nie załączono tutaj tych plików, aby dać zestaw funkcji – w końcu nie chodzi o to, aby podać programistom wszystko,
co może im się kiedykolwiek przydać. To byłoby po prostu śmieszne. Ten rozdział ma zachęcić czytelnika
do stworzenia własnej biblioteki kodu wielokrotnego użytku, dzięki czemu nie będzie musiał wyważać otwartych drzwi
przy każdej następnej aplikacji. Poniższa lista zawiera pliki .js w kolejności alfabetycznej wraz z ich krótkim opisem.
arrays.js
Zawiera funkcje obsługi tablic. Niektóre funkcje pozwolą zaimplementować funkcjonalność wersji 1.2
na starszych przeglądarkach.
cookies.js
Bardzo ważna biblioteka – w większości autorstwa weterana JavaScriptu, Billa Dortcha – pozwalająca
intensywnie wykorzystywać ciasteczka.
dhtml.js
Wiele z tych funkcji pojawiło się w rozdziałach 3. i 4. To jest całkiem porządny zestaw do tworzenia,
pokazywania i ukrywania warstw DHTML, działających na różnych przeglądarkach.
118

events.js
Plik ten zawiera kod umożliwiający i uniemożliwiający przechwytywanie zdarzeń mousemove i keypress
w Netscape Navigatorze i Internet Explorerze.
frames.js
Funkcje te umożliwiają zatrzymanie naszych stron w ramkach lub poza nimi – jak wolimy.
images.js
Kod do tworzenia przewijania obrazków, który pojawił się we wcześniejszych rozdziałach tutaj upakowany
w całość.
navbar.js
Zawiera kod do generacji dynamicznego paska nawigacyjnego opartego na załadowanym dokumencie. Robi
wrażenie.
numbers.js
Zawiera kod pozwalający poprawić błędy zaokrąglania w JavaScripcie i zapewnia formatowanie liczb.
object.js
Zawiera kod tworzenia i badania ogólnych obiektów.
strings.js
Zawiera kilka funkcji do przetwarzania napisów.
Poza navbar.js wszystkie inne pliki .js mają odpowiadający im dokument HTML (na przykład dla arrays.js jest to
arrays.html). Funkcje nie są tu opisywane tak szczegółowo, jak w aplikacjach; w większości wypadków po prostu
nie jest to potrzebne, choć zdarzają się wyjątki. Podczas czytania tego rozdziału warto pomyśleć o tym, jak
poszczególne funkcje mogą rozwiązać ewentualny problem, lub zastanowić się nad taką zmianę danej funkcji, aby
służyła do czegoś pożytecznego.
W każdej części opisującej plik .js zaczynamy od nazwy funkcji, praktycznych zastosowań, potrzebnej wersji
JavaScriptu i listy funkcji w pliku.

arrays.js
Zastosowania praktyczne:
Obsługa tablic.
Wymagana wersja:
JavaScript 1.2.
Funkcje:
avg(), high(), low(), jsGrep(), truncate(), shrink(), integrate(), reorganize()

Te funkcje przetwarzają tablice i zwracają różne użyteczne dane, także inne tablice. Na rysunku 6.1 pokazano arrays.html.
Widać, że zademonstrowano wszystkie funkcje.
Oto lista funkcji z arrays.js i ich zastosowania:
avg()
Zwraca wartość średnią liczb z tablicy.
high()
Zwraca największą wartość z tablicy.
low()
Zwraca najmniejszą wartość z tablicy.
119 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

Rysunek 6.1. Prezentacja możliwości arrays.js


jsGrep()
Dopasowuje napisy i podstawienia we wszystkich elementach tablicy.
truncate()
Zwraca kopię tablicy bez ostatniego elementu.
shrink()
Zwraca kopię tablicy bez pierwszego elementu.
integrate()
Łączy elementy z dwóch tablic, zaczynając od wskazanego indeksu.
reorganize()
Zmienia kolejność elementów tablicy, wybierając elementy w grupach o wskazanej wielkości.
Teraz przyjrzyjmy się kodowi arrays.html, pokazanemu w przykładzie 6.1. Nie ma tu zbyt wiele – po prostu wywołanie
document.write(). Wyświetlany napis zawiera wyniki wywołania wszystkich funkcji na tablicach przykładowych,
someArray() i grepExample().

Przykład 6.1. arrays.html


1 <HTML>
2 <HEAD>
3 <TITLE>Przyk¦ady arrays.js</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2" SRC="arrays.js"></SCRIPT>
5 </HEAD>
6 <BODY>
7 <SCRIPT LANGUAGE="JavaScript1.2">
8 <!--

Przykład 6.1. arrays.html (dokończenie)


9
10 var someArray = new Array(1,2,3,.1098,5,2,3.456,1324.55,-
0.76,45,3,47.234,.00060,65.7,1,3,2,4,55);
11 var grepExample = new Array('Monday', 'Tuesday', 'Wednesday',
12 'Thursday', 'Friday');
13 document.write("<B>Tablica wejściowa: " + someArray + "</B><BR>" +
14 "Average: " + avg(someArray) + "<BR>" +
15 "Lowest: " + low(someArray) + "<BR>" +
16 "Highest: " + high(someArray) + "<BR>" +
17 "Truncate (1): " + truncate(someArray) + "<BR>" +
18 "Shrink (1): " + shrink(someArray) + "<BR>" +
120

19 "Reorganize (4): " + reorganize(someArray, 4) + "<BR>" +


20 "Integrate ('element', 'kolejny', i 'jeszcze jeden', indeks 5): " +
21 integrate(someArray,
22 new Array('element', 'kolejny', 'jeszcze jeden'), 5) + "<BR><BR>" +
23 "<B>Tablica oryginalna grepExample: " + grepExample + "</B><BR>" +
24 "jsGrep(grepExample, /day/, \'day Night\'): " +
25 jsGrep(grepExample, /day/, 'day Night') + "<BR>");
26
27 //-->
28 </SCRIPT>
29 </BODY>
30 </HTML>

Można zauważyć, że oba znaczniki wymagają JavaScriptu 1.2. Jedynym tego powodem jest funkcja jsGrep()
używająca obsługi łańcuchów, dostępnej w tej właśnie wersji. Można uruchamiać ten plik w przeglądarkach
z JavaScriptem 1.1 po usunięciu (lub przebudowaniu) funkcji jsGrep(). Skoro już widzieliśmy działanie funkcji, to
teraz obejrzyjmy je same – przykład 6.2.

Przykład 6.2. arrays.js


1 function avg(arrObj) {
2 var sum = 0;
3 for (var i = 0; i < arrObj.length; i++) {
4 sum += arrObj[i];
5 }
6 return (sum / i);
7 }
8
9 function high(arrObj) {
10 var highest = arrObj[0];
11 for (var i = 1; i < arrObj.length; i++) {
12 highest = (arrObj[i] > highest ? arrObj[i] : highest);
13 }
14 return (highest);
15 }
16
17 function low(arrObj) {
18 var lowest = arrObj[0];
19 for (var i = 1; i < arrObj.length; i++) {
20 lowest = (arrObj[i] < lowest ? arrObj[i] : lowest);
21 }
22 return (lowest);
23 }
24
25 function jsGrep(arrObj, regexp, subStr) {
26 for (var i = 0; i < arrObj.length; i++) {
27 arrObj[i] = arrObj[i].replace(regexp, subStr);
28 }
29 return arrObj;
30 }
31

Przykład 6.2. arrays.js (dokończenie)


32 function truncate(arrObj) {
33 arrObj.length = arrObj.length - 1;
34 return arrObj;
35 }
36
37
38 function shrink(arrObj) {
39 var tempArray = new Array();
40 for(var p = 1; p < arrObj.length; p++) {
41 tempArray[p - 1] = arrObj[p];
42 }
43 return tempArray;
44 }
45
46
47 function integrate(arrObj, elemArray, startIndex) {
48 startIndex = (parseInt(Math.abs(startIndex)) < arrObj.length ?
49 parseInt(Math.abs(startIndex)) : arrObj.length);
50 var tempArray = new Array();
51 for( var p = 0; p < startIndex; p++) {
52 tempArray[p] = arrObj[p];
53 }
121 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

54 for( var q = startIndex; q < startIndex + elemArray.length; q++) {


55 tempArray[q] = elemArray[q - startIndex];
56 }
57 for( var r = startIndex + elemArray.length; r < (arrObj.length +
58 elemArray.length); r++) {
59 tempArray[r] = arrObj[r - elemArray.length];
60 }
61 return tempArray;
62 }
63
64 function reorganize(formObj, stepUp) {
65 stepUp = (Math.abs(parseInt(stepUp)) > 0 ? Math.abs(parseInt(stepUp)) : 1);
66 var nextRound = 1;
67 var idx = 0;
68 var tempArray = new Array();
69
70 for (var i = 0; i < formObj.length; i++) {
71 tempArray[i] = formObj[idx];
72 if (idx + stepUp >= formObj.length) {
73 idx = nextRound;
74 nextRound++;
75 }
76 else {
77 idx += stepUp;
78 }
79 }
80 return tempArray;
81 }

Funkcje avg(), high() i low() nie powinny zaskakiwać. avg() dodaje wszystkie wartości, następnie dzieli sumę
przez arrObj.length i zwraca wynik. Pozostałe funkcje przeszukują tablicę, porównując elementy ze sobą, aby
w końcu określić największy i najmniejszy element.
Funkcja jsGrep() przegląda wszystkie elementy tablicy i realizuje dopasowywanie tekstu lub jego podstawianie. Każdy
czytelnik, zaznajomiony z językiem Perl, zapewne używał procedury grep() już wielokrotnie. Funkcja grep() dostępna
w Perlu jest znacznie silniejszym narzędziem, ale zasada działania jest taka sama.
Funkcje truncate() i shrink() to w JavaScripcie 1.1 proste odpowiedniki funkcji pop() i shift(), dostępnych
w wersji 1.2. pop() i shift() nazywają się analogicznie i tak samo działają, jak podobne procedury Perla.
Funkcja integrate() to także dla wersji 1.1 odpowiednik metody tablic slice(), dostępnej w wersji 1.2.
slice() to również nazwa procedury Perla. Ta funkcja jest dość prosta – choć ma trzy pętle for, to łączna liczba
iteracji zawsze wynosi arrObj.length + elemArray.length.
Funkcja reorganize() zmienia kolejność elementów w tablicy o zadaną krotność. Innymi słowy – zmianie kolejności
tablicy dziesięcioelementowej 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 co 3 odpowiada tablica 0, 3, 6, 9, 1, 4, 7, 2, 5, 8.

cookies.js
Zastosowania praktyczne:
Indywidualne liczniki odwiedzin, powtarzalne formularze, preferencje użytkownika.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
getCookieVal(), GetCookie(), DeleteCookie(), SetCookie()
Czy chcesz obsługiwać zmieniający się stan klienta? A co z sympatycznymi pozdrowieniami dla starych znajomych?
Chcesz ustawić język interfejsu i inne preferencje użytkownika? Ten kod znakomicie ułatwia zapisywanie
i odczytywanie informacji o ciasteczkach. Na rysunkach 6.2, 6.3 i 6.4 pokazano plik cookies.html w działaniu.
Zwróćmy uwagę, że na rysunku 6.2 przy pierwszym załadowaniu strony użytkownik jest proszony o podanie nazwy.
Na rysunku 6.3 pokazano pozdrowienie wyświetlane odwiedzającemu stronę po raz pierwszy, a na rysunku 6.4
zaprezentowano pozdrowienie przeznaczone dla stałych bywalców, z osobistym licznikiem odwiedzin.
Jest to zdecydowanie prosty przykład zastosowania ciasteczek. W rozdziale 7. użyjemy tego samego kodu do
zapamiętania preferencji użytkownika. A tak przy okazji – jeśli ciasteczka są dla kogoś ciężkostrawne, polecam
Unofficial Cookie FAQ (Nieoficjalny Zestaw Często Zadawanych Pytań o Ciasteczka) dostępny pod adresem:
http://www.cookiecentral.com/unofficial_cookie_faq. htm. Strona jest może i nieoficjalna, ale znajdziemy na niej pełne
odpowiedzi na wszelkie swoje pytania z tej dziedziny. Więcej szczegółów znajdzie się w rozdziale 7.Plik cookies.html
122

działa następująco: kiedy użytkownik ładuje stronę, wyszukuje ciasteczka o nazwie user_id. Jeśli nazwa nie istnieje
(jest pusta – null), prosi użytkownika o przedstawienie się. Następnie ustawiane jest ciasteczko user_id o wartości
odpowiadającej nazwie użytkownika i ciasteczko hit_count o wartości 2 (taki numer będzie miała następna wizyta tego
gościa).
Jeśli user_id istnieje, pobierana jest jego wartość oraz wartość hit_count. Pojawienie się user_id sugeruje, że, gość już tu był
wcześniej, można zatem spokojnie założyć, iż ustawiono też hit_count. Wyświetlana jest nazwa użytkownika oraz numer
jego wizyty, następnie hit_count otrzymuje wartość hit_ count+1. Przyjrzyjmy się cookies.js z przykładu 6.3, aby
zorientować się, o co w tym wszystkim chodzi.

Rysunek 6.2. Odwiedzający po raz pierwszy podają swoje imię...

Rysunek 6.3. ...są stosownie witani...


123 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

Rysunek 6.4. ...a później to już prawdziwi znajomi

Przykład 6.3. cookies.js


1 var today = new Date();
2 var expiry = new Date(today.getTime() + 365 * 24 * 60 * 60 * 1000);
3
4 function getCookieVal (offset) {
5 var endstr = document.cookie.indexOf (";", offset);
6 if (endstr == -1) { endstr = document.cookie.length; }
7 return unescape(document.cookie.substring(offset, endstr));
8 }
9
10 function GetCookie (name) {
11 var arg = name + "=";
12 var alen = arg.length;
13 var clen = document.cookie.length;
14 var i = 0;
15 while (i < clen) {
16 var j = i + alen;
17 if (document.cookie.substring(i, j) == arg) {
18 return getCookieVal (j);
19 }
20 i = document.cookie.indexOf(" ", i) + 1;
21 if (i == 0) break;
22 }
23 return null;
24 }
25
26 function DeleteCookie (name,path,domain) {
27 if (GetCookie(name)) {
28 document.cookie = name + "=" +
29 ((path) ? "; path=" + path : "") +
30 ((domain) ? "; domain=" + domain : "") +
31 "; expires=Thu, 01-Jan-70 00:00:01 GMT";

Przykład 6.3. cookies.js(dokończenie)


32 }
33 }
34
35 function SetCookie (name,value,expires,path,domain,secure) {
36 document.cookie = name + "=" + escape (value) +
37 ((expires) ? "; expires=" + expires.toGMTString() : "") +
38 ((path) ? "; path=" + path : "") +
39 ((domain) ? "; domain=" + domain : "") +
40 ((secure) ? "; secure" : "");
41 }
124

Znajdują się tutaj cztery, funkcje ale tak naprawdę będziemy potrzebować tylko trzech z nich: SetCookie(), GetCookie()
oraz DeleteCookie(). getCookieVal() to funkcja wewnętrzna, nigdy niewywoływana bezpośrednio.
Jeśli mamy do dyspozycji funkcję SetCookie(), tworzenie ciasteczek jest proste. Przekazujemy tylko nazwę (której
użyjesz później w GetCookie() przechowywaną informację (na przykład nazwę użytkownika lub liczbę odwiedzin)
i datę dezaktywacji, wszystkie w takiej właśnie kolejności. Należy podać dwa pierwsze parametry, a trzeci jest
określany na podstawie zmiennych today i expiry. Zmienna expiry ustawiana jest na datę o rok dalszą od dnia
załadowania strony. Jest to możliwe dzięki przypisaniu zmiennej today nowego obiektu Date i użyciu metody
getTime(). Poniżej przedstawiono, jak to działa:

Zmienna today to obiekt typu Date, zatem today.getTime() zwraca czas w milisekundach (od początku 1970 roku,
godzina 0:00:00 GMT). W ten sposób mamy już datę w milisekundach, ale termin ważności chcemy określić na jeden
rok. Jako że rok ma 365 dni, dzień ma 24 godziny, godzina 60 minut, minuta 60 sekund, a w końcu sekunda ma 1000
milisekund, mnożymy to wszystko przez siebie (wynik to 3,1536*1010 ms) i dodajemy do getTime().
Składnia GetCookie() i DeleteCookie() jest jeszcze prostsza. Wystarczy tylko podać nazwę związaną z żądanym
ciasteczkiem. Pierwsza z tych funkcji zwraca wartość (lub null, jeśli ciasteczko nie zostanie znalezione), druga
ciasteczko usuwa. Usunięcie oznacza po prostu ustawienie ciasteczka z datą ważności, która już upłynęła.

dhtml.js
Zastosowania praktyczne:
Tworzenie, ukrywanie i pokazywanie warstw DHTML.
Wymagana wersja:
JavaScript 1.2.
Funkcje:
genLayer(), hideSlide(), showSlide(), refSlide()

Ten kod został przedstawiony w dwóch poprzednich rozdziałach (pokaz slajdów i multiwyszukiwarka). Na rysunkach
6.5 i 6.6 pokazano kod tworzący warstwę oraz pozwalający ją ukryć i pokazać na życzenie.
W przykładzie 6.4 pokazano zawartość dhtml.js. Nic nie zostało zmienione, więc szczegółów należy szukać
w rozdziałach 3. i 5.

Rysunek 6.5. Przyciągający wzrok DHTML – to widać

Przykład 6.4. dhtml.js


125 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

1 var NN = (document.layers ? true : false);


2 var hideName = (NN ? 'hide' : 'hidden');
3 var showName = (NN ? 'show' : 'visible');
4 var zIdx = -1;
5 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
6 if (NN) {
7 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
8 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
9 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + zIdx + '>' + copy +
10 '</LAYER>');
11 }
12 else {
13 document.writeln('<DIV ID="' + sName + '" STYLE="position:absolute;
14 overflow:none; left:' + sLeft + 'px; top:' + sTop + 'px; width:' +
15 sWdh + 'px; height:' + sHgt + 'px;' + ' visibility:' + sVis +
16 '; z-Index=' + (++zIdx) + '">' + copy + '</DIV>'
17 );
18 }
19 }
20
21 function hideSlide(name) {
22 refSlide(name).visibility = hideName;
23 }
24
25 function showSlide(name) {
26 refSlide(name).visibility = showName;
27 }
28
29 function refSlide(name) {
30 if (NN) { return document.layers[name]; }
31 else { return eval('document.all.' + name + '.style'); }
32 }

Rysunek 6.6. Teraz już nie widać

events.js
Zastosowania praktyczne:
Przypisywanie obsługi zdarzeń działające w różnych przeglądarkach, śledzenie ruchu myszy.
Wymagana wersja:
JavaScript 1.2.
Funkcje:
enableEffects(), showXY(), keepKeys(), showKeys()

Jeśli nie eksperymentowałeś dotąd ze skryptami obsługi zdarzeń, działającymi w różnych przeglądarkach, to może przyszedł
właśnie czas na pierwsze podejście. Używane są trzy procedury obsługi zdarzenia: onclick, onmousemove
126

i onkeypress. Po kliknięciu gdziekolwiek na powierzchni dokumentu JavaScript przechwyci początkowe położenie


myszki. Potem w pasku stanu wyświetlane będą współrzędne w miarę przesuwania się wskaźnika. Ponowne kliknięcie
wyłączy śledzenie i wyliczy odległość w pikselach między pierwszym kliknięciem a drugim. Zobaczymy to na rysunkach 6.7
i 6.8.
Niezależnie od myszy można także wciskać klawisze, z których każdy będzie na bieżąco pokazywany w pasku stanu.
Kiedy skończysz, wybierz przycisk Pokaż klawisze, a otrzymasz okienko dialogowe JavaScriptu wyświetlające
zebrany ciąg wpisanych klawiszy, które pokazano na rysunku 6.9. Po kliknięciu OK można zaczynać zabawę
od nowa.11

Rysunek 6.7. Współrzędne wskaźnika myszki w pasku stanu

Rysunek 6.8. Odległość między dwoma punktami w pikselach

11
Wciśnięcia klawiszy zbierane są tylko z samego okna dokumentu, jeśli zatem elementem aktywnym jest przycisk Pokaż klawisze, to nic
nie zobaczymy. W takim wypadku należy najpierw kliknąć myszką gdzieś w białe tło (przyp. tłum.).
127 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

Rysunek 6.9. Klawisze, które wciskałeś


Teraz znamy już zawiłości kodowania arkuszy stylów działających na różnych przeglądarkach. Warto zapamiętać:
w jednej przeglądarce znaczniki LAYER, w drugiej DIV.12 Szczęśliwie to samo odnosi się do modelu obsługi zdarzeń.
Jeśli zajrzymy do kodu źródłowego events.html, znajdziemy następujące dwa wiersze kodu JavaScript:
document.onclick = enableEffects;
document.onkeypress = keepKeys;

Procedura obsługi zdarzenia onclick związana jest z funkcją enableEffects(), natomiast obsługa zdarzenia
onkeypress ma związek keepKeys(). Obie funkcje pokazano niżej. Zwróćmy uwagę na to, że nazwy funkcji
podawane są bez nawiasów, więc kod nie wygląda tak, jak można by tego oczekiwać:
document.onclick = enableEffects();
document.onkeypress = keepKeys();

Użycie nawiasów wywołałoby każdą z metod w momencie interpretacji danego wiersza. Tego jednak nie chcemy –
obsługa zdarzenia związana jest ze wskazaniem na funkcję. Spójrzmy na kod przykładu 6.5.

Przykład 6.5. events.js


1 var keys = '';
2 var change = true;
3 var x1, x2, y1, y2;
4
5 function enableEffects(ev) {
6 if(change) {
7 if(document.layers) {
8 x1 = ev.screenX;
9 y1 = ev.screenY;
10 document.captureEvents(Event.MOUSEMOVE);
11 }
12 else {
13 x1 = event.screenX;
14 y1 = event.screenY;
15 }
16 document.onmousemove = showXY;
17 }
18 else {
19 if (document.layers) {

12
Tak naprawdę znaczników DIV można używać także w Netscape Navigatorze 4.x, o ile tylko włączy się wartość pozycji w atrybucie STYLE.
Jednak póki Netscape nie uzupełni obiektowego modelu dokumentu, użycie znacznika LAYER daje dostęp do większej liczby właściwości obiektu
Layer (przyp. aut.).
128

20 x2 = ev.screenX;
21 y2 = ev.screenY;
22 document.releaseEvents(Event.MOUSEMOVE);
23 }
24 else {
25 x2 = event.screenX;
26 y2 = event.screenY;
27 document.onmousemove = null;
28 }
29 window.status = 'Pocz¦tek: (' + x1 + ',' + y1 +
30 ') Koniec: (' + x2 + ',' + y2 + ') Odleg¦oťŠ: ' +
31 (Math.abs((x2 - x1) + (y2 - y1))) + ' pikseli';
32 }
33 change = !change;
34 }
35
36 function showKeys() {
37 if (keys != '') {
38 alert('Wpisa¦eť klawisze: ' + keys);
39 window.status = keys = '';
40 }
41 else { alert('Najpierw musisz coť powciskaŠ.'); }
42 }
43
44 function showXY(ev) {
45 if (document.all) { ev = event; }
46 window.status = 'X: ' + ev.screenX + ' Y: ' + ev.screenY;
47 }
48
49 function keepKeys(ev) {
50 if (document.layers) {
51 keys += String.fromCharCode(ev.which);
52 window.status = 'Wciťniŕty klawisz: ' + String.fromCharCode(ev.which);
53 }
54 else {
55 keys += String.fromCharCode(event.keyCode);
56 window.status = 'Wciťniŕty klawisz: ' + String.fromCharCode(event.keyCode);
57 }
58 }

Funkcja enableEffects() jest centrum obsługi zdarzeń click i mouseover. Zwróćmy szczególną uwagę na wiersze
6, 18 i 33:

if (change) { ....

else { ....

change = !change;

Zmienna change początkowo ma wartość true, po czym wartość ta zmieniana jest na przeciwną (czyli false, później
znów true i tak dalej), przy każdym następnym wywołaniu. Jako że klikanie wywołuje enableEffects(), a change
ma za pierwszym razem wartość true, uruchamiane są wiersze 7–15:
if(document.layers) {
x1 = ev.screenX;
y1 = ev.screenY;
document.captureEvents(Event.MOUSEMOVE);
}
else {
x1 = event.screenX;
y1 = event.screenY;
}

Wiersze te pobierają współrzędne x i y oraz uruchamiają obsługę zdarzenia onousemove. Jeśli istnieje
document.layers, użytkownik stosuje Netscape Navigatora. Tworzony w biegu obiekt zdarzenia znajduje odbicie
w parametrze przekazywanym funkcji o nazwie ev. Zmienne globalne x1 i y1 ustawiane są na wartości odpowiednich
współrzędnych pierwszego kliknięcia. Następnie wywołanie metody dokumentu captureEvents() powoduje
przejęcie zdarzenia mousemove.13
Jeśli document.layers nie istnieje, skrypt zakłada, że użytkownik korzysta z Internet Explorera i podejmuje kroki
w zasadzie takie same, jak opisane powyżej. Model zdarzeń Microsoftu jednak definiuje zdarzenie jako obiekt event,
13
Przynajmniej niektóre wersje Netscape Navigator 4.x zdają się odpowiadać nie na wszystkie kliknięcia myszą i czasem, aby zacząć lub zakończyć
śledzenie wskaźnika, trzeba kliknąć dwa razy. W żadnej dokumentacji nie znalazłem omówienia tego niedociągnięcia (przyp. aut.).
129 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

tam będą oczekiwały właściwości screenX i screenY. Nie są wymagane żadne dodatkowe wywołania metod, więc
mamy tylko wiersz 16:
document.onmousemove = showXY;

Niezależnie od używanej przeglądarki obsługa zdarzenia onousemove jest przypisana przez odwołanie do funkcji
showXY():
function showXY(ev) {
if (document.all) { ev = event; }
window.status = 'X: ' + ev.screenX + ' Y: ' + ev.screenY;
}

Wywołanie showXY() przy każdym ruchu myszy wyświetla w pasku stanu współrzędne wskaźnika myszy. Odwołanie
się do współrzędnych x i y działa w obu przeglądarkach dzięki zastosowaniu sztuczki podobnej, jak opisana wyżej.
showXY() wywoływana jest tak długo, aż użytkownik zdecyduje się znowu kliknąć, co powoduje ponowne wywołanie
enableEffects(). Tym razem jednak zmienna change ustawiona jest na false, więc wywoływany jest kod
z wierszy 19 do 31:
if (document.layers) {
x2 = ev.screenX;
y2 = ev.screenY;
document.releaseEvents(Event.MOUSEMOVE);
}
else {
x2 = event.screenX;
y2 = event.screenY;
document.onmousemove = null;
}
window.status = 'Początek: (' + x1 + ',' + y1 +
') Koniec: (' + x2 + ',' + y2 + ') Odległość: ' +
(Math.abs((x2 - x1) + (y2 - y1))) + ' pikseli';

Zmienne x1 i y1 zawierają współrzędne pierwszego kliknięcia, a nowe zmienne, x2 i y2, ustawiane są na współrzędne
kliknięcia drugiego. Nie trzeba już obsługiwać zdarzenia onmousemove, więc w przypadku Netscape Navigatora
wywoływana jest metoda releaseEvents()– ustawiając document.onmousemove na wartość null.
Pozostaje jeszcze tylko wyświetlić odległość między początkowym a końcowym punktem. Pamiętasz wzór
na odległość? Odpowiedni wzór w wierszach 29–31.14
Teraz zostaje nam jeszcze tylko zająć się zdarzeniem onkeypress. Należy przypomnieć, że document. onkeypress miało
wywoływać funkcję keepKeys(). Oto sama keepKeys() z wierszy 49–58:
function keepKeys(ev) {
if (document.layers) {
keys += String.fromCharCode(ev.which);
window.status = 'Wciśnięty klawisz: ' + String.fromCharCode(ev.which);
}
else {
keys += String.fromCharCode(event.keyCode);
window.status = 'Wciśnięty klawisz: ' + String.fromCharCode(event.keyCode);
}
}

Używając tej samej techniki rozpoznawania przeglądarki ciąg pusty ustawiamy jako wartość zmiennej keys oraz napis
odpowiadający wciśniętemu klawiszowi. Robimy to, stosując – niezależnie od użytej przeglądarki –
String.fromCharCode(). Jednak w JavaScripcie 1.2 klawiszom odpowiadają znaki ISO Latin-1, a JScript używa
standardu Unicode. Liczba dla JavaScriptu przechowywana jest we właściwości which obiektu event. Liczba
dla JScriptu znajduje się we właściwości event.keyCode. Użytkownik wciska zatem ciąg klawiszy, a następnie wybiera
przycisk Pokaż klawisze. Funkcja zgłasza komunikat podający wartość keys, po czym znów tę zmienną zeruje.

frames.js
Zastosowania praktyczne:
Wymuszanie ładowania w ramkach.
Wymagana wersja:
JavaScript 1.1.

14
Jest to odległość w „metryce miasto” – o czym wiedzą ci, którzy mieli kiedyś styczność z topologią. W ramach ćwiczenia proponuję poprawić
funkcję tak, aby wyliczała prawdziwą odległość między punktami (przyp. tłum.).
130

Funkcje:
keepIn(), keepOut()

Ten plik zawiera tylko dwie funkcje. Jedna zatrzymuje dokumenty w danym zestawie ramek, druga natomiast trzyma je
od ramek z daleka. Aby kod z pliku frames.js mógł zadziałać, wymaga użycia wielu plików HTML. Spróbujemy
na przykład załadować do przeglądarki plik ch06\frameset. html. Plik ten zawiera dwie ramki – w jednej z nich
znajduje się plik frames.html, który z kolei używa frames.js w celu zapewnienia, że frames.html zawsze zostanie
załadowany na górze. Dlatego właśnie załadowanie frameset.html daje komunikat o błędzie (do przeglądarki ładowany
jest w końcu frames.html).15

Słów parę o konkurujących modelach zdarzeń


Modele zdarzeń w Netscape Navigatorze 4 i Internet Explorerze 4 mają pewne cechy
wspólne, i całe szczęście. Nadal istnieją jednak między nimi znaczące różnice, z których
może najważniejszą jest to, że o ile zdarzenia Netscape Navigatora przesuwają się w dół
hierarchii (na przykład z window do frame, następnie do document, dalej do form
i w końcu do field), to w Internet Explorerze zdarzenia pączkują do góry (na przykład
z field do form do document do frame do window). Więcej wiadomości o obu modelach
znajdziesz pod podanym niżej adresem URL. Informacje te są niesłychanie istotne, jeśli
zamierzasz stosować złożoną, działającą w różnych przeglądarkach, obsługę zdarzeń.
Aby dowiedzieć się więcej o Netscape Navigatorze, odwiedź stronę:
http://developer.netscape.com/docs/manuals/communicator/jsguiede4/evnt.htm
Aby uzyskać więcej o Internet Explorerze, zajrzyj na stronę:
http://msdn.microsoft.com/developer/sdk/inetsdk/help/dhtml/doc_object/event_model.htm
#dom_event

Jeśli chcemy z kolei upewnić się, że nasz dokument nie zostanie załadowany inaczej niż do wskazanych ramek, też użyj
frames.js. Rysunek 6.10 przedstawia, co się stało, kiedy próbowano załadować ch06\frames2.html. Rezultatem jest
komunikat o błędzie, informujący, że naruszono zasady użycia ramek, a następnie przeglądarka ładuje odpowiedni
zestaw zawierający frames2.html, co widać na rysunku 6.11.
Kod realizujący wszystkie te funkcje jest krótki i przyjemny. Funkcja keepOut() porównuje adres URL dokumentu
z górnego okienka z adresem URL bieżącej ramki. Jeśli właściwości location. href nie pasują do siebie,
keepOut() protestuje i ładuje dokument we własnym górnym oknie. Funkcja keepIn() dokonuje porównania
dokładnie przeciwne i ładuje adres URL z przekazanego argumentu, jeśli porównanie zawiedzie. Plik frames.js
pokazano jako przykład 6.6.

Przykład 6.6. frames.js


1 function keepIn(parentHREF) {
2 if (top.location.href == self.location.href) {
3 alert('[Krach]. . . [Grrrrm]. . . Musisz. . . za¦adowaŠ. . .' +
4 'oryginalne. . . ramki.');
5 top.location.href = parentHREF;
6 }
7 }
8
9 function keepOut() {

15
Z uwagi na automatyczną zmianę strony nie można użyć przycisku Back/Wstecz w swojej przeglądarce. Po użyciu go zostanie załadowana strona
poprzednia, która wyświetli natychmiast komunikat i automatycznie załaduje z powrotem stronę, z której chcieliśmy się wycofać. Jeśli zrobisz coś
takiego na swojej witrynie w Sieci, nie licz na pobłażanie gości (przyp. tłum.).
131 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

Rysunek 6.10. Naruszenie przyjętej polityki stosowania ramek

Przykład 6.6. frames.js (dokończenie)


10 if (top.location.href != self.location.href) {
11 alert('Ten dokument +adnym ramkom siŕ nie k¦ania.');
12 top.location.href = self.location.href;
13 }
14 }

images.js
Zastosowania praktyczne:
Przewijanie obrazków pod myszką.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
imagePreLoad(), imageSwap(), display()

Tak jak funkcje w dhtml.js, tak i te w images.js były już prezentowane w poprzednich rozdziałach. Rozdziały 3., 4. i 5.
zawierają różne wersje kodu z przykładu 6.7. Funkcje te ładują wstępnie obrazki i używają ich jako przewijanych
pod myszą elementów.
132

Rysunek 6.11. Teraz już lepiej

Przykład 6.7. images.js


1 var imgPath = 'images/';
2 var arrayHandles = new Array('out', 'over');
3
4 for (var i = 0; i < arrayHandles.length; i++) {
5 eval('var ' + arrayHandles[i] + ' = new Array()');
6 }
7
8 for (var i = 0; i < imgNames.length; i++) {
9 imagePreLoad(imgNames[i], i);
10 }
11
12 function imagePreLoad(imgName, idx) {
13 for(var j = 0; j < arrayHandles.length; j++) {
14 eval(arrayHandles[j] + "[" + idx + "] = new Image()");
15 eval(arrayHandles[j] + "[" + idx + "].src = '" + imgPath + imgName +
16 arrayHandles[j] + ".gif'");
17 }
18 }
19
20 function imageSwap(imagePrefix, imageIndex, arrayIdx) {
21 document[imagePrefix].src = eval(arrayHandles[arrayIdx] + "[" +
22 imageIndex + "].src");
23 }
24 function display(stuff) { window.status = stuff; }

Jako że są już znane procedury realizowania przewijania obrazków, nie umieszczono tutaj żadnych rysunków.

navbar.js
Zastosowania praktyczne:
Dynamiczna nawigacja na stronie.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
navbar()

Ten plik źródłowy zawiera jedną tylko funkcję, ale za to jaką! Załóżmy, że nasza witryna zawierająca kilka stron, a
na każdej z nich znajduje się pasek nawigacyjny z łączami do stron pozostałych. Czy nie byłoby ciekawie, gdyby
133 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

JavaScript mógł stworzyć taki pasek, zawierający łącza do wszystkich stron poza bieżącą? Na rysunku 6.12 pokazano
ch06\astronomy.html. Pasek nawigacji zawiera łącza do innych stron: o innych dziedzinach nauki, o sporcie, kącik
muzyczny i stronę poświęconą różnym ludziom. Na rysunku 6.13 pokazano dokument, jaki jest ładowany po kliknięciu
łącza Fajni ludzie. Teraz spójrzmy na pasek nawigacji: dodatkowo znajduje się strona o astronomii, natomiast znikli
nasi fajnie ludzie, gdyż właśnie ich oglądamy. Można zrealizować analogiczną funkcję dla dowolnej ilości stron. Jeśli
natomiast zmienią się dokumenty, to wystarczy, że zmienimy tylko plik navbar.js. Zaoszczędzi nam to mnóstwo czasu.

Rysunek 6.12. Strona o astronomii bez łącza do samej siebie


Realizujący nasze zadanie kod jest zadziwiająco prosty. Rozwijamy tylko tablicę navURLs zawierającą adresy URL
stron oraz tablicę linkText treścią łącz. Funkcja navbar() przetwarza kolejno wszystkie nazwy plików i generuje
łącza z odpowiednią treścią, pomijając łącze, którego wskaźnik jest równy właściwości location.href bieżącego
dokumentu. To proste. Zajrzyjmy do kodu przykładu 6.8.

Rysunek 6.13. Strona o fajnych ludziach, znów bez łącza do samej siebie

Przykład 6.8. navbar.js


1 var navURLs = new Array('astronomy.html', 'science.html', 'sports.html',
2 'music.htm', 'people.htm');
3 var linkText = new Array('Astronomia', 'Inne nauki', 'Sport',
4 'Kącik muzyczny', 'Fajni ludzie');
5
6 function navbar() {
7 var navStr= '';
8 for (var i = 0; i < navURLs.length; i++) {
9 if (location.href.indexOf(navURLs[i]) == -1) {
134

10 navStr += ' <B>[</B><A HREF="' + navURLs[i] + '">' + linkText[i] +


11 '</A><B>]</B> ';
12 }
13 }
14 document.writeln('<BR><BR>' + navStr);
15 }

Można znacząco zwiększyć tę funkcjonalność. Dlaczego nie użyć zamiast zwykłych łącz przewijanych obrazków? Jeśli
mamy mnóstwo łącz i nie chcemy ich wszystkich umieszczać w poprzek strony, dlaczego nie zastosować listy wyboru?
Wtedy można stworzyć tych łącz naprawdę wiele, oszczędzając przy tym cenne miejsce na ekranie.

numbers.js
Zastosowania praktyczne:
Poprawienie błędu zaokrąglenia i formatowanie liczb na potrzeby programu pełniącego rolę wózka na zakupy.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
twoPlaces(), round(), totals()

JavaScript realizuje obliczenia zmiennoprzecinkowe nieco inaczej niż tego oczekujemy. W rezultacie wiele uzyskiwanych
wyników ma charakter zaskakujący. W FAQ grupy dyskusyjnej DevEdge dostępnych pod adresem:
http://developer1.netscape.com:80/supprot/faqs/champions/ javascript.html#2-2 podaje się jako przykład mnożenie 0.119
* 100 dające wynik 11.899999. Często też chcemy wyświetlić kwotę w walucie: dolarach i centach lub złotówkach
i groszach. Funkcje z pliku numbers.js mają pomóc w obu tych sytuacjach. Wszystkie trzy funkcje oparte są na
funkcjach dostępnych na stronie http://www.irt.org/script/number.htm utrzymywanej przez Martina Webba. Na rysunku
6.14 pokazano wynik załadowania pliku ch06\numbers.html.

Rysunek 6.14. Dzięki JavaScriptowi liczby wyglądają lepiej


Liczby wyświetlane pod nagłówkiem Dwa miejsca dziesiętne pokazują, jak funkcja twoPlaces() formatuje liczby
jako waluty. Pozostałe dwa nagłówki wskazują różnicę między wyrażeniem 51,02 – 3,8 z i funkcjami round()
i totals() oraz bez nich. Plik numbers.js podano jako przykład 6.9.

Przykład 6.9. numbers.js


1 function twoPlaces(amount) {
2 return (amount == Math.floor(amount)) ? amount + '.00' :
135 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

3 ((amount*10 == Math.floor(amount*10)) ? amount + '0' : amount);


4 }
5
6 function round(number,X) {

Przykład 6.9. numbers.js (dokończenie)


7 X = (!X ? 2 : X);
8 return Math.round(number*Math.pow(10,X))/Math.pow(10,X);
9 }
10
11 function totals(num) {
12 return twoPlaces(Math.floor((num - 0) * 100) / 100);
13 }

Funkcja twoPlaces() zwraca napis reprezentujący otrzymaną liczbę z dodanym 0, .00 lub bez niczego, jeśli liczba
jest już poprawnie sformatowana. Długie wyrażenie warunkowe przekłada się na polskie następująco:
• Jeśli liczba jest równa najmniejszej liczbie całkowitej nie większej od liczby Math.floor(amount), to dodaj
do wyniku .00.
• W przeciwnym wypadku, jeśli liczba pomnożona przez 10 równa jest największej liczbie całkowitej,
nie mniejszej od takiego iloczynu (Math.floor(amount) * 10), zwróć napis z dodanym jednym zerem, 0.
• W innym wypadku zwróć tę samą liczbę, która została przekazana.
Jeśli chodzi o błędy w zaokrąglaniu, funkcja round() zwraca liczbę otrzymaną przez zaokrąglenie liczby do x miejsc
dziesiętnych. Jeśli nie podamy x, wartością domyślną jest 2.

objects.js
Zastosowania praktyczne:
Tworzenie ogólnych obiektów i ich analiza.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
makeObj(), parseObj(), objProfile()

Teraz czas na obiekty JavaScriptu. Tak wiele można z nimi zrobić, tymczasem tak mało jest czasu na ich
przetestowanie. Plik objects.js zawiera dwa narzędzia: jedno to ogólnie konstruktor obiektów, drugie to analizator ich
właściwości. Otwórz plik ch06\objects.html, a uzyskasz wynik pokazany na rysunku 6.15.
parseObj() i objProfile() pokazują właściwości dwóch obiektów: jednego –reprezentowanego przez zmienną
someObject, drugiego – będącego obiektem location bieżącego okna. Warto przyjrzeć się plikowi objects.html
z przykładu 6.10, aby zobaczyć, jak to właściwie działa.

Przykład 6.10. objects.html


1 <HTML>
2 <HEAD>
3 <TITLE>Przykład objects.js</TITLE>
4 <STYLE type="text/css">
5 <!--
6 td { font-family: courier new; font-size: 14}
7 -->
8 </STYLE>
136

Rysunek 6.15. Wyniki działania objects.html

Przykład 6.10. objects.html (dokończenie)


9 <SCRIPT LANGUAGE="JavaScript" SRC="objects.js"></SCRIPT>
10 </HEAD>
11 <BODY>
12 <SCRIPT LANGUAGE="JavaScript">
13 <!--
14
15 function plainOldObject() {
16 this.name = 'nazwa obiektu';
17 this.numba = 1000;
18 this.objInherit = new makeObj('propertyOne', 'thisProperty',
19 'propertyTwo', 'thatProperty', 'propertyThree', 'theOtherProperty');
20 return this;
21 }
22
23 var someObject = new plainOldObject();
24
25 document.write(objProfile('someObject', 'self.location'));
26 //-->
27 </SCRIPT>
28
29 </BODY>
30 </HTML>

Warto zwrócić uwagę, że w wierszu 23 wartość zmiennej someObject ustawiana jest na wartość new
plainOldObject(). Konstruktor plainOldObject() ma kilka właściwości, w tym name, numba i objInherit.
objInherit reprezentuje obiekt składający się z ogólnego konstruktora obiektu makeObj() z pliku objects.js.
Przyjrzyjmy się teraz temu ostatniemu na wydruku 6.11.

Przykład 6.11. objects.js


1 function makeObj() {
2 if (arguments.length % 2 != 0) {
3 arguments[arguments.length] = "";
4 }

Przykład 6.11. objects.js (dokończenie)


5 for ( var i = 0; i < arguments.length; i += 2 ) {
6 this[arguments[i]] = arguments[i + 1] ;
7 }
8 return this;
9 }
10
11 function parseObj(obj) {
12 var objStr = '';
13 for (prop in obj) {
14 objStr += '<TR><TD>W¦aťciwoťŠ: </TD><TD><B>' + prop +
137 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

15 '</B></TD><TD>Typ: </TD><TD><B>' + typeof(obj[prop]) +


16 '</B></TD><TD>WartoťŠ: </TD><TD><B>' + obj[prop] +
17 '</B></TD></TR>';
18 if (typeof(obj[prop]) == "object") {
19 objStr += parseObj(obj[prop]);
20 }
21 }
22 return objStr;
23 }
24
25 function objProfile() {
26 var objTable = '<TABLE BORDER=2 CELLSPACING=0><TR><TD><H1>' +
27 'Opis obiektu</H1></TD></TR>';
28 for (var i = 0; i < arguments.length; i++) {
29 objTable += '<TR><TD><BR><BR><H2><TT>' + (i + 1) + ') ' +
30 arguments[i] + '</H2></TD></TR>';
31 objTable += '<TR><TD><TT><TABLE CELLPADDING=5>' +
32 parseObj(eval(arguments[i])) + '</TABLE></TD></TR>';
33 }
34 objTable += '</TABLE><BR><BR><BR>';
35 return objTable;
36 }

Najpierw przyjrzyjmy się makeObj()– oto stosowne wiersze:


function makeObj() {
if (arguments.length % 2 != 0) {
arguments[arguments.length] = "";
}
for ( var i = 0; i < arguments.length; i += 2 ) {
this[arguments[i]] = arguments[i + 1] ;
}
return this;
}

Konstruktor ten tworzy właściwości, zestawiając pary przekazanych argumentów. Jeśli liczba argumentów jest nieparzysta
(czyli któryś nie ma pary), tworzony jest dodatkowy element pusty w tablicy arguments. Teraz każdy parametr ma
partnera. Następnie makeObj() przeszukuje i grupuje argumenty w pary, przy czym pierwszy element w parze staje się
nazwą właściwości, a drugi wartością tej właściwości. Wobec tego wywołanie makeObj('nazwisko', 'Madonna',
'zawod', 'piosenkarka') zwróci wskaźnik na obiekt o następujących właściwościach:
this.nazwisko = 'Madonna';
this.zawod = 'piosenkarka';

Zmienna objInherit odnosi się teraz do obiektu i ma następujące właściwości:


objInherit.propertyOne = 'thisProperty';
objInherit.propertyTwo = 'thatProperty';
objInherit.propertyThree = 'theOtherProperty';

Zwróćmy uwagę, że wszystkie wartości właściwości to napisy. Można oczywiście przekazywać także liczby, obiekty
i tak dalej. Funkcja makeObj() doskonale nadaje się do tworzenia wielu obiektów, każdego z innymi właściwościami,
bez konieczności definiowania konstruktora dla każdego z nich.
Inny badany obiekt to location. Jest dość prosty, ale jak działa nasza analiza? Funkcje objProfile() i parseObj()
wzajemnie się wywołują w celu analizy „w głąb” właściwości obiektu – tworzą przy tym tablicę opisującą obiekt.
Każdy wiersz tej tablicy to jedna nazwa właściwości, typ obiektu właściwości oraz przypisana wartość. Zacznijmy
od objProfile():
function objProfile() {
var objTable = '<TABLE BORDER=2 CELLSPACING=0><TR><TD><H1>' +
'Opis obiektu</H1></TD></TR>';
for (var i = 0; i < arguments.length; i++) {
objTable += '<TR><TD><BR><BR><H2><TT>' + (i + 1) + ') ' +
arguments[i] + '</H2></TD></TR>';
objTable += '<TR><TD><TT><TABLE CELLPADDING=5>' +
parseObj(eval(arguments[i])) + '</TABLE></TD></TR>';
}
objTable += '</TABLE><BR><BR><BR>';
return objTable;
}

objProfile() to funkcja, którą należy wywołać, a potem przekazać jej parametry. Spójrzmy do wiersza 25 w pliku
objects.html:
document.write(objProfile('someObject', 'self.location'));
138

Przekazywane argumenty absolutnie nie są obiektami, to zwykłe napisy, ale już wkrótce będą elementami obiektu.
Przekazanie napisów pozwala JavaScriptowi wyświetlać te obiekty według nazw. Kiedy utworzone zostaną potrzebne
znaczniki TR i TD, argumenty są przekazywane do funkcji objProfile(), która w wierszu 32 kolejno analizuje je metodą
eval() i przekazuje do parseObj(). Zobaczmy, jak to wygląda:
function parseObj(obj) {
var objStr = '';
for (prop in obj) {
objStr += '<TR><TD>Właściwość: </TD><TD><B>' + prop +
'</B></TD><TD>Typ: </TD><TD><B>' + typeof(obj[prop]) +
'</B></TD><TD>Wartość: </TD><TD><B>' + obj[prop] +
'</B></TD></TR>';
if (typeof(obj[prop]) == "object") {
objStr += parseObj(obj[prop]);
}
}
return objStr;
}

Każdy zanalizowany napis pojawia się jako obiekt i nazywany jest obj. Używając pętli for z instrukcją if, funkcja
parseObj() analizuje kolejno wszystkie właściwości obj, zbierając w zmiennej tekstowej wraz z odpowiednimi
znacznikami HTML nazwy właściwości, ich typy i wartości. parseObj()określa typ obiektu, stosując operator
typeof(). Kiedy już zostaną określone dane właściwości, sprawdza się, czy właściwość ta jest obiektem. Jeśli tak,
parseObj() wywołuje sama siebie rekurencyjnie i przekazuje swojej następnej instancji obiekt będący wartością tej
właściwości (znów jako obj). Dzięki temu możliwa jest analiza kolejnych poziomów zagnieżdżenia obiektów
i pokazanie ich hierarchii.
Kiedy parseObj() nie ma już więcej obiektów do analizy, cały napis z właściwościami, ich typami, wartościami
i znacznikami opisującymi tabelę HTML, znajdujący się w zmiennej objStr, jest zwracany do funkcji
objProfile(). Ta funkcja z kolei łączy ten napis z resztą znaczników, a w końcu całość, jako objTable, jest
dopisywana do strony w wierszu 25 pliku objects.html.

Tego typu funkcje analizy obiektów są przeznaczone do obiektów względnie


niewielkich, jak na przykład typowe obiekty tworzone przez użytkownika. Zarówno
Netscape Navigator, jak i Internet Explorer przeładują się w przypadku skryptu
zanadto obfitującego w wywołania rekursywne. Spróbujmy na przykład zmienić wiersz
25 w pliku objects.html z:
document.write(objProfile('someObject', 'self.location'));

na:
document.write(objProfile('document');

Teraz załadujmy tę stronę do Internet Explorer. Na pewno pojawi się komunikat


o przepełnieniu stosu. Spróbujmy z kolei w Netscape Navigatorze użyć takiego
wiersza 25:
document.write(objProfile('window');

Na konsoli JavaScriptu ukaże się taki komunikat: JavaScript Error: too


much recursion.16

strings.js
Zastosowania praktyczne:
Operacje na napisach, sortowanie alfabetyczne, zliczanie wystąpień.
Wymagana wersja:
JavaScript 1.2.
Funkcje:
camelCaps(), prepStr(), wordCount(), reorder()

Funkcje te pokazują, co można zrobić z tekstem, w szczególności z tekstem wprowadzonym przez użytkownika. Po
otworzeniu w swojej przeglądarce pliku ch06\string.html, zobaczymy obrazek taki, jak na rysunku 6.16.
Mamy tu trzy formularze służące do demonstracji działania trzech funkcji. Pierwszy formularz zawiera element
TEXTAREA służący do wprowadzenia tekstu. Kiedy użytkownik skończy i wciśnie przycisk Zliczaj, funkcja

16
Zbyt wiele [poziomów] rekursji (przyp. tłum.).
139 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

wordCount() wygeneruje nową stronę z tabelą. Tabela zestawia wszystkie użyte słowa z TEXTAREA i podaje liczbę
ich wystąpień. Wyniki widać na rysunku 6.17.
Druga formatka także zawiera TEXTAREA do wprowadzania tekstu. Użytkownik może wybrać opcję Upper lub Lower,
a wtedy pierwszy znak każdego słowa zmieniony zostanie odpowiednio na wielką lub małą literę. To zadanie realizuje
funkcja camelCaps().Może ona przydać się do sprawdzania danych, szczególnie przy wprowadzaniu
przez użytkowników nazwisk czy adresów. Wyniki jej działania pokazuje rysunek 6.18.
Trzeci formularz posiada TEXTAREA na słowa użytkownika, które zostaną posortowane alfabetycznie. Rysunek 6.19
przedstawia wyniki. Wielokrotne wybieranie Sortuj powoduje zmianę kolejności sortowania przemiennie na rosnącą
i malejącą. Teraz, kiedy już odbył się pokaz lalek, czas sprawdzić, kto pociąga za sznurki.

Rysunek 6.16. Trzy formularze do znęcania się nad danymi znakowymi, zaczynamy od zliczania słów

Rysunek 6.17. Tablica ze słowami i liczbą ich wystąpień


140

Rysunek 6.18. Zmiana pierwszej litery każdego słowa na wielką

Rysunek 6.19. Alfabetyczne sortowanie słów


Przykład 6.12 zawiera kod pliku strings.js.
Przykład 6.12. strings.js
1 function wordCount(str) {
2 var wordArray = new Array();

Przykład 6.12. strings.js (dokończenie)


3 str = prepStr(str);
4 var tempArray = str.split(' ').sort();
5 var count = 1;
6 for (var i = 0; i < tempArray.length; i++) {
7 if (wordArray[tempArray[i]]) {
8 wordArray[tempArray[i]]++;
9 }
10 else { wordArray[tempArray[i]] = 1; }
11 }
12 if (output) { return wordArray; }
13 else {
14 var arrStr = '';
15 for (word in wordArray) {
141 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

16 if (word != "") {
17 arrStr += '<TR><TD>' + word + '</TD><TD>' + wordArray[word] +
18 '</TD></TR>';
19 count++;
20 }
21 }
22 return '<TABLE BORDER=0><TR><TD WIDTH=300 VALIGN=TOP ROWSPAN=' +
23 count + '><B>Tekst pierwotny</B><BR><I>' + str +
24 '</I><TD><B>Wyraz</B><TD><B>Liczba</B></TR>' + arrStr +
25 '</TABLE>';
26 }
27 }
28
29 function prepStr(str) {
30 str = str.toLowerCase();
31 str = str.replace(/['"-]/g, "");
32 str = str.replace(/\W/g, " ");
33 str = str.replace(/\s+/g, " ");
34 return str;
35 }
36
37 function camelCaps(str, theCase) {
38 var tempArray = str.split(' ');
39 for (var i = 0; i < tempArray.length; i++) {
40 if (theCase) {
41 tempArray[i] = tempArray[i].charAt(0).toUpperCase() +
42 tempArray[i].substring(1);
43 }
44 else {
45 tempArray[i] = tempArray[i].charAt(0).toLowerCase() +
46 tempArray[i].substring(1);
47 }
48 }
49 return tempArray.join(' ');
50 }
51
52 var order = true;
53
54 function reorder(str) {
55 str = prepStr(str);
56 str = str.replace(/\d/g, "");
57 order = !order;
58 if(!order) { str = str.split(' ').sort().join(' '); }
59 else { str = str.split(' ').sort().reverse().join(' '); }
60 return str.replace(/^\s+/, "");
61 }

Aby zrealizować zliczanie słów z pierwszego formularza, wordCount() wykonuje następujące czynności:

1.usunięcie wszystkich znaków – poza literami, cyframi, podkreśleniami i spacjami,


2.utworzenie tablicy na wszystkie słowa z tekstu,
3.zliczenie wystąpień poszczególnych słów,
4.wyświetlenie wyników w postaci tabeli.
Pierwszy krok realizuje inna funkcja, prepStr(), znajdująca się w wierszach 29 do 35:
function prepStr(str) {
str = str.toLowerCase();
str = str.replace(/['"-]/g, "");
str = str.replace(/\W/g, " ");
str = str.replace(/\s+/g, " ");
return str;
}

Napis najpierw jest przekształcany na małe litery (nie trzeba rozróżniać „Domu” od „domu”), odbywa się szereg
przekształceń tekstu. W wierszu 31 usuwane są cudzysłowy i myślniki, w wierszu 32 wszystkie znaki inne niż litery, cyfry
i podkreślenia zamieniane są na pojedyncze spacje. W końcu ciągi sąsiadujących spacji przekształca się na spacje
pojedyncze. W ten sposób tekst został oczyszczony i możemy nie obawiać się haseł takich, jak „?” czy
„w cudzysłowie”.
Wracamy do wordCount(). prepStr() zwraca napis składający się ze słów porozdzielanych spacjami. Dzięki temu
możemy, stosując funkcję split(), zrealizować punkt 2. Punkt 3. wykonywany jest w wierszach 6–11:
142

for (var i = 0; i < tempArray.length; i++) {


if (wordArray[tempArray[i]]) {
wordArray[tempArray[i]]++;
}
else { wordArray[tempArray[i]] = 1; }
}

W formularzu drugim tekst wpisany przez użytkownika przekazujemy do funkcji camelCaps(). Funkcja ta ma też
drugi argument, wartość logiczną, decydującą, czy należy dane zmienić na litery wielkie, czy małe. Oto ta funkcja:
function camelCaps(str, theCase) {
var tempArray = str.split(' ');
for (var i = 0; i < tempArray.length; i++) {
if (theCase) {
tempArray[i] = tempArray[i].charAt(0).toUpperCase() +
tempArray[i].substring(1);
}
else {
tempArray[i] = tempArray[i].charAt(0).toLowerCase() +
tempArray[i].substring(1);
}
}
return tempArray.join(' ');
}

Zmienna lokalna tempArray staje się tablicą ze wszystkimi słowami z tekstu. W naszym przypadku „słowo” to tekst
między spacjami. Teraz należy po kolei w każdym słowie zamienić pierwszą literę na wielką lub małą. Kiedy już to
zrobimy, funkcja zwróci napis składający się z nowych słów porozdzielanych spacjami. Funkcja camelCaps() zwraca
zatem spacje, które wcześniej usunęła, wykonując split().
Jeśli chodzi o trzeci formularz, funkcja reorder() wykonuje albo sort(), albo sort() w odwrotnej kolejności.
Zobaczmy poniżej:
var order = true;

function reorder(str) {
str = prepStr(str);
str = str.replace(/\d/g, "");
order = !order;
if(!order) { str = str.split(' ').sort().join(' '); }
else { str = str.split(' ').sort().reverse().join(' '); }
return str.replace(/^\s+/, "");
}

Tak jak w przypadku wordCount(), i tym razem prepStr() przygotowuje najpierw przekazany tekst. Warto
zauważyć jednak, że przez wywołanie str.replace(/\d/g, "") usuwane są także cyfry. Zmienna order jest
zmieniana na wartość przeciwną niż dotąd, określa ona kolejność sortowania. Teraz zastanówmy się, kroki powinno się
podjąć, aby posortować dane, czy to normalnie, czy odwrotnie. W pierwszym przypadku trzeba:
1.rozbić tekst do tablicy,
2.posortować elementy tablicy,
3.złączyć zawartość tablicy w napis.
W drugim wypadku należy:
1.rozbić tekst do tablicy,
2.posortować elementy tablicy,
3.odwrócić kolejność elementów tablicy,
4.złączyć zawartość tablicy w napis.
W wierszach 58–59 pliku strings.js używamy wartości zmiennej order do podjęcia decyzji, którą metodę wybrać.
Następnie napis jest zwracany (ewentualnie pozbawiony wiodących spacji, które mogły pojawić się w wyniku
zastosowania join()).

Kierunki rozwoju
Tutaj nie ma żadnych ograniczeń. Można oczywiście dodawać do tych plików świetne funkcje, można ulepszać funkcje
również już istniejące. Jednak swoje funkcje w tych samych plikach. Możemy tworzyć grupę stron sieciowych i
143 Rozdział 6 - Realizacja plików źródłowych JavaScriptu

nazwać plik źródłowy zgodnie z nazwą tych stron, dając mu tylko rozszerzenie .js. Wspaniale. Wstawmy te funkcje,
których będziemy potrzebować, i już mamy gotowy komplet własnych narzędzi. Należy tylko pamiętać, aby stosować
układ dla siebie najwygodniejszy. Nie wolno pozwolić, aby to pliki .js nami kierowały, to one przecież mają nam służyć.
Cechy aplikacji:
 Modyfikowalne łącza, obrazki tła
i właściwości czcionki
 Elastyczna aplikacja obsługi ustawień
użytkownika
 Dziesiątki możliwych układów
 Łatwe do dostosowania ustawienia
językowe, kolory, obrazki i DHTML

7
Prezentowane techniki:
 Konwencje nazewnicze znów dochodzą
do głosu
 Dynamiczny DHTML

Ustawienia
użytkownika
oparte na
ciasteczkach

W tym rozdziale może się znaleźć aplikacja całkiem bezwartościowa, ale nie warto przerzucać jeszcze kartek. To
właśnie tutaj znajdzie się kod, który umożliwi dodanie do naszej witryny niezwykłej wprost funkcjonalności. Chodzi
o obsługę ustawień użytkownika. Zastanówmy się nad tym jakie słowo jest najbliższe każdemu sieciowemu wędrowcy?
„Ja”.
Tak, użytkownicy to samolubny typ ludzki, który zawsze myśli o swoich zainteresowaniach i upodobaniach. Cokolwiek
ludzie robią, zawsze starają się wyszukiwać rzeczy im odpowiadające. Dlatego właśnie amatorzy DHTML przesiadują
w Strefie Dynamicznego HTML (http://www.dhtmlzone. com/), zwolennicy terapii zakupowej odwiedzają strony
Shopping.com (http://www.shopping. com/ibuy/), a domorośli astronomowie odwiedzają witrynę Sky & Telescope
(http://www.skypub. com/). Użycie fikcyjnej aplikacji daje możliwość personalizacji naszej witryny, nawet jeśli chodzi
tylko o zapamiętanie identyfikatora użytkownika. Używając ciasteczek JavaScriptu, goście będą mogli dostosować
naszą witrynę do swoich wymagań.
Załóżmy, że tworzymy witrynę dla internetowych inwestorów, którzy mają do wydania nieco gotówki. Goście dostają
darmowe członkostwo w fikcyjnym cybercentrum Wall Street o nazwie Take -A-Dive Brokerage $ervices. Nie mogą tu
handlować, ale mogą otrzymać specjalizowaną stronę z zestawem łącz do ich ulubionych innych witryn finansowych
i elektronicznych centrów informacyjnych. Na rysunku 7.1 pokazano, co się dzieje, kiedy użytkownik po raz pierwszy
odwiedza stronę (\ch07\ dive.html). Jest to pierwsza wizyta, zatem trzeba przekierować użytkownika do strony
z ustawieniami użytkownika.
145 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

Na rysunku 7.2 pokazano stronę, na której użytkownik może ustawić swoje preferencje (\ch07\ prefs.html). Jest to długi
formularz umożliwiający użytkownikowi określenie nazwiska, wieku, zawodu i rodzaju strategii inwestycyjnej. Ciąg pól
opcji pozwala wybrać, które z dostępnych łącz związanych z finansami (ewentualnie – czy w ogóle jakiekolwiek) mają
być umieszczane na specjalizowanej stronie domowej.

Rysunek 7.1. Nowi goście przechodzą bezpośrednio na stronę z ustawieniami użytkownika


Formularz zawiera szereg list opcji, za pomocą których użytkownik wybiera obrazek tła, rozmiar czcionki i i jej rodzaj
na podstawie pokazanych miniaturek. Kiedy ustalono już wszystko, kliknięcie Zapisz zapisuje dokonane wybory
do pliku z ciasteczkami przeglądarki, co pokazano na rysunku 7.3. Potwierdzenie OK przekierowuje użytkownika
na stronę dive.html, która zawiera wszystkie ustawienia zgodne z wyborami dokonanymi przez użytkownika.
Dobrze, ale co będzie, jeśli użytkownik zmieni zdanie? Po prostu wybierze łącze Ustawienia i znów będzie na stronie
prefs.html. Zwróćmy uwagę na fakt, że zapamiętane zostały bieżące ustawienia użytkownika. Zachowano wszystkie
informacje o użytkowniku, jak również zaznaczono wybrane wcześniej pola opcji, a nawet obrazki tła i inne szczegóły.
Teraz użytkownik może pobawić się z ustawieniami, aby stwierdzić, co mu najbardziej odpowiada. Na rysunku 7.4
pokazano jedną z wielu możliwych kombinacji.

Wymagania programu
Aplikacja ta została napisana w wersji 1.2 języka JavaScript. Konieczne jest to z uwagi na stosowanie arkuszy stylów
i operacje na tekście. Użytkownicy będą musieli użyć wersji 4. lub nowszej Netscape Navigatora czy Internet Explorera.
Aplikacja ta może zostać znacznie rozszerzona – używana się wówczas bardzo prostego arkusza stylów. Jedyne ograniczenie
to umiejętność posługiwania się DHTML.
Wspomniana aplikacja bazuje na użyciu ciasteczek, zatem elementami ograniczającymi są ich specyfikacje dla obu
przeglądarek. Specyfikacje te znajdziemy pod następującymi adresami URL:
146

Rysunek 7.2. Ustawianie preferencji użytkownika


dla Netscape Navigatora:
http://developer1.netscape.com:80/docs/manuals/communicator/jsguide4/cookies.htm
dla Internet Explorera:
http://msdn.microsoft.com/msdn-
online/workshop/author/dhtml/reference/properties/cookie.asp
Nie martwmy się – w aplikacji dalecy jesteśmy od kresu możliwości ciasteczek w obu przeglądarkach.

Struktura programu
Aplikacja ta składa się z dwóch stron HTML (prefs.html oraz dive.html) i pliku źródłowego JavaScript (cookies.js). Oto
krótki opis każdego z tych elementów:
prefs.html
Strona ta używana jest do określenia ustawień strony dive.html. Jej postać też zależy od zewnętrznych informacji
zawartych w pliku cookies, gdyż informacje te stosowane są do sprawdzenia, czy użytkownik wprowadził już
jakieś ustawienia i do odpowiedniego pokazania formularza.
147 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

Rysunek 7.3. Po kliknięciu Zapisz użytkownik jest pytany, czy chce obejrzeć stronę dostosowaną do jego wymagań
dive.html
Ta strona z kolei jest tworzona zgodnie z wybranymi przez użytkownika ustawieniami i zawiera samą treść.
cookies.js
Plik ten zawiera funkcje stosowane do zapisywania ustawień użytkownika w ciasteczkach i odczytywania ich
stamtąd. Odwołania do cookies.js znajdują się w obu plikach HTML. Funkcje GetCookie() i SetCookie() –
wywoływane z kodu – pochodzą właśnie z naszego pliku .js.
Pliki prefs.html i dive.html są nowe, natomiast cookies.js to plik źródłowy z rozdziału 6. – tam też znajdzie się opis
zawartych w nim funkcji.

prefs.html
Choć sekwencja zrzutów ekranów sugerować może istnienie prostego, określonego ciągu zdarzeń (kiedy użytkownik
zaczyna bez własnych ustawień), załóżmy, że gość na naszej ustawienia już ma swoje ustawienia i do prefs.html wraca,
aby coś poprawić. Sądzę, że w takiej sytuacji znacznie łatwiej będzie nam omawiać kod. Przykład 7.1 zawiera plik
prefs.html.

Przykład 7.1. prefs.html


1 <HTML>
2 <HEAD>
148

Rysunek 7.4. Przykładowa strona dostosowana do ustawień użytkownika

Przykład 7.1. prefs.html (ciąg dalszy)


3 <TITLE>Ustawienia u+ytkownika Take-A-Dive</TITLE>
4 <STYLE type="text/css">
5 BODY, TD { font-family: Arial; }
6 </STYLE>
7 <SCRIPT LANGAUGE="JavaScript1.2" SRC="cookies.js"></SCRIPT>
8 <SCRIPT LANGUAGE="JavaScript1.2">
9
10 var imagePath = 'images/';
11 var newsNames = new Array(
12 new Array('The Wall Street Journal','http://www.wsj.com/'),
13 new Array('Barron\'s Online','http://www.barrons.com/'),
14 new Array('CNN Interactive','http://www.cnn.com/'),
15 new Array('MSNBC','http://www.msnbc.com/'),
16 new Array('Fox News','http://www.foxnews.com/')
17 );
18
19 var indexNames = new Array(
20 new Array('The New York Stock Exchange','http://www.nyse.com/'),
21 new Array('NASDAQ','http://www.nasdaq.com/'),
22 new Array('Dow Jones Indexes','http://www.dowjones.com/')
23 );
24
25 var strategy = new Array(
26 new Array('Cheap', 'Grywam tylko o małe stawki'),
27 new Array('Stingy', 'Gram ostrożnie'),
28 new Array('Conservative', 'Jestem konserwatystą'),
29 new Array('Moderate', 'Gram typowo'),
30 new Array('Agressive', 'Gram agresywnie'),

Przykład 7.1. prefs.html (ciąg dalszy)


31 new Array('Willing to sell mother', 'Zaprzedałbym własną duszę!')
32 );
33
34 var background = new Array(
35 new Array(imagePath + 'goldthumb.gif', 'Gold Bars'),
36 new Array(imagePath + 'billsthumb.gif', 'Dollar Bills'),
37 new Array(imagePath + 'fistthumb.gif', 'Fist of Cash'),
38 new Array(imagePath + 'currency1thumb.gif', 'Currency 1'),
39 new Array(imagePath + 'currency2thumb.gif', 'Currency 2')
40 );
41
149 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

42 var face = new Array(


43 new Array('times', 'Times Roman'),
44 new Array('arial', 'Arial'),
45 new Array('courier', 'Courier New'),
46 new Array('tahoma', 'Tahoma')
47 );
48
49 var size = new Array(
50 new Array('10', 'Small'),
51 new Array('12', 'Medium'),
52 new Array('14', 'Large'),
53 new Array('16', 'X-Large')
54 );
55
56 indexNames = indexNames.sort();
57 newsNames = newsNames.sort();
58
59 var allImages = new Array();
60
61 var imageNames = new Array(
62 'courier10', 'courier12', 'courier14', 'courier16',
63 'arial10', 'arial12', 'arial14', 'arial16',
64 'times10', 'times12', 'times14', 'times16',
65 'tahoma10', 'tahoma12', 'tahoma14', 'tahoma16',
66 'goldthumb', 'billsthumb', 'fistthumb', 'currency1thumb',
67 'currency2thumb', 'blank'
68 );
69
70 for (var i = 0; i < imageNames.length; i++) {
71 allImages[i] = new Image();
72 allImages[i].src = imagePath + imageNames[i] + '.gif';
73 }
74
75 function makePath(formObj) {
76 var fontName = imagePath +
77 formObj.face.options[formObj.face.selectedIndex].value +
78 formObj.size.options[formObj.size.selectedIndex].value + '.gif'
79 swapImage("fontImage", fontName);
80 }
81
82 function swapImage(imageName, imageBase) {
83 document[imageName].src = imageBase;
84 }
85
86 function genSelect(name, select, onChangeStr) {
87 var optStr = "";
88 var arrObj = eval(name);
89 for (var i = 0; i < arrObj.length; i++) {
90 optStr += '<OPTION VALUE="' + arrObj[i][0] +
91 (i == select ? '" SELECTED' : '"') + '>' + arrObj[i][1];
92 }
93 return '<SELECT NAME="' + name + '"' + (onChangeStr ? ' onChange="' +
94 onChangeStr + ';"' : '') + '>' + optStr + '</SELECT>';
95 }

Przykład 7.1. prefs.html (ciąg dalszy)


96
97 function genBoxes(name) {
98 var boxStr = '';
99 for (var i = 0; i < arrObj.length; i++) {
100 boxStr += '<INPUT TYPE=CHECKBOX NAME="' + name + i + '" VALUE="' +
101 arrObj[i][0] + ',' + arrObj[i][1] + '"> ' + arrObj[i][0] + '<BR>'
102 }
103 return boxStr;
104 }
105
106 function getPrefs(formObj) {
107 var prefStr = GetCookie('userPrefs');
108 if (prefStr == null) { return false; }
109 var prefArray = prefStr.split('-->');
110 for (var i = 0; i < prefArray.length; i++) {
111 var currPref = prefArray[i].split('::');
112 if (currPref[1] == "select") {
113 formObj[currPref[0]].selectedIndex = currPref[2];
114 }
115 else if (currPref[1] == "text") {
150

116 formObj[currPref[0]].value = currPref[2];


117 }
118 else if (currPref[1] == "checkbox") {
119 formObj[currPref[0]].checked = true;
120 }
121 }
122 return true;
123 }
124
125 function setPrefs(formObj) {
126 var prefStr = '';
127 var htmlStr = '';
128 for (var i = 0; i < formObj.length; i++) {
129 if (formObj[i].type == "select-one") {
130 prefStr += formObj[i].name + '::select::' +
131 formObj[i].selectedIndex + '-->';
132 htmlStr += formObj[i].name + '=' +
133 formObj[i].options[formObj[i].selectedIndex].value + '-->';
134 }
135 else if (formObj[i].type == "text") {
136 if (formObj[i].value == '') { formObj[i].value = "Not Provided"; }
137 prefStr += formObj[i].name + '::text::' +
138 safeChars(formObj[i].value) + '-->';
139 htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
140 }
141 else if (formObj[i].type == "checkbox" && formObj[i].checked) {
142 prefStr += formObj[i].name + '::checkbox::' + '-->';
143 htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
144 }
145 }
146 SetCookie('userPrefs', prefStr, expiry);
147 SetCookie('htmlPrefs', htmlStr, expiry);
148 if (confirm('Zmieniono ustawienia. Przełączyć się na dostosowaną stronę?')) {
149 self.location.href = "dive.html";
150 }
151 }
152
153 function safeChars(str) {
154 return str.replace(/::|=|-->/g, ':;');
155 }
156
157 function populateForm(formObj) {
158 if (getPrefs(formObj)) {

Przykład 7.1. prefs.html (ciąg dalszy)


159 makePath(formObj);
160 swapImage('bkgImage',
161 formObj.background.options[formObj.background.selectedIndex].value);
162 }
163 else { resetImage(document.forms[0]); }
164 }
165
166 function resetImage(formObj) {
167 swapImage('bkgImage', formObj.background.options[0].value);
168 swapImage('fontImage', imagePath + formObj.face.options[0].value +
169 formObj.size.options[0].value + '.gif');
170 }
171
172 </SCRIPT>
173 </HEAD>
174 <BODY BGCOLOR=FFFFFF onLoad="populateForm(document.forms[0]);">
175 <DIV ID="setting">
176 <H2>Ustawienia u+ytkownika Take-A-Dive</H2>
177 Wybierz ustawienia najbardziej Ci odpowiadające<BR>
178
179 <UL>
180 <LI><B>Zapisz</B> zapisanie zmian
181 <LI><B>Wyczyść</B> - wyczyszczenie formularza
182 <LI> <B>Cofnij</B> - powrót do strony z łączami
183 </UL>
184
185 <FORM>
186 <TABLE BORDER=1 CELLBORDER=0 CELLPADDING=0 CELLSPACING=1>
187 <TR>
188 <TD COLSPAN=2>
189 <BR>
151 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

190 <H3>Dane o inwestorze</H3>


191 </TD>
192 </TR>
193 <TR>
194 <TD>Nazwisko</TD>
195 <TD><INPUT TYPE=TEXT NAME="investor"></TD>
196 </TR>
197 <TR>
198 <TD>Wiek</TD>
199 <TD><INPUT TYPE=TEXT NAME="age"></TD>
200 </TR>
201 <TR>
202 <TD>Strategia</TD>
203 <TD>
204 <SCRIPT LANGUAGE="JavaScript1.2">
205 document.write(genSelect('strategy', 3));
206 </SCRIPT>
207 </TD>
208 </TR>
209 <TR>
210 <TD>Zawód</TD>
211 <TD>
212 <INPUT TYPE=TEXT NAME="occupation">
213 </TD>
214 <TR>
215 <TD COLSPAN=2>
216 <BR>
217 <H3>łącza inwestora</H3>
218 </TD>
219 </TR>
220 <TR>
221 <TD><B>Nowości<B></TD>
222 <TD>
223 <SCRIPT LANUAGE="JavaScript1.2">

Przykład 7.1. prefs.html (ciąg dalszy)


224 document.write(genBoxes('newsNames'));
225 </SCRIPT>
226 </TD>
227 </TR>
228 <TR>
229 <TD><B>Indeksy giełdowe</B></TD>
230 <TD>
231 <SCRIPT LANUAGE="JavaScript1.2">
232 document.write(genBoxes('indexNames'));
233 </SCRIPT>
234 </TD>
235 </TR>
236 <TR>
237 <TD COLSPAN=2>
238 <BR>
239 <H3>Układ ekranu</H3>
240 </TD>
241 </TR>
242 <TR>
243 <TD>
244 <B>Tło</B>
245 <BR>
246 <SCRIPT LANGUAGE="JavaScript1.2">
247 document.write(genSelect('background', 0,
248 "swapImage('bkgImage',
249 this.options[this.selectedIndex].value)"));
250 </SCRIPT>
251 </TD>
252 <TD>
253 <TD><IMG SRC="images/blank.gif"
254 NAME="bkgImage" WIDTH=112 HEIGHT=60>
255 </TD>
256 </TR>
257 <TR>
258 <TD>
259 <B>Rodzaj czcionki</B>
260 <BR>
261 <SCRIPT LANGUAGE="JavaScript1.2">
262 document.write(genSelect('face', 0, "makePath(this.form)"));
263 </SCRIPT>
152

264 </TD>
265 <TD ROWSPAN=2>
266 <IMG SRC="images/blank.gif" NAME="fontImage"
267 WIDTH=112 HEIGHT=60>
268 </TD>
269
270 </TR>
271 <TR>
272 <TD>
273 <B>Rozmiar czcionki</B>
274 <BR>
275 <SCRIPT LANGUAGE="JavaScript1.2">
276 document.write(genSelect('size', 0, "makePath(this.form)"));
277 </SCRIPT>
278 </TD>
279 </TR>
280 </TABLE>
281 <BR><BR>
282 <INPUT TYPE=BUTTON VALUE="Zapisz" onClick="setPrefs(this.form);">
283 <INPUT TYPE=RESET VALUE="Wyczyść" onClick="resetImage(this.form);">
284 <INPUT TYPE=BUTTON VALUE="Cofnij" onClick="location.href='dive.html';">
285 <!--
286 <INPUT TYPE=BUTTON VALUE="Show"
287 onClick="alert(GetCookie('userPrefs')); alert(GetCookie('htmlPrefs'));">

Przykład 7.1. prefs.html (dokończenie)


288 <INPUT TYPE=BUTTON VALUE="Erase"
289 onClick="DeleteCookie('userPrefs'); DeleteCookie('htmlPrefs');">
290 //-->
291 </FORM>
292 </DIV>
293 </BODY>
294 </HTML>

Wiersze 10–68 to rzecz łatwa. Zmienna w wierszu 10 służy do określenia ścieżki do obrazków, reszta zmiennych służy
do definiowania wyglądu dive.html. Wartością wszystkich zmiennych są „wielowymiarowe” tablice, czyli tablice tablic.
Oto na przykład tablica jednowymiarowa:
var jedenWymiar = new Array("Ten", "Tamten", "Jeszcze Inny");

Do elementów łatwo się odwołać – wystarczy podać w nawiasach kwadratowych indeks, na przykład jedenWymiar[0].
W tablicy „wielowymiarowej” każdy element jest tablicą z innymi elementami:
var dwaWymiary =
new Array(
new Array(1,2,3),
new Array(4,5,6),
new Array(7,8,9)
);

Teraz dwaWymiary[0] oznacza new Array(1,2,3). Odnosi się zatem do trzech wartości: 1, 2 i 3, a żeby odwołać się
do jednej tylko z tych liczb, należy użyć drugiej pary nawiasów:
dwaWymiary[0][0] // odnosi się do 1
dwaWymiary[0][1] // odnosi się do 2
dwaWymiary[0][2] // odnosi się do 3
dwaWymiary[1][0] // odnosi się do 4
dwaWymiary[1][1] // odnosi się do 5
dwaWymiary[1][2] // odnosi się do 6

Terminu „wielowymiarowe” pojawił się w cudzysłowie, albowiem w JavaScripcie formalnie tablice wielowymiarowe
nie istnieją, po prostu się je emuluje. Wróćmy teraz do naszego skryptu: każda ze zmiennych deklarowanych
w wierszach 11–68 to zestaw tablic z danymi opisanymi w tabeli 7.1.
Jak to sugeruje termin tablica dwuwymiarowa, każdy element tablicy sam jest dwuelementową tablicą. W zasadzie
jeden element zawiera wyświetlany tekst, natomiast drugi zawiera tekst, który pełni rolę identyfikatora. Na przykład
wartością size[0][0] jest 10 – taka wartość będzie używana do określenia wielkości czcionki. Jednak size[0][1]
ma wartość Small – i taki tekst będzie pokazywany na stronie, zatem dla użytkownika czcionka 10-punktowa to
czcionka Small. Podobnie zdeklarowano pozostałe tablice.
Wiersze 56–57 zawierają wywołania metody sort() obiektu Array, dzięki czemu łącza do nowinek i notowań są
porządnie ułożone. Nie jest to konieczne, ale dwa dodatkowe wiersze kodu nie zaszkodzą. W pliku prefs.html używana
jest spora ilość grafiki, więc dobrze odpowiednie obrazki zawczasu załadować. Można to wykonać w wierszach 70–73:
for (var i = 0; i < imageNames.length; i++) {
153 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

allImages[i] = new Image();


allImages[i].src = imagePath + imageNames[i] + '.gif';
}

Tabela 7.1. Zmienne zawierające informacje o stronach użytkownika


Nazwa tablicy Zawartość
newsNames Nazwy i adresy URL nowinek finansowych.
indexNames Nazwy i adresy URL serwisów z notowaniami giełdowymi.
strategy Nazwy i rodzaje strategii inwestycyjnych, w ramach których mogą się
określić użytkownicy.
background Nazwy i adresy URL dostępnych obrazków tła.
face Uchwyty obrazków (więcej na ten temat zaraz) i nazwy dostępnych grup
czcionek.
size Uchwyty obrazków i nazwy dostępnych rozmiarów czcionek.
allImages Obecnie pusta tablica, używana później do przechowywania wstępnie
załadowanych obrazków w celu ułatwienia dostępu.
imageNames Tablica zawierająca uchwyty obrazków; napisy te pomagają ładować
wstępnie obrazki.

Każdy element imageNames jest napisem. Jest to uchwyt obrazka, który w połączeniu ze zmienną imagePath
i tekstem stałym .gif umożliwi iterację przez elementy imageNames i wstępne ładowanie potrzebnych obrazków.
Zapamiętajmy, że uchwyty obrazków (takie jak courier10) nie są nazwami narzuconymi. Przyjęta konwencja
nazewnictwa przyda się dalej, co jeszcze będzie omawiane.
Jeśli przejrzy resztę kodu między znacznikami SCRIPT, zauważy, że wszystko inne zdefiniowano jako funkcje. Wobec tego
wszelkie fragmenty kodu są wywołane skądś indziej. Ma to miejsce dwukrotnie:
• podczas ładowania treści HTML,
• w obsłudze zdarzenia onLoad, w znaczniku BODY.
Przyjrzyjmy się zatem kodowi HTML za JavaScriptem (w wierszach 174–294).

Formularz ustawień użytkownika


Interfejsem jest formularz z polami tekstowymi, polami opcji i listami wyboru, w których użytkownik zaznacza
preferowane ustawienia. Stworzenie pól tekstowych to kwestia ich zwykłego zakodowania (przynajmniej tym razem)
i ustawienie żądanych nazw. Odpowiednio – nazwisko, wiek i zawód znajdziemy w wierszach 195, 199 i 212.
Następne zadanie to określenie preferowanej strategii gry. Kategorie obejmują cały wachlarz zachowań, od bardzo
ostrożnych po wyjątkowo agresywne. Tym razem użytkownik nie wypełnia pola tekstowego, ale wybiera jedną z opcji
z listy. Zamiast wstawić normalny znacznik OPTION, można użyć funkcji JavaScriptu, która dynamicznie wygeneruje
odpowiednią listę. Stosujemy do tego funkcję genSelect() – wiersze 205, 247–249, 262 i 276. Wywołanie z wiersza
205 pozwala wybrać strategię inwestycyjną, tymczasem pozostałe wywołania służą do określenia tła, typu i rozmiaru
czcionki. Oto funkcja genSelect() z wierszy 86–95:
function genSelect(name, select, onChangeStr) {
var optStr = "";
var arrObj = eval(name);
for (var i = 0; i < arrObj.length; i++) {
optStr += '<OPTION VALUE="' + arrObj[i][0] +
(i == select ? '" SELECTED' : '"') + '>' + arrObj[i][1];
}
return '<SELECT NAME="' + name + '"' + (onChangeStr ? ' onChange="' +
onChangeStr + ';"' : '') + '>' + optStr + '</SELECT>';
}

Podobną funkcję genSelect() widzieliśmy w rozdziale 5. Funkcja ta generowała tam listę wyboru, pozwalającą
wybierać tekst reprezentujący liczbę całkowitą. Tym razem znaczniki OPTION nie są liczbami, ale zawartością tablicy.
Spójrz na wywołanie genSelect() w wierszu 205:
document.write(genSelect('strategy', 3));
154

Funkcja otrzymuje dwa argumenty: napis strategy oraz liczbę 3. „Zaraz – pomyślisz sobie – przecież mamy tablicę
o nazwie strategy, po co więc przekazujemy taki napis?”
Zgadza się, mamy tablicę strategy. Może i dobrze byłoby przekazywać jako argument jej zawartość, tyle tylko, że
każda lista wyboru musi mieć swoją nazwę, według której będziemy ją identyfikować. Aby sobie uprościć zadanie, każdej
liście nadamy nazwę taką samą, jaką ma odpowiadająca jej tablica. Dlatego właśnie przekazujemy taki napis.
Następnie zmiennej arrObj przypisywana jest zinterpretowana wartość napisu; eval(name) oznacza
eval('strategy'), co daje nam odwołanie do tablicy strategy. Teraz genSelect() ma zarówno tablicę (arrObj)
do wypełnienia znaczników OPTION, jak i nazwę listy wyboru (name).
Drugi przekazany argument jest liczbą całkowitą dostępną jako select. Wartość ta oznacza opcję wybraną domyślnie.
Jeśli wartość i równa jest select, odpowiedni znacznik OPTION otrzymuje atrybut SELECTED. Gdy select równe
jest 0, domyślnie wybrana będzie pierwsza opcja; jeśli 1, druga, i tak dalej. Odpowiednie wyrażenie znajdziemy
w wierszu 91:
(i == select ? '" SELECTED' : '"')

Po pobraniu z tablicy strategy wszystkich elementów i stworzeniu optStr ze znacznikami OPTION, w wierszach
93–94 całość jest sklejana z obejmującymi ją znacznikami SELECT.
No dobrze, a co z trzecim argumentem zdefiniowanym w genSelect()? Nazywa się on onChangeStr i w tym wywołaniu
nie został ustawiony, jednak pojawia się w innych miejscach. Oto na przykład wiersze 247–249:
document.write(genSelect('background', 0,
"swapImage('bkgImage',
this.options[this.selectedIndex].value)"));

W tym wywołaniu parametr name jest napisem równym background, select ma wartość 0, a onChangeStr
ustawiono na swapImage('bkgImage', this.options[this.selectedIndex].value). Otóż, kiedy
genSelect() otrzymuje trzeci argument, jest on doklejany do procedury obsługi zdarzenia onChange tworzonej listy.
Jeśli argument ten nie zostanie podany, procedura taka w ogóle nie jest tworzona. Listy wyboru z wierszy 247–249, 262
i 276 używają tego zdarzenia do pokazania obrazków stosownie do wybranych opcji. Kiedy zmienimy wielkość
czcionki ze Small na Medium, dalej na Large i X-Large, odpowiednio do tego będzie pokazywany coraz większy
obrazek odpowiadający wybranej wielkości.
Listy wyboru nie są jedynymi elementami tworzonymi w tej formatce dynamicznie przez JavaScript. Funkcja genBoxes()
generuje dwie grupy pól opcji: jedną dla łącz nowinek, drugą dla łącz notowań giełdowych. Wywołania znajdziesz
w wierszach 224 i 232. Oto funkcja genBoxes() z wierszy 97–104:
function genBoxes(name) {
var boxStr = '';
for (var i = 0; i < arrObj.length; i++) {
boxStr += '<INPUT TYPE=CHECKBOX NAME="' + name + i + '" VALUE="' +
arrObj[i][0] + ',' + arrObj[i][1] + '"> ' + arrObj[i][0] + '<BR>'
}
return boxStr;
}

Mamy tutaj do czynienia z sytuacją podobną, jak w genSelect(). Napis odpowiadający żądanej tablicy
przekazywany jest i interpretowany w funkcji. Poszczególne elementy tablicy stają się polami opcji, w końcu wynik
wstawiany jest do dokumentu.

Ładowanie zapisanych ustawień


Kiedy załadowano już HTML, można do pustego formularza wstawić wcześniejsze ustawienia (o ile były w ogóle
wybrane). Procedura obsługi zdarzenia onLoad, umieszczona w znaczniku BODY, wywołuje funkcję populateForm()
i przekazuje kopię pustego formularza, na którym mają być przeprowadzane wszystkie operacje. Oto
populateForm() z wierszy 157–164:
function populateForm(formObj) {
if (getPrefs(formObj)) {
makePath(formObj);
swapImage('bkgImage',
formObj.background.options[formObj.background.selectedIndex].value);
}
else { resetImage(document.forms[0]); }
}

Funkcja populateForm() tak naprawdę jest tylko nadzorcą i do pracy wykorzystuje inne funkcje. Działa następująco:
jeśli ustawiono wcześniej preferencje, odpowiednie pola wypełniane są danymi odczytanymi z ciasteczek. Następnie
155 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

określane jest tło i obraz czcionki, które odpowiadają ustawieniom odpowiednich list wyboru. W przypadku braku
jakichkolwiek ustawień nic nie jest wykonywane. populateForm() do sprawdzenia ewentualnych ustawień
użytkownika stosuje funkcję getPrefs() z wierszy 106–123:
function getPrefs(formObj) {
var prefStr = GetCookie('userPrefs');
if (prefStr == null) { return false; }
var prefArray = prefStr.split('-->');
for (var i = 0; i < prefArray.length; i++) {
var currPref = prefArray[i].split('::');
if (currPref[1] == "select") {
formObj[currPref[0]].selectedIndex = currPref[2];
}
else if (currPref[1] == "text") {
formObj[currPref[0]].value = currPref[2];
}
else if (currPref[1] == "checkbox") {
formObj[currPref[0]].checked = true;
}
}
return true;
}

Także ta funkcja ma duże znaczenie. Działa w taki sposób, że pobiera z ciasteczek ustawienia związane z nazwą
userPrefs. Jeśli wartość ta jest pusta, zwracana jest wartość false. Oznacza to, że nie ustawiono userPrefs
w document.cookie. Jeśli jednak userPrefs nie jest puste, oznacza to, że wprowadzono już jakieś ustawienia.
W naszym przykładzie userPrefs jest puste, ale sprawdźmy od razu, co się stanie, jeśli userPrefs już będzie coś
zawierać.
Jeśli usePrefs zawiera potrzebne wartości, getPrefs() tworzy tablicę, rozdzielając wartość prefStr według
ogranicznika użytego do złączenia poszczególnych ustawień w setPrefs(). Stosuje się tu napis -->. Teraz elementy
prefsArray zawierają napisy ograniczone przez ::, określające rodzaj parametru i jego wartość. Przypisanie
poszczególnych wartości z odpowiednimi elementami formularza realizowane jest przez iteracyjną analizę elementów
prefsArray i przypisywanie ich kolejno – stosownie do ich typu. Lepiej wyjaśnią to wiersze 110–121:
for (var i = 0; i < prefArray.length; i++) {
var currPref = prefArray[i].split('::');
if (currPref[1] == "select") {
formObj[currPref[0]].selectedIndex = currPref[2];
}
else if (currPref[1] == "text") {
formObj[currPref[0]].value = currPref[2];
}
else if (currPref[1] == "checkbox") {
formObj[currPref[0]].checked = true;
}
}

Pamiętajmy, że użytkownik może ustawiać swoje preferencje na trzy sposoby:


• wybierając opcję z listy wyboru,
• wpisując tekst w polu tekstowym,
• zaznaczając pole opcji.
Wobec tego wartości prefsArray zawierają identyfikator typu elementu formularza (text, checkbox lub select-one) oraz
napis reprezentujący wartość elementu formularza, a oddzielone są od siebie dwoma dwukropkami, ::. Zaraz to się wyjaśni.
Poniżej pokazano kilka przykładów elementów prefsArray.
strategy::select::0
Element formularza strategy jest listą wyboru i wybrano opcję 0 (OPTION 0).
newsNames0::checkbox::Barron's Online,http://www.barrons.com/
Element newsNames0 jest polem opcji i ma wartość Barron's Online,http://www.barrons.com/.
investor::text::Jerry Bradenbaugh
Element investor jest polem tekstowym, a jego wartość to Jerry Bradenbaugh.
Jeśli chodzi o pętlę for z wiersza 110, służy ona do przeglądania elementów tablicy prefsArray, przy czym
wartością zmiennej lokalnej currPref staje się tablica powstająca w wyniku rozbicia elementu prefsArray[i]
przy każdym wystąpieniu ::. Znaczy to, że currPref będzie miała trzy elementy (dwa dla pól opcji). Jako że
currPref[1] zawiera identyfikator typu elementu formularza, jej sprawdzenie pozwoli określić, co getPrefs()ma
zrobić z currPref[0] i currPref[2].
156

Jeśli currPref[1] równe jest select, getPrefs() używa wiersza 113 do przypisania listy wyboru o nazwie – określonej
w currPref[0] – opcji związanej z selectedIndex w currPref[2] – faktycznie jest to parseInt(currPref[2]),
ale JavaScript „wie”, że napis należy przekształcić na liczbę.
Jeśli currPref[1] równe jest text, getPrefs() w wierszu 116 przypisuje currPref[2] polu tekstowemu
o nazwie currPref[0].
Jeśli w końcu currPref[1] równe jest checkbox, getPrefs() w wierszu 119 ustawia właściwość checked pola
opcji o nazwie currPref[0] na true. W tym wypadku currPref[2] nie istnieje, a do określenia stanu opcji
wystarczy istnienie odpowiedniego wpisu w ciasteczku.
Cały opisany proces zachodzi dla wszystkich elementów prefsArray. Kiedy już cała tablica zostanie przeanalizowana,
formularz opisuje wszystkie ustawienia użytkownika. Zatem getPrefs()zrealizowała swoje zadanie i zwraca true, aby
poinformować populateForm(), że wszystko się udało.

Składanie obrazków
Zostało jeszcze tylko jedno do zrobienia: synchronizacja obrazków tła i czcionki z opcjami wybranymi w formularzu.
Zwróćmy uwagę na to, że oba atrybuty SRC są ustawione w HTML na images/ blank.gif. Jest to po prostu obojętna
nazwa użyteczna, póki nie dokonano zmiany zgodnie z ustawieniem użytkownika. Funkcja populateForm() realizuje
omawiane zadanie w wierszach 159–161:
makePath(formObj);
swapImage('bkgImage',
formObj.background.options[formObj.background.selectedIndex].value);

Znów populateForm() sama niczego nie dokonuje, tylko wywołuje makePath() i swapImage(), które realizują
przewijanie obrazków. Tak naprawdę zresztą realizuje to tylko swapImage(), natomiast makePath() po prostu
przetwarza kilka napisów w celu określenia ścieżki plików na jej potrzeby. Przyjrzyjmy się najpierw funkcji, czyli
swapImage(). W wierszach 160–161 funkcji tej przekazywane są dwa argumenty, a samą funkcję znajdziemy
w wierszach 82–84:
function swapImage(imageName, imageBase) {
document[imageName].src = imageBase;
}

Parametr imageName jest nazwą obiektu Image, który będzie przewijany. Z kolei parametr imageBase to adres URL
obrazka. Oto przykład tego, co jest tutaj przekazywane:
formObj.background.options[formObj.background.selectedIndex].value)

Całkiem pokaźny argument, ale jest to po prostu wartość wybranego znacznika OPTION. Jako że getPrefs() ustawiła
już listy wyboru zgodnie z wcześniejszymi ustawieniami użytkownika, na pewno znajdzie się jakiś obrazek
odpowiadający ustawieniom. Zajrzyjmy do tabeli 7.2, gdzie pokazano wartość znacznika OPTION, tekst opcji (czyli
widziany przez użytkownika) oraz argument przekazywany do swapImage().
Wydaje się to dość proste. swapImage() otrzymuje wartość obecnie wybranej opcji, a jednocześnie jest to adres URL
obrazka, zatem pobranie obrazka odbywa się natychmiast. Takiego samego kodu, jakiego użyliśmy do obsługi zmiany
wartości listy wyboru tła. Oto kod HTML wygenerowany podczas ładowania strony. Zauważmy, że oba argumenty
przekazywane do swapImage() są uderzająco podobne do tego, co widzieliśmy w wierszu 161:
<SELECT NAME="background" onChange="swapImage('bkgImage',
this.options[this.selectedIndex].value);">

Ustawiliśmy już obrazek tła, zatem przyszedł czas wykonać czcionki. Tutaj rzecz się nieco komplikuje. W przypadku
tła wystarczyło zwykłe przewijanie związane z pojedynczą listą wyboru. W przypadku czcionki jest w zasadzie
podobnie, ale musimy uwzględnić dwie listy wyboru. Przyjrzyjmy się, jak są nazwane odpowiednie obrazki z czcionką.
W tabeli 7.3 pokazano wartości OPTION i odpowiadający im tekst w obu listach wyboru.

Tabela 7.2. Możliwe ustawienia tła


Wartość OPTION Tekst opcji Argument przekazywany
swapImage()
images/goldthumb.gif Gold Bars images/goldthumb.gif
images/billsthumb.gif Dollar Bills images/billsthumb.gif
images/fistthumb.gif Fist of Cash images/fistthumb.gif
images/currency1thumb.gif Currency 1 images/currency1thumb.gif
157 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

images/currency2thumb.gif Currency 2 images/currency2thumb.gif

Tabela 7.3. Opcje rodzaju czcionki i jej wielkości


Wartość opcji Wartość opcji Tekst opisujący typ Tekst opisujący
opisującej typ opisującej wielkość czcionki rozmiar czcionki
czcionki czcionki
Timesroman 10 Times Roman Small
Arial 12 Arial Medium
Courier 14 Courier Large
Tahoma 16 Tahoma X-Large

Zobaczmy teraz, co się dzieje, kiedy łączymy ze sobą obie opcje – oto możliwe kombinacje:
timesroman10 arial10 courier10 tahoma10
timesroman12 arial12 courier12 tahoma12
timesroman14 arial14 courier14 tahoma14
timesroman16 arial16 courier16 tahoma16

Czy nie przypomina to wstępnie ładowanych obrazków tablicy imageNames z wiersza 61? Tak,
to rzeczywiście to samo. Okazuje się, że jednak jest to ciekawa aplikacja. Teraz trzeba
jeszcze złożyć w całość adresy URL. Musimy wywołać swapImage(), ale najpierw powinniśmy
przygotować adresy URL – z pomocą przychodzi nam funkcja makePath() (populateForm()
wywołuje ją w wierszu 159). Oto wywoływana funkcja z wierszy 75–80:
function makePath(formObj) {
var fontName = imagePath +
formObj.face.options[formObj.face.selectedIndex].value +
formObj.size.options[formObj.size.selectedIndex].value + '.gif'
swapImage("fontImage", fontName);
}

Jako parametr funkcja makePath() pobiera kopię obiektu formularza. Odpowiednie wartości pobieramy z formObj, a
następnie dodajemy końcówkę .gif. Teraz zmienna lokalna fontName zawiera napis określający prawidłowy obrazek.
Sprawę kończy wywołanie swapImage() w wierszu 79. Oczywiście wszystko to odbywa się przy założeniu, że
wcześniej użytkownik dokonał wyboru swoich ustawień. Jeśli getPrefs() jednak zwróciła false, populateForm()
wywołuje resetImage() w wierszu 163, aby ustawić obrazki związane z opcją 0 dla tła i czcionki. Więcej
szczegółów na ten temat pojawiły się dalej, kiedy będziemy omawiać czyszczenie formularza.
A teraz chwila przerwy na podsumowanie:
• Elementy formularza zostały zapisane na stronie, przy czym niektóre były zapisywane przez wywołania
genSelect() i genBoxes().

• Informacje z ciasteczek zostały pobrane przez funkcję getPrefs() i użyte do ustawiania poszczególnych
elementów formularza.
• Obrazki tła i czcionek były synchronizowane – zgodnie z wyborami użytkownika –przez funkcje
swapImage() i makePath().

Wprowadzanie zmian
Użytkownik widzi, że strona z preferencjami „zapamiętała” jego ostatnie ustawienia. Teraz zajmijmy się tym, co się
dzieje, kiedy użytkownik zdecyduje się na zmiany.
Z punktu widzenia użytkownika wprowadzanie zmian jest proste, gdyż wystarczy wpisać do pól tekstowych nowy
tekst, wybrać inne opcje na listach wyboru czy zaznaczyć lub odznaczyć pola opcji. Można następnie wybrać przycisk
Zapisz i gotowe. Kiedy jednak skończy się zadanie użytkownika, to nasze dopiero się zaczyna. Przyjrzyjmy się kodowi
obsługi przycisku Zapisz z wiersza 282:
<INPUT TYPE=BUTTON VALUE="Zapisz" onClick="setPrefs(this.form);">

Wygląda całkiem typowo. Wywoływana jest funkcja setPrefs(), która dostaje jako parametr kopię formularza. Tak
naprawdę ciekawe rzeczy zaczynają się od wierszy 125–151:
function setPrefs(formObj) {
158

var prefStr = '';


var htmlStr = '';
for (var i = 0; i < formObj.length; i++) {
if (formObj[i].type == "select-one") {
prefStr += formObj[i].name + '::select::' +
formObj[i].selectedIndex + '-->';
htmlStr += formObj[i].name + '=' +
formObj[i].options[formObj[i].selectedIndex].value + '-->';
}
else if (formObj[i].type == "text") {
if (formObj[i].value == '') { formObj[i].value = "Not Provided"; }
prefStr += formObj[i].name + '::text::' +
safeChars(formObj[i].value) + '-->';
htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
}
else if (formObj[i].type == "checkbox" && formObj[i].checked) {
prefStr += formObj[i].name + '::checkbox::' + '-->';
htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
}
}
SetCookie('userPrefs', prefStr, expiry);
SetCookie('htmlPrefs', htmlStr, expiry);
if (confirm('Zmieniono ustawienia. Przełączyć się na dostosowaną stronę?')) {
self.location.href = "dive.html";
}
}

Funkcja setPrefs() generuje dwa napisy: jeden jest przypisywany zmiennej lokalnej prefStr, a drugi zmiennej
lokalnej htmlStr. Potrzebujemy dwóch ciasteczek: jednego do wypełnienia formularza na omawianej stronie, drugiego
zaś do wygenerowania odpowiedniej postaci strony dive. html. Przechowywane informacje wydają się w obu
wypadkach niemalże identyczne, tyle tylko, że w każdym wypadku dane są inaczej zapisane. Zaraz to zobaczymy. Oto
podstawowy zarys działania funkcji setPrefs():
1.iteracja formObj z tworzeniem dwóch tekstów ciasteczek w oparciu o wartości elementów formularza,
2.zapisanie obu ciasteczek w pliku,
3.umożliwienie użytkownikowi przejście do strony dive.html, aby mógł obejrzeć efekt wprowadzonych zmian.

Krok 1. Iteracja formObj


To nie powinien być problem. Od samego początku tej książki wiele razy realizowaliśmy taką iterację, a tutaj dzieje się
to samo, tyle tylko, że setPrefs() musi wiedzieć, czego szukać.
Spójrzmy jeszcze raz na formularz ustawień. Można zauważyć, że każdy element (poza przyciskami na dole) jest polem
tekstowym, listą wyboru lub polem opcji. Wobec tego setPrefs() musi wiedzieć tylko, co ma zrobić, kiedy wartość
formObj[i] opisuje poszczególne typy elementów. Oto wiersze 129–144, w których znajdziemy wytyczne:
if (formObj[i].type == "select-one") {
prefStr += formObj[i].name + '::select::' +
formObj[i].selectedIndex + '-->';
htmlStr += formObj[i].name + '=' +
formObj[i].options[formObj[i].selectedIndex].value + '-->';
}
else if (formObj[i].type == "text") {
if (formObj[i].value == '') { formObj[i].value = "Not Provided"; }
prefStr += formObj[i].name + '::text::' +
safeChars(formObj[i].value) + '-->';
htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
}
else if (formObj[i].type == "checkbox" && formObj[i].checked) {
prefStr += formObj[i].name + '::checkbox::' + '-->';
htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
}

Jedną z bardzo interesujących, ale niedocenianych właściwości elementu formularza jest właściwość type, która
zawiera określenie typu elementu. setPrefs() musi być świadoma istnienia tylko trzech typów: select-one, text
i checkbox. Złożona instrukcja if – pokazana w powyższym kodzie – powoduje nieco inne działanie
dla poszczególnych typów elementów. Funkcja setPrefs() w ten czy inny sposób realizuje następujące funkcje:
• Łączy nazwę elementu formularza, napis określający jego typ i ewentualnie wartość lub wybraną pozycję –
wszystkie rozdzielone separatorami.
• Łączy napis z istniejącymi już wartościami prefStr i htmlStr.
159 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

Jeśli element jest listą wyboru, do napisu dołącza się identyfikator wybranej pozycji. Gdy element jest polem opcji,
do napisu dodawana jest nazwa tego pola. Jeśli natomiast element jest polem tekstowym, dodawane są jego nazwa
i wartość. Zwróćmy uwagę, że funkcja safeChars() przekształca wartości pól tekstowych. Wynika to stąd, że
wartości związane z listami wyboru i polami opcji są od razu narzucone i wiadomo, że nie będą zawierały niejasnych
znaków. W polu tekstowym użytkownik może wpisać co zechce, a w szczególności np. jeden z ciągów używanych
przez nas jako ograniczniki (::, --> i =). To mogłoby kompletnie zdezorganizować działanie naszej aplikacji podczas
analizy zawartości ciasteczek. Oto owa funkcja czyszcząca z wierszy 153–155:
function safeChars(str) {
return str.replace(/::|=|-->/g, ':;');
}

Funkcja safeChars() po prostu usuwa wszystkie zarezerwowane napisy ze wszystkich danych wprowadzonych
przez użytkownika i zwraca wynik takiej operacji. Wszystkie napisy określające nazwę/ typ, wartość lub wybraną pozycję
są ograniczone przez -->. Każdy fragment tekstu w zmiennej prefStr ograniczony jest przez ::, natomiast
w htmlStr używa się znaku =. Nie jest konieczne stosowanie akurat takich ograniczników, ale są one dość proste
i przez to wygodne. Oto przykład, jak mogą wyglądać te napisy podczas tworzenia ich na podstawie tego samego
formularza:
prefStr może wyglądać następująco:
investor::text::Not Provided-->age::text::Not Provided-->strategy::select::3--
>occupation::text::Not Provided-->newsNames0::checkbox::-->newsNames1::checkbox::--
>newsNames2::checkbox::-->newsNames4::checkbox::-->indexNames0::checkbox::--
>indexNames2::checkbox::-->background::select::2-->face::select::3-->size::select::2-->

Natomiast htmlStr może wyglądać tak:


investor=Not Provided-->age=Not Provided-->strategy=Moderate-->occupation=Not Provided--
>newsNames0=Barron's Online,http://www.barrons.com/-->newsNames1=CNN
Interactive,http://www.cnn.com/-->newsNames2=Fox News,http://www.foxnews.com/--
>newsNames4=The Wall Street Journal,http://www.wsj.com/-->indexNames0=Dow Jones
Indexes,http://www.dowjones.com/-->indexNames2=The New York Stock
Exchange,http://www.nyse.com/-->background=images/fistthumb.gif-->face=tahoma-->size=14-->

Pamiętajmy, że --> rozdziela hasła formularza zarówno w prefStr, jak i w htmlStr, natomiast :: i = rozdzielają
poszczególne fragment jednego elementu, każdy w innej zmiennej. Tę informację uzyskałem usuwając komentarze,
które dotąd ukrywały wiersze 286–289. Dodatkowe przyciski umożliwiają wyświetlenie wartości zmiennych
i usunięcie zawartości ciasteczek. Nasi użytkownicy takich przycisków nie będą potrzebowali, ale nam mogą one
pomóc w uruchamianiu aplikacji.
Jeśli powyższy kod wygląda jak nieciekawa zmienna, nie należy się przejmować. Dekodowaniem htmlStr zajmiemy
się, kiedy będziemy przygotowywać kod dive.html. Na szczęście już mamy za sobą rozkodowanie prefStr w funkcji
getPrefs(). Dobrze byłoby zajrzeć do tej sekcji. Porównanie tego, jak setPrefs() zestawia informacje
w ciasteczko i jak getPrefs()odczytuje, z pewnością może ułatwić zrozumienie działania całości.

Krok 2. Zapisanie danych do pliku cookies


Kiedy już prefStr i htmlStr zawierają komplet informacji o ustawieniach użytkownika, wywołanie SetCookie()
w wierszach 146–147 zapisuje informacje w pliku ciasteczek.
Netscape Navigator informacje te trzyma w pliku cookies.txt. Oto fragment mojego pliku:
.hotwired.com TRUE / FALSE 2145917529 p_uniqid
2sfurM4NNMfDKAqQ8A
.hotbot.com TRUE / FALSE 946739221 p_uniqid
3MarneJsXGwNqxWbFA
www.allaire.com FALSE / FALSE 2137622729 CFTOKEN 97611446

Z kolei Internet Explorer 4.x oraz 5.x przechowuje poszczególne ciasteczka w osobnych plikach, które są nazywane
zgodnie z nazwami domen, skąd dane ciasteczko pochodzi, i nazwą użytkownika, który był zarejestrowany
w momencie ustawiania ciasteczka. Oto część moich plików z komputera wyposażonego w Windows NT (loguję się
jako administrator):
Cookie:administrator@altavista.com
Cookie:administrator@amazon.com
Cookie:administrator@builder.com
Cookie:administrator@cnn.com
Cookie:administrator@dejanews.com
Cookie:administrator@hotbot.com
Cookie:administrator@infoseek.com
160

Krok 3. Pokazanie użytkownikowi nowych ustawień


Ostatnie zadanie SetPrefs() to przekierowanie użytkownika na stronę dive.html, aby mógł obejrzeć efekt swojego
działania. Oto stosowny kod z wierszy 148–150:
if (confirm('Zmieniono ustawienia. Przełączyć się na dostosowaną stronę?')) {
self.location.href = "dive.html";
}

W ten sposób zakończyliśmy omawianie działania prefs.html. Jest jednak jeszcze jedna jego funkcja, którą
w większości wypadków można pominąć, a mianowicie czyszczenie zawartości formularza.

Zerowanie formularza
Czy nie wystarczyłby zwykły przycisk <INPUT TYPE=RESET>? Owszem, taki przycisk czyści zawartość tekstowych
okienek edycyjnych, usuwa zaznaczenia z pól opcji i ustawia wszystkie listy wyboru na OPTION 0. Świetnie, ale
nie zostaną wtedy usunięte nasze obrazki tła i czcionek. W obu wypadkach muszą one zostać ustawione
na odpowiadające OPTION 0, dlatego też wiersz 283 wygląda następująco:
<INPUT TYPE=RESET VALUE="Wyczyść" onClick="resetImage(this.form);">

Nie tylko zerowany jest sam formularz, ale wywoływana jest też funkcja resetImage(), znajdująca się w wierszach
166–170:
function resetImage(formObj) {
swapImage('bkgImage', formObj.background.options[0].value);
swapImage('fontImage', imagePath + formObj.face.options[0].value +
formObj.size.options[0].value + '.gif');
}

Jest to kolejna funkcja wyręczająca się innymi– wywołuje ona dwukrotnie swapImage(). Za pierwszym razem
pokazywany jest obrazek tła odpowiadający ustawieniu OPTION 0 (czyli formObj.
background.options[0].value). Za drugim razem dzieje się to samo z obrazkiem czcionki. Podobnie, jak
w przypadku użycia makePath(), resetImages() tworzy ze zmiennej imagePath i ustawień list czcionki (obie równe
są zero, nie używamy tym razem selectedIndex) oraz stałego tekstu .gif nazwę pliku. Oba wywołania ustawiają
odpowiednie obrazki.
W ten sposób doszliśmy do końca prefs.html i możemy przejść do opisania dive.html.

dive.html
Zmieniły się ustawienia użytkownika i czas teraz zobaczyć, jaki jest wizualny rezultat. Zadanie nie jest trudne, ale
niektóre jego szczegóły mogą przyprawić o ból głowy. Wydaje się oczywiste, że dane będą pochodziły z ciasteczek.
Wydobyte stamtąd informacje użyte zostaną na trzy sposoby:
• Do określenia adresu URL obrazka tła.
• Do określenia adresów i treści łącz.
• Jako część arkusza stylów, opisująca rodzaj i rozmiar użytej czcionki.
W trakcie analizy kodu spotkamy się ze wszystkimi zastosowaniami, a na razie zapoznajmy się z plikiem dive.html
pokazanym w przykładzie 7.2.

Przykład 7.2. dive.html


1 <HTML>
2 <HEAD>
3 <TITLE>
4 Twoja strona łącz Take-A-Dive
5 </TITLE>
6 <SCRIPT LANGAUGE="JavaScript1.2" SRC="cookies.js"></SCRIPT>
7 <SCRIPT LANGUAGE="JavaScript1.2">
8 <!--
9
10 var newsNames = new Array();
11 var indexNames = new Array();
12
13 function getAttributes() {
14 var htmlStr = GetCookie('htmlPrefs');
15 if (htmlStr == null) {
16 alert('Witaj. Najpierw musisz określiĆ swoje preferencje.' +
17 'Kiedy wciśniesz OK, załadowana zostanie odowiednia strona.');
18 self.location.href = 'prefs.html';
19 }
20 var htmlArray = htmlStr.split('-->');
161 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

21 for (var i = 0; i < htmlArray.length; i++) {


22 var tagInfo = htmlArray[i].split('=');
23 if (tagInfo[0] != "") {
24 if (tagInfo[0].indexOf('newsNames') == 0) {
25 newsNames[newsNames.length] = tagInfo[1];
26 }
27 else if (tagInfo[0].indexOf('indexNames') == 0) {
28 indexNames[indexNames.length] = tagInfo[1];
29 }
30 else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
31 }
32 }
33 }
34
35 getAttributes();
36
37 function genLinks(linkArr) {
38 var linkStr = '';
39 for (var i = 0; i < linkArr.length; i++) {
40 var linkParts = linkArr[i].split(',')
41 linkStr += '&nbsp; &nbsp; - <A HREF="' + linkParts[1] + '"> ' +
42 linkParts[0] + '</A><BR>'
43 }
44 return linkStr;
45 }
46
47 //-->
48 </SCRIPT>
49 <SCRIPT LANGUAGE="JavaScript1.2">
50 document.write('<STYLE type="text/css"> TD
51 { font-family: ' + face + '; font-size: ' + size + 'pt; } </STYLE>');
52 </SCRIPT>
53 </HEAD>
54 <SCRIPT LANGUAGE="JavaScript">
55 document.write('<BODY BACKGROUND="' +
56 background.replace(/thumb/, "") + '">');
57 </SCRIPT>
58 <TABLE BORDER=0>
59 <TR>
60 <TD VALIGN=TOP COLSPAN=4>

Przykład 7.2. dive.html (ciąg dalszy)


61 <H2>Take-A-Dive Brokerage $ervices</H2>
62 </TD>
63 </TR>
64 <TR>
65 <TD VALIGN=TOP COLSPAN=4>
66 Take-A-Dive Brokerage Services ma pomóc TOBIE zainwestować
67 pieniądze. <BR> Nasze motto brzmi "<I>Wchodzimy w to.</I>"
68 Oto Twój profil
69 i wybrane przez Ciebie łącza.
70 <BR><BR>
71 </TD>
72 </TR>
73 <TR>
74 <TD VALIGN=TOP>
75 Nazwisko:</TD>
76 <TD VALIGN=TOP>
77 <SCRIPT LANGUAGE="JavaScript1.2">document.write(investor);</SCRIPT>
78 </TD>
79 <TD VALIGN=TOP>
80 Wiek:</TD>
81 <TD VALIGN=TOP>
82 <SCRIPT LANGUAGE="JavaScript1.2">document.write(age);</SCRIPT>
83 </TD>
84 </TR>
85 <TR>
86 <TD VALIGN=TOP>
87 Strategia:
88 </TD>
89 <TD VALIGN=TOP>
90 <SCRIPT LANGUAGE="JavaScript1.2">
91 document.write(strategy);
92 </SCRIPT>
93 </TD>
94 <TD VALIGN=TOP>
95 Zawód:
96 </TD>
97 <TD VALIGN=TOP>
98 <SCRIPT LANGUAGE="JavaScript1.2">
162

99 document.write(occupation);
100 </SCRIPT>
101 </TD>
102 </TR>
103 <TR>
104 <TD VALIGN=TOP COLSPAN=2>
105 <BR><BR>
106 łcza do stron z nowinkami<BR>
107 <SCRIPT LANGUAGE="JavaScript1.2">
108 document.writeln(genLinks(newsNames));
109 </SCRIPT>
110 <TD VALIGN=TOP COLSPAN=2>
111 <BR><BR>
112 łącza do stron z notowaniami giełdowymi <BR>
113 <SCRIPT LANGUAGE="JavaScript1.2">
114 document.write(genLinks(indexNames));
115 </SCRIPT>
116 </TD>
117 </TR>
118 <TR>
119 <TD VALIGN=TOP COLSPAN=2>
120 <BR><BR>
121 [ <A HREF="prefs.html">Ustawienia</A> ]
122 </TD>
123 </TR>
124 </TABLE>
125 </BODY>
126 </HTML>

Analiza ciasteczek
Przyjrzyjmy się znacznikom SCRIPT. Już na podstawie ich rozmieszczenia nietrudno zgadnąć, że ta strona jest
naprawdę tylko szablonem, który będzie w biegu wypełniany właściwą treścią. Pierwszym krokiem ku temu musi być
przeanalizowanie zapisów z ciasteczek – wywoływana jest funkcja getAttributes(). Aby móc dynamicznie określić
wygląd strony, będziemy tych informacji potrzebować szybko, jeszcze przed załadowaniem strony. Dlatego właśnie
getAttributes() wywoływana jest w wierszu 35, czyli zaledwie dwa wiersze po jej zdefiniowaniu. Oto wiersze 13–
33:
function getAttributes() {
var htmlStr = GetCookie('htmlPrefs');
if (htmlStr == null) {
alert('Witaj. Najpierw musisz określić swoje preferencje.' +
'Kiedy wciśniesz OK, załadowana zostanie odowiedniednia strona.');
self.location.href = 'prefs.html';
}
var htmlArray = htmlStr.split('-->');
for (var i = 0; i < htmlArray.length; i++) {
var tagInfo = htmlArray[i].split('=');
if (tagInfo[0] != "") {
if (tagInfo[0].indexOf('newsNames') == 0) {
newsNames[newsNames.length] = tagInfo[1];
}
else if (tagInfo[0].indexOf('indexNames') == 0) {
indexNames[indexNames.length] = tagInfo[1];
}
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
}
}
}

Zmienna lokalna otrzymuje wartość zwracaną przez funkcję GetCookie(). W pliku prefs.html potrzebne dane
o formularzu były zapisane w ciasteczku prefStr, ale w dive.html będziemy potrzebować ciasteczka htmlStr. Jeśli
okaże się, że htmlStr jest puste, należy przyjąć, że użytkownik jeszcze nie wprowadził swoich ustawień, więc należy
go o tym poinformować i przekierować na stronę prefs.html, aby uzupełnił dane.
Jeśli wartość htmlStr nie jest pusta, rozbijemy ją funkcją split() między każdymi ogranicznikami -->, dzięki
czemu otrzymamy lokalną tablicę htmlArray. Pętla for kolejno przypisze wartości wszystkim elementom. Warto
zaznaczyć, że jest to niemalże identyczna konstrukcja logiczna, jak getPrefs() w pliku prefs.html. Zajrzyjmy
do wierszy 110–120 w prefs.html i porównaj je z wierszami 20–32 w pliku dive.html:
var htmlArray = htmlStr.split('-->');
for (var i = 0; i < htmlArray.length; i++) {
var tagInfo = htmlArray[i].split('=');
if (tagInfo[0] != "") {
if (tagInfo[0].indexOf('newsNames') == 0) {
newsNames[newsNames.length] = tagInfo[1];
}
163 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

else if (tagInfo[0].indexOf('indexNames') == 0) {
indexNames[indexNames.length] = tagInfo[1];
}
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
}
}

Twarzą w twarz z nieznanym


Ciekawe getPrefs() w prefs.html wie, że będzie pracować z formularzem opisanym w formObj, przypisuje
wartości, pola opcji i wybory stosownie do tego getAttributes() sprawa nie jest tak prosta – trzeba mieć
przynajmniej jakieś pojęcie o tym, czego oczekiwać w ciasteczku. Wiemy na przykład, że będą tam pewne informacje
o łączach do nowinek inwestycyjnych, jak również łącza do notowań giełdowych. Kto wie, ileż ich tam może być: 0, 10,
a może 50? Jako że jest to niewiadoma, obie grupy danych wstawimy do osobnych tablic zdefiniowanych w wierszach 10
i 11:
var newsNames = new Array();
var indexNames = new Array();

Zmienna newsNames będzie miała dane łącz do stron z nowinkami, a indexNames takie same informacje dotyczące
notowań giełdowych. Przyjrzyjmy się przykładowej wartości ciasteczka z poprzedniej sekcji. Zwróćmy uwagę
na pogrubiony tekst:
investor=Not Provided-->age=Not Provided-->strategy=Moderate-->occupation=Not Provided--
>newsNames0=Barron's Online,http://www.barrons.com/-->newsNames1=CNN
Interactive,http://www.cnn.com/-->newsNames2=Fox News,http://www.foxnews.com/--
>newsNames4=The Wall Street Journal,http://www.wsj.com/-->indexNames0=Dow Jones
Indexes,http://www.dowjones.com/-->indexNames2=The New York Stock
Exchange,http://www.nyse.com/-->background=images/fistthumb.gif-->face=tahoma-->size=14-->

Pogrubione nazwy oznaczają informacje przeznaczone do umieszczenia we wspomnianych wyżej tablicach. Wiemy
teraz, że jakieś zmienne się pojawią, ale o jakich nazwach? Ile ich będzie? Kod w dive.html nie wyjaśni żadnych
tajemnic, ale to nie szkodzi. Jeśli tylko wiemy jakie są nazwy zmiennych, nie trzeba wszystkiego kodować w dive.html.
Aby to zrozumieć, przyjrzyj się jeszcze raz przykładowej wartości uzyskanej z GetCookies('htmlPrefs'),
zwracając znowu uwagę na pogrubiony tekst:
investor=Not Provided-->age=Not Provided-->strategy=Moderate-->occupation=Not Provided--
>newsNames0=Barron's Online,http://www.barrons.com/-->newsNames1=CNN
Interactive,http://www.cnn.com/-->newsNames2=Fox News,http://www.foxnews.com/--
>newsNames4=The Wall Street Journal,http://www.wsj.com/-->indexNames0=Dow Jones
Indexes,http://www.dowjones.com/-->indexNames2=The New York Stock
Exchange,http://www.nyse.com/-->background=images/fistthumb.gif-->face=tahoma-->size=14-->

Wytłuszczony kod tego przykładu odpowiada nazwom zmiennych, które zaraz zdefiniujemy. Pętla for
w getAttributes() jest w stanie obsłużyć zarówno przypisania elementów tablic, jak i deklaracje „nieznanych”
zmiennych. Oto wiersze od 22 do 31:
var tagInfo = htmlArray[i].split('=');
if (tagInfo[0] != "") {
if (tagInfo[0].indexOf('newsNames') == 0) {
newsNames[newsNames.length] = tagInfo[1];
}
else if (tagInfo[0].indexOf('indexNames') == 0) {
indexNames[indexNames.length] = tagInfo[1];
}
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
}

Każdy element htmlArray zawiera znak równości (=), rozdzielający identyfikator od naprawdę interesującej nas
wartości. W każdej iteracji pętli for htmlArray[i] rozbijana jest funkcją split() na znaku =, a uzyskana tak
subtablica umieszczana jest w zmiennej lokalnej tagInfo. Jeśli tagInfo[0] nie jest ciągiem pustym, mamy
poprawną parę identyfikator + nazwa. Trzeba sprawdzić, czy uzyskany wynik nie jest napisem równym, a to z uwagi
na sposób tworzenia tablicy w funkcji split() w JScripcie.
Każda poprawna para należy do jednej z dwóch kategorii: jest albo elementem tablicy, albo zwykłą zmienną. Jeśli dana
para ma należeć do tablicy, znów zostanie przyporządkowana do jednego z dwóch typów: albo opisuje łącze strony
z nowinkami, albo z notowaniami giełdowymi. Poniższa instrukcja warunkowa określa, co należy zrobić w zależności
od zachodzących okoliczności:
if (tagInfo[0].indexOf('newsNames') == 0) {
newsNames[newsNames.length] = tagInfo[1];
}
else if (tagInfo[0].indexOf('indexNames') == 0) {
164

indexNames[indexNames.length] = tagInfo[1];
}
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }

Z uwagi na konwencję nazewniczą przyjętą w prefs.html, jeśli tagInfo[0] zawiera napis newNames, musi być związana
z łączami do nowinek, a wtedy wartość tagInfo[1] jest przypisywana następnemu wolnemu elementowi
w newsNames. Jeśli tagInfo[0] zawiera napis indexNames, musi być związana z łączami notowań giełdowych
i wówczas wartość tagInfo[1] przypisywana jest następnemu wolnemu elementowi w indexNames. Gdy
tagInfo[0] nie zawiera żadnego z powyższych napisów, musi to być nazwa zmiennej, która ma zostać
zadeklarowana, a jej wartością ma być tagInfo[1]. Kod w wierszu 30 będzie wiedział już, co należy zrobić:
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"');

Kiedy skończy się działanie pętli for, wygenerowany zostanie następujący kod:
newsNames[0] = 'Barron's Online,http://www.barrons.com/';
newsNames[1] = 'CNN Interactive,http://www.cnn.com/';
newsNames[2] = 'Fox News,http://www.foxnews.com/';
newsNames[3] = 'The Wall Street Journal,http://www.wsj.com/';

− są to łącza do nowości dla inwestorów.


indexNames[0] = 'Dow Jones Indexes,http://www.dowjones.com/';
indexNames[1] = 'The New York Stock Exchange,http://www.nyse.com/';

− to są z kolei łącza do notowań giełdowych.


var investor = 'Not Provided';
var age = 'Not Provided';
var strategy = 'Moderate';
var occupation = 'Not Provided';
var background = 'images/fistthumb.gif';
var face = 'tahoma';
var size = '14';

− a to są zmienne opisujące wygląd strony.

Techniki języka JavaScript:


konwencje nazewnicze jeszcze raz
Poruszyliśmy już temat rozsądnych konwencji nazewniczych. Warto teraz może o tym
pomyśleć, jak powstają zmienne opisujące wygląd strony oraz elementy łącz do nowości
i do notowań. Zaczęło się jeszcze, zanim jakikolwiek kod w dive.html został zinterpreto-
wany, zanim stało się to w prefs.html, po prostu nawet zanim użytkownik po raz pierwszy
cokolwiek poprawił w formularzu w prefs.html. Zaczęło się od nazwania pól formularza.
Każda ze zmiennych zawierających łącza ma identyfikator (na przykład newsNames0 lub
indexNames3) zawierający nazwę listy wyboru w prefs.html. Każda zmienna, opisująca
wygląd strony, ma nazwę odpowiadającą jednemu z elementów formularza, na przykład
background lub size. Nazwa została wstawiona do treści ciasteczka. Starannie przemy-
ślane konwencje nazewnicze nie tylko ułatwiają pracę, ale niektóre rzeczy w ogóle umoż-
liwiają. Zawsze należy dobrze przemyśleć ich użycie w kodzie.
Pamiętajmy, że podane nazwy nigdzie w kodzie bezpośrednio nie występują. Aby sięgnąć
do ich wartości, możemy przeglądać wartości newsNames i indexNames, jednak aby
dotrzeć do zmiennych, musimy znać z góry ich nazwy.

Mamy już wszystkie dane potrzebne do stworzenia strony zgodnie z wymaganiami użytkownika. Kiedy teraz zapiszemy
informacje na stronie, nasza rola będzie skończona. Użyjemy metody document. write(), aby umieścić wszystko
na stronie. Metodę tę wywołamy ośmiokrotnie – wszystkie wywołania zestawiono i omówiono w tabeli 7.4.

Tabela 7.4. Tworzenie kodu HTML przy pomocy wywołań document.write()


Wiersze Kod Opis
50–51 document.write('<STYLE type="text/css"> tworzenie arkusza stylów
TD { font-family: ' + face +
'; font-size: ' + size + 'pt; }
165 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

</STYLE>');
55–56 document.write('<BODY BACKGROUND="' określenie adresu URL obrazka
+ tła
background.replace(/thumb/, "") +
'">');
77 document.write(investor); podanie nazwiska inwestora
82 document.write(age); podanie wieku inwestora
91 document.write(strategy); określenie strategii
inwestowania
108 document.write(genLinks(news)); dopisanie łącz do stron
z nowościami
114 document.write(genLinks(indexes)); dopisanie łącz do stron
z notowaniami giełdowymi

Choć wywołania od trzeciego do szóstego w zasadzie same się objaśniają, to pierwsze dwa i ostatnie dwa są nieco
bardziej złożone. Zacznijmy od wierszy 50–51:
document.write('<STYLE type="text/css"> TD { font-family: ' +
face + '; font-size: ' + size + 'pt; } </STYLE>');

To wywołanie dopisuje do strony arkusz stylów, ale wstawimy tu zmienne face i size, określające rodzaj czcionki
i jej wielkość.

Techniki języka JavaScript:


dynamiczny DHTML
Dynamiczny DHTML to moja nazwa JavaScriptu generującego DHTML w biegu. Zasta-
nówmy się: możemy użyć document.write() do generowania HTML, a nawet
dalszego kodu JavaScriptu. Jeśli połączymy tę funkcjonalność z dodatkowymi
możliwościami arkuszy stylów, mamy naprawdę ogromne możliwości formatowania przy
zastosowaniu bardzo niewielkiej ilości kodu. Oto przykład tego w omawianej aplikacji:
document.write('<STYLE type="text/css"> TD
{ font-family: ' + face + '; font-size: ' + size + 'pt; } </STYLE>');

Właściwie rzecz polega na wstawienie zmiennych face i size. Teraz typ czcionki i jej
rozmiar określone są przez dwie zmienne, których wartość można zawsze zmienić. Nieźle
jak na jednowierszowy arkusz stylów. Pomyślmy, jakie możliwości daje nam stworzenie
dużego arkusza stylów z opisem nagłówków, elementów formularzy i tak dalej. Arkusze
stylów umożliwiają precyzyjne określenie wyglądu dokumentów, natomiast generowanie
tych arkuszy przez JavaScript ułatwia dynamiczną realizację tej kontroli.

Skoro już umiemy tworzyć dynamicznie arkusze stylów, przejdźmy do ustawienia właściwego obrazka tła. Oto wiersze
55 i 56:
document.write('<BODY BACKGROUND="' +
background.replace(/thumb/, "") + '">');

Pamiętajmy, że zmienna background zawiera images/fistthumb.gif. Świetnie, tyle tylko, że mamy miniaturę
obrazka z tła, a nam potrzebny jest oryginał. Nie ma sprawy, każda miniatura nazwana została tak, jak obrazek
pełnoformatowy, z tym, że dodano jej w nazwie słowo thumb. Zatem wystarczy, że z zawartości zmiennej background
usuniemy „thumb”, aby otrzymać images/fist.gif – pomoże w tym metoda replace().
W ostatnich dwóch wywołaniach metody document.write() używamy jedynie innej funkcji zdefiniowanej w naszej
stronie, genLinks(). Funkcja ta jest podobna do – znanych już nam z prefs .html – genBoxes() i genSelect(),
gdyż także tworzy się pętla po wszystkich elementach tablicy, aby wygenerować odpowiedni kod HTML. Jedna różnica
polega na tym, że ta funkcja zwraca zestaw łącz, a nie pól opcji i znaczników OPTION. Omawiane kwestie mieszczą się
między wierszami 37 a 45:
function genLinks(linkArr) {
var linkStr = '';
166

for (var i = 0; i < linkArr.length; i++) {


var linkParts = linkArr[i].split(',')
linkStr += '&nbsp; &nbsp; - <A HREF="' + linkParts[1] + '"> ' +
linkParts[0] + '</A><BR>'
}
return linkStr;
}

Funkcja genLinks() ma – jako jedyny argument – otrzymywać tablicę tekstów. Pierwszy fragment każdego elementu
to napis do wyświetlenia jako tekst łącza, drugi natomiast to adres URL do atrybutu HREF. Są one oddzielone od siebie
przecinkiem, dzięki czemu użycie metody split() i przypisanie wyników do zmiennej lokalnej linkParts pozwala
uzyskać potrzebne fragmenty. Pętla for działa jak zwykle, tworząc tekst z łączami, który będzie zwrócony po
zakończeniu.

Kierunki rozwoju
Nawet najmniejszy stopień kreatywności pozwoli nam znaleźć sobie miejsce do poprawek w tej aplikacji. Oto kilka
możliwości:
• Dodaj pola umożliwiające manipulację kolorem tła komórek tabeli i kolorem czcionki, a także innymi,
wybranymi atrybutami wyglądu.
• Pozwól użytkownikom na wybranie gotowych schematów wyglądu stron.
• Dodaj kilka pól tekstowych, aby użytkownik mógł dodać własne wybrane strony (z nazwami).
• Dodawaj bannery reklamowe – stosownie do preferencji użytkownika.

Więcej ustawień wyglądu


Użytkownicy lubią, kiedy mogą wybierać, i wybierać, i wybierać... Wszystko, co tylko umieścimy na stronie
użytkownika, może być przedmiotem jego poprawek. Dotyczy to treści i grafiki w warstwach, innych ramkach
i osobnych oknach.

Gotowe schematy wyglądu stron


Idea gotowych, tematycznych schematów wyglądu interfejsu pochodzi z Windows 95. Zamiast umożliwiać
użytkownikom wybieranie poszczególnych elementów, jak rodzaj czcionki, jej rozmiar i kolor, możemy dać im gotowe
schematy, które będą mogli wybrać jednym kliknięciem myszki. Załóżmy, że mamy stronę sieciową związaną z muzyką.
Pomyślmy o takiej muzycznej liście wyboru:
<SELECT NAME="tematy" onChange="swapImage('theImage',
this.options[this.selectedIndex].value);">
<OPTION VALUE="brak">Brak
<OPTION VALUE="bigband">Big Band
<OPTION VALUE="rocknroll"> Rock and Roll
<OPTION VALUE="rap">Rap
<OPTION VALUE="country">Country
<OPTION VALUE="reggae">Reggae
<OPTION VALUE="grunge">Grunge
<OPTION VALUE="jazz">Jazz
<OPTION VALUE="club">Muzyka klubowa
</SELECT>

Każda z tych opcji może być związana z jakąś ikoną. Można nawet użyć genSelect() i swapImage() w prefs.html
do stworzenia listy i realizacji przewijania. Pamiętajmy jednak, że wybierając jeden z tych rodzajów muzyki, będziemy
musieli jakoś wyłączyć poszczególne cechy wyglądu, jak obrazek tła i opis czcionki. Zwróćmy uwagę na to, że
pierwsza opcja to brak tematu przewodniego. Warto dodać również znacznik OPTION pozwalający użytkownikom
określić w miarę potrzeb własne układy strony.

Umożliwienie użytkownikom tworzenia własnych łącz


Formularz ustawień strony Take-A-Dive umożliwia użytkownikowi wybrać łącza spośród łącz przez nas wcześniej
zdefiniowanych. Zawsze można też dodać kilka pól tekstowych, gdzie użytkownik będzie mógł podać swoje ulubione
łącza. Poniższa tabela powinna stanowić dobry punkt startowy:
<TABLE>
<TR>
167 Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach

<TD><B>Dodatkowe łącza</B></TD>
<TD>
<INPUT TYPE=BUTTON VALUE=" Dodaj " onClick=addOpt(hits.form);">
<INPUT TYPE=BUTTON VALUE=" Usuń " onClick="deleteOpt(this.form);">
</TD>
</TR>
<TR>
<TD>Nazwa łącza</TD>
<TD><INPUT TYPE=TEXT NAME="linkname" SIZE=20></TD>
</TR>
<TR>
<TD>Adres URL łącza</TD>
<TD><INPUT TYPE=TEXT NAME="linkURL" SIZE=20></TD>
</TR>
</TABLE>

Użytkownicy mogą teraz dodawać i usuwać łącza, wpisując nazwę łącza i jego adres URL, a następnie wybierając
Dodaj lub Usuń. Można również wstawiać te zmienne do tablicy, a łącza dodawać i usuwać funkcjami addOpt()
i delOpt() wskazanymi w powyższym kodzie. Ambitni programiści mogą, mógłbyś też utworzyć listę wyboru
wyświetlającą łącza w miarę ich dodawania i usuwania.

Marketing bezpośredni
Dlaczego nie przeprowadzić własnej kampanii reklamowej zgodnej z profilem zainteresowań użytkownika?
W przypadku tej niby-inwestycyjnej strony można by sprawić, żeby użytkownicy uważający się za ostrożnych
otrzymywali propozycje inwestycji pewnych i niskodochodowych, jak obligacje. Z kolei inwestorzy gotowi zaprzedać
duszę diabłu dostawaliby propozycje bardzo ryzykowne, ale potencjalnie przynoszące duże zyski, oraz propozycje
inwestycji zagranicznych.
Cechy aplikacji:
 Uniwersalny wózek sklepowy działa
po stronie klienta
 Przyjazny dla użytkownika interfejs
ułatwia zakupy
 Nie jest wymagane żadne przetwarzanie
po stronie serwera (póki nie przyjdzie
do zapłaty)

8
 Program rejestruje wszystkie wybory i na
bieżąco sumuje zakupy
 Baza danych umożliwia sprawne
wyszukiwanie towarów
Prezentowane techniki:
 Obsługa wielu okien i dokumentów
 Użycie obiektów do zapisu stanu klienta
 Dodawanie właściwości obiektów
 Wielokrotne użycie bazy danych
JavaScriptu
 Zaokrąglanie liczb i konwersja tekstu

Shopping Bag
– wózek
sklepowy
stworzony
w JavaScripcie

Jeśli w tej książce należałoby wskazać pojedynczą aplikację o najbardziej rozbudowanych funkcjach i najsolidniejszą,
to byłaby to właśnie aplikacja opisana w tym rozdziale. Wystarczy, że dodamy do niej grafikę i szczegółowy opis
produktów, a już będziemy mieć gotowy wózek sklepowy w swoim sieciowym punkcie handlowym. Do wyświetlania
danych o swoich produktach nie trzeba tworzyć żadnych dodatkowych plików, zrobi to ta aplikacja właśnie. Nie trzeba
też na serwerze wyliczać żadnych podatków ani nic sumować, wszystko to wykona nasza aplikacja. Do dodania czy
wyjęcia towaru z koszyka wystarczy jedno lub dwa kliknięcia myszy. W przeciwieństwie do analogicznych aplikacji
działających po stronie serwera nie musimy na nic czekać.

Shopping Bag w dwóch słowach


Coś, co ma być dla klienta naprawdę łatwe w użyciu i intuicyjnie rozumiane, zwykle wymaga dodatkowej pracy
programisty. Tak jest i tym razem. Jednak to, co dostaniemy w efekcie, jest tej pracy warte. Opis działania aplikacji
Shopping Bag i opis kodu oparte są na przykładzie.
Oto czteroetapowy proces działania:
169 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

1.Aplikacja jest ładowana.


2.Kupujący przegląda produkty według kategorii i je wyszukuje, wybiera przy tym kilka z nich.
3. Zadowolony klient przegląda dokonane przez siebie wybory i zmienia ilości czy rodzaje towarów.
4.Ostatecznie usatysfakcjonowany decyduje się potwierdzić zakupy i zapłacić.
Aplikacja zawiera też kilka prostych reguł, które muszą być zachowane przez kupujących. Będą one omówione w tych
sekcjach, których dotyczą. Pomóżmy użytkownikowi – niech będzie to pani Daisy Skąpiradło – rozstać się z jego
ciężko zarobionymi pieniędzmi.

Etap 1. Ładowanie aplikacji


Otwarcie pliku ch08\index.html spowoduje pojawienie ekranu pokazanego na rysunku 8.1. Jest to po prostu rodzaj
ekranu wstępnego. Kiedy Daisy kliknie łącze Zaczynamy, pokaże się ekran taki, jak na rysunku 8.2.

Rysunek 8.1. Ciepłe przywitanie w naszej aplikacji


Jest to ekran początkowy (i jednocześnie ekran pomocy) ładowany wraz ze znajdującą się pod nim ramką nawigacyjną.
Warto się temu przyjrzeć przed podjęciem kolejnych kroków.
Po co dwa okna przeglądarki? Nic nie stoi na przeszkodzie, aby wszystko zrobić w jednym, ale w ten sposób mamy
więcej miejsca na prezentację swoich towarów. Poza tym użytkownicy nie będą odciągani od prezentowanych
przez nas treści przyciskami takimi, jak Bookmarks czy Search. Oznacza to, że więcej uwagi poświęcą samej ofercie.
A skoro już przy tym jesteśmy: warto zauważyć, że można tę stronę wykorzystać do rejestrowania użytkowników, aby
odróżnić stałych bywalców od przypadkowych gości.
170

Rysunek 8.2. Interfejs Shopping Bag

Etap 2. Przeglądanie towarów i wybór


Dobra, Daisy już weszła. Czas się rozejrzeć. Wybiera Pokaż wszystkie kategorie i otrzymuje listę kategorii z łączami.
Wyświetlona zostanie tablica dostępnych kategorii produktów. No tak: budynki, jedzenie, narzędzia – któż czegoś
takiego nie chciałby kupić w Sieci? Kiedy już pani Skąpiradło nieco ochłonie po pierwszym szoku spowodowanym
bogactwem oferty, stwierdza, że kończą się jej już zapasy domowe i w związku z tym postanawia sprawdzić, co ma jej
do zaoferowania sklep Shopping Bag.

Reguła 1. Kiedy załadowana zostanie aplikacja Shopping Bag, użytkownik musi


wybrać jedną z opcji: „Pokaż wszystkie kategorie” lub „Wyszukiwanie produktów” po
czym zdecydować się na łącze kategorii, z której produkty chce oglądać. Potem już
wybieranie wszystkich przycisków z paska nawigacyjnego będzie działało zgodnie
z oczekiwaniami.

Uruchamia łącze Buildings, po czym znów jest zaskoczona, ale tym razem bajecznie niskimi cenami towarów takich,
jak stodoła, zamek czy wieża. Nie mogąc opanować drżenia rąk, dochodzi do igloo, co pokazano na rysunku 8.3.
Wybiera Daj mi to i igloo pojawia się w jej koszyku, jak o tym informuje komunikat.17

17
Kategorie i poszczególne towary nadal mają nazwy angielskie – wynika to z konwencji stosowanych przez autora, a mianowicie z tego, że
identyfikatory towarów (i ich obrazków) oraz identyfikatory kategorii są pokazywane użytkownikowi. Jeszcze raz zatem podkreślam, o czym była
już mowa w jednym z wcześniejszych rozdziałów – czym innym jest identyfikator, a czym innym opis prezentowany użytkownikowi (przyp.
tłum.).
171 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

Rysunek 8.3. Wkładanie igloo do koszyka

Reguła 2. Wybór „Daj mi to” powoduje włożenie do koszyka tylko jednego produktu
danego rodzaju. Użytkownik może zmieniać ilości, wybierając opcję „Przegląd/korekta
koszyka”, tak samo może coś z koszyka usunąć.

Nadal polując na okazję, nasza niezmordowana klientka decyduje się użyć opcji wyszukiwania towarów aby zobaczyć,
czy znajdzie jeszcze dla siebie coś ciekawego. Wybiera zatem Wyszukiwanie produktów i otrzymuje prosty ekran,
pokazany na rysunku 8.4. Wpisuje do okienka edycyjnego 1.15, aby znaleźć towary w takiej właśnie cenie.
Przypadkowo trafia – jest pięć takich produktów, co widać na rysunku 8.5. Zwraca uwagę na frytki, ogląda je dokładnie
i szybko wkłada do koszyka.
Teraz nasza Daisy kieruje się do zestawienia dostępnych kategorii – klika jeszcze raz Pokaż wszystkie kategorie. Tym
razem jej uwagę przyciągają ubrania (clothing). Kiedy klika to łącze,
172

Rysunek 8.4. Tutaj zaczyna się wyszukiwanie towarów

Rysunek 8.5. Jedno wyszukiwanie, wiele okazji


trafia na krawat za niewiarygodną wprost cenę 1 dolara 15 centów. Wcześniej to przegapiła, ale teraz już nie daruje –
wkłada go do koszyka.

Etap 3: Przeglądanie zamówienia i zmiany w nim


Daisy stwierdza, że na dzisiaj ma już dosyć i wybiera opcję Przegląd/korekta koszyka. Generowany jest odpowiedni
ekran, co pokazano na rysunku 8.6. Zwróćmy uwagę, że aplikacja pamiętała o wszystkim, co było wkładane
do koszyka, wraz z podatkiem, kosztem wysyłki i sumą zakupów.
173 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

Rysunek 8.6. Zawartość koszyka Daisy, wraz z cenami


Nadal podekscytowana możliwością posiadania igloo Daisy zwiększa ich ilość na sześć. Mieszka w dość ciepłym klimacie,
musi się więc liczyć z dużym zużyciem – lepiej wziąć na zapas. Frytki też nieźle wyglądają, więc bierze drugą ich paczkę.
Niestety, okazuje się, że jej portfel nie jest tak zasobny, jak jej się to wydawało, więc musi zrezygnować z krawata. Cóż,
przecież to nie ostatnia wizyta.

Reguła 3. Kupujący w celu zapisania zmian w koszyku muszą użyć przycisku „Zmiana
koszyka”. Innymi słowy, zmiany nie są dokonywane tylko przez skorygowanie liczby
w kolumnie „Ilość” ani przez zaznaczenie pola w kolumnie „Usuń”.

Nietrudno zauważyć, jak Daisy zabiera się do zmiany zawartości swojego koszyka. Zmienia ilości odpowiednich
towarów w listach wyboru, a produkty do usunięcia zaznacza w kolumnie Usuń. Następnie nasza bohaterka klika
przycisk Zmiana koszyka, co powoduje ponowne wyświetlenie zawartości, odwzorowujące dokonane zmiany.
Spójrzmy na rysunek 8.7. Jej koszyk zawiera teraz 6 igloo, dwie paczki frytek i jeden krawat. Łączne koszty wraz
z opodatkowaniem i wysyłką wynoszą 6 190,75 dolarów.
174

Rysunek 8.7. Daisy zmieniła zawartość swojego koszyka

Reguła 4. Przesłanie zamówienia do Shopping Bag całkowicie wypróżnia koszyk. Jeśli


chcemy kupić coś jeszcze, zaczynamy zbieranie towarów od nowa.

Etap 4. Płacenie
Usatysfakcjonowana swoimi poczynaniami Daisy wybiera przycisk Do kasy, który powoduje otwarcie formularza
pokazanego na rysunku 8.8. Daisy może teraz wpisać informacje związane z jej zamówieniem, przesłać je
i niecierpliwie czekać na przesyłkę pocztową.
I tak to właśnie działa... Kolejny zadowolony klient. Przejdźmy teraz dalej, aby zobaczyć, jakiż to kod tak się podoba
naszym klientom.

Wymagania programu
W Shopping Bag używany jest JavaScript w wersji 1.2 oraz pewne cechy CSS, więc przeglądarki w wersji 3.x
nie wystarczą. Pamiętajmy jednak, że wielu użytkowników nadal ich używa. Można
175 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

Rysunek 8.8. Formularz zamówienia


bez problemu usunąć wszystkie elementy CSS, dzięki czemu nasza aplikacja zadziała w Netscape Navigatorze i
Internet Explorerze w wersjach 3.x.
Jeśli chodzi o obciążenie systemu, warto przewidzieć obciążenie co najmniej 500 pozycji towarowych. W końcu dodanie
jednego produktu to dodanie tylko jednego wiersza kodu. Ja testowanie zakończyłem po osiągnięciu blisko 700 pozycji
na maszynie z procesorem 120MHz i 128MB pamięci RAM. Jeśli ktoś nie zamierza konkurować z WalMart, Shopping Bag
powinien być dla niego w sam raz.

Struktura programu
Teraz przyjrzyjmy się schematowi działania Shopping Bag. Na rysunku 8.9 pokazano, jak użytkownik zaczyna zakupy,
przegląda i wybiera produkty, następnie wprowadza ostateczne zmiany w koszyku, wypełnia informacje o płatności
i przesyłce oraz wysyła w końcu zamówienie do serwera.
Aplikacja ta składa się z ośmiu plików. Poniższa lista je zestawia i podaje ich znaczenie:
index.html
Strona początkowa, która zawiera obsługę okien.
shopset.html
Zestaw ramek dla całoekranowego okna dodatkowego. Zawiera pliki intro.html i manager.html.
intro.html
Domyślna strona największej z ramek, zawiera też dokument z pomocą, opisujący poszczególne funkcje w pasku
nawigacji.
176

Rysunek 8.9. Shopping Bag w skrócie

manager.html
Jest to centrum dowodzenia naszej aplikacji. Tutaj znajdują się najważniejsze funkcje, które przede wszystkim
będziemy omawiać w tym rozdziale.
inventory.js
Funkcje, konstruktory i tablice pozwalające stworzyć ofertę towarową naszego sklepu. Znaczna część
przetwarzania odbywa się podczas ładowania stron.
search/index.html
Zestaw ramek ładujący aplikację wyszukującą towary. Tak naprawdę jest to zmodyfikowana wersja omawianej w
rozdziale 1. wyszukiwarki.
search/main.html
Strona pomocy przeglądarki, zawiera też przykłady.
search/nav.html
„Mózg” wyszukiwarki.
Z powodu wielkości tej aplikacji oraz że o użytym tu kodzie JavaScript do obsługi warstw można przeczytać w innych
rozdziałach (,, 4., 6., 9., 10. i 11.), Shopping Bag będziemy omawiać inaczej niż dotychczasowe aplikacje.
Tym razem nie zanalizujemy każdego pliku od początku do końca, a zamiast tego omówimy sposób realizacji
przez aplikację pięciu podstawowych funkcji:
1.ładowanie aplikacji: tworzenie bazy towarowej i przygotowanie do wyświetlenia,
2.prezentacja produktów: zmiana kategorii i produktów,
3.dodawanie produktów do koszyka: rejestracja wszystkiego, co znajduje się w koszyku,
177 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

4.wyszukiwanie produktów: wyszukiwanie tekstu w spisie inwentarza,


5.zmiana zawartości koszyka i obsługa kasowa: zmiany i płacenie.
Jeśli porównamy poprzednie opisy plików z opisami w tym rozdziale, możemy mniej więcej rozpoznać, który z nich
za co jest odpowiedzialny. Punkt 1. powyżej związany jest z index.html, shopset.html, inventory.js; punkty 2., 3. i 5.
realizuje manager.html; punkt 4. jest w całości realizowany przez funkcje z podkatalogu search.
Kod nadal będziemy omawiali w zasadzie plik po pliku, ale od czasu do czasu zrobimy dygresję, aby jakieś zagadnienie
omówić dodatkowo. Każda z pięciu części została zapisana z punktu widzenia działań użytkownika, jak wyszukiwanie,
zmiana ilości towaru, uzyskiwanie pomocy i tak dalej. Omówimy też zastosowane techniki programistyczne. Zaczniemy
od ładowania naszej aplikacji.

Etap 1. Ładowanie aplikacji


JavaScript i procedury zakodowane w przeglądarkach wykonają za nas większość pracy, choć użytkownik też ma coś
do powiedzenia. Zastanówmy się, jak ładowana jest pierwsza strona, index. html. Jej kod znajduje się w przykładzie
8.1.

Przykład 8.1. index.html


1 <HTML>
2 <HEAD>
3 <TITLE>Shopping Bag</TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6 #welcome { text-align: center; margin-top: 150}
7 //-->
8 </STYLE>
9 <SCRIPT LANGUAGE="JavaScript">
10 <!--
11 var shopWin = null;
12 var positionStr = '';
13 function whichBrowser() {
14 if(navigator.appVersion < 4) {
15 alert("Aby użyć Shopping Bag, musisz mieć MSIE lub Navigatora" +
16 " w wersji co najmniej 4.x.")
17 return false;
18 }
19 return true;

Przykład 8.1. index.html (dokończenie)


20 }
21
22 function launch() {
23 if(!whichBrowser()) { return; }
24 if(navigator.appName == "Netscape")
25 { positionStr = ",screenX=0,screenY=0"; }
26 else { positionStr = ",fullscreen=yes"; }
27 if(shopWin == null) {
28 shopWin = open("shopset.html", "", "width=" + screen.width +
29 ",height=" + screen.height + positionStr);
30 }
31 }
32 function closeUpShop() {
33 if (shopWin != null) {
34 if (typeof(shopWin) == "object") {
35 shopWin.close();
36 }
37 }
38 }
39 window.onunload = closeUpShop;
40 //-->
41 </SCRIPT>
42 </HEAD>
43 <BODY>
44 <DIV ID="welcome">
45 <H1>Witaj w aplikacji Shopping Bag!!!</H1>
46 <A HREF="javascript: launch();">Zaczynamy</A>
47 </DIV>
48 </BODY>
49 </HTML>
178

Może to wyglądać jak mnóstwo kodu JavaScript na stronie wyświetlającej raptem kilka słów na ekranie. Jednak ten
dodatkowy kod służy ulepszeniu aplikacji. JavaScript tutaj właśnie definiuje i ustawia obiekty najwyższego poziomu,
potrzebne do obsługi aplikacji wielookienkowej, oraz określa rodzaj przeglądarki, co będzie potrzebne w nowo
otwartym oknie.

Elementy najwyższego poziomu


Zmienne i funkcje z wierszy 11 i 32–38 wymuszają następującą zależność: jeśli zamknięte zostanie okno główne,
zamknąć się ma też jego okno potomne. W przeciwnym wypadku nasza aplikacja mogłaby się załamać, jeśli
użytkownik zechciałby po zamknięciu okna głównego na przykład przeładować wspomniane okno potomne.
Wiersz 11:
var shopWin = null;

A oto wiersze 32–38:


function closeUpShop() {
if (shopWin != null) {
if (typeof(shopWin) == "object") {
shopWin.close();
}
}
}

Zmienna shopWin, początkowo ustawiana na wartość null, używana jest później do ustawienia obiektu okna
potomnego (zajrzyjmy do wiersza 27). Funkcja closeUpShop() wywoływana jest przy zamknięciu tego okna, a
funkcja sprawdza, czy użytkownik nadal ma otwarte okno potomne – jeśli tak, to je zamyka. Jeśli shopWin nie jest
równa null i jest typu object, okno potomne musi być otwarte. closeUpShop() zamyka to okno przed skończeniem
działania aplikacji.
W tym momencie tylko jedno interesuje użytkownika: jeżeli kliknie Zaczynamy, otworzy się nowe okno. Następnie otwarty
zostanie w nim zestaw ramek shopset.html, którego kod znajdziemy w przykładzie 8.2.

Przykład 8.2. shopset.html


1 <HTML>
2 <HEAD>
3 <TITLE>Zestaw ramek Shopping Bag</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5 <!--
6 function resetOpener() {
7 opener.shopWin = null;
8 }
9 //-->
10 </SCRIPT>
11 </HEAD>
12 <FRAMESET ROWS="80%,20%" FRAMEBORDER=0 BORDER=0 onLoad="self.focus();"
13 onUnLoad="resetOpener();">
14 <FRAME SRC="intro.html" NORESIZE>
15 <FRAME SRC="manager.html" NORESIZE>
16 </FRAMESET>
17 </HTML>

Jest to typowy zestaw ramek, z których jedna jest związana z plikiem intro.html, druga z manager. html. Nie ma tutaj
zbyt wiele JavaScriptu, ale sprawdźmy przynajmniej, co jest:
function resetOpener() {
opener.shopWin = null;
}

Wiersze 6–8 zawierają funkcję resetOpener(), wywoływaną, kiedy z okna głównego jest usuwany dokument. Gdy
ustawimy opener.shopWin na null, resetOpener() umożliwia użytkownikowi zamknięcie okna potomnego Shopping
Bag i ponowne otworzenie go za pomocą jednego łącza Zaczynamy.
Może to się wydawać oczywiste, a nawet zbyteczne. Zwróćmy jednak uwagę, że w index.html (wiersz 27) dodatkowe okno
jest otwierane tylko wtedy, gdy shopWin równe jest null. Zamknięcie okna nie ustawia shopWin na null, zatem
właśnie resetOpener() może tu pomóc. Zwróćmy uwagę również na to, że obsługa zdarzenia onLoad w znaczniku
FRAMESET jest ustawiona na self. focus(). Dzięki temu nowe okno nie jest otwierane ani ładowane poza oknem
głównym, gdyż w przeciwnym wypadku użytkownik zastanawiałby się, co się właściwie stało.
179 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

Przede wszystkim chodzi o ładowanie zestawu ramek. Istnieją jeszcze trzy strony, które trzeba załadować: intro.html,
manager.html oraz inventory.js. intro.html to statyczna strona pomocy. Ładowanie manager.html, odbywa się wraz ze
stroną do niej włączoną, czyli inventory.js. Warto zatrzymać się dłużej przy manager.html, czym zajmiemy się dalej w tym
rozdziale, natomiast inventory.js omówimy już teraz. Jest to plik dość długi, ale przynajmniej zorientujemy się, jak należy
tworzyć spis dostępnych towarów.

inventory.js
Plik inventory.js zawiera trzy funkcje. Pierwsze dwie to konstruktory: jedna produktu, druga kategorii. Ostatnia funkcja
tworzy tablice obiektów tworzonych przez te konstruktory. Spójrzmy na przykład 8.3.

Techniki języka JavaScript:


zarządzanie wieloma oknami i dokumentami
Kiedy pracujemy z aplikacją korzystającą tylko z okna głównego przeglądarki, nie musi-
my specjalnie przejmować się kwestią okien. Jeśli jednak otwieramy inne okno, musimy
być ostrożni. Czy okno ma być zawsze na wierzchu, czy schowane? Czy jego okno
macierzyste jest nadal otwarte? Co się dzieje, jeśli jedno z nich zostanie zamknięte?
Prawdopodobnie nie będziemy musieli zajmować się wszystkimi tymi kwestiami, ale o tych
zagadnieniach trzeba jednak pamiętać. Można nad wszystkim zapanować, jeśli utworzy
się zmienne, których wartości będą opisywały stan poszczególnych okien. Na przykład
zmienna shopWin ma wartość odpowiadającą obiektowi zdalnego okna lub wartość null,
jeśli okno to jest zamknięte. Shopping Bag podejmuje na podstawie tej wartości pewne
działania. Podobne zachowania można też zrealizować w ramkach.
Zmienne gimmeControl i browseControl mają podobne funkcje – monitorują treść dokumentów.
Innymi słowy w zależności od tego, co jest wyświetlone, odpowiednio zachowuje się
aplikacja.

Przykład 8.3. inventory.js


1 function product(name, description, price, unit) {
2 this.name = name;
3 this.description = description;
4 this.price = price;
5 this.unit = unit;
6 this.plu = name.substring(0, 3).toUpperCase() +
7 parseInt(price).toString();
8 this.icon = new Image();
9 return this;
10 }
11 function category(name, description) {
12 this.name = name;
13 this.description = description;
14 this.prodLine = eval(name);
15 var imgDir = "images/" + name.toLowerCase() + "/";
16 for (var i = 0; i < this.prodLine.length; i++) {
17 this.prodLine[i].icon.src = imgDir +
18 this.prodLine[i].name.toLowerCase() + ".gif";
19 }
20 return this;
21 }
22 function makeProducts() {
23 Appliances = new Array(
24 new product("Dryer",
25 "Stylowa, pastelowa obudowa z dwuprzyciskową obsługą",
26 263.37 ,
27 "sztuka"),
28 new product("Hairdryer",
29 "Kolorowe, żółte wzornictwo, trwały kabel. Dobry zakup.",
30 1.15,
31 "para"),
32 new product("Oven",
33 "Pochodzący z lat 50-tych XIX wieku piec węglowy momentalnie " +
34 "zwęgli ulubione dania.",
35 865.78,
180

36 "sztuka"),
37 new product("Radio",
38 "Rewolucyjna technologia jednokanałowa. Generator szumów " +
39 "w zestawie.",
40 15.43,
41 "sztuka"),
42 new product("Toaster",
43 "Toster typu barbecue. Szansa porażenia prądem tylko średnia.",
44 25.78,
45 "sztuka"),
46 new product("Washer",
47 "Wyręcza Cię niemalże we wszystkim.",
48 345.61,
49 "sztuka")
50 );
51
52 Buildings = new Array(
53 new product("Barn",
54 "Kompletne wyposażenie, z przerdzewiałym silosem i gnijącymi " +
55 "drzwiami. Chlew sprzedawany osobno.",
56 6350.57,
57 "sztuka"),
58 new product("Lighthouse",
59 "Zbudowana z cementu, doskonałe żarówki. Zasilana trzema " +
60 "paluszkami (kupowane osobno).",
61 12351.15,
62 "sztuka"),
63 new product("Igloo",
64 "Zbudowane z dobieranych bloków śniegowych, zawiera komin i " +
65 "5-tonowe urządzenie klimatyzacyjne.",

Przykład 8.3. inventory.js (ciąg dalszy)


66 954.76,
67 "sztuka"),
68 new product("City",
69 "Domy, ulice, latarnie, horyzont. Doskonała okazja dla hurtowników.",
70 334165.95,
71 "sztuka"),
72 new product("Castle",
73 "Surowy, średniowieczny projekt, z aligatorami w fosie i mostem" +
74 " zwodzonym z pilotem.",
75 93245.59,
76 "sztuka"),
77 new product("Tower",
78 "Naprawdę wysoka. Doskonale nadaje się do zjednywania " +
79 "przyjaciół i obserwacji lasu.",
80 24345.87,
81 "para")
82 );
83
84 Clothing = new Array(
85 new product("Bowtie",
86 "Gruby, czerwony materiał. Doskonała na bezużteczne prezenty " +
87 "gwiazdkowe i urodzinowe.",
88 5.41,
89 "pięć"),
90 new product("Necktie",
91 "Bądź pierwszym (i zapewne jedynym) w Twoim bloku. Zrobiony " +
92 "z doskonałego płótna żaglowego.",
93 1.15,
94 "sztuka"),
95 new product("Purse",
96 "Interesujące, zielone sukno. Odpędza większość ssaków.",
97 18.97,
98 "sztuka"),
99 new product("Jacket",
100 "Sztuczne futro wzbogacone włóknem szklanym. Można prać w pralce.",
101 180.72,
102 "sztuka"),
103 new product("Glove",
104 "Kryje wszystkie cztery palce plus kciuk. Zmysłowy, lateksowy wzór.",
105 6.59,
106 "trzy"),
107 new product("Dress",
108 "Z ciuchów. Można stosować też jako obrus podczas pikniku.",
109 7.99,
181 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

110 "sztuka"),
111 new product("Watch",
112 "Wspaniała replika. Nie podaje czasu, ale bardzo ładnie wygląda.",
113 6.19,
114 "sztuka")
115 );
116
117 Electronics = new Array(
118 new product("Camcorder",
119 "Zasilanie energią słoneczną, darmowy mikrofon - idealny " +
120 "do szantażowania krewnych.",
121 60.45,
122 "sztuka"),
123 new product("Stereo",
124 "Kwadrofoniczny dźwięk ośmiościeżkowy. Opcjonalnie git marynara" +
125 " i glany.",
126 54.91,
127 "sztuka"),
128 new product("Speaker",
129 "Doskonały kawałek śmiecia hi-fi. Najlepiej działa bez podłączania.",
130 1.90,

Przykład 8.3. inventory.js (ciąg dalszy)


131 "sztuka"),
132 new product("Remote",
133 "Dziesiątki przycisków. Steruje wszystkim: telewizją, wideo, " +
134 "kolumną, zwierzętami domowymi i lokalnym samorządem.",
135 465.51,
136 "sztuka"),
137 new product("Cellphone",
138 "Blaszanka, działa nawet do 10 metrów. Gustowny liliowy " +
139 "plastik.",
140 64.33,
141 "sztuka"),
142 new product("Camera",
143 "Robi doskonałe, jednokolorowe zdjęcia. Kompostoodporny.",
144 2.95,
145 "sztuka"),
146 new product("Television",
147 "Model obsługuje dwa kanały UHF. Cudo!",
148 22.57,
149 "sztuka")
150 );
151
152 Food = new Array(
153 new product("Cheese",
154 "Poczekaj, aż go poczujesz. Sery pleśniowe mogą się schowaĆ.",
155 3.05,
156 "gomułka"),
157 new product("Fries",
158 "Więcej oleju niż w garażu. Smak nie do podrobienia.",
159 1.15,
160 "pudełko"),
161 new product("Eggs",
162 "Typowa przystawka śniadaniowa.",
163 1.07,
164 "tuzin"),
165 new product("Drumstick",
166 "Ta noga pterodaktyla niewątpliwie zachwyci.",
167 100.00,
168 "pół tony"),
169 new product("Chips",
170 "Zapach otwartej torebki. Gwarantujemy, gwarantujemy za ich " +
171 "stęchłość lub zwracamy pieniądze.",
172 1.59,
173 "torebka"),
174 new product("Shrimp",
175 "Doskonałe na surowo, serwowaĆ w temperaturze wyższej od pokojowej.",
176 2.95,
177 "sztuka")
178 );
179
180 Hardware = new Array(
181 new product("Chainsaw",
182 "Sam zostań bobrem - ta piła Ci to umożliwi.",
183 226.41,
182

184 "sztuka"),
185 new product("Cycle",
186 "Zetnij całe pole pszenicy w parę chwil - zupełnie jak " +
187 "Ponury żeniec.",
188 11.15,
189 "sztuka"),
190 new product("Hammer",
191 "Utwardzona główka stalowa, rączka z włókna szklanego. Wiadomo," +
192 " bez młota nie robota.",
193 9.87,
194 "sztuka"),
195 new product("Lawnmower",

Przykład 8.3. inventory.js (dokończenie)


196 "Samojezdna (jeśli tylko ją trochę popchniesz).",
197 165.95,
198 "sztuka"),
199 new product("Pliers",
200 "Doskonałe do radzenia sobie z brwiami i włosami w nosie.",
201 6.59,
202 "sztuka"),
203 new product("Stake",
204 "Dwa w jednym: może służyć jako śledź do namiotu lub jako broń na wampiry.",
205 3.95,
206 "para")
207 );
208
209 Music = new Array(
210 new product("Bongos",
211 "Doskonałe do hałasowania przy różnych okazjach.",
212 35.50,
213 "czynele"),
214 new product("Piano",
215 "Nie jest może zbyt wielkie, ale dla Twojego dziecka " +
216 "całkowicie wystarczy.",
217 1001.40,
218 "sztuka"),
219 new product("Notes",
220 "Masz do wyboru A, B, C, D, E, F i G. Możliwość użycia " +
221 "w dowolnej piosence.",
222 2.97,
223 "nuta"),
224 new product("Guitar",
225 "Brzęk, brzęk. Oto Twoja droga ku chwale i bogactwie.",
226 241.11,
227 "sztuka"),
228 new product("Trumpet",
229 "Solidny, mosiężny korpus, brak wentyli. Dołączono dodatkowy " +
230 "ustnik.",
231 683.59,
232 "sztuka")
233 );
234
235 categorySet = new Array(
236 new category("Appliances", "Sprzęt kuchenny ułatwiający Ci życie"),
237 new category("Buildings", "Budowle, którym nie sposób się oprzeć"),
238 new category("Clothing", "Ciuchy być może modne w 21 wieku"),
239 new category("Electronics", "Szykowne gadżety, które wyczyszczą " +
240 "Ci portfel"),
241 new category("Food", "Najlepsze produkty dostępne kiedykolwiek " +
242 "w Sieci"),
243 new category("Hardware", "Wszelkiej maści narzędzia ogólnego " +
244 "zastosowania"),
245 new category("Music", "Najgorętsze instrumenty z miejsc, " +
246 "o których w życiu nie słyszałeś")
247 );
248 }

Cechy produktów
Przypomnijmy sobie obiekty JavaScriptu, których używaliśmy w poprzednich rozdziałach? Znów mamy z nimi
do czynienia. Każdy produkt traktowany jest jako obiekt z kilkoma właściwościami, czyli każdy produkt ma
następujące właściwości:
name
Nazwa produktu
183 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

description
Krótki opis produktu
price
Cena produktu
unit
Jednostka, w jakiej dany produkt jest sprzedawany, na przykład tuzin, para, sztuka
plu
Numer katalogowy, używany do obsługi magazynowej i przetwarzania zamówień
icon
Obrazek produktu
Aby uzyskać pożądany wynik, definiujemy konstruktor produkcji następująco – wiersze 1–10:
function product(name, description, price, unit) {
this.name = name;
this.description = description;
this.price = price;
this.unit = unit;
this.plu = name.substring(0, 3).toUpperCase() +
parseInt(price).toString();
this.icon = new Image();
return this;
}

Zwróćmy uwagę, że tworzonych jest sześć właściwości, ale oczekuje się tylko czterech parametrów. Liczba właściwości
i liczba oczekiwanych argumentów nie muszą iść z sobą w parze, ale pamiętaj też, że każda właściwość otrzymuje jednak
jakąś wartość. Pierwsze cztery – name, description, price i unit otrzymują wartości z parametrów.
Inaczej dzieje się z plu. Jest to złożenie właściwości name i price. Bierze się pod uwagę pierwsze trzy litery nazwy
produktu przekształcone na wielkie litery oraz cenę. Skoro zatem łódź (boat) kosztuje 5 501 dolarów, to jej kodem jest
BOA5501. Pamiętajmy, że jest to kod z góry narzucony. Produkty sprzedawane w naszym sklepie będą miały własne
kody. Można tak postąpić, aby uprościć nasze zadanie. Ostatnia właściwość to ikona, której przypisujemy nowy obiekt
Image. W tym wypadku parametr też jest zbędny.

Cechy kategorii produktów


Wiemy, że tak naprawdę każdy produkt jest obiektem product. Podobnie każda kategoria produktów jest obiektem
category. Jak produkty mają właściwości, tak i kategorie je posiadają. Oto właściwości obiektu category:
name
Nazwa kategorii
description
Krótki opis kategorii
prodLine
Wszystkie produkty (obiekty product) danej kategorii
Konstruktor kategorii znajduje się w wierszach 11–21:
function category(name, description) {
this.name = name;
this.description = description;
this.prodLine = eval(name);
var imgDir = "images/" + name.toLowerCase() + "/";
for (var i = 0; i < this.prodLine.length; i++) {
this.prodLine[i].icon.src = imgDir +
this.prodLine[i].name.toLowerCase() + ".gif";
}
return this;
}

Każda kategoria ma trzy właściwości: napis name, kolejny napis description i tablicę prodLine. Właściwości name
i description są proste, ale skąd się bierze tablica i jak użyć do niej eval()? Wątpliwości zaraz się wyjaśnią, ale
można już określić podstawową strukturę: niezależnie od nazwy kategorii, należące do niej produkty są tablicą o takiej
samej nazwie. Jeśli na przykład nazwiemy kategorię stereos, tablica zawierająca produkty stereo nazywać się
będzie stereos. Oznacza to, że prodLine stanowi kopię zmiennej stereo, która jest tablicą różnych produktów typu
stereo.
184

Pamiętajmy, że każdy produkt ma właściwość o nazwie icon, będącą obiektem Image, któremu nie przypisaliśmy jego
źródła. Teraz jeszcze skorzystajmy nieco więcej z kategorii. Nie tylko każda kategoria zawiera w tablicy o określonej
nazwie produkty danego typu, ale też obrazki tejże kategorii znajdują się w tablicy o takiej samej nazwie.
Wszystkie produkty muzyczne, kategorii music, znajdują się w katalogu music/. Obrazki kategorii hardware mieszczą
się w katalogu hardware/, i tak dalej. Wygląda to nawet logicznie. Jeśli mamy taką strukturę katalogów na dysku, to
możemy ładować wstępnie obrazki z bieżącej kategorii podczas jej ładowania. Oto wiersze 16–19, które to zadanie
realizują:
for (var i = 0; i < this.prodLine.length; i++) {
this.prodLine[i].icon.src = imgDir +
this.prodLine[i].name.toLowerCase() + ".gif";
}

Jeśli obejrzymy dokładnie strukturę podkatalogów ch08, znajdziemy coś takiego:


images/
appliances/
buildings/
clothing/
electronics/
food/
hardware/
music/

W wierszu 17 ustawiamy właściwość SRC poszczególnych ikon (obiektu Image) na tekst images/ z dodaną nazwą
kategorii, zapisaną małymi literami, ukośnikiem, nazwą produktu zapisaną małymi literami, i przyrostkiem .gif. Znów
wracamy do zagadnienia konwencji nazewniczych, o czym mówiono w kilku poprzednich rozdziałach. Obrazki
poszczególnych produktów znajdują się w plikach o analogicznych nazwach, w podkatalogach o nazwach kategorii.
Oto odpowiedni wzorzec tworzenia nazwy pliku graficznego:
URL_obrazka = images/kategoria/produkt.gif

Jeśli przejrzymy katalog ch08\images\, zauważymy, że każda nazwa obrazka odpowiada jakiemuś produktowi
Shopping Bag w katalogu odnoszącym się do kategorii produktów. Dzięki temu mamy wszystko uporządkowane,
możemy dodawać, usuwać i sprawdzać nasze produkty.

Jeśli mamy wiele dużych obrazków, zastanówmy się, czy nie lepiej byłoby ominąć
wstępnego ładowania obrazków. Niewątpliwe dobrze jest mieć wszystko na maszynie
klienta, kiedy nawigacja nie wymaga żadnych właściwie opóźnień. Jeśli mamy
mnóstwo dużych obrazków wysokiej jakości, użytkownik może nie zechce czekać, aż
załaduje się na jego komputer 500 kB obrazków. Należy liczyć na swoje własne
rozeznanie.

Tworzenie produktów i kategorii


Widzieliśmy już konstruktory, teraz czas z nich skorzystać. Najpierw trzeba utworzyć produkty, a potem zabierzemy się
za kategorie. Zajmuje się tym wszystkim funkcja makeProducts(). Oto wiersze 22–248. Jako że stale korzystamy
z jednego i tego samego konstruktora produktu, to podamy tutaj jej wersję skróconą:
function makeProducts() {
Appliances = new Array(
new product("Dryer",
"Stylowa, pastelowa obudowa z dwuprzyciskową obsługą",
263.37 ,
"sztuka"),
new product("Hairdryer",
"Kolorowe, żółte wzornictwo, trwały kabel. Dobry zakup.",
1.15,
"para"),
new product("Oven",
"Pochodzący z lat 50-tych XIX wieku piec węglowy momentalnie " +
"zwęgli ulubione dania.",
865.78,
"sztuka"),
new product("Radio",
"Rewolucyjna technologia jednokanałowa. Generator szumów " +
"w zestawie.",
15.43,
"sztuka"),
new product("Toaster",
"Toster typu barbecue. Szansa porażenia prądem tylko średnia.",
185 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

25.78,
"sztuka"),
new product("Washer",
"Wyręcza Cię niemalże we wszystkim.",
345.61,
"sztuka")
);
...
... i tak dalej ...
...
categorySet = new Array(
new category("Appliances", "Sprzęt kuchenny ułatwiający Ci życie"),
new category("Buildings", "Budowle, którym nie sposób się oprzeć"),
new category("Clothing", "Ciuchy być może modne w 21 wieku"),
new category("Electronics", "Szykowne gadżety, które wyczyszczą " +
"Ci portfel"),
new category("Food", "Najlepsze produkty dostępne kiedykolwiek " +
"w Sieci"),
new category("Hardware", "Wszelkiej maści narzędzia ogólnego " +
"zastosowania"),
new category("Music", "Najgorętsze instrumenty z miejsc, " +
"o których w życiu nie słyszałeś")
);
}

Najpierw produkty. Zmienna Appliances staje się tablicą, której każdy element jest obiektem opisującym produkt.
Każde wywołanie product zawiera wszystkie potrzebne argumenty: nazwę, opis, cenę i jednostkę sprzedażną. Dzieje się
tak dla wszystkich naszych kategorii.
Teraz pozostaje zająć się kategoriami. Nazwy kategorii mamy już na miejscu (Appliances, Buildings, Clothing
i tak dalej); teraz wystarczy poinformować o nich także naszą aplikację, co robimy w wierszach 235–248:
categorySet = new Array(
new category("Appliances", "Sprzęt kuchenny ułatwiający Ci życie"),
new category("Buildings", "Budowle, którym nie sposób się oprzeć"),
new category("Clothing", "Ciuchy być może modne w 21 wieku"),
new category("Electronics", "Szykowne gadżety, które wyczyszczą " +
"Ci portfel"),
new category("Food", "Najlepsze produkty dostępne kiedykolwiek " +
"w Sieci"),
new category("Hardware", "Wszelkiej maści narzędzia ogólnego " +
"zastosowania"),
new category("Music", "Najgorętsze instrumenty z miejsc, " +
"o których w życiu nie słyszłeś")
);
}

Zmienna categorySet także jest tablicą. Każdy element tej tablicy jest tworzony dwuargumentowym konstruktorem.
Pierwszy argument otrzymuje właściwość name, drugi właściwość description. Spójrzmy jeszcze raz na 14. wiersz
konstruktora obiektu kategorii:
this.prodLine = eval(name);

Właściwość prodLine jako wartość otrzymuje eval(name), zatem wywołanie category() w wierszu 249 oznacza
ustawienie prodLine na wartość eval("Appliances"), czyli Appliances. Teraz kategoria o nazwie „Appliances” zna
wszystkie swoje produkty (w tablicy prodLine). Każdy element categorySet opisuje jedną kategorię produktów, dzięki
czemu proste jest ich dodawanie i usuwanie.

Tworzenie koszyka na zakupy


Mamy już nasze produkty. Pozostaje jeszcze zrobienie koszyka sklepowego. Koszyk ten musi mieć kilka właściwości,
które pozwolą obsłużyć płatności, oraz tablicę, w której będziemy zaznaczać wszystkie wybrane przez klienta towary.
Konstruktor Bag(), zawarty w pliku manager.html, opisuje jeden tylko koszyk. Oto wiersze 21–31, które pochodzą
z przykładu 8.4 pokazanego dalej w tym rozdziale:
function Bag() {
this.taxRate = .06;
this.taxTotal = 0;
this.shipRate = .02;
this.shipTotal = 0;
this.subTotal = 0;
this.bagTotal = 0;
this.things = new Array();
}
shoppingBag = newBag();
186

Mamy tu dwie wielkości procentowe, taxRate i shipRate. Pierwsza z nich to współczynnik do określenia podatku
od sprzedaży, druga to współczynnik pozwalający wyliczyć opłatę za przesyłkę. Podatek będzie trzeba oczywiście zmienić
stosownie do swoich potrzeb, ale tutaj przynajmniej widzimy, jak to działa. Trzy pozostałe zmienne, taxTotal,
subTotal i shipTotal zawierają sumę podatku, sumę cen wybranych produktów oraz ich ilości, a także całkowitą
kwotę do zapłaty. Ostatnia zmienna to tablica things, która będzie zawierać produkty wybrane przez użytkownika
wraz z ich ilościami. Zmienna shoppingBag jest następnie ustawiana na new Bag(). Możemy więc już iść na zakupy.

Techniki języka JavaScript:


zapisywanie stanu klienta w obiektach JavaScriptu
Zwróćmy uwagę, że we właściwościach shoppingBag() rejestrowane są wszystkie wy-
brane produkty i sumy. W ten sposób można stopniowo zbierać dane podczas działania
aplikacji. Ogólnie rzecz biorąc, użytkownik może dowolnie zmieniać ilości i wybrane
produkty, a właściwości shoppingBag dostosują się do tego. Tak również należy to
programować.

Etap 2. Pokazanie towarów


Po załadowaniu aplikacji użytkownik będzie chciał obejrzeć naszą ofertę towarową. W tym celu może przemieszczać się
między kategoriami, używając łącz Poprzednia kategoria i Następna kategoria, lub między produktami, wybierając
Poprzedni produkt i Następny produkt. Spójrzmy jak to działa. Przypomnijmy sobie wiersze 235–247 pliku
inventory.js:
categorySet = new Array(
new category("Appliances", "Sprzęt kuchenny ułatwiający Ci życie"),
new category("Buildings", "Budowle, którym nie sposób się oprzeć"),
new category("Clothing", "Ciuchy być może modne w 21 wieku"),
new category("Electronics", "Szykowne gadżety, które wyczyszczą " +
"Ci portfel"),
new category("Food", "Najlepsze produkty dostępne kiedykolwiek " +
"w Sieci"),
new category("Hardware", "Wszelkiej maści narzędzia ogólnego " +
"zastosowania"),
new category("Music", "Najgorętsze instrumenty z miejsc, " +
"o których w życiu nie słyszałeś")
);

Tablica categorySet zawiera siedem obiektów category. Do pierwszego możemy się odwołać przez
categorySet[0], do drugiego przez categorySet[1] i tak dalej. Bez względu na to, jaki produkt użytkownik
ogląda, Shopping Bag zna jedynie odpowiedni numer kategorii (0–6). Jeśli użytkownik zdecyduje się cofnąć
do kategorii poprzedniej, aplikacja od numeru bieżącej kategorii odejmie jeden i pokaże pierwszy produkt z tej nowej
kategorii. Jeśli bieżącą kategorią była kategoria 0 i użytkownik chce się cofnąć, aplikacja zmienia numer na ostatnią
kategorię (u nas jest to 6).
Jeśli użytkownik chce zmienić kategorię na następną, do numeru kategorii bieżącej dodawane jest 1. Jeśli kategoria
bieżąca jest ostatnia, to kategorią następną jest kategoria o numerze 0.
To samo dotyczy także produktów. Każda kategoria zawiera pewną ilość produktów. Shopping Bag zna numer produktów
w każdej kategorii, zatem odwołanie się do produktu następnego lub poprzedniego spowoduje dodanie lub odjęcie
jedności od numeru produktu właśnie pokazywanego.
Jeśli użytkownik jest przy ostatnim produkcie danej kategorii i chce przejść do produktu następnego, pokazywany jest
produkt o numerze 0 kategorii następnej. Gdy użytkownik jest przy produkcie pierwszym i chce przejść
do poprzedniego, pokazywany jest ostatni produkt kategorii poprzedniej.
Jeśli cały ten opis wprowadził nieco zamieszania, to powinien wszystko rozjaśnić diagram z rysunku 8.10, który
pokazuje, jak nasza aplikacja wiedzie użytkownika po kategoriach. Tak samo działa nawigacja po produktach. Kiedy
dochodzimy do ostatniego produktu w danej kategorii, otrzymujemy produkt pierwszy kategorii następnej.

manager.html
Opisana powyżej nawigacja realizowana jest przez plik manager.html, który pokazano jako przykład 8.4.
187 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

Przykład 8.4. manager.html


1 <HTML>
2 <HEAD>
3 <TITLE>Menedżer Shopping Bag</TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6 TD {font-weight: bold; margin-left: 20; margin-right: 20; padding: 10}
7 //-->
8 </STYLE>
9 </HEAD>
10 <BODY onLoad="freshStart(); makeProducts();" LINK=BLUE ALINK=BLUE VLINK=BLUE>
11 <SCRIPT LANGUAGE="JavaScript1.2" SRC="inventory.js"></SCRIPT>
12
13 <SCRIPT LANGUAGE="JavaScript1.2">
14 <!--
15 var gimmeControl = false;
16 var browseControl = false;
17 var curCLoc = -1;
18 var curPLoc = -1;

Rysunek 8.10. Nawigacja po kategoriach

Przykład 8.4. manager.html (ciąg dalszy)


19 var infoStr = '';
20 var shoppingBag;
21 function Bag() {
22 this.taxRate = .06;
23 this.taxTotal = 0;
24 this.shipRate = .02;
25 this.shipTotal = 0;
26 this.subTotal = 0;
27 this.bagTotal = 0;
28 this.things = new Array();
29 }
30
31 shoppingBag = new Bag();
32
33 function showStore() {
34 gimmeControl = false;
35 var header = '<HTML><TITLE>Kategoria</TITLE><BODY BGCOLOR=FFFFFF>';
36 var intro = '<H2>Kategorie produktów Shopping Bag</H2><B>';
188

37 var footer = '</DL></BLOCKQUOTE></BODY></HTML>';


38 var storeStr = '<BLOCKQUOTE><DL>';
39 for (var i = 0; i < categorySet.length; i++) {
40 storeStr += '<DT><A HREF="javascript: parent.frames[1].reCall(' +
41 i + ', 0);">' + categorySet[i].name + '</A>' +
42 '<DD>' + categorySet[i].description + '<BR><BR>';

Przykład 8.4. manager.html (ciąg dalszy)


43 }
44 infoStr = header + intro + storeStr + footer;
45 parent.frames[0].location.replace(
46 "javascript: parent.frames[1].infoStr");
47 }
48
49 function portal() {
50 gimmeControl = false;
51 parent.frames[0].location.href = "search/index.html";
52 }
53 function display(cOffset, pOffset) {
54 if(!browseControl) {
55 alert("Zacznij zakupy od wybrania kategorii produktów lub " +
56 "wyszukując produkty.");
57 return;
58 }
59 gimmeControl = true;
60 if (curPLoc + pOffset < 0 || curPLoc + pOffset ==
61 categorySet[curCLoc].prodLine.length) {
62 if (curPLoc + pOffset < 0) {
63 if (curCLoc - 1 < 0) { curCLoc = categorySet.length - 1; }
64 else { curCLoc--; }
65 curPLoc = categorySet[curCLoc].prodLine.length - 1;
66 }
67 else if (curPLoc + pOffset == categorySet[curCLoc].prodLine.length) {
68 if (curCLoc + 1 == categorySet.length) { curCLoc = 0; }
69 else { curCLoc++; }
70 curPLoc = 0;
71 }
72 }
73 else {
74 if (curCLoc + cOffset < 0 || curCLoc + cOffset ==
75 categorySet.length) {
76 curCLoc = (curCLoc + cOffset < 0 ? categorySet.length - 1 : 0);
77 }
78 else { curCLoc += cOffset; }
79 if (cOffset == -1 || cOffset == 1) { curPLoc = 0; }
80 else if (pOffset == 0) {
81 curPLoc = (curPLoc >= categorySet[curCLoc].prodLine.length ? 0 :
82 curPLoc)
83 }
84 else { curPLoc = curPLoc + pOffset; }
85 }
86 infoStr = '<HTML><HEAD><TITLE>Nazwa produktu</TITLE></HEAD>' +
87 '<BODY><TABLE CELLPADDING=3><TR><TD VALIGN=TOP COLSPAN=2>' +
88 '<FONT FACE=Tahoma><H2>Shopping Bag: <I>' +
89 categorySet[curCLoc].name + '</I></H2><TR>' +
90 '<TD VALIGN=TOP><IMG SRC="' +
91 categorySet[curCLoc].prodLine[curPLoc].icon.src +
92 '"></TD><TD VALIGN=TOP><FONT FACE=Tahoma>' +
93 '<B>Nazwa: </B>' + categorySet[curCLoc].prodLine[curPLoc].name +
94 '<BR><B>Opis: </B>' +
95 categorySet[curCLoc].prodLine[curPLoc].description + '<BR>' +
96 '<B>Cena: </B> $' +
97 numberFormat(categorySet[curCLoc].prodLine[curPLoc].price) + '/' +
98 categorySet[curCLoc].prodLine[curPLoc].unit + '<BR>' +
99 '<B>PLU: </B>' + categorySet[curCLoc].prodLine[curPLoc].plu +
100 '</TD></TR></TABLE></BODY></HTML>';
101 parent.frames[0].location.href =
102 "javascript: parent.frames[1].infoStr";
103 }
104
105 function reCall(cReset, pReset) {
106 browseControl = true;
107 curCLoc = cReset;

Przykład 8.4. manager.html (ciąg dalszy)


189 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

108 curPLoc = pReset;


109 display(0, 0);
110 }
111
112 function gimmeOne() {
113 if (!gimmeControl) {
114 alert("Nie ma na ekranie nic, co mógłbyś dostać.");
115 return;
116 }
117 for (var i = 0; i < shoppingBag.things.length; i++) {
118 if (categorySet[curCLoc].prodLine[curPLoc].plu ==
119 shoppingBag.things[i].plu) {
120 alert("Już to masz. Ilość możesz zmienić, wybierając " +
121 "Widok/Zmiana koszyka.");
122 return;
123 }
124 }
125 shoppingBag.things[shoppingBag.things.length] =
126 categorySet[curCLoc].prodLine[curPLoc];
127 shoppingBag.things[shoppingBag.things.length - 1].itemQty = 1;
128 shoppingBag.things[shoppingBag.things.length - 1].category =
129 categorySet[curCLoc].name;
130 alert("W porządku, wkładamy " +
131 shoppingBag.things[shoppingBag.things.length - 1].name +
132 " do koszyka.");
133 }
134
135 function showBag() {
136 if (shoppingBag.things.length == 0) {
137 alert("Twój koszyk jest obecnie pusty. Włóż coś do niego.");
138 return;
139 }
140 gimmeControl = false;
141 var header = '<HTML><HEAD><TITLE>Twój koszyk</TITLE>' +
142 '</HEAD><BODY BGCOLOR=FFFFFF ' +
143 onLoad="parent.frames[1].runningTab(document.forms[0]);">';
144 var intro = '<H2>Twój koszyk!!!</H2>' +
145 '<FORM onReset="' +
146 'setTimeout(\'parent.frames[1].runningTab(document.forms[0])\', ' +
147 '25);">';
148 var tableTop = '<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=5>' +
149 '<TR><TH><B>Index' +
150 '<TH><B>Produkt<TH><B>Kategoria' +
151 '<TH><B>PLU<TH><B>Cena jednostkowa' +
152 '<TH><B>Ilość<TH><B>Kwota' +
153 '<TH><B>Usuń' +
154 '</TR>';
155 var itemStr = '';
156 for (var i = 0; i < shoppingBag.things.length; i++) {
157 itemStr += '<TR>' +
158 '<TD ALIGN=CENTER>' + (i + 1) + '</TD>' +
159 '<TD>' + shoppingBag.things[i].name + '</TD>' +
160 '<TD>' + shoppingBag.things[i].category + '</TD>' +
161 '<TD>' + shoppingBag.things[i].plu + '</TD>' +
162 '<TD ALIGN=RIGHT>$' +
163 parent.frames[1].numberFormat(shoppingBag.things[i].price) +
164 '</TD>' +
165 '<TD ALIGN=CENTER>' +
166 parent.frames[1].genSelect(shoppingBag.things[i].price,
167 shoppingBag.things[i].itemQty, i) + '</TD>' +
168 '<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 VALUE="' +
169 parent.frames[1].numberFormat(shoppingBag.things[i].price *
170 shoppingBag.things[i].itemQty) +
171 '" onFocus="this.blur();"></TD>' +
172 '<TD ALIGN=CENTER><INPUT TYPE=CHECKBOX></TD>' +

Przykład 8.4. manager.html (ciąg dalszy)


173 '</TR>';
174 }
175 var tableBottom = '<TR>' +
176 '<TD ALIGN=RIGHT COLSPAN=6>SubTotal:</TD>' +
177 '<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="subtotal" ' +
178 'onFocus="this.blur();"></TD></TR>' +
179 '<TR>' + '<TD ALIGN=RIGHT COLSPAN=6> + 6% Tax:</TD>' +
180 '<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="tax" ' +
181 'onFocus="this.blur();"></TD></TR><TR><TD ALIGN=RIGHT COLSPAN=6>' +
190

182 '2% Shipping:</TD><TD ALIGN=CENTER><INPUT TYPE=TEXT ' +


183 'SIZE=10 NAME="ship" onFocus="this.blur();"></TD></TR>' +
184 '<TR>' +
185 '<TD ALIGN=RIGHT COLSPAN=3><INPUT TYPE=BUTTON VALUE="Do kasy" ' +
186 'onClick="parent.frames[1].checkOut(this.form);"></TD>' +
187 '<TD ALIGN=RIGHT><INPUT TYPE=RESET VALUE="Wyzeruj ilości"></TD>' +
188 '<TD ALIGN=RIGHT><INPUT TYPE=BUTTON VALUE="Zmiana koszyka" ' +
189 'onClick="parent.frames[1].changeBag(this.form, true);"></TD>' +
190 '<TD ALIGN=RIGHT>Suma:</TD><TD ALIGN=CENTER>' +
191 '<INPUT TYPE=TEXT NAME="total" SIZE=10 onFocus="this.blur();">' +
192 '</TD></TR>';
193
194 var footer = '</TABLE></FORM></BODY></HTML>';
195 infoStr = header + intro + tableTop + itemStr + tableBottom + footer;
196 parent.frames[0].location.replace(
197 'javascript: parent.frames[1].infoStr');
198 }
199
200 function genSelect(priceAgr, qty, idx) {
201 var selStr = '<SELECT onChange="this.form.elements[' + (idx * 3 + 1) +
202 '].value = this.options[this.selectedIndex].value; ' +
203 'parent.frames[1].runningTab(this.form);">';
204 for (var i = 1; i <= 10; i++) {
205 selStr += '<OPTION VALUE="' + numberFormat(i * priceAgr) + '"' +
206 (i == qty ? ' SELECTED' : '') + '>' + i;
207 }
208 selStr += '</SELECT>';
209 return selStr;
210 }
211
212 function runningTab(formObj) {
213 var subTotal = 0;
214 for (var i = 0; i < shoppingBag.things.length; i++) {
215 subTotal += parseFloat(formObj.elements[(i * 3) + 1].value);
216 }
217 formObj.subtotal.value = numberFormat(subTotal);
218 formObj.tax.value = numberFormat(subTotal * shoppingBag.taxRate);
219 formObj.ship.value = numberFormat(subTotal * shoppingBag.shipRate);
220 formObj.total.value = numberFormat(subTotal +
221 round(subTotal * shoppingBag.taxRate) +
222 round(subTotal * shoppingBag.shipRate));
223 shoppingBag.subTotal = formObj.subtotal.value;
224 shoppingBag.taxTotal = formObj.tax.value;
225 shoppingBag.shipTotal = formObj.ship.value;
226 shoppingBag.bagTotal = formObj.total.value;
227 }
228
229 function numberFormat(amount) {
230 var rawNumStr = round(amount) + '';
231 rawNumStr = (rawNumStr.charAt(0) == '.' ? '0' + rawNumStr : rawNumStr);
232 if (rawNumStr.charAt(rawNumStr.length - 3) == '.') {
233 return rawNumStr
234 }
235 else if (rawNumStr.charAt(rawNumStr.length - 2) == '.') {
236 return rawNumStr + '0';
237 }

Przykład 8.4. manager.html (ciąg dalszy)


238 else { return rawNumStr + '.00'; }
239 }
240 function round(number,decPlace) {
241 decPlace = (!decPlace ? 2 : decPlace);
242 return Math.round(number * Math.pow(10,decPlace)) /
243 Math.pow(10,decPlace);
244 }
245
246 function changeBag(formObj, showAgain) {
247 var tempBagArray = new Array();
248 for (var i = 0; i < shoppingBag.things.length; i++) {
249 if (!formObj.elements[(i * 3) + 2].checked) {
250 tempBagArray[tempBagArray.length] = shoppingBag.things[i];
251 tempBagArray[tempBagArray.length - 1].itemQty =
252 formObj.elements[i * 3].selectedIndex + 1;
253 }
254 }
255 shoppingBag.things = tempBagArray;
191 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

256 if(shoppingBag.things.length == 0) {
257 alert("Twój koszyk jest już pusty. Włóż tam coś.");
258 parent.frames[1].showStore();
259 }
260 else { showBag(); }
261 }
262
263 function checkOut(formObj) {
264 gimmeControl = false;
265 if(!confirm("Czy masz już wszystko, czego potrzebujesz, " +
266 "w potrzebnych Ci ilościach? Pamiętaj, że do usunięcia czegoś " +
267 "lub zmiany ilości musisz wybrać przycisk zmiany. Jeśli jesteś " +
268 "gotów, wciśnij OK.")) {
269 return;
270 }
271 if(shoppingBag.things.length == 0) {
272 showStore();
273 return;
274 }
275 var header = '<HTML><TITLE>Shopping Bag - płatności</TITLE>' +
276 '<BODY BGCOLOR=FFFFFF>';
277
278 var intro = '<H2>Shopping Bag - płatności</H2><FORM METHOD=POST ' +
279 'ACTION="http://www.serve.com/hotsyte/cgi-bin/bag.cgi" ' +
280 'onSubmit="return parent.frames[1].cheapCheck(this);">';
281
282 var shipInfo = '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=5>' +
283 '<TR><TD><B>Informacje o wysyłce</TD></TR>'+
284 '<TR><TD>Imię</TD>' + '<TD><INPUT TYPE=TEXT NAME="fname"></TD>' +
285 '</TR><TR><TD>Nazwisko</TD>' +
286 '<TD><INPUT TYPE=TEXT NAME="lname"></TD></TR><TR><TD>Firma</TD>' +
287 '<TD><INPUT TYPE=TEXT NAME="cname"></TD></TR><TR>' +
288 '<TD>adres - ulica I</TD><TD><INPUT TYPE=TEXT NAME="saddress1">' +
289 '</TD></TR><TR><TD>adres - ulica II</TD>' +
290 '<TD><INPUT TYPE=TEXT NAME="saddress2"></TD></TR><TR>' +
291 '<TD>Miasto</TD>' + '<TD><INPUT TYPE=TEXT NAME="city"></TD></TR>' +
292 '<TR><TD>Województwo/region</TD>' +
293 '<TD><INPUT TYPE=TEXT NAME="stpro"></TD></TR><TR>' +
294 '<TD>Kraj</TD>' + '<TD><INPUT TYPE=TEXT NAME="country"></TD></TR>' +
295 '<TR><TD>Kod pocztowy</TD><TD><INPUT TYPE=TEXT NAME="zip"></TD>' +
296 '</TR><TR><TD><BR><BR></TD></TR></TABLE>';
297
298 var payInfo = '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=5>' +
299 '<TR><TD><B>Informacje o płatności</TD></TR>'+
300 '<TR><TD>Typ karty kredytowej &nbsp; &nbsp; &nbsp; </TD>' +
301 '<TD>Visa <INPUT TYPE=RADIO NAME="ctype" VALUE="visa" CHECKED>' +
302 &nbsp; &nbsp; &nbsp; ' +

Przykład 8.4. manager.html (ciąg dalszy)


303 'Amex <INPUT TYPE=RADIO NAME="ctype" VALUE="amex"> ' +
304 '&nbsp; &nbsp; &nbsp; ' +
305 'Discover <INPUT TYPE=RADIO NAME="ctype" VALUE="disc"> ' +
306 &nbsp; &nbsp; &nbsp; </TD>' + '</TR>' +
307 '<TR><TD>Numer karty kredytowej</TD>' +
308 '<TD><INPUT TYPE=TEXT NAME="cnumb"></TD></TR><TR>' +
309 '<TD>Data ważności</TD><TD><INPUT TYPE=TEXT NAME="edate"></TD>' +
310 '</TR><TR><TD><INPUT TYPE=SUBMIT VALUE="Wyślij zamówienie"></TD>' +
311 '<TD><INPUT TYPE=RESET VALUE="Wyczyść dane"></TD>' + '</TR>' +
312 '</TABLE>';
313
314 var itemInfo = '';
315 for (var i = 0; i < shoppingBag.things.length; i++) {
316 itemInfo += '<INPUT TYPE=HIDDEN NAME="prod' + i +
317 '" VALUE="' + shoppingBag.things[i].plu + '-' +
318 shoppingBag.things[i].itemQty + '">';
319 }
320 var totalInfo = '<INPUT TYPE=HIDDEN NAME="subtotal" VALUE="' +
321 shoppingBag.subTotal + '">' +
322 '<INPUT TYPE=HIDDEN NAME="taxtotal" VALUE="' +
323 shoppingBag.taxTotal + '">' +
324 '<INPUT TYPE=HIDDEN NAME="shiptotal" VALUE="' +
325 shoppingBag.shipTotal + '">' +
326 '<INPUT TYPE=HIDDEN NAME="bagtotal" VALUE="' +
327 shoppingBag.bagTotal + '">';
328
329 var footer = '</FORM></BODY></HTML>';
192

330
331 infoStr = header + intro + shipInfo + payInfo + itemInfo +
332 totalInfo + footer;
333 parent.frames[0].location.replace(
334 'javascript: parent.frames[1].infoStr');
335 }
336
337 function cheapCheck(formObj) {
338 for (var i = 0; i < formObj.length; i++) {
339 if (formObj[i].type == "text" && formObj.elements[i].value == "") {
340 alert ("Musiszy wypełnić wszystkie pola.");
341 return false;
342 }
343 }
344 if(!confirm("Jeśli wszystko już poprawisz, wybierz OK w celu " +
345 "wysłania zamówienia lub wybierz Anuluj w celu zrobienia zmian.")) {
346 return false;
347 }
348 alert("Dziękujemy. Już niedługo będziemy mogli skorzystać z Twoich pieniążków.");
349 shoppingBag = new Bag();
350 showStore();
351 return true;
352 }
353
354 function help() {
355 gimmeControl = false;
356 parent.frames[0].location.href = "intro.html";
357 }
358
359 function freshStart() {
360 if(parent.frames[0].location.href != "intro.html") { help(); }
361 }
362
363 //-->
364 </SCRIPT>
365 <TABLE ALIGN=CENTER BORDER=0>
366 <TR>

Przykład 8.4. manager.html (dokończenie)


367 <TD>
368 <A HREF="javascript: gimmeOne();">Daj mi to<A>
369 </TD>
370 <TD>
371 <A HREF="javascript: showBag();">Przegląd/korekta koszyka<A>
372 </TD>
373 <TD>
374 <A HREF="javascript: showStore();">Pokaż wszystkie kategorie<A>
375 </TD>
376 <TD>
377 <A HREF="javascript: portal();">Wyszukiwanie produktów<A>
378 </TD>
379 <TD>
380 <A HREF="javascript: help();">Pomoc<A>
381 </TD>
382 </TR>
383 </TABLE>
384 <TABLE ALIGN=CENTER BORDER=0>
385 <TR>
386 <TD> <!-- Cofnięcie się o jedną kategorię //-->
387 <A HREF="javascript: display(-1,0);">Poprzednia kategoria<A>
388 </TD>
389 <TD> <!-- Cofnięcie się o jeden produkt //-->
390 <A HREF="javascript: display(0,-1);">Poprzedni produkt<A>
391 </TD>
392 <TD> <!-- Jeden produkt do przodu //-->
393 <A HREF="javascript: display(0,1);">Następny produkt<A>
394 </TD>
395 <TD> <!-- Jedna kategoria do przodu //-->
396 <A HREF="javascript: display(1,0);">Następna kategoria<A>
397 </TD>
398 </TR>
399 </TABLE>
400 </BODY>
401 </HTML>
193 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

Jeszcze tylko jedna krótka uwaga. Zauważmy, ile kodu wstawiono do znacznika BODY? Na początku odbywa się
wstępne ładowanie obrazków i tworzenie obiektów. Netscape Navigator w takiej sytuacji może wyświetlić nieciekawe,
szare tło okna (lub w naszym wypadku ramki), dopóki wszystko nie zostanie przygotowane. Dopiero wówczas
przeglądarka może zabrać się za interpretację dalszego ciągu dokumentu. Wtedy interpretowany jest właśnie atrybut
BGCOLOR.

Zmienne
Dalej znajduje się kod, który wyświetla oferowane przez nas produkty. W wierszach 15–18 ustawiane są cztery
zmienne, a w wierszach 53–103 mieści się funkcja display(). Zmienna gimmeControl informuje naszą aplikację,
czy na ekranie jest jakiś produkt, którego mógłby zażyczyć sobie klient. Zmienna browseControl wymusza
przestrzeganie reguły, że przed rozpoczęciem przeglądania oferty użytkownik musi wybrać zestaw wszystkich reguł lub
zacząć od wyszukiwania (zobacz: reguła 1.). Obie zmienne będą używane stale w aplikacji, ale najpierw używa ich di-
splay(), więc przyjrzyjmy im się:
var gimmeControl = false;
var browseControl = false;
var curCLoc = -1;
var curPLoc = -1;

Zmienne curCLoc i curPLoc zawierają indeksy – odpowiednio – bieżącej kategorii i bieżącego produktu. Są to liczby,
o których mówiliśmy w poprzedniej sekcji. Choć obie mają w tej chwili wartość -1, to gdy tylko użytkownik coś
wybierze, ich wartości się zmienią. Więcej o nich powiemy za chwilę. Teraz zobaczmy, jak wygląda funkcja
display() zawarta w wierszach 53–103:
function display(cOffset, pOffset) {
if(!browseControl) {
alert("Zacznij zakupy od wybrania kategorii produktów lub " +
"wyszukując produkty.");
return;
}
gimmeControl = true;
if (curPLoc + pOffset < 0 || curPLoc + pOffset ==
categorySet[curCLoc].prodLine.length) {
if (curPLoc + pOffset < 0) {
if (curCLoc - 1 < 0) { curCLoc = categorySet.length - 1; }
else { curCLoc--; }
curPLoc = categorySet[curCLoc].prodLine.length - 1;
}
else if (curPLoc + pOffset == categorySet[curCLoc].prodLine.length) {
if (curCLoc + 1 == categorySet.length) { curCLoc = 0; }
else { curCLoc++; }
curPLoc = 0;
}
}
else {
if (curCLoc + cOffset < 0 || curCLoc + cOffset ==
categorySet.length) {
curCLoc = (curCLoc + cOffset < 0 ? categorySet.length - 1 : 0);
}
else { curCLoc += cOffset; }
if (cOffset == -1 || cOffset == 1) { curPLoc = 0; }
else if (pOffset == 0) {
curPLoc = (curPLoc >= categorySet[curCLoc].prodLine.length ? 0 :
curPLoc)
}
else { curPLoc = curPLoc + pOffset; }
}
infoStr = '<HTML><HEAD><TITLE>Nazwa produktu</TITLE></HEAD>' +
'<BODY><TABLE CELLPADDING=3><TR><TD VALIGN=TOP COLSPAN=2>' +
'<FONT FACE=Tahoma><H2>Shopping Bag: <I>' +
categorySet[curCLoc].name + '</I></H2><TR>' +
'<TD VALIGN=TOP><IMG SRC="' +
categorySet[curCLoc].prodLine[curPLoc].icon.src +
'"></TD><TD VALIGN=TOP><FONT FACE=Tahoma>' +
'<B>Nazwa: </B>' + categorySet[curCLoc].prodLine[curPLoc].name +
'<BR><B>Opis: </B>' +
categorySet[curCLoc].prodLine[curPLoc].description + '<BR>' +
'<B>Cena: </B> $' +
numberFormat(categorySet[curCLoc].prodLine[curPLoc].price) + '/' +
categorySet[curCLoc].prodLine[curPLoc].unit + '<BR>' +
'<B>PLU: </B>' + categorySet[curCLoc].prodLine[curPLoc].plu +
'</TD></TR></TABLE></BODY></HTML>';
parent.frames[0].location.href =
194

"javascript: parent.frames[1].infoStr";
}

display()
Przed funkcją display() stoją trzy zadania:
1.sprawdzenie, czy można wyświetlić produkt,
2.sprawdzenie, którą kategorię czy produkt użytkownik chce oglądać,
3.wyświetlenie informacji o wybranym produkcie.
Zadanie pierwsze jest proste. Jeśli wartością browseControl jest true, odpowiedź na postawione pytanie jest
twierdząca. browseControl początkowo ma wartość false. Kiedy użytkownik raz już wybierze produkt
za pośrednictwem wyszukiwania lub wybierze jedną z kategorii, wartością browseControl staje się true. Teraz
display() może zabrać się za zadania 2. i 3. Jako że wyświetlany będzie produkt, zmienna gimmeControl jest
ustawiana na true.
Zauważmy, że funkcja display() oczekuje dwóch argumentów, cOffset i pOffset. Jeden zawiera wartość
wskazującą, jak daleko należy przenieść się z bieżącej kategorii, druga, jak daleko trzeba przenieść się względem
bieżącego produktu. Oba parametry mogą być liczbami dodatnimi, ujemnymi, mogą też mieć wartość zero. Aby to
uprościć, załóżmy, że nasza pani Daisy Skąpiradło spełniła warunek reguły 1. i może teraz używać do nawigacji łącz
Poprzedni i Następny. Spójrzmy do kodu obsługującego te łącza, a znajdziesz go w wierszach 386–397:
<TD> <!-- Cofnięcie się o jedną kategorię //-->
<A HREF="javascript: display(-1,0);">Poprzednia kategoria<A>
</TD>
<TD> <!-- Cofnięcie się o jeden produkt //-->
<A HREF="javascript: display(0,-1);">Poprzedni produkt<A>
</TD>
<TD> <!-- Jeden produkt do przodu //-->
<A HREF="javascript: display(0,1);">Następny produkt<A>
</TD>
<TD> <!-- Jedna kategoria do przodu //-->
<A HREF="javascript: display(1,0);">Następna kategoria<A>
</TD>

Każde z tych łączy wywołuje funkcję display()i przekazuje jej parę liczb całkowitych. W tabeli 8.1 wyjaśniono
znaczenie poszczególnych zestawów parametrów. Pamiętajmy, że curCLoc to numer kategorii, a curPLoc to numer
produktu.

Tabela 8.1. Ustawianie wartości curCLoc i curPLoc


Łącze Przekazywane argumenty Interpretacja
Poprzednia kategoria -1, 0 Od curCLoc odejmij 1, curPLoc
bez zmian.
Poprzedni produkt 0, -1 curCLoc bez zmian, od curPLoc
odejmij 1.
Następny produkt 0, 1 curCLoc bez zmian, do curPLoc dodaj 1.
Następna kategoria 1, 0 Do curCLoc dodaj 1, curPLoc bez zmian.

Wyjątki od reguły
To wszystko ma sens. Jeśli chcemy cofnąć się o jedną kategorię, odejmujemy od numeru kategorii bieżącej 1. Jeśli
chcemy przejść do następnego produktu, do numeru bieżącego produktu dodajemy 1. Istnieją jednak trzy wyjątki, które
wymagają specjalnego potraktowania:
1.Nie ma kategorii lub produktu o numerze -1. Jeżeli któryś z numerów wynosi 0 i użytkownik chce się jeszcze
cofnąć, jesteśmy na krawędzi katastrofy.
2.Nie istnieje kategoria o numerze categorySet[categorySet.length]. Jako że kategorii jest
categorySet.length, najwyższy dostępny numer to categorySet.length-1. Jeśli właśnie taki jest numer
kategorii, a użytkownik wybierze następny produkt lub następną kategorię, znowu nie jest najlepiej. To samo
rozumowanie odnosi się też do produktów.
Nawigowanie między kategoriami zawsze pokazuje pierwszy produkt kategorii, niezależnie od tego, jaki produkt
użytkownik oglądał poprzednio.
195 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

W wierszach 60–85 zapisano odpowiedni kod obsługujący te trzy wyjątki. Dość intensywnie używamy tu
zagnieżdżonych instrukcji warunkowych, więc przyjrzyjmy się temu dokładnie:
if (curPLoc + pOffset < 0 || curPLoc + pOffset ==
categorySet[curCLoc].prodLine.length) {
if (curPLoc + pOffset < 0) {
if (curCLoc - 1 < 0) { curCLoc = categorySet.length - 1; }
else { curCLoc--; }
curPLoc = categorySet[curCLoc].prodLine.length - 1;
}
else if (curPLoc + pOffset == categorySet[curCLoc].prodLine.length) {
if (curCLoc + 1 == categorySet.length) { curCLoc = 0; }
else { curCLoc++; }
curPLoc = 0;
}
}
else {
if (curCLoc + cOffset < 0 || curCLoc + cOffset ==
categorySet.length) {
curCLoc = (curCLoc + cOffset < 0 ? categorySet.length - 1 : 0);
}
else { curCLoc += cOffset; }
if (cOffset == -1 || cOffset == 1) { curPLoc = 0; }
else if (pOffset == 0) {
curPLoc = (curPLoc >= categorySet[curCLoc].prodLine.length ? 0 :
curPLoc)
}
else { curPLoc = curPLoc + pOffset; }
}

Poniższy pseudokod powinien nieco lepiej wyjaśnić, co się właściwie dzieje. Numery wierszy w nawiasach odpowiadają
odpowiednim wierszom naszego pliku:
1 JEŚLI numer produktu będzie zbyt mały lub zbyt duży, TO (73)
2 JEŚLI numer produktu będzie zbyt mały, TO (74)
3 JEŚLI numer kategorii będzie zbyt mały, TO
numer kategorii niech będzie liczbą kategorii -1 (75)
4 INACZEJ numer kategorii zmniejsz o 1 (76)
5 numer produktu niech będzie liczbą produktów
w nowej kategorii minus 1 (77)
6 INACZEJ JEŚLI numer produktu będzie zbyt duży, TO (79)
7 JEŚLI numer kategorii będzie zbyt duży, TO
numer kategorii ustaw na 0 (80)
8 INACZEJ numer kategorii zmniejsz o 1 (81)
9 numer produktu niech będzie równy 0 (82)
10 INACZEJ (85)
11 JEŚLI numer kategorii będzie zbyt duży lub zbyt mały, to (86)
12 JEŚLI numer kategorii jest zbyt mały, TO
zmniejsz numer kategorii o 1 (87)
13 INACZEJ numer kategorii ustaw na 0 (88)
14 INACZEJ numer kategorii zwiększ o zadaną wielkość (89)
15 JEŚLI przesunięcie kategorii równe jest -1 lub 1, TO
ustaw numer produktu na 0 (90)
16 INACZEJ JEŚLI przesunięcie produktu równe jest 0, TO (91)
17 JEŚLI numer produktu jest większy bądź równy liczbie
produktów w kategorii,
TO ustaw numer produktu na 0 (92)
18 INACZEJ produkt powiększ o zadane przesunięcie (94)

Najbardziej zewnętrzny blok warunkowy JEŚLI obsługuje zmienne, jeśli numer produktu spełnia jeden z dwóch
pierwszych warunków podanych wyżej. Najbardziej zewnętrzny blok INACZEJ, obsługuje zmienne jeśli jeden z dwóch
pierwszych warunków spełnia numer kategorii. Aby obsłużyć warunek trzeci, w wierszu 80 ustawiamy numer produktu
na zero, jeśli kategoria zmienia się o jeden do przodu lub wstecz.

Tworzenie wyświetlanej strony


Znając numer kategorii i produktu, nasza aplikacja może stworzyć stronę HTML wyświetlającą odpowiedni produkt.
Niemalże cała reszta kodu funkcji display() ma za zadanie umieścić na ekranie wybrany produkt – wiersze 86–102:
infoStr = '<HTML><HEAD><TITLE>Nazwa produktu</TITLE></HEAD>' +
'<BODY><TABLE CELLPADDING=3><TR><TD VALIGN=TOP COLSPAN=2>' +
'<FONT FACE=Tahoma><H2>Shopping Bag: <I>' +
categorySet[curCLoc].name + '</I></H2><TR>' +
'<TD VALIGN=TOP><IMG SRC="' +
categorySet[curCLoc].prodLine[curPLoc].icon.src +
'"></TD><TD VALIGN=TOP><FONT FACE=Tahoma>' +
'<B>Nazwa: </B>' + categorySet[curCLoc].prodLine[curPLoc].name +
196

'<BR><B>Opis: </B>' +
categorySet[curCLoc].prodLine[curPLoc].description + '<BR>' +
'<B>Cena: </B> $' +
numberFormat(categorySet[curCLoc].prodLine[curPLoc].price) + '/' +
categorySet[curCLoc].prodLine[curPLoc].unit + '<BR>' +
'<B>PLU: </B>' + categorySet[curCLoc].prodLine[curPLoc].plu +
'</TD></TR></TABLE></BODY></HTML>';
parent.frames[0].location.href =
"javascript: parent.frames[1].infoStr";

Jak widzimy, wszystko polega na tworzeniu jednego, długiego napisu HTML w początkowo pustej zmiennej infoStr.
Zwróćmy uwagę, jak istotne są zmienne curPLoc i curCLoc przy wybieraniu informacji o produkcie.
categorySet[curCLoc] wskazuje właściwą kategorię, a categorySet [curCLoc].prodLine[curPLoc] odnosi się
do odpowiedniego produktu. Kiedy wartości curCLoc i curPLoc zostaną raz określone, można wyświetlić wszystkie
informacje o produkcie w dowolny już sposób.
Kiedy infoStr zawiera już cały kod potrzebny do wyświetlenia produktu, właściwość href ramki górnej ustawiana
jest przy użyciu protokołu javascript: tak, aby jego wartością była wartość tej zmiennej. Pamiętajmy, że z uwagi
na zasięg protokołu należy podać pełne odwołanie do zmiennej, czyli parent.frames[1].infoStr zamiast
zwykłego infoStr. Szczegóły na ten temat znajdują się w rozdziale 2.

Etap 3. Pokazanie wszystkich kategorii


Wybranie łącza Pokaż wszystkie kategorie to inny sposób przeglądania produktów. Funkcja showStore() z wierszy
33–47 jest do tego zadania odpowiednio przygotowana:
function showStore() {
gimmeControl = false;
var header = '<HTML><TITLE>Kategoria</TITLE><BODY BGCOLOR=FFFFFF>';
var intro = '<H2>Kategorie produktów Shopping Bag</H2><B>';
var footer = '</DL></BLOCKQUOTE></BODY></HTML>';
var storeStr = '<BLOCKQUOTE><DL>';
for (var i = 0; i < categorySet.length; i++) {
storeStr += '<DT><A HREF="javascript: parent.frames[1].reCall(' +
i + ', 0);">' + categorySet[i].name + '</A>' +
'<DD>' + categorySet[i].description + '<BR><BR>';
}
infoStr = header + intro + storeStr + footer;
parent.frames[0].location.replace(
"javascript: parent.frames[1].infoStr");
}

Wyświetlenie pierwszego produktu


Reguła 1. wymaga, aby przy pierwszym (i tylko pierwszym) wyświetlaniu produktów użytkownik użył łącza wszystkich
kategorii lub wyszukiwania produktów. Wyszukiwanie zostanie wkrótce omówione, teraz zajmijmy się pokazywaniem
wszystkich kategorii. Jest to dość proste. Funkcja showStore() po prostu analizuje poszczególne elementy tablicy
categorySet, generując listę łącz z nazwami i opisami tych kategorii. Po dopisaniu ostatniej kategorii zmienna
zawierająca tę listę (czyli infoStr) staje się wartością właściwości href ramki górnej. Zwróćmy uwagę, że każdy atry-
but HREF równy jest:
javascript: parent.frames[1].reCall(' + i + ', 0)

Kliknięcie łącza którejś z kategorii wywoła funkcję reCall() z pliku manager.html. Znajduje się ona w wierszach
105–110:
function reCall(cReset, pReset) {
browseControl = true;
curCLoc = cReset;
curPLoc = pReset;
display(0, 0);
}

Funkcja reCall() spodziewa się dwóch argumentów, numeru kategorii oznaczonego jako i w wierszu 42 i wartości 0.
Wartość i przypisywana jest curCLoc. To określa oczywiście kategorię, którą użytkownik chce obejrzeć. Liczba 0
przypisywana jest zmiennej curPLoc. Przypomnijmy sobie wyjątek od reguł o numerze 3? Zmiana kategorii zawsze
pokazuje użytkownikowi pierwszy jej produkt, czyli prodLine[0].
Kiedy już wszystko to zostanie wykonane, wywoływana jest funkcja display(), która jako argumenty dostaje dwa
zera. Gdy w wierszach 60–85 sprawdzany jest pierwszy warunek, zakładamy, że użytkownik ogląda produkt lub
kategorię o numerze większym bądź mniejszym niż bieżące wartości curCLoc i curPLoc. Jednak funkcja reCall()
197 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

już te wartości ustawiła, więc nie ma potrzeby nigdzie się już przenosić. Użytkownik chce zobaczyć produkt
o parametrach określonych bieżącymi wartościami curCLoc i curPLoc. Przekazanie dwóch zer to właśnie oznacza
i kod z wierszy 60–85 z tym się pogodzi.

Gdzie tu jest DHTML?


Zauważmy, że na stronach produktów w ogóle nie pojawia się DHTML. Nie ma warstw, ale czy nie powinny się tu
pokazać? Większość stosowanych obecnie przeglądarek używa JavaScriptu w wersji 1.2. Kilka rozdziałów tej książki
poświęcono tworzeniu DHTML przenośnego między różnymi przeglądarkami. Dlaczego teraz się z tego wycofywać?
Czy kiedy nasza aplikacja się ładuje, nie moglibyśmy tworzyć warstw dla poszczególnych produktów i następnie je
ukrywać i pokazywać według potrzeb? No tak, moglibyśmy, ale...
Ładowanie wstępne zbyt dużej liczby obrazków może nie wyjść nikomu na zdrowie. Jak wspomniano już wcześniej,
jeśli mamy mnóstwo grafiki, to wszelkiego rodzaju uprzednie jej ładowanie wystawia na próbę cierpliwość odbiorcy.
Tworzenie warstw dla poszczególnych produktów oznacza konieczność załadowania wszystkich obrazków, natomiast
czysty kod HTML świetnie się ze swojego zadania wywiązuje i bez tego.
Teraz użytkownik może swobodnie poruszać się między kategoriami i produktami. Czas zobaczyć, co się dzieje, kiedy
użytkownik zdecyduje się coś kupić, wkładając to do swojego koszyka.

Etap 4. Dodawanie produktów do koszyka


Wkładanie czegokolwiek do koszyka jest proste. Użytkownik klika łącze, nazwane subtelnie Daj mi to. Wywoływana jest
funkcja o nazwie gimmeOne() z wierszy 112–133:
function gimmeOne() {
if (!gimmeControl) {
alert("Nie ma na ekranie nic, co mógłbyś dostać.");
return;
}
for (var i = 0; i < shoppingBag.things.length; i++) {
if (categorySet[curCLoc].prodLine[curPLoc].plu ==
shoppingBag.things[i].plu) {
alert("Już to masz. Ilość możesz zmienić, wybierając " +
"Widok/Zmiana koszyka.");
return;
}
}
shoppingBag.things[shoppingBag.things.length] =
categorySet[curCLoc].prodLine[curPLoc];
shoppingBag.things[shoppingBag.things.length - 1].itemQty = 1;
shoppingBag.things[shoppingBag.things.length - 1].category =
categorySet[curCLoc].name;
alert("W porządku, wkładamy " +
shoppingBag.things[shoppingBag.things.length - 1].name +
" do koszyka.");
}

Funkcja ta najpierw upewnia się, że obecnie na ekranie jest coś, co można włożyć do koszyka. Zmienna
gimmeControl ma wartość true, jeśli tylko wyświetlony zostanie produkt. W przeciwnym wypadku funkcje
wyświetlające na ekranie dane ustawiają tę zmienną na false. W takim wypadku użytkownik dostaje ostrzeżenie
i funkcja gimmeOne() kończy swoje działanie. W przeciwnej sytuacji nasza funkcja przegląda elementy tablicy
things, która jest właściwością obiektu shoppingBag, aby sprawdzić, czy taki produkt nie znajduje się już
u użytkownika w koszyku.
Funkcja gimmeOne() nie oczekuje żadnych argumentów, polega jedynie na wartości zmiennych curCLoc i curPLoc.
Jednak może to prowadzić do ciekawej sytuacji – warto zadać pytanie, czy produkt w koszyku nadal jest obiektem
product? Jeśli ktoś odpowiedział tak, ma rację. Jednak każdy produkt w koszyku użytkownika musi być innym
produktem, o nieco bardziej złożonej strukturze. Nadal ma on nazwę, opis, PLU i cenę, ale poza tym musimy jakoś
określić wielkość danego zakupu i jego kategorię.
Każdy element things powinien wobec tego mieć dodatkowe, dynamicznie dodawane właściwości. W wierszach 125–
129 zobaczysz, jak funkcja gimmeOne() dodaje takie specjalizowane obiekty product do tablicy things i jak
dopisuje im nowe właściwości:
shoppingBag.things[shoppingBag.things.length] =
categorySet[curCLoc].prodLine[curPLoc];
shoppingBag.things[shoppingBag.things.length - 1].itemQty = 1;
shoppingBag.things[shoppingBag.things.length - 1].category =
categorySet[curCLoc].name;
198

shoppingBag.things[shoppingBag.things.length] oznacza odwołanie się do obiektu product określonego


przez categorySet[curCLoc].prodLine[curPLoc]. W ten sposób do koszyka dodawany jest „zwykły” produkt.
Następne dwa wiersze wprowadzają odpowiednio właściwość itemQty (ilość), ustawiając ją początkowo na 1. oraz
category, nazwę kategorii, do której należy dodawany produkt.

Na końcu funkcja gimmeOne() musi jeszcze poinformować użytkownika, że pobranie nowego towaru przebiegło
poprawnie.
Proces ten powtarza się, póki użytkownik chce jeszcze wkładać coś do koszyka, a w końcu przychodzi czas zapłaty.

Techniki języka JavaScript:


dodawanie właściwości obiektów
Istnieje kilka metod dodawania właściwości do obiektów tworzonych przez użytkownika.
Najprostsza metoda to wymyślenie właściwości, jej wartości i dodanie ich do obiektu.
Każdy element tablicy things jest obiektem product, ale produkty te mają mieć dwie
nowe właściwości, itemQty i category. Oto odpowiedni kod:
shoppingBag.things[shoppingBag.things.length - 1].itemQty = 1;
shoppingBag.things[shoppingBag.things.length - 1].category =
categorySet[curCLoc].name;

Obiekty te zostały jednak już utworzone, zatem konieczne jest każdorazowe dodawanie
właściwości, jedna po drugiej. Jeśli będziemy chcieli wszystkim obiektom tworzonym
w przyszłości dodać nowe właściwości, zastosujemy właściwość prototype. Załóżmy,
że chcemy dodać do wszystkich produktów cenę sprzedaży:
product.prototype.salePrice = 0.00;

Wszystkie tworzone obiekty będą teraz miały właściwość salePrice – z wartością do-
myślną 0.00.

Wyszukiwanie produktów
Zauważmy, że wyszukiwanie produktów to tak naprawdę przeszczep z rozdziału 1. Wyszukiwarka działająca po stronie
klienta została zmodyfikowana, aby lepiej odpowiadała potrzebom naszego sklepu. Wszystko jednak odbywa się
bardzo podobnie. Ograniczone zostały możliwości obsługi złożonych warunków wyszukiwania do logicznego LUB.
Jeśli zatem użytkownik wprowadzi jakiś tekst, odnajdowane są wszystkie produkty zawierające choć jedno słowo
ze słów podanych przez użytkownika. Nie mamy logicznej koniunkcji warunków ani wyszukiwania przez adres URL,
ale i tak możliwości naszej wyszukiwarki są tutaj wystarczające. Nie włączano w tym miejscu też żadnych nowych
możliwości, które nie byłyby prezentowane wcześniej, w rozdziale 1. Użytkownicy mogą podać tekst pusty, wciskając
po prostu klawisz ENTER. Realizowane jest wtedy wyszukiwanie puste, które w rezultacie daje wszystkie produkty
z bazy danych. Tym razem nie będziemy już tego kodu omawiali tak szczegółowo, jak w rozdziale 1., ale warto
przeczytać następnych kilka akapitów, aby zobaczyć, jak łatwo można rozszerzać rozsądnie zorganizowane aplikacje
JavaScriptowe. W końcu zresztą użytkownik, tak jak we wszystkich wyszukiwarkach, chce po prostu na podany tekst
uzyskać odpowiedź w postaci ciągu łącz. Aby bez problemu włączyć to do naszego sklepu, musimy wyszukiwarkę
nieco dostosować:
• Wyniki muszą być wyświetlane zgodnie z systemem nawigacji kategoria + produkt, opisanym w etapie 2.
• Musimy być w stanie przeszukiwać istniejącą już bazę towarową.
• Trzeba zwracać łącza do wszystkich produktów z bazy danych.
Na szczęście wszystkie te zmiany można zrealizować w jednym pliku, search/nav.html. Dzięki temu nie trzeba
dokładnie studiować następnych setek wierszy kodu, ale wystarczy się przyjrzeć tylko odpowiednim jego fragmentom.

Odwzorowanie produktów i kategorii


Jeśli wszystko ma działać poprawnie, musimy pewne rzeczy zmienić. Zmiany te dotyczą utworzenia dwóch nowych
zmiennych oraz nowej funkcji:
var ref = top.frames[1];
var prodProfiles = new Array();
kategoria/produkt.
199 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

function genProfile() {
for (var i = 0; i < ref.categorySet.length; i++) {
for (var j = 0; j < ref.categorySet[i].prodLine.length; j++) {
prodProfiles[prodProfiles.length] = new Array(i, j);
}
}
}

Zmienna ref używana jest jako wskaźnik na top.frames[1]. Ponieważ większość obiektów i zmiennych używanych
w tej przeglądarce znajduje się w pliku manager.html, odwoływanie się do obiektu w obiekcie da w efekcie długie
zapisy z mnóstwem kropek. Użycie ref przynajmniej trochę skróci zapis. Zmienna prodProfiles początkowo jest
pustą tablicą, ale wypełniana jest dzięki wywołaniu funkcji genProfile().
Funkcja genProfile() ma tylko jedno zadanie: przygotować metodę odwoływania się do dowolnego obiektu
product z dowolnej kategorii na podstawie indeksów, produktu i kategorii.

Załóżmy na przykład, że zmienna i, numer kategorii, równa jest 1 oraz j, numer produktu, równa jest 2. Jeśli zajrzymy
do inventory.js, zauważymy, że w takim przypadku categorySet[i]. prodLine[j] oznacza igloo z kategorii
buildings, budynki. Przypomina to przecinanie się współrzędnych na mapie.
Zagnieżdżone pętle for w funkcji genProfile() pozwalają przejrzeć kolejne kategorie category(i) oraz produkty
product(j). Możemy powiedzieć, że genProfile() rejestruje w tablicy prodProfiles pary liczb oznaczających
produkty i kategorie.
Ktoś mógłby zadać pytanie, czyż nie w taki sposób odwoływaliśmy się do produktów do tej pory, Owszem, tak, ale
funkcje wyszukiwarki teraz wiedzą, jakie są możliwe kombinacje. Dzięki temu do produktów łatwo jest się odwoływać
(zatem też je wyszukiwać i wyświetlać).

Przeszukiwanie istniejącej bazy danych


Pierwotna wersja przeszukiwała tytuł, opis i adres strony sieciowej. W przypadku naszego sklepu uwzględniamy
podobnie zagadnienia. Problem polega na tym, że musimy to wykonać zgodnie z istniejącą bazą danych. Na szczęście
możemy wprowadzić kilka zmian w funkcji allowAny() z pliku search/nav.html:
function allowAny(t) {
var findings = new Array();
for (var i = 0; i < prodProfiles.length; i++) {
var compareElement =
ref.categorySet[prodProfiles[i][0]].prodLine[prodProfiles[i][1]].
name + " " +
ref.categorySet[prodProfiles[i][0]].prodLine[prodProfiles[i][1]].
description + " " +
ref.categorySet[prodProfiles[i][0]].prodLine[prodProfiles[i][1]].
price.toString() + " " +
ref.categorySet[prodProfiles[i][0]].prodLine[prodProfiles[i][1]].
plu;
compareElement = compareElement.toUpperCase();
for (var j = 0; j < t.length; j++) {
var compareString = t[j].toUpperCase();
if (compareElement.indexOf(compareString) != -1) {
findings[findings.length] = new Array(prodProfiles[i][0],
prodProfiles[i][1]);
break;
}
}
}
verifyManage(findings);
}

Tak naprawdę niewiele się zmieniło. Jednak powinna nas zainteresować zmiana wyszukiwanego obiektu. Użytkownik
może wprowadzić nazwę produktu, jego opis, cenę i PLU. Funkcja allowAny() łączy wszystkie te cztery cechy
produktów w całość i taki napis porównywany jest z przekazanymi do wybierania słowami. Jeśli wszystko do siebie
pasuje, w tablicy findings pojawia się nowy element, będący tablicą o wartościach prodProfiles[i][0]
i prodProfiles[i][1]. Pamiętajmy, że te dwa elementy to liczby całkowite, które będą użyte wkrótce
do wyświetlenia wyników.

Obsługa nawigacji między produktami i kategoriami


Załóżmy, że w wyniku wyszukiwania uzyskaliśmy zestaw odpowiedzi. Teraz musimy go umieścić na stronie. Jeśli
jednak zdecydowałby się zakodować wyświetlanie produktów w wyszukiwarce, ignorując cały układ kategorii
200

i produktów, stałby się szybką ofiarą zapalenia nadgarstka. Kiedy na stronie wynikowej pokazywane będą łącza
do jakichkolwiek produktów, powinniśmy wyświetlać produkty jak zwykle, aby móc następnie standardowo
nawigować między nimi, stosując opcje Poprzedni i Następny. Oto odpowiednie zmiany w funkcji
formatResults():
function formatResults(results, reference, offset) {
docObj.open();
docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki wyszukiwania</TITLE>\n</HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=780 BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' +
'<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP><B>' +
'Zapytanie: <I>' +
parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
'Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ' +
(reference + offset > results.length ? results.length :
reference + offset) +
' z ' + results.length + '</I><BR><BR>' + '<B>' +
'\n\n<!- Początek wyników //-->\n\n\t<DL>');
var currentRecord = (results.length < reference + offset ?
results.length : reference + offset);
for (var i = reference; i < currentRecord; i++) {
docObj.writeln('\n\n\t<DT>' + '<FONT SIZE=4>' +
'<A HREF="javascript: top.frames[1].reCall(' + results[i][0]+
', ' + results[i][1] + ')">' +
ref.categorySet[results[i][0]].prodLine[results[i][1]].name +
'</A></FONT>\t<DD>' +
ref.categorySet[results[i][0]].prodLine[results[i][1]].description +
'\t<DD>' + 'Cena: <I>$' +
ref.numberFormat(ref.categorySet[results[i][0]].
prodLine[results[i][1]].price) +
'</I> &nbsp; &nbsp; &nbsp; ' + 'PLU Number: <I>' +
ref.categorySet[results[i][0]].prodLine[results[i][1]].plu +
'</I><P>');
}
docObj.writeln('\n\t</DL>\n\n<!- End result set //-->\n\n');
prevNextResults(results.length, reference, offset);
docObj.writeln('<HR NOSHADE WIDTH=100%>' +
'</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
docObj.close();
document.forms[0].query.select();
}

Każdy wynik zawiera nazwę produktu, jego opis, cenę i numer PLU. Funkcja przegląda elementy results i pobiera
odpowiednie informacje z prodLine na podstawie liczb results[i][0] i results[i][1]. Innymi słowy, wyniki
wyglądają tak:
results = new Array(
new Array(0, 1), // Pamiętaj, że element 0 oznacza
new Array(2, 2), // kategorię, element 1 oznacza numer
new Array(4, 1) // produktu
);

Zatem wynikiem jest suszarka (kategoria 0, produkt 1), torebka (kategoria 2, produkt 2) oraz frytki (kategoria 4,
produkt 1). Używając takich par liczb, łatwo jest ograniczyć ilość danych i jednocześnie zarejestrować potrzebne nam
fakty.
201 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

Techniki języka JavaScript:


Wielokrotne użycie bazy danych JavaScriptu
Szczęśliwi są ci programiści, którzy mogą użyć danych zaklętych w tablice i obiekty
JavaScriptu, a jeszcze szczęśliwsi ci, którzy mogą sięgnąć do informacji innych aplikacji
bez konieczności powtórnego kodowania wszystkiego. Tak właśnie dzieje się w
przypadku wyszukiwarki produktów w naszej aplikacji. Z uwagą na jej dość prostą
strukturę, nie trzeba przebudowywać bazy danych w celu jej przeszukiwania.
Wystarczy kilka zmienionych wierszy kodu w wyszukiwarce i wszystko działa wspaniale.
Jeśli ktoś chce sięgać do swoich danych w bazie z różnych aplikacji, musi pamiętać, że
tylko zachowanie iście spartańskiej prostoty pozwoli tych danych używać bez potrzeby
dodatkowego kodowania.
W celu wyświetlania produktów na ekranie, potrzebujemy pary liczb. Pętla for w format-
Results() pokazuje nazwy, opisy, ceny i numery PLU, używając właśnie tych liczb:
ref.categorySet[results[i][0]].prodLine[results[i][1]].name
ref.categorySet[results[i][0]].prodLine[results[i][1]].description
ref.numberFormat(ref.categorySet[results[i][0]].
prodLine[results[i][1]].price
ref.categorySet[results[i][0]].prodLine[results[i][1]].plu

Każdy wynik wyświetlany będzie jako tekst podobny do tego poniżej:


Hairdrye
Kolorowe, żółte wzornictwo, trwały kabel. Dobry zakup.
Cena: $1.15 Numer PLU: HAI1

Kod w łączach
Wyniki zostały wyświetlone, ale jak teraz to zakodować, aby w łączu użyty został system nawigacyjny, o którym tyle
mówiono?
W funkcji formatResults() znajdziemy następujące rozwiązanie:
'<A HREF="javascript: top.frames[1].reCall(' + results[i][0]+
', ' + results[i][1] + ')">'

W każdym łączu używany jest protokół javascript: wywołujący funkcję reCall(), będącą tą samą funkcją, która
jest używana z do oglądania produktów w przypadku przeglądania wszystkich kategorii. Jak zapewne pamiętamy,
funkcja reCall() oczekuje dwóch argumentów: numeru kategorii i numeru produktu. W taki sposób używaliśmy tego
w naszej wyszukiwarce. Pozostaje tylko jedno: włączyć każdy z elementów z par liczb do wywołania, i gotowe. Zatem
suszarka (hairdryer) ma następujące łącze:
'<A HREF="top.frames[1].reCall(0, 1)">Hairdryer</A>

Spójrz teraz, co się dzieje z liczbami 0 i 1, kiedy pojawiają się w wywołaniu reCall():
function reCall(cReset, pReset) {
browseControl = true;
curCLoc = cReset;
curPLoc = pReset;
display(0, 0);
}

Zmienna curCLoc otrzymuje wartość kategorii z cReset, podobnie curPLoc numer produktu z pReset.
Wyszukiwarka świetnie koegzystuje z całą resztą aplikacji i naprawdę wymaga niewiele modyfikacji.

Etap 5. Zmiana zamówienia, płacenie


Kiedy użytkownik nie ma już ani grosza lub niczego więcej nie chce, czas skierować się ku drzwiom. Kliknięcie łącza
Przegląd/korekta koszyka powoduje wyświetlenie ekranu podobnego, jak na rysunku 8.7. Koszyk użytkownika nie może
ograniczyć się tylko do wyświetlenia swojej zawartości, ale musi też spełniać następujące wymagania:
• Wyświetlać każdy produkt z koszyka, jego kategorię, numer PLU i cenę jednostkową.
202

• Zapewnić interaktywny formularz, w którym można będzie zmienić ilości produktów, usuwać produkty
i przeliczać wartości.
• Wyświetlać bieżące sumy dla określonych wielkości zakupów, sumy pośrednie i wszelkie odpowiednie
podatki.
Nie powinno zatem zaskakiwać, że jest tu także kilka funkcji gotowych, aby zaraz zacząć pełnić swoje zadania
w Shopping Bag. Oto one:
showBag()
Wyświetla zawartość koszyka.
genSelect()
Generuje dynamiczną listę wyboru umożliwiającą zmianę ilości produktów.
runningTab()
Steruje obliczeniami i wyświetla ceny i wartości.
numberFormat()
Zapewnia dokładność obliczeń i jednolity sposób wyświetlania, w formacie 0.00.
round()
Zapewnia potrzebną dokładność obliczeń.
changeBag()
Usuwa wybrane przez użytkownika produkty i zmienia ich zamawiane ilości.
Funkcja showBag() wywoływana jest, gdy tylko użytkownik wybierze odpowiednie łącze. Znajdziemy ją w wierszach
135–198:
function showBag() {
if (shoppingBag.things.length == 0) {
alert("Twój koszyk jest obecnie pusty. Włóż coś do niego.");
return;
}
gimmeControl = false;
var header = '<HTML><HEAD><TITLE>Twój koszyk</TITLE>' +
'</HEAD><BODY BGCOLOR=FFFFFF ' +
onLoad="parent.frames[1].runningTab(document.forms[0]);">';
var intro = '<H2>Twój koszyk!!!</H2>' +
'<FORM onReset="' +
'setTimeout(\'parent.frames[1].runningTab(document.forms[0])\', ' +
'25);">';
var tableTop = '<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=5>' +
'<TR><TH><B>Index' +
'<TH><B>Produkt<TH><B>Kategoria' +
'<TH><B>PLU<TH><B>Cena jednostkowa' +
'<TH><B>Ilość<TH><B>Kwota' +
'<TH><B>Usuń' +
'</TR>';
var itemStr = '';
for (var i = 0; i < shoppingBag.things.length; i++) {
itemStr += '<TR>' +
'<TD ALIGN=CENTER>' + (i + 1) + '</TD>' +
'<TD>' + shoppingBag.things[i].name + '</TD>' +
'<TD>' + shoppingBag.things[i].category + '</TD>' +
'<TD>' + shoppingBag.things[i].plu + '</TD>' +
'<TD ALIGN=RIGHT>$' +
parent.frames[1].numberFormat(shoppingBag.things[i].price) +
'</TD>' +
'<TD ALIGN=CENTER>' +
parent.frames[1].genSelect(shoppingBag.things[i].price,
shoppingBag.things[i].itemQty, i) + '</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 VALUE="' +
parent.frames[1].numberFormat(shoppingBag.things[i].price *
shoppingBag.things[i].itemQty) +
'" onFocus="this.blur();"></TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=CHECKBOX></TD>' +
'</TR>';
}
var tableBottom = '<TR>' +
'<TD ALIGN=RIGHT COLSPAN=6>SubTotal:</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="subtotal" ' +
'onFocus="this.blur();"></TD></TR>' +
'<TR>' + '<TD ALIGN=RIGHT COLSPAN=6> + 6% Tax:</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="tax" ' +
203 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

'onFocus="this.blur();"></TD></TR><TR><TD ALIGN=RIGHT COLSPAN=6>' +


'2% Shipping:</TD><TD ALIGN=CENTER><INPUT TYPE=TEXT ' +
'SIZE=10 NAME="ship" onFocus="this.blur();"></TD></TR>' +
'<TR>' +
'<TD ALIGN=RIGHT COLSPAN=3><INPUT TYPE=BUTTON VALUE="Do kasy" ' +
'onClick="parent.frames[1].checkOut(this.form);"></TD>' +
'<TD ALIGN=RIGHT><INPUT TYPE=RESET VALUE="Wyzeruj ilości"></TD>' +
'<TD ALIGN=RIGHT><INPUT TYPE=BUTTON VALUE="Zmiana koszyka" ' +
'onClick="parent.frames[1].changeBag(this.form, true);"></TD>' +
'<TD ALIGN=RIGHT>Suma:</TD><TD ALIGN=CENTER>' +
'<INPUT TYPE=TEXT NAME="total" SIZE=10 onFocus="this.blur();">' +
'</TD></TR>';

var footer = '</TABLE></FORM></BODY></HTML>';


infoStr = header + intro + tableTop + itemStr + tableBottom + footer;
parent.frames[0].location.replace(
'javascript: parent.frames[1].infoStr');
}

Jak sami się przekonamy, funkcja showBag() nie wykonuje właściwie nic poza wygenerowaniem tablicy i formularza
z rysunku 8.7. Najpierw jednak funkcja musi sprawdzić, czy w koszyku w ogóle cokolwiek się znajduje:
if (shoppingBag.things.length == 0) {
alert("Twój koszyk jest obecnie pusty. Włóż coś do niego.");
return;
}

Jeśli things.length równe jest 0, oznacza to, że użytkownik niczego do koszyka nie włożył i nie ma już nic
do roboty. Jeśli koszyk zawiera cokolwiek, możemy iść dalej. W wierszach 140–154 ustawiane są zmienne header,
intro i tableTop, zawierające górną część tabeli z nagłówkami i kolumnami. Jak widać, showBag() wywołuje kilka
innych funkcji:
gimmeControl = false;
var header = '<HTML><HEAD><TITLE>Twój koszyk</TITLE>' +
'</HEAD><BODY BGCOLOR=FFFFFF ' +
onLoad="parent.frames[1].runningTab(document.forms[0]);">';
var intro = '<H2>Twój koszyk!!!</H2>' +
'<FORM onReset="' +
'setTimeout(\'parent.frames[1].runningTab(document.forms[0])\', ' +
'25);">';
var tableTop = '<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=5>' +
'<TR><TH><B>Index' +
'<TH><B>Produkt<TH><B>Kategoria' +
'<TH><B>PLU<TH><B>Cena jednostkowa' +
'<TH><B>Ilość<TH><B>Kwota' +
'<TH><B>Usuń' +
'</TR>';

Zauważmy, że generowany jest tylko zwykły kod statyczny, jedynie obsługa zdarzenia onLoad polega na wywołaniu
parent.frames[1].runningTab() – za chwilę sprawdzimy, jak to działa. Kiedy już ustalona zostanie zawartość
nagłówka tabeli, czas przejrzeć kolejno produkty włożone przez użytkownika do koszyka. Jak można się domyślić, funkcja
showBag() realizuje pętlę z things. length kroków i tworzy w ten sposób w każdym kroku jeden wiersz danych. Oto kod
z wierszy 155–174:
var itemStr = '';
for (var i = 0; i < shoppingBag.things.length; i++) {
itemStr += '<TR>' +
'<TD ALIGN=CENTER>' + (i + 1) + '</TD>' +
'<TD>' + shoppingBag.things[i].name + '</TD>' +
'<TD>' + shoppingBag.things[i].category + '</TD>' +
'<TD>' + shoppingBag.things[i].plu + '</TD>' +
'<TD ALIGN=RIGHT>$' +
parent.frames[1].numberFormat(shoppingBag.things[i].price) +
'</TD>' +
'<TD ALIGN=CENTER>' +
parent.frames[1].genSelect(shoppingBag.things[i].price,
shoppingBag.things[i].itemQty, i) + '</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 VALUE="' +
parent.frames[1].numberFormat(shoppingBag.things[i].price *
shoppingBag.things[i].itemQty) +
'" onFocus="this.blur();"></TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=CHECKBOX></TD>' +
'</TR>';
}
204

Tworzenie list wyboru


Aby dopasować się do istniejących już nagłówków tabeli, pętla for tworzy kolumny z indeksem towarów (tak, abwy
można było produkty kolejno zliczać), nazwą, kategorią, PLU, ceną jednostkową, listą wyboru z ilością, łączną
wartością oraz polem opcji, pozwalającym usunąć dany produkt. Każda z tych wartości ma swój własny znacznik TD.
Stworzenie listy wyboru ilości jest nieco bardziej złożone niż zwykle i warto się temu procesowi dokładniej przyjrzeć.
Lista tworzona jest przez funkcję genSelect(). W tej książce znajdują się też inne wersje tejże funkcji, które zapewne
są już znane czytelnikowi. Oto wersja używana obecnie – z wierszy 200–210:
function genSelect(priceAgr, qty, idx) {
var selStr = '<SELECT onChange="this.form.elements[' + (idx * 3 + 1) +
'].value = this.options[this.selectedIndex].value; ' +
'parent.frames[1].runningTab(this.form);">';
for (var i = 1; i <= 10; i++) {
selStr += '<OPTION VALUE="' + numberFormat(i * priceAgr) + '"' +
(i == qty ? ' SELECTED' : '') + '>' + i;
}
selStr += '</SELECT>';
return selStr;
}

Funkcja ta oczekuje trzech parametrów: ceny towaru, dotychczasowej ilości i liczby (będącej wartością zmiennej i pętli
for), pozwalającej sięgnąć do pola tekstowego, które będzie pokazane zaraz listą wyboru. Wstępnie można ustawić
dopuszczalną wielkość zamówienia na 10, jak i dowolnie zwiększyć tę wielkość. Aby utworzyć listę, genSelect()
przechodzi przez kolejne liczby od 1 do 10 i tworzy znaczniki OPTION zgodnie z podaną niżej składnią, przypisując
wynik przyrostowo do zmiennej selStr:
selStr += '<OPTION VALUE="' + numberFormat(i * priceAgr) + '"' +
(i == qty ? ' SELECTED' : '') + '>' + i;

Poszczególne znaczniki OPTION są proste. Wartością atrybutów VALUE jest cena jednostkowa przemnożona przez i, czyli
liczbę związaną z daną opcją. Na przykład produkt w cenie 1 dolara da następujące znaczniki OPTION:
<OPTION VALUE="1.00" SELECTED>1
<OPTION VALUE="2.00">2
<OPTION VALUE="3.00">3
<OPTION VALUE="4.00">4
<OPTION VALUE="5.00">5
<OPTION VALUE="6.00">6
<OPTION VALUE="7.00">7
<OPTION VALUE="8.00">8
<OPTION VALUE="9.00">9
<OPTION VALUE="10.00">10

Jeśli i równe jest obecnej ilości danego produktu (qty), odpowiedni znacznik otrzymuje atrybut SELECTED. Jako że
domyślna ilość wszystkich produktów wynosi 1, początkowo zawsze będzie wybrana opcja z tekstem „1”. Jest to bardzo
użyteczne, kiedy ilości te będą się zmieniały, czym za chwilę się zajmiemy.
Pominięto przedtem wartość początkową selStr, dlatego warto teraz do tego wrócić:
var selStr = '<SELECT onChange="this.form.elements[' + (idx * 3 + 1) +
'].value = this.options[this.selectedIndex].value; ' +
'parent.frames[1].runningTab(this.form);">';

Każda lista wyboru związana jest z procedurą obsługi zdarzenia onChange, która zmienia wartość elements[(idx*3)+1]
na wartość opcji właśnie wybranej. Pamiętajmy, że każda wartość OPTION jest iloczynem ceny jednostkowej produktu
przez liczbę 1 do 10. Jeśli w przypadku przykładu z towarem za jednego dolara użytkownik wybierze z listy wartość 4,
wartość elements[(idx*3)+1] zmieni się na 4.00. Jest to trochę skomplikowane. Właściwie z którym elementem
formularza mamy do czynienia? Aby odpowiedzieć na to pytanie, zajrzyjmy do kodu z wierszy 166–167:
parent.frames[1].genSelect(shoppingBag.things[i].price,
shoppingBag.things[i].itemQty, i)

Teraz spójrzmy na argumenty, jakich w wierszu 200 oczekuje genSelect():


function genSelect(priceAgr, qty, idx) {

Jak widać, wartością idx zawsze jest bieżąca wartość i, inicjalizowana i zwiększana o 1 w wierszu 16. Jeśli
użytkownik ma w koszyku 10 produktów, idx będzie miał wartości od 1 do 10, zatem znaczniki wyboru utworzone
w showBage() dadzą w wyniku coś takiego:
<!-- Dla pierwszego produktu //-->
<SELECT onChange='this.form.elements[1].value =
this.options[this.selectedIndex].value;
205 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

parent.frames[1].runningTab(this.form);'>
<!-- Dla drugiego produktu //-->
<SELECT onChange='this.form.elements[4].value =
this.options[this.selectedIndex].value;
parent.frames[1].runningTab(this.form);'>
<!-- Dla trzeciego produktu //-->
<SELECT onChange='this.form.elements[10].value =
this.options[this.selectedIndex].value;
parent.frames[1].runningTab(this.form);'>

...i tak dalej. Zastanówmy się: form.elements[1] to tekst pola tuż za pierwszą listą wyboru, a przynajmniej tak
będzie, bo w chwili tworzenia obsługi zdarzenia onChange pole to jeszcze nie istniało. form.elements[4] odnosi się
do pola tekstowego zaraz za listą wyboru w następnym wierszu. Odwołanie się do pola tekstowego to kwestia
wyliczenia, jaki indeks zostanie mu przypisany już po utworzeniu formularza. I tak właśnie doszliśmy do wyrażenia
(idx*3)+1.

Każdy wybrany produkt wyświetlany jest w jednym wierszu tabeli, każdy wiersz zawiera trzy elementy formularza
w jednakiej kolejności:
• listę wyboru ilości,
• pole tekstowe wyświetlające łączną wartość produktu,
• pole opcji pozwalające produkt usunąć.
Oznacza to, że pierwsze pole tekstowe to elements[1], a następne to elements[4]. Pole tekstowe jest drugim
elementem każdej trzyelementowej grupy. Funkcja genSelect() tworzy odpowiedni kod, mnożąc indeks za każdym
razem przez 3 i dodając 1.

Zapisywanie rachunku
A co z resztą kodu obsługi zdarzenia onChange? Nie tylko pokazywane jest podsumowanie danego produktu, ale też
wywoływana jest funkcja runningTab(), przeliczająca ogólną sumę zakupu. Oto funkcja runningTab() z wierszy
212–227:
function runningTab(formObj) {
var subTotal = 0;
for (var i = 0; i < shoppingBag.things.length; i++) {
subTotal += parseFloat(formObj.elements[(i * 3) + 1].value);
}
formObj.subtotal.value = numberFormat(subTotal);
formObj.tax.value = numberFormat(subTotal * shoppingBag.taxRate);
formObj.ship.value = numberFormat(subTotal * shoppingBag.shipRate);
formObj.total.value = numberFormat(subTotal +
round(subTotal * shoppingBag.taxRate) +
round(subTotal * shoppingBag.shipRate));
shoppingBag.subTotal = formObj.subtotal.value;
shoppingBag.taxTotal = formObj.tax.value;
shoppingBag.shipTotal = formObj.ship.value;
shoppingBag.bagTotal = formObj.total.value;
}

Funkcja ta jest całkiem prosta, gdyż realizuje trzy podstawowe operacje:


1.wylicza i wyświetla sumę pośrednią, która jest sumą wszystkich sum produktów (wiersze 213–217),
2. wylicza i wyświetla podatek od sprzedaży, koszty wysyłki i sumę całkowitą (wiersze 218–222),
3.zapisuje sumy we właściwościach obiektu shoppingBag (wiersze 223–226).
Funkcje numberFormat() i round() zapewniają, że wszelkie wyliczenia matematyczne będą wykonywane dokładnie
i wyświetlane w postaci 0.00 lub .00. Spójrzmy na wiersze 229–239:
function numberFormat(amount) {
var rawNumStr = round(amount) + '';
rawNumStr = (rawNumStr.charAt(0) == '.' ? '0' + rawNumStr : rawNumStr);
if (rawNumStr.charAt(rawNumStr.length - 3) == '.') {
return rawNumStr
}
else if (rawNumStr.charAt(rawNumStr.length - 2) == '.') {
return rawNumStr + '0';
}
else { return rawNumStr + '.00'; }
}
206

Funkcja numberFormat() po prostu zwraca zaokrągloną wartość amount w formacie 0.00. Do zaokrąglenia służy funkcja
round(), która jest wywoływana z amount jako parametrem. Funkcja round() zaokrągla liczbę domyślnie do dwóch
miejsc dziesiętnych po przecinku w taki oto sposób:
function round(number,decPlace) {
decPlace = (!decPlace ? 2 : decPlace);
return Math.round(number * Math.pow(10,decPlace)) /
Math.pow(10,decPlace);
}

Techniki języka JavaScript:


zaokrąglanie liczb i konwersja napisów
Można by pomyśleć, że wymnożenie w JavaScripcie ceny 1,15 przez 3 paczki frytek nie
powinno być wielkim wyzwaniem, gdyż wszyscy doskonale wiemy, że jest to po prostu
3,45. No dobrze, spróbujmy (do pokazania wyniku użyjmy funkcji alert()).
Otrzymamy zaskakujący wynik: 3.4499999999999997. Skąd się wzięło takie cudo?
JavaScript przechowuje liczby zmiennoprzecinkowe jako liczby 64-bitowe zgodne
ze standardem IEEE-754. Z powodu takiego właśnie zapisu w pewnych sytuacjach mogą
pojawiać się takie różnice. W celu uzyskania bliższych informacji na ten temat warto
zajrzeć pod adresy:
http://help.netscape.com/kb/client/970930-1.html
http://www.psc.edu/general/software/packages/ieee/ieee.html
Niezależnie od tego, jaka jest przyczyna powyższego stanu rzeczy, musimy znaleźć jakieś
rozwiązanie zastępcze. Może by tak poprosić JavaScript o wymnożenie 115 * 3? Otrzy-
mujemy stukrotność oczekiwanego wyniku, czyli 345, zatem wszystko jest w porządku.
Skoro poprawnie działa zatem arytmetyka całkowitoliczbowa, to możemy z niej skorzy-
stać, po czym wynik przekształcimy z typu Number na String, dodając przy tym w od-
powiednim miejscu przecinek dziesiętny. Tak właśnie działają funkcje numberFormat()
i round(). Jeśli musimy zrobić jakieś dalsze wyliczenia, z powrotem przekształcamy na-
pis na liczbę, usuwamy kropkę dziesiętną i liczymy.
Jeśli argument amount, będący liczbą równy jest samemu sobie po zaokrągleniu funkcją
Math.round(), to jest liczbą całkowitą, zatem należy dodać mu na końcu .00, aby uzyskać
dane w odpowiednim formacie. Gdy amount*10 równe jest Math.round (amount*10), to
jest to liczba w postaci 0.0 i należy do niej dodać jedno zero. W przeciwnym wypadku
przekazany parametr ma co najmniej części setne (.00) i żadne operacje na tekstach nie są
potrzebne.

Opakowanie showBag(): pokazywanie podsumowań


Teraz każdy produkt ma swój własny wiersz w tabeli z odpowiednimi kontrolkami na wyliczenia i do usuwania tego
produktu Czas dodać kilka ostatnich wierszy. Zawierają one pola formularza pozwalające wyświetlić sumy pośrednie,
łączne podatki i sumę całkowitą. Pojawiają się też tam przyciski Do kasy, Wyzeruj ilości oraz Zmiana koszyka. Oto
wiersze 175–194:
var tableBottom = '<TR>' +
'<TD ALIGN=RIGHT COLSPAN=6>SubTotal:</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="subtotal" ' +
'onFocus="this.blur();"></TD></TR>' +
'<TR>' + '<TD ALIGN=RIGHT COLSPAN=6> + 6% Tax:</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="tax" ' +
'onFocus="this.blur();"></TD></TR><TR><TD ALIGN=RIGHT COLSPAN=6>' +
'2% Shipping:</TD><TD ALIGN=CENTER><INPUT TYPE=TEXT ' +
'SIZE=10 NAME="ship" onFocus="this.blur();"></TD></TR>' +
'<TR>' +
'<TD ALIGN=RIGHT COLSPAN=3><INPUT TYPE=BUTTON VALUE="Do kasy" ' +
'onClick="parent.frames[1].checkOut(this.form);"></TD>' +
'<TD ALIGN=RIGHT><INPUT TYPE=RESET VALUE="Wyzeruj ilości"></TD>' +
'<TD ALIGN=RIGHT><INPUT TYPE=BUTTON VALUE="Zmiana koszyka" ' +
'onClick="parent.frames[1].changeBag(this.form, true);"></TD>' +
'<TD ALIGN=RIGHT>Suma:</TD><TD ALIGN=CENTER>' +
'<INPUT TYPE=TEXT NAME="total" SIZE=10 onFocus="this.blur();">' +
207 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

'</TD></TR>';
var footer = '</TABLE></FORM></BODY></HTML>';

Oglądając ten kod, nietrudno zauważyć, że pola wyświetlające sumy początkowo są puste. Wywołanie runningTab()
w ramach obsługi zdarzenia onLoad wstawia do tych pól odpowiednie wartości. Zauważmy też, że w każdym z pól
znajduje się taki kod:
onFocus='this.blur();'

Jako że nie należy pozwalać użytkownikowi na modyfikowanie zawartości tych pól, kliknięcie myszą w to pole
powoduje i tak utratę przez nie kursora, dzięki czemu nasi klienci nie mogą sobie dowolnie modyfikować wyliczonych
danych.
Teraz przyjrzyjmy się trzem naszym przyciskom.

Przycisk Do kasy
Kiedy użytkownik ma już dość kupowania, musi przekazać dane o sposobie płatności i wysłać zamówienie. Kliknięcie
omawianego klawisza powoduje wywołanie funkcji checkOut(). Funkcja ta wykonuje dwie rzeczy:
1.generuje formularz zamówienia z danymi o płatności,
2.generuje dodatkowe pola ukryte (HIDDEN), które odpowiadają wszystkim wybranym produktom.
Funkcja ta jest długa, więc podzielimy ją na dwie części. Oto wiersze 263-312:
function checkOut(formObj) {
gimmeControl = false;
if(!confirm("Czy masz już wszystko, czego potrzebujesz, " +
"w potrzebnych Ci ilościach? Pamiętaj, że do usunięcia czegoś " +
"lub zmiany ilości musisz wybrać przycisk zmiany. Jeśli jesteś " +
"gotów, wciśnij OK.")) {
return;
}
if(shoppingBag.things.length == 0) {
showStore();
return;
}
var header = '<HTML><TITLE>Shopping Bag - płatności</TITLE>' +
'<BODY BGCOLOR=FFFFFF>';

var intro = '<H2>Shopping Bag - płatności</H2><FORM METHOD=POST ' +


'ACTION="http://www.serve.com/hotsyte/cgi-bin/bag.cgi" ' +
'onSubmit="return parent.frames[1].cheapCheck(this);">';

var shipInfo = '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=5>' +


'<TR><TD><B>Informacje o wysyłce</TD></TR>'+
'<TR><TD>Imię</TD>' + '<TD><INPUT TYPE=TEXT NAME="fname"></TD>' +
'</TR><TR><TD>Nazwisko</TD>' +
'<TD><INPUT TYPE=TEXT NAME="lname"></TD></TR><TR><TD>Firma</TD>' +
'<TD><INPUT TYPE=TEXT NAME="cname"></TD></TR><TR>' +
'<TD>adres - ulica I</TD><TD><INPUT TYPE=TEXT NAME="saddress1">' +
'</TD></TR><TR><TD>adres - ulica II</TD>' +
'<TD><INPUT TYPE=TEXT NAME="saddress2"></TD></TR><TR>' +
'<TD>Miasto</TD>' + '<TD><INPUT TYPE=TEXT NAME="city"></TD></TR>' +
'<TR><TD>Województwo/region</TD>' +
'<TD><INPUT TYPE=TEXT NAME="stpro"></TD></TR><TR>' +
'<TD>Kraj</TD>' + '<TD><INPUT TYPE=TEXT NAME="country"></TD></TR>' +
'<TR><TD>Kod pocztowy</TD><TD><INPUT TYPE=TEXT NAME="zip"></TD>' +
'</TR><TR><TD><BR><BR></TD></TR></TABLE>';
var payInfo = '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=5>' +
'<TR><TD><B>Informacje o płatności</TD></TR>'+
'<TR><TD>Typ karty kredytowej &nbsp; &nbsp; &nbsp; </TD>' +
'<TD>Visa <INPUT TYPE=RADIO NAME="ctype" VALUE="visa" CHECKED>' +
&nbsp; &nbsp; &nbsp; ' +
'Amex <INPUT TYPE=RADIO NAME="ctype" VALUE="amex"> ' +
'&nbsp; &nbsp; &nbsp; ' +
'Discover <INPUT TYPE=RADIO NAME="ctype" VALUE="disc"> ' +
&nbsp; &nbsp; &nbsp; </TD>' + '</TR>' +
'<TR><TD>Numer karty kredytowej</TD>' +
'<TD><INPUT TYPE=TEXT NAME="cnumb"></TD></TR><TR>' +
'<TD>Data ważności</TD><TD><INPUT TYPE=TEXT NAME="edate"></TD>' +
'</TR><TR><TD><INPUT TYPE=SUBMIT VALUE="Wyślij zamówienie"></TD>' +
'<TD><INPUT TYPE=RESET VALUE="Wyczyść dane"></TD>' + '</TR>' +
'</TABLE>';
208

To długi fragment, ale jest to tylko statyczny kod HTML. Generowany jest formularz płatności, jaki pokazano
na rysunku 8.8. Formularz zawiera pola, w których użytkownik może wpisać podstawowe dane dotyczące płatności.
Każde z tych pól ma niepowtarzalną nazwę, dzięki czemu skrypt działający na serwerze może poszczególne informacje
prawidłowo zinterpretować.
Ostatnia część funkcji checkOut() przygotowuje pola HIDDEN, w których będą rejestrowane produkty wybrane
przez użytkownika. Poniżej przedstawiono wiersze 314–319:
var itemInfo = '';
for (var i = 0; i < shoppingBag.things.length; i++) {
itemInfo += '<INPUT TYPE=HIDDEN NAME="prod' + i +
'" VALUE="' + shoppingBag.things[i].plu + '-' +
shoppingBag.things[i].itemQty + '">';
}

W ten sposób generowane są pola HIDDEN o nazwie prod uzupełnionej wartością zmiennej i. Wartość składniowo
zgodna jest z PLU-ilość. Jeśli zatem użytkownik zażyczy sobie dwóch torebek frytek, pole HIDDEN ustawione
zostanie na VALUE="FRI1-2". Kiedy pola HIDDEN zostaną już stworzone, funkcja checkOut() zbierze wszystkie
wartości sum z tych pól – wiersze 320–327:
var totalInfo = '<INPUT TYPE=HIDDEN NAME="subtotal" VALUE="' +
shoppingBag.subTotal + '">' +
'<INPUT TYPE=HIDDEN NAME="taxtotal" VALUE="' +
shoppingBag.taxTotal + '">' +
'<INPUT TYPE=HIDDEN NAME="shiptotal" VALUE="' +
shoppingBag.shipTotal + '">' +
'<INPUT TYPE=HIDDEN NAME="bagtotal" VALUE="' +
shoppingBag.bagTotal + '">';

Dodajemy teraz jeszcze przyciski Wyślij zamówienie i Wyczyść dane (pozwalający wyzerować cały formularz
zamówienia). Zanim przejdziemy dalej, zwróćmy uwagę, że obsługa zdarzenia onSubmit tego – wygenerowanego w locie
– formularza wywołuje funkcję cheapCheck(). Funkcja ta nie robi nic poza sprawdzeniem, czy w przesyłanym
formularzu nie pozostało puste żadne z pól, których wypełnienie jest obowiązkowe. Oto wspomniana funkcja z wierszy
337-352:
function cheapCheck(formObj) {
for (var i = 0; i < formObj.length; i++) {
if (formObj[i].type == "text" && formObj.elements[i].value == "") {
alert ("Musiszy wypełnić wszystkie pola.");
return false;
}
}
if(!confirm("Jeśli wszystko już poprawisz, wybierz OK w celu " +
"wysłania zamówienia lub wybierz Anuluj w celu zrobienia zmian.")) {
return false;
}
alert("Dziękujemy. Już niedługo będziemy mogli skorzystać z Twoich pieniędzy.");
shoppingBag = new Bag();
showStore();
return true;
}

Jeśli którekolwiek z wymaganych pól jest puste, funkcja cheapCheck() ostrzega o tym użytkownika i zwraca wartość
false, co powoduje zablokowanie wysyłania formularza. Zapewne zechcemy zaimplementować funkcję
przeprowadzającą dokładniejszą kontrolę zawartości formularza, ale tak przynajmniej mamy jakiś punkt wyjściowy.
Zwróćmy też uwagę, że jeśli użytkownik poprawnie wypełnił formularz, wartością zmiennej shoppingBag staje się
nowy obiekt Bag(), a użytkownik jest znowu kierowany do strony z wszystkimi kategoriami oferowanych towarów, co
realizuje funkcja showStore().

Koniec wyświetlania
Teraz, kiedy zmienne header, intro, tableTop, itemStr, totalInfo, tableBottom i footer mają odpowiednie
wartości umożliwiające wyświetlenie strony, funkcja zestawia informacje na ekranie – wiersze 195–197:
infoStr = header + intro + tableTop + itemStr + tableBottom + footer;
parent.frames[0].location.replace(
'javascript: parent.frames[1].infoStr');
209 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

A po stronie serwera?
Po stronie klienta zakupy już się skończyły. Jak teraz użytkownik ma zapłacić za towary i jaką drogą je otrzyma? Potrzebny
będzie na pewno jeszcze jakiś program przetwarzający dane po stronie serwera, który zarejestruje zamówienia na przykład
w bazie danych. Autor załączył do plików prosty skrypt CGI języka Perl, który tworzy pliki ASCII opisujące
poszczególne transakcje i zapisuje wszystkie produkty i dane o płatnościach w tych plikach. Chodzi o plik
\ch08\bag.cgi. Poniższa procedura wskazuje, jak ten plik uruchomić. Zwróćmy uwagę, czy na naszym serwerze sieciowym
zainstalowany jest Perl i katalog, w którym znajduje się plik bag.pl, ma ustawione prawa do zapisu i wykonania.
1.Skopiuj plik bag.cgi do katalogu, z którego uruchamiasz skrypty CGI (na przykład cgi-bin).
2.W wierszu 279 zmień wartość atrybutu ACTION na adres URL, pod jakim dostępny jest plik bag.cgi.
Kiedy użytkownik zdecyduje się złożyć zamówienie, uruchomiony zostanie skrypt bag.cgi, który przetworzy dane
i zwróci potwierdzenie transakcji. Warto zauważyć, że bezpieczeństwo transakcji wymagałoby zainstalowania serwera
SSL lub jakiegoś kodowania danych, zawierających numer karty kredytowej i danych zamawiającego.

Przycisk Wyzeruj ilości


Jest to po prostu przycisk typu RESET, który wycofuje wszelkie zmiany danych w formularzu. Jeśli jednak wybierzemy
przycisk Zmiana koszyka, trwale zmienimy zawartość koszyka i ilości towarów.

Przycisk Zmiana koszyka


Załóżmy, że użytkownik zmienił ilość niektórych produktów i dla kilku z nich zaznaczył opcję usunięcia. Wybór przycisku
Zmiana koszyka spowoduje wprowadzenie tych zmian w życie, a następnie ponowne wyświetlenie koszyka z nowymi
ustawieniami. Oto funkcja changeBag() z wierszy 246–261:
function changeBag(formObj, showAgain) {
var tempBagArray = new Array();
for (var i = 0; i < shoppingBag.things.length; i++) {
if (!formObj.elements[(i * 3) + 2].checked) {
tempBagArray[tempBagArray.length] = shoppingBag.things[i];
tempBagArray[tempBagArray.length - 1].itemQty =
formObj.elements[i * 3].selectedIndex + 1;
}
}
shoppingBag.things = tempBagArray;
if(shoppingBag.things.length == 0) {
alert("Twój koszyk jest już pusty. Włóż tam coś.");
parent.frames[1].showStore();
}
else { showBag(); }
}

Zasada działania jest dość prosta:


1.utwórz pustą tablicę tempBagArray,
2.przeglądaj kolejne elementy tablicy things,
3.jeśli nie zaznaczono pola opcji, dodaj do temBagArray następny element,
4.ustaw ilość dodanego produktu na ilość ustawioną przez użytkownika w odpowiedniej
liście wyboru,
5.przypisz zawartość tablicy tempBagArray tablicy things i wyświetl ponownie
zawartość koszyka.
Do odpowiedniego pola opcji odwołujemy się tak samo, jak funkcja runningTab() odwołuje się do poszczególnych
produktów, tyle że numerem pola opcji, jeśli i oznacza indeks produktu, jest (i*3)+2. Jeśli po wykonaniu opisanego
algorytmu w tablicy tempBagArray nic się nie znajduje, użytkownik usunął z koszyka wszystko, więc należy mu
o tym przypomnieć i poprosić go o ponowny wybór towarów.

Zapomniane funkcje
Omówiliśmy już wszystko, zostały nam jeszcze tylko trzy niewielkie funkcje. Nie odgrywają one zbyt wielkiej roli, ale
warto o nich wspomnieć. Są to funkcje portal(), help() i freshStart(). Oto funkcja portal() z wierszy 49–52:
function portal() {
210

gimmeControl = false;
parent.frames[0].location.href = "search/index.html";
}

Jako że wyszukiwarka nie będzie wyświetlała żadnych produktów, przed załadowaniem strony search/ index.html zmienna
gimmeControl ustawiana jest na false. To samo dotyczy funkcji help() z wierszy 354–357:
function help() {
gimmeControl = false;
parent.frames[0].location.href = "intro.html";
}

Jedyna różnica polega na tym, że do parent.frames[0] ładowany jest plik intro.html. W końcu przyjrzyjmy się
funkcji freshStart():
function freshStart() {
if(parent.frames[0].location.href != "intro.html") { help(); }
}

Funkcja ta powoduje, że za każdy razem, kiedy użytkownik załaduje shopset.html (lub go przeładuje),
parent.frames[0] zacznie się od intro.html. Funkcję tę znajdziemy w obsłudze zdarzenia onLoad, w wierszu 10.

Kierunki rozwoju
Choć odrobina kreatywności pozwoli znaleźć w tej aplikacji swoje miejsce. Oto kilka możliwości, które przychodzą
autorowi na myśl:
• zwiększenie „inteligencji” produktów,
• zwiększenie możliwości wyszukiwania,
• dodanie obsługi ciasteczek dla często odwiedzających nas klientów.

Inteligentniejsze towary
Nie chodzi o to, żeby każdy produkt miał jakiś iloraz inteligencji. Załóżmy jednak, że do konstruktora obiektu product
dodamy właściwość – tablicę zawierającą nazwy i pary numerów kategorii i produktu – towarów związanych
z towarem właśnie pokazywanym. Konstruktor mógłby wyglądać na przykład tak:
function product(name, description, price, unit, related) {
this.name = name;
this.description = description;
this.price = price;
this.unit = unit;
this.related = related;
this.plu = name.sustring(0,3).toUpperCase() +
parseInt(price).toString();
this.icon = new Image();
return this;
}

Argument related jest tablicą przypisywaną właściwości related. Kiedy użytkownik ogląda jakiś produkt, możemy
przejrzeć elementy related, generując łącza do produktów innych –zbliżonych. W ten sposób reklamujemy swoje
inne produkty.
Jeśli nie wydaje się to zbyt ciekawe, odwiedźmy księgarnię amazon.com. Wyszukajmy książkę na jakiś interesujący nas
temat. Kiedy klikniemy jedno z łącz wyniku, wraz z wybraną książką dostaniemy także łącza do innych książek
kupowanych przez osoby, które zaopatrzyły się też w książkę przez nas wybraną.

Zwiększenie możliwości wyszukiwania


Znów wracamy do zagadnienia zwiększenia możliwości wyszukiwarki, ale w przypadku aplikacji Shopping Bag mamy
kilka możliwości. Najpierw możemy się zastanowić nad umożliwieniem wyszukiwania według logicznej koniunkcji,
AND. To nie będzie zbyt trudne – wystarczy skopiować funkcję requireAll() z aplikacji z rozdziału 1., następnie
trzeba ją zmodyfikować zgodnie z opisem dotyczącym allowAny(). Należy również zmodyfikować validate(),
aby wskazać, jaka funkcja wyszukiwania ma zostać użyta.
Zamiast przeszukiwać całą bazę danych, użytkownik może zechcieć przejrzeć dane tylko jednej lub więcej kategorii.
Zastanówmy się nad dodaniem do nav.html następującej listy, umożliwiającej dokonanie wyboru wielokrotnego:
<SELECT MULTIPLE SIZE=5>
211 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie

<OPTION VALUE="Appliances">Artykuły AGD


<OPTION VALUE="Building">Budynki
<OPTION VALUE="Cloting">Ubrania
<OPTION VALUE="Electronics">Elektronika
<OPTION VALUE="Food">Jedzenie
<OPTION VALUE="Hardware">Urządzenia
<OPTION VALUE="Music">Muzyka
</SELECT>

Choć tutaj kategorie wpisane są „na sztywno”, to można też zastosować logikę podobną, jak w genSelect(), aby uzyskać
bardziej dynamiczne działanie. Kiedy wyszukiwane są produkty, możemy przeszukiwać je jedynie z kategorii
wybranych przez użytkownika, dzięki czemu ograniczamy zakres przeszukiwania.
Następna możliwość rozbudowy to umożliwienie wyszukiwania według zakresu cen. Jeśli kupujący szuka produktów
poniżej 50 dolarów, droższych od 100 dolarów lub w cenie między 50 a 100, może użyć operatorów >, <, >= i <=. W tym
celu będziemy musieli dokonać odpowiednich zmian w funkcjach validate() i formatResults().

Obsługa ciasteczek
Załóżmy, że mamy sklep, w którym niektóre towary często się zmieniają. Niektórzy klienci mogą w związku z tym
często nas odwiedzać – dobrze byłoby więc, jeśli nie musieliby wypełniać wszystkich danych o sobie. Dlaczego
nie dodać zatem funkcji korzystających z ciasteczek z rozdziału 7.? W ten sposób dane o użytkowniku byłyby trzymane
w przeglądarce u klienta i można byłoby do nich sięgać, kiedy tylko użytkownik chce coś zamówić. Jest to nieco
wymagające zadanie dla programisty, ale kiedy skończymy, będziemy uważali się za herosów JavaScriptu. Choć
funkcje GetCookie() i SetCookie() umożliwiają zapisywanie i odczytywanie ciasteczek, trzeba stworzyć funkcję
zbierającą dane z poszczególnych pól (zapewne w jeden, długi napis) i drugą, która będzie dane odczytywała,
wstawiając je do pól formularza.
Cechy aplikacji:
 Aplikacja pozwala szyfrować dane,
stosując rożne szyfry
 Obiektowa konstrukcja umożliwia łatwe
dodawanie nowych metod szyfrowania
 Aplikacja prezentacyjna i narzędzie
dla naszych gości
Prezentowane techniki:

9
 Przypisywanie metod do obiektów
 Jeszcze o dopasowywaniu napisów i ich
podmienianiu
 Nieco o dziedziczeniu w JavaScripcie
 Alternatywna składnia

Szyfry
w JavaScripcie

Teraz czas na chwilę odprężenia umysłowego. Ten rozdział nie jest tak śmiertelnie poważny, podstawą tworzonej
aplikacji jest czysta zabawa – techniki szyfrowania w JavaScripcie. Aplikacja przekształca wiadomość tekstową w coś,
co wygląda jak cyfrowe śmieci, czytelne tylko dla tych, którzy znają klucz do tego sekretu.
Interfejs pokazany na rysunku 9.1 jest dość prosty. Wybrano szyfr Cezara, widać wobec tego opis tego szyfru, listę
wyboru umożliwiającą wybranie przesunięcia i pole tekstowe ułatwiają wpisanie tekstu, który będzie szyfrowany
i deszyfrowany.
213 Rozdział 9 - Szyfry w JavaScripcie

Rysunek 9.1. Interfejs szyfrów


Tekst „JavaScript jest językiem skryptowym przyszłości, nieprawda?” wpisano do okienka tekstowego. Wybranie
z listy wyboru przesunięcia liczby 6 i następnie kliknięcie przycisku Zaszyfruj da tekst zakodowany, tak jak
na rysunku 9.2. Oto ten tekst:
pg1gyixovz pkyz p54qoks yqx4vzu24s vx54y5uio tokvxg2jg

Rysunek 9.2. Użycie szyfru Cezara


Wybór przycisku Odszyfruj powoduje przywrócenie tekstu do pierwotnej postaci. Zwróćmy uwagę, że tekst wynikowy
zapisany jest małymi literami.18 Szyfr Vigenere działa bardzo podobnie. Wybranie tego szyfru z listy wyboru
spowoduje pokazanie ekranu z rysunku 9.3.

18
Kodowany jest tylko alfabet łaciński, przedtem „podejrzane” znaki są przekształcane. Dlatego właśnie po powrocie do tekstu pierwotnego, czyli
odszyfrowaniu go, polskie litery będą wymienione na inne znaki. Jeśli chcemy to sprawdzić, ustawmy przesunięcie na 0 i wtedy wybierzmy
szyfrowanie (przyp. tłum.).
214

Kiedy przychodzi do szyfrowania, nie ma już listy z wyborem numeru kluczowego, ale pojawi się okienko tekstowe,
w którym należy podać słowo kluczowe lub nawet frazę kluczową. Teraz do kodowania użyjemy wyrażenia „code junky” –
wynik pokazano na rysunku 9.4, a otrzymany tekst ma postać:
loye1w4sdv mi1d tn0yliv 5uf03ws5iz dtd1w88ps pwht0u9ny

Oczywiście odcyfrowanie tego tekstu przy użyciu takiego samego kodu spowoduje przywrócenie tekstu do postaci
pierwotnej.19 Zakładam, że czytelnik nie musi znać się na szyfrach, więc zaczynamy od krótkiego kursu na ten temat.
Wyjaśnione zostaną podstawowe pojęcia i szczegółowo omówimy dwa szyfry używane w naszej aplikacji.

Rysunek 9.3. Interfejs szyfru Vigenere

Rysunek 9.4. Szyfr Vigenere w działaniu

19
Z tym samym zastrzeżeniem, co poprzednio – konwersji podlegają tylko znaki alfabetu łacińskiego (przyp. tłum.).
215 Rozdział 9 - Szyfry w JavaScripcie

Jak działają szyfry


No dobrze, czym zatem jest szyfr? Szyfr to algorytm lub zestaw algorytmów, przekształcających wiadomość tekstową
na coś, co wydaje się być bezsensownym zlepkiem symboli. Wiadomość oryginalną mogą odczytać jedynie osoby
wtajemniczone. Poniższe definicje pomogą ogólnie zrozumieć szyfrowanie i związane z nim techniki programowe.
Tekst otwarty jest tym, co przekazuje się do odbiorców, czyli wiadomością oryginalną.
Tekst zaszyfrowany to oryginalna wiadomość poddana działaniu algorytmu szyfrującego. Tekst ten znowu staje się
tekstem otwartym, kiedy zostanie odszyfrowany.
W wielu szyfrach używane są klucze – jeden lub więcej. Klucz to tekst lub zestaw bitów używanych do szyfrowania
i odszyfrowywania danych. Firma RSA Data Security, Inc. (http://www.rsa. com/), przodująca na rynku technologii
kryptograficznych, definiuje klucz tak, że służy on do przekształcania tekstu otwartego w tekst zaszyfrowany. Kluczem
może być cokolwiek, na przykład słowo „repozytorium”, zdanie „Najlepsze kasztany rosną na placu Pigalle”, liczba
dwójkowa 10011011, a nawet całkiem dziwaczny napis, jak „%_-.;,(<<*&^”.
Szyfry, w których nadawca i odbiorca posługują się tym samym kluczem, nazywamy szyframi z kluczem symetrycznym.
Jeśli każdy klucz jest inny i jeden podawany jest do publicznej wiadomości, a drugi zna tylko odbiorca, mamy
do czynienia z szyfrowaniem z kluczem publicznym. W naszej aplikacji używamy tylko szyfrowania symetrycznego.
Istnieją setki opisanych metod szyfrowania. Niektóre mają już tysiące lat, a wymyślone zostały przez wielkich
uczonych starożytności. Inne mają ledwie kilka tygodni i zostały stworzone przez nawiedzonego nastolatka, który
doznał olśnienia po ustanowieniu rekordowego wyniku w grze Tomb Raider. Niezależnie od ich pochodzenia – szyfry
dzielą się na trzy podstawowe kategorie: ukrycie, przestawienie i podstawienie.
Szyfry stosujące ukrycie zawierają tekst otwarty z tekstem zaszyfrowanym. Odbiorca musi wiedzieć, które litery i cyfry
należy odrzucić w celu odczytania wiadomości. Oto mały przykład:
l234u34b09i23ę87776c32z123e090k098o88787l2a33d234ę

Wystarczy pominąć wszystkie cyfry, by odczytać zdanie lubię czekoladę. Spójrzmy na kolejny przykład.
U Ciebie i Elizy kwitną astry jakieś.

Pierwsze litery kolejnych wyrazów powiedzą, że należy uciekać. W obu przypadkach odgadnięcie tekstu nie było
specjalnie skomplikowane, ale wielu ludzi doszło do niesłychanej wprost wprawy w wymyślaniu takich ukrytych
wiadomości. Przy okazji zauważmy, że w tego typu szyfrach tekst zaszyfrowany nie jest w ogóle potrzebny, co widać
było w powyższych przykładach. Pomyślmy o sympatycznym atramencie, który znika, kiedy wysycha. Bardziej
drastycznym przykładem może być niejaki Histiasz, który w piątym wieku przed naszą erą zgolił głowę zaufanemu
niewolnikowi i wytatuował na niej wiadomość. Kiedy włosy odrosły, Histiasz posłał go z wieścią. Arystagoras zgolił
głowę niewolnika i odczytał w ten sposób instrukcje dotyczące wzniecenia powstania.
Szyfry przestawne także zachowują wszystkie znaki pierwotnej wiadomości, a ich zasada działania polega po prostu
na przestawieniu kolejności tekstów wiadomości szyfrowanej. Oto przykład:
ei be ic do yż el az ik yt ka la gs ol

Sklejmy te grupy liter w całość i przeczytajmy od końca – no tak, znowu „los Galaktyki zależy od Ciebie”.
Szyfry podstawiane zamieniają każdy znak na inny znak lub symbol. Oto przykład:
17-29-24-15-8-25-28-15-14-19-15-12-4-30-3-1-17-12

Jeśli każdej literze przypiszemy liczbę, oznaczającą jej kolejny numer w alfabecie (polskim), uzyskamy zdanie „myślę
tylko liczbami” (bo m jest siedemnastą literą, y dwudziestą dziewiątą, i tak dalej). Cyfry zastępcze mogą być użyte
niezależnie od stosowanego alfabetu. Oba szyfry stosowane w naszej aplikacji należą do grupy szyfrów podstawianych.

Kilka słów o łamaniu kodów


Generowany przez tę aplikację zaszyfrowany tekst może w pierwszej chwili wyglądać dość skomplikowanie. Tak
naprawdę dobry kryptoanalityk mógłby je złamać w ciągu kilku minut za pomocą tylko kartki i ołówka. Na szczęście
można zapewnić znacznie wyższy poziom bezpieczeństwa, jeśli zastosuje się algorytmy takie, jak RSA, IDEA czy
potrójny DES. Autor nie może pokazać, jak łamać takie szyfry, ale może pokazać, na co łatwo jest nabrać szyfry
podstawiane i przestawne.
216

Podstawową bronią agenta jest częstość występowania liter. Niektóre litery w każdym języku w typowym tekście
potocznym występują częściej, inne rzadziej. W angielskim najczęstsze litery to kolejno E, T, N, R, O, A, I i S,
najrzadziej występujące to J, K, Q, X i Z.20
Inna metoda to analiza par i trójek znaków. Także dwuznaki i trójznaki mają swoje częstości występowania.
Wspomniany przed chwilą podręcznik jako najczęstsze dwuznaki angielskiego podaje EN, ER, RE, NT, TH, ON i IN,
jako najrzadsze DF, HU, IA, LT i MP. Z kolei najpopularniejsze trójznaki to ENT, ION, AND, ING, IVE, THO i FOR,
najrzadsze – ERI, HIR, IET, DER i DRE.
Litery pojawiające się najczęściej oraz występowanie dwuznaków i trójznaków nie tylko może zasugerować niektóre
litery, ale też wskazać prawdopodobnych ich sąsiadów. Zastanówmy się, ilu dwu- i trójznaków używamy na co dzień:
na, do, od, się, i tak dalej. Choć jednak szyfry omawiane w naszej aplikacji nie są doskonałe, to możemy się z nimi
trochę pobawić, a i tak będą w stanie zniechęcić przypadkowe, wścibskie osoby.21

Szyfr Cezara
Szyfr ten, stosowany przez Juliusza Cezara do kontaktowania się z jego generałem, jest pierwszym szyfrem, o którym
wiadomo, że był stosowany do zabezpieczania przesyłania komunikatów. Stosowany algorytm jest prostym
przesunięciem liter o 1 do 25 miejsc w alfabecie (od b do z), zatem przesunięcie o 3 spowoduje przejście litery a na d.
Natomiast litery z końca alfabetu przesuwane są z powrotem na litery początkowe, zatem przy przesunięciu o 3 pozycje
litera z zostanie zastąpiona literą c. Liczba określająca przesunięcie jest kluczem do szyfrowania i odszyfrowania
wiadomości.
Zwróćmy uwagę, że jeśli zostanie wybrany jakiś klucz, to wszystkie wystąpienia danej litery będą miały już stale takie
same odpowiedniki – szyfr Cezara jest monoalfabetyczny.

Szyfr Vigenere
Ten szyfr z kolei zaprezentowany został przez matematyka Błażeja de Vigenere w XVI wieku. Jest to szyfr
polialfabetyczny, gdyż używa się w nim więcej niż jednego alfabetu szyfrującego. Innymi słowy literze a nie zawsze
musi odpowiadać litera d, jak to miało miejsce w przypadku szyfru Cezara z przesunięciem 3.
Zamiast liczby w tym szyfrze używa się słowa kluczowego. Załóżmy, że chcemy zakodować propozycję spotkania się
o północy, „meet at midnight”, i zamierzamy użyć jako słowa kluczowego nazwy szyfru, vinegar. Litery słowa kluczowego
należy ustawić kolejno obok liter szyfrowanego tekstu:
vine ga rvinegar
meet at midnight

W porządku. V to dwudziesta druga litera alfabetu, I jest dziewiąta. n, e, g, a i r to odpowiednio litery o numerach 14, 5,
7, 1 i 18. Zatem litera m tekstu szyfrowanego zostanie przesunięta o 22 pozycje, pierwsze e o 9 pozycji, drugie e o 14,
i tak dalej. W rezultacie otrzymamy:
hmrx gt ddlammhk

Jeśli się nad tym zastanowić, to właściwie mamy tu do czynienia z generowaniem szyfrów Cezara – od nowa
dla każdego następnego znaku.

Jeśli czytelnik chce dowiedzieć się nieco więcej o szyfrach i zna język angielski, może
ściągnąć podręcznik armii amerykańskiej w postaci szeregu plików PDF spod adresu
http://www.und. nodak.edu/org/crypto/crypto/army.field.manual/separate.chaps/.
Ta kopia dokumentów znajduje się w witrynie Crypto Drop Box. Można zajrzeć
na stronę główną http://www.und.nodak.edu/org/crypto/crypto/ - znajduje się tu dość
materiałów, aby mieć zajęcie na wiele dni.

Wymagania programu
W aplikacji tej używamy JavaScriptu 1.2 i DHTML, konieczne jest zatem użycie przeglądarki w wersji 4.x lub
nowszej. Wielokrotnie używamy tutaj dopasowywania i podmiany tekstów, więc wersja 1.2 w pełni odpowiada naszym
wymaganiom.
20
Z podręcznika U.S. Army Field Manual 34-40-2 (przyp. aut.).
21
Pamiętajmy tylko o jednym: jeśli chcemy takiej technologii użyć na swoich stronach sieciowych, to odbiorcy wraz ze stronami otrzymają także
pełny kod JavaScriptu, zawierający metody szyfrowania (przyp. tłum.).
217 Rozdział 9 - Szyfry w JavaScripcie

Struktura programu
Na szczęście w tej aplikacji używane są tylko dwa pliki. Co więcej, będziemy oglądać kod tylko jednego z nich. Owe
dwa pliki to index.html oraz dhtml.js (ten drugi omawialiśmy już w rozdziale 6.). Zanim zajrzymy do kodu,
zastanówmy się jeszcze, jak ta aplikacja powinna wyglądać teoretycznie. Aplikacja została skonstruowana w pełni
obiektowo. Koszyk na zakupy z rozdziału 8. też był tworzony jako aplikacja obiektowa, ale w bieżącym rozdziale
pójdziemy jeszcze dalej.
W tej aplikacji używamy dwóch szyfrów. Każdy szyfr jest w jakiejś mierze podobny do innych, bez względu na to,
jakiego jest rodzaju. Pamiętajmy tylko, że istnieją trzy podstawowe grupy szyfrów. W naszej aplikacji mają być dwa
szyfry podstawiane, szyfr Cezara i Vigenere. Na rysunku 9.5 pokazano podstawowy układ opisanej hierarchii.

Rysunek 9.5. Struktura klas szyfrów


Z rysunku wynika, że obiekty klas CencealmentCipher, TranspositionCipher i SubstitutionCipher
dziedziczą po obiekcie Cipher.22 Wobec tego szyfry Cezara i Vigenere będą obiektami klasy SubstitutionCipher,
a zawierać będą wszystkie właściwości i metody tej klasy.
Aby trochę sobie poszerzyć horyzonty, zastanówmy się, jak by można było ten model rozwinąć. Na rysunku 9.6
pokazano, że łatwo do tej hierarchii byłoby dodać inne rodzaje szyfrów bez zmieniania czegokolwiek w strukturze już
istniejącej. Pogrubione elementy opisują część używaną w aplikacji.
Jak widać, do stworzonej tak struktury można dodawać dowolnie wiele innych rodzajów szyfrów i konkretnych
szyfrów bez konieczności modyfikowania istniejącego kodu. Można też tworzyć kolejne klasy potomne, znajdujące się
w strukturze jeszcze głębiej. Pamiętajmy o tym, kiedy będziemy na kolejnych stronach omawiać odpowiedni kod.
Zobaczymy, jak łatwo jest do tej aplikacji dodawać kolejne szyfry bez konieczności zmieniania czegokolwiek.
Spójrzmy teraz na plik index.html, pokazany w przykładzie 9.1.

Przykład 9.1. index.html


1 <HTML>
2 <HEAD>
3 <TITLE>Szyfr</TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6 BODY { margin-left: 50 px; font-family: arial; }
7 I { font-weight: bold; }
8 //-->
9 </STYLE>
10 <SCRIPT LANGUAGE="JavaScript1.2" SRC="dhtml.js"></SCRIPT>

22
Nazwy oznaczają odpowiednio: szyfr z ukryciem, szyfr przestawny i szyfr z podstawianiem; Cipher oznacza szyfr (przyp. tłum.).
218

Rysunek 9.6. Rozszerzanie struktury klas szyfrów

Przykład 9.1. index.html (ciąg dalszy)


11 <SCRIPT LANGUAGE="JavaScript1.2">
12 <!--
13
14 var caesar = '<FONT SIZE=2>Stworzony przez Juliusza Cezara szyfr polega ' +
15 'na przesuwaniu liter. Zwykły tekst jest szyfrowany przez przesunięcie ' +
16 'każdego znaku alfabetu o ustaloną liczbę znaków.' +
17 '<BR><BR>Na przykład przesuwanie o 1 zmienia <I>a</I> na <I>b</I>, ' +
18 '<I>b</I> na <I>c</I> i tak dalej.' +
19 'Znaki z końca alfabetu zamieniane są z powrotem na znaki z początku, ' +
20 'zatem na przykład <I>z</I> zamieniane jest na <I>a</I>. W aplikacji ' +
21 'do alfabetu dodawane są także cyfry, wobec czego <I>9</I> zamieniane ' +
22 'jest na <I>a</I>. Proces odwrotnego przesunięcia umożliwia odcyfrowanie ' +
23 'danych.' +
24 '<BR><FORM>Przesunięcie: ' +
25 genSelect('Shift', 35, 0, 0) +
26 '</FORM><BR>Uwaga: mówi się, że Cezar preferował użycie przesunięcia 3.'
27
28 var vigenere = '<FONT SIZE=2>Stworzony przez znanego matematyka Błażeja ' +
29 'VigenÚre, szyfr VigenÚre uważać można za "dynamiczną" wersję szyfru ' +
30 'Cezara. Zamiast przesuwać wszystkie znaki o ustaloną liczbę znaków ' +
31 'w alfabecie, przesunięcie jest zmieniane w zależności od liter w słowie ' +
32 'wybranym jako klucz. Jeśli słowem tym będzie na przykład <I>dog</I>, to ' +
33 'wobec tego, że litery <I>d</I>, <I>o</I> i <I>g</I> są odpowiednio ' +
34 'literami o numerach 4, 15 i 7, każdy znak będzie przesuwany odpowiednio ' +
35 'o 4, 15 i 7 pozycji. W aplikacji uwzględniono też cyfry, zatem i słowo ' +
36 'kluczowe zawierać może litery i cyfry.' +
37 '<BR><BR><FORM>Słowo kluczowe: <INPUT TYPE=TEXT NAME="KeyWord" SIZE=25>' +

Przykład 9.1. index.html (ciąg dalszy)


38 '</FORM><BR>. Uwaga: szyfr ten ma wiele odmian, z których jedną wymyślił ' +
39 'Lewis Carroll, autor "Alicji w Krainie Czarów".';
40
41 var curCipher = "caesar";
42
43 function Cipher() {
44 this.purify = purify;
45 this.chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
46 }
47
48 function purify(rawText) {
49 if (!rawText) { return false; }
50 var cleanText = rawText.toLowerCase();
51 cleanText = cleanText.replace(/\s+/g,' ');
52 cleanText = cleanText.replace(/[^a-z0-9\s]/g,'');
53 if(cleanText.length == 0 || cleanText.match(/^\s+$/) != null) {
54 return false;
55 }
219 Rozdział 9 - Szyfry w JavaScripcie

56 return cleanText
57 }
58
59 function SubstitutionCipher(name, description, algorithm) {
60 this.name = name;
61 this.description = description;
62 this.substitute = substitute;
63 this.algorithm = algorithm;
64 }
65 SubstitutionCipher.prototype = new Cipher;
66
67 function substitute(baseChar, shiftIdx, action) {
68 if (baseChar == ' ') { return baseChar; }
69 if(action) {
70 var shiftSum = shiftIdx + this.chars.indexOf(baseChar);
71 return (this.chars.charAt((shiftSum < this.chars.length) ?
72 shiftSum : (shiftSum % this.chars.length)));
73 }
74 else {
75 var shiftDiff = this.chars.indexOf(baseChar) - shiftIdx;
76 return (this.chars.charAt((shiftDiff < 0) ?
77 shiftDiff + this.chars.length : shiftDiff));
78 }
79 }
80
81 function caesarAlgorithm (data, action) {
82 data = this.purify(data);
83 if(!data) {
84 alert('Nieprawidłowy tekst dla: ' + (action ? 'cipher.' : 'decipher.'));
85 return false;
86 }
87 var shiftIdx =
88 (NN ? refSlide("caesar").document.forms[0].Shift.selectedIndex :
document.forms[1].Shift.selectedIndex);
89 var cipherData = '';
90 for (var i = 0; i < data.length; i++) {
91 cipherData += this.substitute(data.charAt(i), shiftIdx, action);
92 }
93 return cipherData;
94 }
95
96 function vigenereAlgorithm (data, action) {
97 data = this.purify(data);

Przykład 9.1. index.html (ciąg dalszy)


98 if(!data) {
99 alert('Nieprawidłowy tekst dla: ' + (action ? 'cipher.' : 'decipher.'));
100 return false;
101 }
102 var keyword = this.purify((NN ?
103 refSlide("vigenere").document.forms[0].KeyWord.value :
104 document.forms[2].KeyWord.value));
105 if(!keyword || keyword.match(/\^s+$/) != null) {
106 alert('Nieprawidłowe słowo kluczowe dla: ' +
107 (action ? 'ciphering.' : 'deciphering.'));
108 return false;
109 }
110 keyword = keyword.replace(/\s+/g, '');
111 var keywordIdx = 0;
112 var cipherData = '';
113 for (var i = 0; i < data.length; i++) {
114 shiftIdx = this.chars.indexOf(keyword.charAt(keywordIdx));
115 cipherData += this.substitute(data.charAt(i), shiftIdx, action);
116 keywordIdx = (keywordIdx == keyword.length - 1 ? 0 : keywordIdx + 1);
117 }
118 return cipherData;
119 }
120
121 var cipherArray = [
122 new SubstitutionCipher("caesar", caesar, caesarAlgorithm),
123 new SubstitutionCipher("vigenere", vigenere, vigenereAlgorithm)
124 ];
125
126 function showCipher(name) {
127 hideSlide(curCipher);
128 showSlide(name);
220

129 curCipher = name;


130 }
131
132 function routeCipher(cipherIdx, data, action) {
133 var response = cipherArray[cipherIdx].algorithm(data, action);
134 if(response) {
135 document.forms[0].Data.value = response;
136 }
137 }
138
139 //-->
140 </SCRIPT>
141 </HEAD>
142 <BODY BGCOLOR=#FFFFFF>
143
144 <DIV>
145 <TABLE BORDER=0>
146 <TR>
147 <TD ALIGN=CENTER COLSPAN=3>
148 <IMG SRC="images/cipher.jpg">
149 </TD>
150 </TR>
151 <TR>
152 <TD VALIGN=TOP WIDTH=350>
153 <FORM>
154 <SELECT NAME="Ciphers"
155 onChange="showCipher(this.options[this.selectedIndex].value);">
156 <OPTION VALUE="caesar">Szyfr Cezara
157 <OPTION VALUE="vigenere">Szyfr VigenÚre
158 </SELECT>
159 </TD>
160 <TD ALIGN=CENTER>

Przykład 9.1. index.html (dokończenie)


161 <TEXTAREA NAME="Data" ROWS="15" COLS="40" WRAP="PHYSICAL"></TEXTAREA>
162 <BR><BR>
163 <INPUT TYPE=BUTTON VALUE="Zaszyfruj"
164 onClick="routeCipher(this.form.Ciphers.selectedIndex,
165 this.form.Data.value, true);">
166 <INPUT TYPE=BUTTON VALUE="Odszyfruj"
167 onClick="routeCipher(this.form.Ciphers.selectedIndex,
168 this.form.Data.value, false);">
169 <INPUT TYPE=BUTTON VALUE=" Wyzeruj "
170 onClick="this.form.Data.value='';">
171 </FORM>
172 </TD>
173 </TR>
174 </TABLE>
175 </DIV>
176
177 <SCRIPT LANGUAGE="JavaScript1.2">
178 <!--
179 document.forms[0].Ciphers.selectedIndex = 0;
180 genLayer("caesar", 50, 125, 350, 200, showName, caesar);
181 genLayer("vigenere", 50, 125, 350, 200, hideName, vigenere);
182 //-->
183 </SCRIPT>
184 </BODY>
185 </HTML>

Najpierw interpretowany jest kod pliku źródłowego JavaScriptu dhtml.js. Kod z tego pliku używa DHTML
do ustawienia warstw i wygenerowania w biegu list wyboru. Niedługo zajmiemy się tym tematem. Dalej interesujący
kod znajdziemy w wierszach 14–39. Zmienne caesar i vigenere jako wartości otrzymują tekst HTML. Każda z nich,
jak można zgadnąć, definiuje warstwę interfejsu szyfru. Wszystko jest statyczne poza jednym wywołaniem funkcji
genSelect() w wartości zmiennej caesar:
genSelect('Shift', 35, 0, 0)

Tworzona jest lista Shift, która zawiera liczby od 0 do 35, przy czym początkowo zaznaczona jest opcja 0.
Wartościami atrybutów VALUE i TEXT stają się kolejne liczby. Jest to kod żywcem wzięty z rozdziału 5. Jeśli czytelnik
zrobił sobie bibliotekę JavaScriptu, jak to sugerowano w rozdziale 6., zapewne ma tam ów kod. Funkcja ta
zdefiniowana jest na końcu pliku dhtml.js.
221 Rozdział 9 - Szyfry w JavaScripcie

Definiowanie szyfru
Poniższych kilka wierszy kodu służy do zdefiniowania wszystkich możliwych szyfrów – wiersze 43–46 zawierają
konstruktor obiektu Cipher:
function Cipher() {
this.purify = purify;
this.chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
}

Ten konstruktor jest niewielki.23 Jeśli ktoś oczekiwał jakiejś złożonej definicji z wszelkiego rodzaju równaniami
różniczkowymi i geometrią sferyczną, pozwalającą rozłożyć czwarty wymiar, to mógł się rozczarować. Konstruktor
Cipher() służy do definiowania szyfrów na dość wysokim poziomie ogólności. W tej aplikacji warto zrobić tylko dwa
założenia co do stosowanych szyfrów:
• Wszystkie potrafią sformatować dane użytkownika (przy użyciu jednej metody) – niezależnie, czy jest to tekst
otwarty, czy zaszyfrowany.
• Każdy szyfr wie, jakie znaki będą szyfrowane (przy użyciu jednej właściwości).

Techniki języka JavaScript:


przypisywanie metod obiektom
Nawet tak mały konstruktor Cipher() wprowadza tutaj pewną nową myśl: obiekty dotąd
tworzone w poprzednich rozdziałach miały jedynie właściwości, tym czasem konstruktor
Cipher() ma właściwość chars, ale również metodę purify().

Właściwości łatwo jest przypisywać. Po prostu ustawia się wartość zmiennej, stosując
składnię this.nazwa_zmiennej. Metody przypisuje się już nieco inaczej. Najpierw de-
finiuje się funkcję, potem używając tej samej składni this.nazwa_zmiennej, należy się
do tej funkcji odwołać. Dokładnie to ma miejsce w konstruktorze Cipher(). W skrypcie
zdefiniowano funkcję purify(), natomiast obiekt Cipher ma zmienną this.purify,
odwołującą się do metody purify. Zwrócmy uwagę na to, że w tym wypadku nie używa
się nawiasów. Tak właśnie oznacza się odwołanie do funkcji. Gdyby this.purify przy-
pisać purify(), wywołana zostałaby funkcja purify() i zmienna this.purify
otrzymałaby wartość przez taką funkcję zwróconą.
Odwołanie się do funkcji w konstruktorze przypisuje metodę purify() każdej zmiennej,
której wartością jest new Cipher(). Tak właśnie będzie z elementami tablicy cipher-
Array, o czym będzie można wkrótce się przekonać.

Niezależnie od tego, jak dane będą szyfrowane i deszyfrowane, muszą spełniać pewne warunki:
• Każdy znak musi być literą od a do z lub cyfrą od 0 do 9. Wszystkie inne znaki będą pomijane. Wielkość liter
nie ma znaczenia.
• Spacje nie będą szyfrowane ani deszyfrowane. Szereg sąsiadujących spacji będzie traktowany jako spacja
pojedyncza.
• Znaki końca wiersza także będą konwertowane na pojedyncze spacje.
Całkiem sympatyczne i proste zasady. I proste. Teraz pozostaje tylko wcielić je w życie – zrobi to funkcja purify()
z wierszy 48–57:24
function purify(rawText) {
if (!rawText) { return false; }
var cleanText = rawText.toLowerCase();
cleanText = cleanText.replace(/\s+/g,' ');
cleanText = cleanText.replace(/[^a-z0-9\s]/g,'');
if(cleanText.length == 0 || cleanText.match(/^\s+$/) != null) {

23
Jeśli chcemy kodować polski alfabet, musimy rozszerzyć w konstruktorze zestaw dostępnych znaków. Pamiętajmy jednak, że musimy zastosować
kodowanie polskich liter takie, jakiego używamy w systemie operacyjnym. Z drugiej strony JavaScript domyślnie używa zestawu ISO Latin-1 (ISO
8859-1), który nie ma w ogóle polskich liter (przyp. tłum.).
24
Jeśli chcemy szyfrować polskie litery, i tę funkcję musimy poprawić – aby nie usuwała polskich liter (bo nie należą one do założonego zakresu
znaków). (przyp. tłum.)
222

return false;
}
return cleanText
}

Zwrócona zostanie jedna z dwóch wartości: false lub sformatowany tekst gotowy do szyfrowania. W pierwszym
przypadku (false), szyfrowanie jest przerywane. Jeśli zmienna rawText zawiera cokolwiek, co należy sformatować,
purify() skonwertuje to najpierw na małe litery:
cleanText = cleanText.replace(/\s+/g,' ');

Użycie wyrażeń regularnych umożliwia zastosowanie metody replace() obiektu String do odnalezienia ciągów
spacji i zastąpienia ich spacjami pojedynczymi, niezależnie od ich ilości. Następnie funkcja purify() zastępuje
wszystkie inne znaki (spoza zakresów a–z i 0–9) oraz spacje znakiem pustym. W ten sposób usuwane są znaki nienadające
się do szyfrowania. Używamy do tego znów metody replace():
cleanText = cleanText.replace(/[^a-z0-9\s]/g,'');

Zatem formatowanie mamy już z głowy. Przyszedł czas sprawdzić, czy zostało jeszcze coś sensownego
do zaszyfrowania. Jeśli tylko ostateczny napis zawiera przynajmniej jeden znak z zadanych zakresów, wszystko jest
w porządku. W dwóch przypadkach może być inaczej:
• Po usunięciu znaków „nieszyfrowalnych” nic nam już nie zostało.
• Po usunięciu znaków „nieszyfrowalnych” zostały nam tylko spacje.

Techniki języka JavaScript:


jeszcze o dopasowywaniu napisów i podstawianiu
Za wyrażenia regularne należałoby JavaScript 1.2 kochać. W omawianej aplikacji uży-
wamy tej cechy znacznie intensywniej, niż czyniliśmy to dotąd. Spójrzmy jeszcze raz na
wyrażenie regularne z wiersza 52:
/[^a-z0-9\s]/g

Choć nie jest ono długie, to zastosowana składnia może powodować pewne zmieszanie.
W tym wyrażeniu używamy zanegowanego zestawu znaków. Innymi słowy – do wzorca
pasuje wszystko spoza podanego zestawu. W ogóle nawiasów kwadratowych możemy
używać do wskazywania zbioru znaków, które nas interesują (lub, jak w naszym
wypadku, które chcemy pominąć). Oto inne, prostsze wyrażenie:
/[a-z]/g

Wyrażenie to pasuje do dowolnych małych liter alfabetu łacińskiego. Litera g na końcu


oznacza, że wyszukujemy wszystkich pasujących znaków, a nie tylko ich pierwszego wy-
stąpienia. Możemy wstawić więcej tego typu zakresów:
/[a-z0-9\s]/g

To wyrażenie pasuje także do dowolnych małych liter alfabetu łacińskiego, ale także do
cyfr i spacji. Jednak w przypadku naszej aplikacji chcemy wybrać wszystkie znaki poza
wskazanymi, dlatego właśnie na początku nawiasu wstawiamy karetkę (^), która odwraca
normalne znaczenie nawiasów – i w ten sposób otrzymujemy wyrażenie takie, jakie
mamy w funkcji:
/[^a-z0-9\s]/g

Opisane zasady to tylko sam początek dopasowywania wyrażeń regularnych. Możemy


używać wyrażeń regularnych do sprawdzania i formatowania numerów ubezpieczenia,
numerów dowodów osobistych, adresów poczty elektronicznej, adresów URL, numerów
telefonów, kodów pocztowych, dat, czasu i tak dalej. Pełne omówienie wyrażeń regular-
nych wraz ze znaczeniem znaków specjalnych można znaleźć w opisie nowości Java-
Scriptu w wersji 1.2, pod adresem http://developer1.netscape.com:80/docs/manuals/
communicator/jsguide/regexp.htm.

Jeśli zachodzi któryś z powyższych warunków, czas całą operację odwołać i sięgnąć po lepsze dane. Stosowna kontrola
wykonywana jest w wierszach 53–55. Te wiersze decydują o zwróceniu przez purify() wartości false, jeśli
nie pasują nam dane:
223 Rozdział 9 - Szyfry w JavaScripcie

if(cleanText.length == 0 || cleanText.match(/^\s+$/) != null) {


return false;
}

Jeśli chodzi o wybranie odpowiednich znaków, obiekt Cipher używa następującego napisu:
this.chars = 'abcdefghijklmnopqrstuvwxyz0123456789';

Tworzenie szyfru z podstawianiem


Teraz, kiedy mamy już konstruktor obiektów Cipher, zajmijmy się bardziej specyficznymi obiektami tego typu.
Przejdziemy konkretnie do obiektów służących do obsługi szyfrów z podstawianiem, których konstruktorem jest
SubstitutionCipher(). Przyjrzyjmy się dokładniej wierszom 59–65:
function SubstitutionCipher(name, description, algorithm) {
this.name = name;
this.description = description;
this.substitute = substitute;
this.algorithm = algorithm;
}
SubstitutionCipher.prototype = new Cipher;

Zakładamy, że każdy obiekt Cipher wie, jak sformatować dane użytkownika. Teraz możemy poczynić dalsze
założenia dotyczące już tylko szyfrów z podstawianiem:
1.Każdy z nich ma nazwę i opis.
2.Każdy używa ogólnej metody służącej do podstawiania znaków – zarówno w ramach szyfrowania, jak
i deszyfrowania.
3.Każdy ma specyficzną dla siebie implementację ogólnej metody podstawiania. W ten sposób zapewniamy, że
stosowane będą różne metody podstawiania dla różnych szyfrów.
4.Każdy obiekt SubstitutionCipher jest jednocześnie obiektem Cipher.
Nadanie nazwy i opisu jest proste. Wystarczy po prostu przekazać do konstruktora dwa napisy. Jako opisów użyjemy
zmiennych caesar i vigenere, ustawionych wcześniej w kodzie HTML. Tak oto poradziliśmy sobie z założeniem
pierwszym. Co teraz ze zdefiniowaniem ogólnej metody podstawiania? Ma ona umożliwiać podstawianie jednych
znaków za inne. No właśnie – każde wywołanie metody zwróci jeden znak, który będzie zastępował inny znak.

Podstawowa metoda podstawiania


Każdy obiekt SubstitutionCipher używa tej samej metody do zamiany pojedynczego znaku chars na inny.
Pokazana poniżej funkcja substitute() zdefiniowana została jako metoda dla każdego wystąpienia obiektu
SubstitutionCipher:
function substitute(baseChar, shiftIdx, action) {
if (baseChar == ' ') { return baseChar; }
if(action) {
var shiftSum = shiftIdx + this.chars.indexOf(baseChar);
return (this.chars.charAt((shiftSum < this.chars.length) ?
shiftSum : (shiftSum % this.chars.length)));
}
else {
var shiftDiff = this.chars.indexOf(baseChar) - shiftIdx;
return (this.chars.charAt((shiftDiff < 0) ?
shiftDiff + this.chars.length : shiftDiff));
}
}

Metoda ta oczekuje trzech argumentów. baseChar to znak, który będzie zastępowany, shiftIdx to liczba całkowita
mówiąca, o ile należy przesunąć znaki, action to wartość logiczna informująca, czy przekazany znak baseChar
należy traktować jako część tekstu otwartego, czy tekstu zaszyfrowanego. Aby spacje nie były na nic zamieniane,
w pierwszym wierszu przekazany znak jest zwracany bez zmian, jeśli jest on spacją. W przeciwnym wypadku metoda
użyje parametru action, aby zdecydować, jak traktować przesunięcie. Jeśli action ma wartość true, realizowane jest
szyfrowanie; w przeciwnym wypadku zachodzi deszyfrowanie.
Pamiętajmy, że chars zawiera napis wszystkich znaków podlegających przetwarzaniu. Algorytm szyfrujący po prostu
określa indeks znaku baseChar w chars, a następnie wybiera znak o indeksie powiększonym o zadane przesunięcie,
czyli indeks baseChar plus shiftIdx.
224

Oto przykład. Załóżmy, że baseChar to litera d, shiftIdx równe jest 8, a chars.indexOf('d') równe jest 3.
W wierszu 70 widzimy:
var shiftSum = shiftIdx + this.chars.indexOf(baseChar);

Zmienna shiftSum równa jest 11 (8+3), a chars.charAt(11) to litera l. Taką właśnie wartość w tym wypadku
zwróci funkcja substitute(). Wydaje się to proste. I takie jest, ale załóżmy teraz, że baseChar to litera o,
a shiftIdx równa jest 30. W tej sytuacji shiftSum równa jest 45, natomiast chars ma tylko 36 znaków (a–z i 0–9).
Zatem chars.charAt(45) nie istnieje.
Kiedy nasz algorytm dojdzie do ostatniego znaku chars, musi wrócić do początku i zacząć dalsze zliczanie od 0.
Można w tym celu użyć operatora modulo. Operator ten zwraca całkowitą resztę z dzielenia przez siebie dwóch liczb.
Oto kilka przykładów:
4 % 3 = 1 – 4 dzielone przez 3 daje resztę 1.
5 % 3 = 2 – 5 dzielone przez 3 daje resztę 2.
6 % 3 = 0 – 6 dzieli się przez 3 bez reszty.
Wystarczy zatem zwrócić wynik działania modulo, dlatego zamiast ustawiać shiftSum na 45, ustawmy tę zmienną
na shiftSum % chars.length, czyli 9.25 chars.charAt(9) to litera j. To wyjaśnia, dlaczego w końcu zwracamy
taką dziwną wartość:26
return (this.chars.charAt((shiftSum < this.chars.length) ?
shiftSum : (shiftSum % this.chars.length)));

W tym wypadku funkcja substitute() zwróci chars.charAt(shiftSum) lub chars.charAt (shiftSum %


this.chars.length), zależnie od wartości shiftSum i długości zmiennej chars. A co ze słowem kluczowym this?
Być może ktoś się zastanawia, co ono tutaj robi. Pamiętajmy, że substitute() nie jest zwykłą funkcją, ale metodą
obiektu SubstitutionCipher. Użycie this w metodzie odnosić się będzie do zmiennej obiektowej. Jako że
SubstitutionCipher dziedziczy wszystkie właściwości Cipher, nowa zmienna zawierać też będzie właściwość
chars.

Podobna procedura obowiązuje w przypadku algorytmu deszyfrującego. Jedyna zmiana polega na tym, że aby uzyskać
w chars odpowiedni znak, odejmujemy shiftIdx. W tym wypadku shiftDiff uzyskuje wartość różnicy indeksu
znaku baseChar i wartości shiftIdx:
var shiftDiff = this.chars.indexOf(baseChar) - shiftIdx;

Znów rzecz jest dość prosta. Jeśli jednak shiftDiff jest mniejsza od 0, mamy taki sam problem, jak wtedy, gdy shiftSum
była większa od chars.lenfth-1. Rozwiązaniem jest dodanie do shiftDiff wartości chars.length. Tak...
dodanie. shiftDiff jest ujemna, zatem dodanie długości tablicy chars da w wyniku liczbę mniejszą od tej długości,
której będziemy mogli użyć jako potrzebnego nam indeksu. W poniższym fragmencie kodu podejmowana jest decyzja, czy
funkcja substitute() jako indeksu użyć ma wartości shiftDiff, czy shiftDiff + chars. ength:
return (this.chars.charAt((shiftDiff < 0) ?
shiftDiff + this.chars.length : shiftDiff));

Różne podstawienia do różnych szyfrów


Sprawdziliśmy właśnie, co mają wspólnego wszystkie obiekty SubstitutionCipher – metodę substitute(). Teraz
przyjrzyjmy się, co je różni. Konstruktor oczekuje argumentu o nazwie algorithm. Argument ten nie jest napisem,
wartością logiczną, liczbą czy nawet obiektem. Jest on odwołaniem do funkcji, która będzie użyta do wywołania
metody substitute().
W przypadku szyfru Cezara przekazujemy odwołanie do funkcji caesarAlgorithm(), tymczasem w przypadku
szyfru Vigenere – odwołanie do funkcji vigenereAlgorithm(). Przyjrzyjmy się teraz obu sytuacjom.

Algorytm szyfru Cezara


Spośród dwóch omawianych szyfrów algorytm szyfru Cezara jest prostszy. Odpowiedni kod zawierają wiersze 81–94:
function caesarAlgorithm (data, action) {

25
Pamiętaj że aby użyć modulo, musimy liczyć od 0. Jeśli liczymy od 1, tą metodą nie uzyskamy prawidłowego wyniku (przyp. tłum.).
26
Wyjaśnia tylko częściowo. Warunek shiftSum < this.chars.length jest zbędny, gdyż można byłoby po prostu zapisać return
(this.chars.charAt(shiftSum % this.chars.length)); – wynik będzie taki sam. Jeśli liczbę zapiszemy jako a*b+c,
gdzie liczymy modulo b, to wartość a nie ma znaczenia. Jeśli warunek autora jest spełniony, to po prostu a=0, ale w wyniku i tak otrzymamy c
(przyp. tłum.).
225 Rozdział 9 - Szyfry w JavaScripcie

data = this.purify(data);
if(!data) {
alert('Nieprawidłowy tekst dla: ' + (action ? 'cipher.' : 'decipher.'));
return false;
}
var shiftIdx =
(NN ? refSlide("caesar").document.forms[0].Shift.selectedIndex :
document.forms[1].Shift.selectedIndex);
var cipherData = '';
for (var i = 0; i < data.length; i++) {
cipherData += this.substitute(data.charAt(i), shiftIdx, action);
}
return cipherData;
}

Pierwszych kilka wierszy formatuje dane, a następnie sprawdza się, czy zostały jeszcze jakieś znaki do przetwarzania.
Dane z parametru data formatowane są przez wywołanie funkcji purify(). O ile tylko purify() nie zwróci
wartości false, przetwarzanie trwa dalej. Szczegóły dotyczące samej funkcji purify() i zwracanych przez nią
wartości opisano wcześniej.
Następnie należy określić, o ile znaków dane mają być przesuwane. Tym razem jest to proste – otrzymujemy wartość
z listy wyboru z warstwy caesar. Nie wspomniano jeszcze o tym, ale możemy przeskoczyć do przodu, do wierszy
180–181, i zobaczyć, jak warstwy są tworzone. Jeśli jednak chodzi o odwoływania się do elementów formularza
z warstw, DOM Netscape Navigatora i DOM Internet Explorera się różnią. Lista wyboru nazywa się Shift.
W Netscape Navigatorze odwołanie wygląda tak:
document.layers['caesar'].document.forms[0].Shift.selectedIndex

W Internet Explorerze wygląda ono następująco:


document.forms[1].Shift.selectedIndex

Zmienna shiftIdx rozwiązuje tę różnicę przy pomocy zmiennej globalnej NN, która pozwala określić typ używanej
przeglądarki. Wywołanie refSlide() w wierszu 88 jest wygodnym sposobem odwołania się
do document.layers["caesar"]. Teraz, kiedy wartość shiftIdx została określona, funkcja
caesarAlgorithm() iteracyjnie sprawdza dane data.length razy, każdorazowo wywołując funkcję
substitute() i łącząc uzyskaną z niej wartość z początkowo pustą zmienną cipherData. Każdorazowo
przekazywany jest też parametr action, który pozwala zdecydować, czy chodzi o zaszyfrowanie danych, czy ich
odszyfrowanie. Kiedy skończy się iteracja, caesarAlgorithm() zwraca cipherData, która to zmienna zawiera teraz
przekształcony napis.

Algorytm szyfru Vigenere


Prostszy algorytm szyfrowania już objaśniliśmy, teraz zabierzmy się za vigenereAlgorithm(). Podstawowa różnica
polega na tym, że argument shiftIdx przekazywany do substitute() w funkcji caesarAlhorithm() miał wartość
stałą. Tym razem shiftIdx może się zmieniać przy każdym wywołaniu substitute() (i zwykle się zmienia). Inna
różnica polega na tym, że zamiast liczby użytkownik wybiera słowo kluczowe. Oto wiersze 96–119:
function vigenereAlgorithm (data, action) {
data = this.purify(data);
if(!data) {
alert('Nieprawidłowy tekst dla: ' + (action ? 'cipher.' : 'decipher.'));
return false;
}
var keyword = this.purify((NN ?
refSlide("vigenere").document.forms[0].KeyWord.value :
document.forms[2].KeyWord.value));
if(!keyword || keyword.match(/\^s+$/) != null) {
alert('Nieprawidłowe słowo kluczowe dla: ' +
(action ? 'ciphering.' : 'deciphering.'));
return false;
}
keyword = keyword.replace(/\s+/g, '');
var keywordIdx = 0;
var cipherData = '';
for (var i = 0; i < data.length; i++) {
shiftIdx = this.chars.indexOf(keyword.charAt(keywordIdx));
cipherData += this.substitute(data.charAt(i), shiftIdx, action);
keywordIdx = (keywordIdx == keyword.length - 1 ? 0 : keywordIdx + 1);
}
return cipherData;
}
226

Jak właśnie widać, sięganie do formularzy i ich elementów w warstwach wymaga


stosowania w różnych przeglądarkach różnej składni. Obiektowy model dokumentu
(DOM) w Netscape Navigatorze różni się od tegoż modelu w Internet Explorerze.
Nie pierwszy raz spotykamy się z tym w naszej książce. Tak naprawdę większa część
kodu pliku dhtml.js ma na celu jedynie tworzenie i obsługę warstw w obu tych
przeglądarkach. Warto wyświadczyć sobie przysługę i upewnić się, kiedy trzeba
dostosować się do obu przeglądarek, a kiedy nie. Póki modele DOM nie zostaną
ujednolicone, przydatne będą poniższe zasoby.
Obiekty DHTML Microsoftu:
http://www.microsoft.com/workshop/author/dhtml/reference/objects.asp
Podręcznik Netscape do arkuszy stylów i aplikacji JavaScript działających po stronie
klienta: http://developer1.netscape.com:80/docs/manuals/communicator/dynhtml/
jss34.htm oraz http:/developer.netscape.com/docs/manuals/js/client/jsref/index.htm

Pierwszych pięć wierszy ma taką samą postać, jak w funkcji caesarAlgorithm(). Przeprowadzane jest takie samo
formatowanie oraz identyczna kontrola danych. Następnych kilka wierszy postępuje podobnie ze słowem kluczowym.
Słowo to pochodzi z pola formularza z warstwy vigenere. Pamiętajmy o zachowaniu zgodności z oboma modelami
DOM.
W Netscape Navigatorze odwołanie wygląda tak:
document.layers['vigenere'].document.forms[0].KeyWord.value

W Internet Explorerze to samo odwołanie ma taką oto postać:


document.forms[2].KeyWord.value

Wartość zmiennej keyword określana jest następująco:


var keyword = this.purify((NN ?
refSlide("vigenere").document.forms[0].KeyWord.value :
document.forms[2].KeyWord.value));

Zwróćmy uwagę na ponowne użycie metody purify(). Jest ona wprawdzie przystosowana głównie do przetwarzania
tekstu otwartego i tekstu zaszyfrowanego, ale wymagania wobec słowa kluczowego są bardzo podobne. Jako że metoda
substitute() umożliwia podstawianie jedynie znaków z napisu chars, słowo kluczowe też musi składać się tylko
z takich znaków. Dopuszczalne słowa kluczowe to ludzie, maszyny, init2wnit czy 1lub2lub3. Jednak użycie
innych znaków, spoza chars, też jest możliwe. Pamiętajmy, że funkcja purify() usuwa wszystkie znaki nienależące
do zakresu a–z ani 0–9, a także zastępuje wszystkie grupy spacji i znaki nowego wiersza pojedynczymi spacjami. O ile
użytkownik może jako słowo kluczowe podać 1@@#derft, to purify() i tak przekształci to słowo na 1derft, które
już zawiera tylko znaki dopuszczalne. Teraz weźmy pod uwagę słowo kluczowe z białymi znakami – usunięte one
zostaną w wierszu 110:
keyword = keyword.replace(/\s+/g, '');

Zasada jest następująca: jeśli w słowie kluczowym znajdzie się choć jeden znak należący do chars, można takiego słowa
użyć w funkcji vigenereAlgorithm().

Jak działa shiftIdx


Tekst otwarty lub zaszyfrowany oraz słowo kluczowe zostały już sformatowane. Pozostaje teraz tylko zastąpić
wszystkie znaki ich odpowiednikami. Z definicji szyfru Vigenere wynika, że każdy znak tekstu jest szyfrowany lub
deszyfrowany zgodnie ze wskaźnikiem kolejnego znaku w słowie kluczowym – i tak dochodzimy do wierszy 111–118:
var keywordIdx = 0;
var cipherData = '';
for (var i = 0; i < data.length; i++) {
shiftIdx = this.chars.indexOf(keyword.charAt(keywordIdx));
cipherData += this.substitute(data.charAt(i), shiftIdx, action);
keywordIdx = (keywordIdx == keyword.length - 1 ? 0 : keywordIdx + 1);
}
return cipherData;

Używając zmiennej keywordIdx zaczynającej swoje działanie od 0, możemy uzyskać indeksy poszczególnych znaków
słowa kluczowego następująco:
keyword.charAt(keywordIdx)
227 Rozdział 9 - Szyfry w JavaScripcie

Dla każdego znaku z data zmienna shiftIdx ustawiana jest na wartość indeksu w chars znaku
keyword.charAt(keywordIdx). Zmienna cipherData jest następnie uzupełniana o wynik metody
substitute(), która otrzymuje kopię data.charAt(i) i shiftIdx oraz action. Zwiększając następnie
keywordIdx o 1, przygotowujemy się do następnego kroku iteracji.

Obiekty SubstitutionCipher też są obiektami Cipher


Ponieważ wszystkie szyfry, niezależnie od ich rodzaju, muszą mieć tę samą podstawową charakterystykę, konstruktor
SubstitutionCipher musi odziedziczyć wszystkie właściwości Cipher. Rozwiązuje to jeden wiersz:
SubstitutionCipher.prototype = new Cipher;

Teraz każdy nowy obiekt SubstitutionCipher automatycznie będzie miał właściwość chars i metodę purify().
Wobec tego każdy obiekt SubstitutionCipher staje się takim bardziej specyficznym obiektem Cipher.

Techniki języka JavaScript:


parę słów o dziedziczeniu w języku JavaScript
Jak powiedziano w poprzednim rozdziale, w JavaScripcie stosuje się dziedziczenie oparte
na prototypowaniu, a nie na dziedziczeniu klas, jak to ma miejsce w językach takich, jak
Java. W rozdziale 8., kiedy mówiliśmy o dodawaniu właściwości obiektów, pokazano, jak
dodać właściwości do już istniejących obiektów. Właściwości prototype konstruktora
można też użyć do realizacji dziedziczenia. To właśnie ma miejsce w wierszu 65. Sub-
stitutionCipher dziedziczy wszystkie właściwości Cipher. W ten sposób możemy
naprawdę skorzystać z możliwości programowania obiektowego (przynajmniej w rozu-
mieniu JavaScriptu). Więcej informacji o dziedziczeniu w JavaScripcie można znaleźć
w witrynie DevEdge Online firmy Netscape:
http://developer1.netscape.com:80/docs/manuals/communicator/jsobj/contents.htm#1030750

Tworzenie nowych obiektów SubstitutionCipher


Do tej pory widzieliśmy sposób działania dwóch szyfrów. Teraz czas zastanowić się, jak tworzyć obiekty
reprezentujące te szyfry i jak stworzyć interfejs do nich. Tworzenie obiektów zajmuje tylko cztery wiersze, od 121
do 124:

var cipherArray = [
new SubstitutionCipher("caesar", caesar, caesarAlgorithm),
new SubstitutionCipher("vigenere", vigenere, vigenereAlgorithm)
];

Zmienna cipherArray jest tablicą, której każdy element stanowi obiekt SubstitutionCipher. Po co w ogóle
umieszczać je w tablicy? Aplikacja wie, którego szyfru ma użyć, dzięki opcji listy wyboru z pierwszej strony. Zaraz się
tym zajmiemy.
228

Techniki języka JavaScript:


składnia alternatywna
W JavaScripcie 1.2 możemy zastąpić kod typu
var myArray = new Array(1,2,3);

jego skróconą wersją:


var myArray = [1,2,3];

Można tworzyć też obiekty w biegu, na przykład zamiast


function myObj() {
this.nazwa="Nowy obiekt";
this.opis = "Obiekt starej daty";
}

można napisać:
var myObj = {nazwa: "Nowy obiekt", opis: "Obiekt nowej daty"};

Zwróćmy uwagę, że pary nazwa-wartość tak właściwości, jak i metod, oddzielane są od


siebie przecinkiem. Składnię taką obsługują zarówno Internet Explorer, jak i Netscape
Navigator w wersjach 4.x. Wybór składni zależy od indywidualnych preferencji.

Warto zauważyć, że każde wywołanie konstruktora SubstitutionCipher() przekazuje mu oczekiwane przez niego
napisy, nazwę i opis, a także wskaźnik do funkcji, która będzie przypisana właściwości algorithm poszczególnych
obiektów. Tak właśnie tworzone są nasze obiekty. Przyjrzyjmy się teraz interfejsowi. Wszystko zachodzi między
znacznikami BODY:
<DIV>
<TABLE BORDER=0>
<TR>
<TD ALIGN=CENTER COLSPAN=3>
<IMG SRC="images/cipher.jpg">
</TD>
</TR>
<TR>
<TD VALIGN=TOP WIDTH=350>
<FORM>
<SELECT NAME="Ciphers"
onChange="showCipher(this.options[this.selectedIndex].value);">
<OPTION VALUE="caesar">Szyfr Cezara
<OPTION VALUE="vigenere">Szyfr Vigenére
</SELECT>
</TD>
<TD ALIGN=CENTER>
<TEXTAREA NAME="Data" ROWS="15" COLS="40" WRAP="PHYSICAL"></TEXTAREA>
<BR><BR>
<INPUT TYPE=BUTTON VALUE="Zaszyfruj"
onClick="routeCipher(this.form.Ciphers.selectedIndex,
this.form.Data.value, true);">
<INPUT TYPE=BUTTON VALUE="Odszyfruj"
onClick="routeCipher(this.form.Ciphers.selectedIndex,
this.form.Data.value, false);">
<INPUT TYPE=BUTTON VALUE=" Wyzeruj "
onClick="this.form.Data.value='';">
</FORM>
</TD>
</TR>
</TABLE>
</DIV>

Kod ten tworzy dwuwierszową tabelę. Wiersz górny zawiera grafikę, przy czym COLSPAN znacznika TD ustawiono na 2.
Wiersz dolny zawiera dwie komórki z danymi. Lewa zawiera listę wyboru i ma postać:
<SELECT NAME="Ciphers"
onChange="showCipher(this.options[this.selectedIndex].value);">
<OPTION VALUE="caesar">Szyfr Cezara
<OPTION VALUE="vigenere">Szyfr Vigenére
</SELECT>
229 Rozdział 9 - Szyfry w JavaScripcie

Lista pozwala określić interfejs, którego szyfr ma być wyświetlany. Jako że w naszej aplikacji mamy tylko dwa szyfry,
musi to być jeden z nich. Procedura obsługi zdarzenia onChange wywołuje funkcję showCipher(), przekazując jej
wartość wybranej opcji. Sama funkcja jest całkiem krótka, a znajdziesz ją w wierszach 126–130:
function showCipher(name) {
hideSlide(curCipher);
showSlide(name);
curCipher = name;
}

Kod ten może wydać się znany. Coś podobnego widzieliśmy już w rozdziałach 3. i 6. Funkcje hideSlide()
i showSlide() znajdziemy w pliku dhtml.js, a dokładny opis mieści się w rozdziale 3.
Zwróćmy uwagę, że komórka danych ma szerokość 350 pikseli. W przeciwieństwie do komórki z listą wyboru, ta
wydaje się pustawa. Na szczęście mamy dwie warstwy, które będą ją wypełniały. Tworzące je wywołania znajdziemy
w wierszach 180 i 181. Funkcja genLayer(), tworząca warstwy szyfrów, także znajduje się pliku dhtml.js. Też jest to
funkcja omawiana już wcześniej i w tym rozdziale nie będziemy się nią zajmowali:
genLayer("caesar", 50, 125, 350, 200, showName, caesar);
genLayer("vigenere", 50, 125, 350, 200, hideName, vigenere);

W ten sposób powstaje tekst pokazywany wraz z każdym z szyfrów, w przypadku szyfru Cezara dodatkowo tworzona
jest lista wyboru, a w przypadku szyfru Vigenere – pole tekstowe. Jak już wspomniano, między tymi szyframi możemy
wybierać za pomocą listy wyboru na górze strony – ta właśnie lista wyświetli odpowiednią warstwę.
Jeśli chodzi o pozostałą komórkę w dolnym wierszu tabeli, zawiera ona wielowierszowe pole tekstowe oraz trzy
przyciski. Oto odpowiedni kod z wierszy 161–170:
<TEXTAREA NAME="Data" ROWS="15" COLS="40" WRAP="PHYSICAL"></TEXTAREA>
<BR><BR>
<INPUT TYPE=BUTTON VALUE="Zaszyfruj"
onClick="routeCipher(this.form.Ciphers.selectedIndex,
this.form.Data.value, true);">
<INPUT TYPE=BUTTON VALUE="Odszyfruj"
onClick="routeCipher(this.form.Ciphers.selectedIndex,
this.form.Data.value, false);">
<INPUT TYPE=BUTTON VALUE=" Wyzeruj "
onClick="this.form.Data.value='';">

Pole tekstowe zawiera tekst otwarty lub zaszyfrowany. Przycisk Zaszyfruj powoduje zaszyfrowanie tekstu z pola, a
przycisk Odszyfruj wywołuje odszyfrowanie tego tekstu. Oba te przyciski wywołują tę samą funkcję,
routeCipher().Przekazują zawartość pola tekstowego, a wywołania różnią się tylko ostatnim parametrem –
o wartości odpowiednio true lub false.

Dobór odpowiedniego szyfru


Wybór odpowiedniego szyfru jest łatwy. Jest to zawsze szyfr odpowiadający indeksowi listy wyboru z górnej części
formularza i indeksowi cipherArray. Wynika to zresztą z treści funkcji routeCipher():
function routeCipher(cipherIdx, data, action) {
var response = cipherArray[cipherIdx].algorithm(data, action);
if(response) {
document.forms[0].Data.value = response;
}
}

Funkcja ta oczekuje trzech argumentów. Dwa z nich już omówiliśmy: data to przekazywany tekst, action to owa
wartość true lub false. Pierwszy argument – cipherIdx – pochodzi natomiast
z document.forms[0].Ciphers.selectedIndex. Musi być to 0 lub 1. Tak czy inaczej wywołana zostanie
odpowiednia metoda algorithm() z określonego obiektu SubstitutionCipher z tablicy cipherArray. Jeśli
algorithm() zwróci wartość inną niż false, to będzie to tekst wynikowy – zaszyfrowany lub odszyfrowany.

Na koniec
Zapewne łatwo się było domyślić, że kod z wiersza 179:
document.forms[0].Ciphers.selectedIndex = 0;

Po prostu ustawia wybraną opcję górnej listy wyboru na opcję pierwszą. W ten sposób wybrana opcja odpowiada
pokazanej warstwie szyfru, nawet jeśli użytkownik przeładuje stronę.
230

Kierunki rozwoju
O ile ta aplikacja w swojej obecnej postaci nieźle nadaje się do zabawy, następnym krokiem będzie użycie jej
do wysyłania poczty elektronicznej. W tym celu wystarczy wykonać trzy proste kroki. Najpierw należy skopiować
poniższą funkcję i wkleić ją między znaczniki SCRIPT:
function sendText(data) {
paraWidth = 70;
var iterate = parseInt(data.length / paraWidth);
var border = '\n-------\n';
var breakData = '';
for (var i = 1; i <= iterate; i++) {
breakData += data.substring((i - 1) * paraWidth,
i * paraWidth) + '\r';
}
breakData += data.substring((i - 1) * paraWidth, data.length);
document.CipherMail.Message.value = border + breakData + border;
document.CipherMail.action =
"mailto:someone@somewhere.com\?subject=The Top Secret Message";
return true;
}

W ten sposób tuż przed wysłaniem wiadomości formatuje się jej treść. Polega to na wstawieniu znaków końca wiersza
między każdymi znakami o numerach paraWidth. W ten sposób zapewniamy, że wiadomość ta u odbiorcy nie będzie
jednym zbyt długim wierszem. Następnie należy dodać drugie pole do formularza, po końcowym znaczniku FORM
w dokumencie wstawić następujący kod:
<FORM NAME="CipherMail" ACTION="" METHOD="POST"
ENCTYPE="text/plain" onSubmit="return
sendText(document.forms[0].Data.value);">
<INPUT TYPE=HIDDEN NAME="Message">
<INPUT TYPE=SUBMIT VALUE=" Send ">
</FORM>

Formularz ten, nazwany CipherMail, zawiera jedno pole ukryte (HIDDEN). Ostatnią czynnością jest zmiana sposobu
odwołania się do formularza w obu funkcjach, zawierających algorytm szyfru.
Zmieńmy wiersze 87–89:
var shiftIdx = (NN ?
refSlide("caesar").document.forms[0].Shift.selectedIndex :
document.forms[1].Shift.selectedIndex);

na następujące:
var shiftIdx = (NN ?
refSlide("caesar").document.forms[0].Shift.selectedIndex :
document.forms[2].Shift.selectedIndex);

Następnie sprowadź wiersze 102–104:


var keyword = this.purify((NN ?
refSlide("vigenere").document.forms[0].KeyWord.value :
document.forms[2].KeyWord.value));

do poniższej postaci:
var keyword = this.purify((NN ?
refSlide("vigenere").document.forms[0].KeyWord.value :
document.forms[3].KeyWord.value));

Musimy wprowadzić wszystkie te zmiany, gdyż w poprzednim kroku dodaliśmy do hierarchii kolejny formularz.
Funkcja sendText() ustala wartość ukrytego pola na wartość tekstu z wielowierszowego pola tekstowego. Następnie
sendText() wysyła ten formularz, którego atrybut ACTION ustawiono na mailto:twój-e-mail@twój-serwer-e-
mail.com. Na rysunku 9.7 pokazano wygląd przychodzącej wiadomości. Jest to widok mojego konta Hotmail.

P.S. Wszystko to zadziała tylko wtedy, gdy użytkownik ma prawidłowo skonfigurowanego klienta poczty elektronicznej
Netscape Navigatora lub Internet Explorera, ale zwykle nie ma z tym problemów.
P.S.2 W pliku \ch09\cipher2.html znajdziemy już gotowe wszystkie poprawki.
Cechy aplikacji:
 Wysłanie własnoręcznie
zaprojektowanej kartki pocztowej
 Użytkownicy mogą umieścić w tle
wybrane obrazki
 Wygodny interfejs
 Podgląd wiadomości
Prezentowane techniki:

10
 Różnicowanie kodu strony
 Komunikacja między ramkami
 Optymalizacja działania funkcji

Elektroniczne
życzenia:
poczta
elektroniczna
metodą przenieś
i upuść

Ta aplikacja służy jedynie do zabawy. Użytkownicy będą skłonni spędzić na naszej stronie długie godziny, jeśli
umożliwimy im wysyłanie z niej życzeń znajomym i ukochanym; życzeń wypełnionych zabawnymi rysunkami
i z najdziwniejszymi tłami. Na rysunku 10.1 pokazano interfejs początkowy.
232

Rysunek 10.1. Domyślny ekran elektronicznych życzeń


Po lewej stronie znajduje się formularz, który użytkownicy wypełniają, podając między innymi adres odbiorcy, treść
wiadomości i rodzaj życzeń. Klikając przycisk Tło ->, użytkownik może wybrać tło, a klikając przycisk Ikony ->,
może wybrać obrazki.
Po prawej stronie pojawia się wynik pracy. Tutaj użytkownik widzi dostępne tła i dostępne ikony. Użytkownik może
ikonę wybierać i przeciągać ją nad obszar tła. Wszystkie ikony są włączane do życzeń, co pokazano na rysunku 10.2.

Rysunek 10.2. Czy poznajesz kogoś na tej fotografii grupowej?


Kiedy życzenia są już gotowe, użytkownik może obejrzeć swoją pracę na bieżącym etapie klikając przycisk Test.
Otwiera się wówczas osobne okno pokazujące, jak w danej chwili wygląda wynik jego pracy – a więc rezultat, jaki
zobaczy odbiorca to rysunek 10.3.
Kiedy użytkownik uzna wynik swej pracy za zadawalający, wybiera przycisk Wyślij. Wtedy formularz jest wysyłany
do czekającego już na dane skryptu na serwerze, tworzącego wiadomość poczty elektronicznej, i zwraca ostateczną
stronę z potwierdzeniem i przyciskiem Wyślij (rysunek 10.4). Kiedy przycisk ten zostanie kliknięty, skrypt wysyła
wiadomość do odbiorcy, podając adres URL kartki z życzeniami.
233 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

Wymagania programu
Z uwagi na użycie DHTML i wielu funkcji arkusza stylów będziemy potrzebowali Netscape Navigatora lub Internet
Explorera w wersji co najmniej 4.x. Program został stworzony z myślą o monitorze pracującym w rozdzielczości
co najmniej 1024×768, choć można go zmodyfikować tak, aby pracował także w rozdzielczości 800×600 – niżej już
lepiej nie schodzić.
Działanie aplikacji wymaga też serwera sieciowego ze środowiskiem umożliwiającym uruchamianie skryptów. Niech
nikogo nie przeraża to, że nie ma żadnego doświadczenia z wykonywaniem

Rysunek 10.3. Oto wynik jaki zobaczy odbiorca

Rysunek 10.4. Udało się: teraz wyślij tylko ten formularz


takich skryptów. Przygotowałem skrypt, który łatwo można zainstalować na niemalże dowolnym serwerze sieciowym.
Skrypt ten został stworzony w języku Perl. Wystarczy go skopiować do odpowiedniego katalogu i ustawić nieco
pozwoleń. Szczegóły na ten temat można znaleźć w dodatku C.

Struktura programu
Jest to kolejna aplikacja, w której przed analizą kodu zajmiemy się przestudiowaniu sposobu działania aplikacji.
Na rysunku 10.7 pokazano wprowadzanie przez użytkownika adresu poczty elektronicznej i treści wiadomości,
wybieranie rodzaju życzeń i tła, nałożenie ikon. Użytkownik ogląda swoje dzieło, a kiedy jest zadowolony, dane można
wysłać na serwer, i tak dalej.
Aplikacja działa na dwóch poziomach: na przeglądarce klienta i na serwerze sieciowym. To na przeglądarce użytkownik
tworzy całą kartkę z życzeniami: tło, obrazki i samą wiadomość. Kiedy użytkownik przesyła formularz HTML,
informacja jest odsyłana do serwera sieciowego, gdzie powstaje plik odpowiadający tworzonym życzeniom. Serwer
zwraca formularz HTML, umożliwiający użytkownikowi wysłanie wiadomości z życzeniami. Wiadomość ta zawiera
tak naprawdę tylko informację o cyfrowych życzeniach i łącze, które zaprowadzi odbiorcę do tych życzeń.
234

Rysunek 10.5. Zawiadomienie o elektronicznej kartce z życzeniami

Rysunek 10.6. Sposób działania elektronicznych życzeń


Teraz zajmijmy się aplikacją od strony klienta, a później przejdziemy do serwera. Mamy cztery pliki:
index.html
Interfejs najwyższego poziomu – zawiera zestaw ramek.
back.html
Zawiera przestrzeń roboczą pozwalającą wybrać rodzaj pozdrowień, tło i obrazki.
front.html
Interfejs do tworzenia i wysyłania wiadomości.
greet.pl
Skrypt działający po stronie klienta, używany do stworzenia i zapisania danych z życzeniami w pliku, następnie
stworzenia formularza HTML umożliwiającego wysłanie wiadomości odbiorcy.
Jak widać na pokazywanych w tym rozdziale ekranach, interfejs jest podzielony na dwie części. Plik back.html
pokazuje użytkownikowi tworzone życzenia, natomiast plik front.html zawiera formularz do wprowadzania danych,
umożliwia podanie adresu i treści wiadomości, wybranie tła i wstawienie żądanych obrazków (określanych tutaj jako
ikony). Oba dokumenty są wywoływane w pliku index.html. Szczegóły znajdują się w przykładzie 10.1.
235 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

Rysunek 10.7. Logika elektronicznych pozdrowień: jak użytkownik otrzymuje wiadomość

Przykład 10.1. index.html


1 <HTML>
2 <HEAD>
3 <TITLE>Cyber Greetings</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5 <!--
6
7 var greetings = [
8 'Choose One', 'Family Reunion!',
9 'Get Well Soon','Thinking Of You',
10 'Big Party!', 'Psst... You\'re Invited.',
11 'Happy Birthday!', 'Congratulations!',
12 'We\'re Gonna Miss U', 'Just A Reminder',
13 'Don\'t Forget'
14 ];
15
16 var baseURL = ".";
17
18 //-->
19 </SCRIPT>
20 </HEAD>
21 <FRAMESET COLS="450,*" FRAMEBORDER="2" BORDER="0">
22 <FRAME SRC="front.html" NAME="Front" NORESIZE>
23 <FRAME SRC="back.html" NAME="Back" NORESIZE SCROLLING="NO">
24 </FRAMESET>
25 </HTML>

Plik index.html zawiera tablicę greetings – wiersze 7–14. To właśnie stąd użytkownicy mogą wybierać rodzaj życzeń.
baseURL zawiera katalog bazowy aplikacji na serwerze sieciowym. W tym katalogu mamy wszystko: cztery pliki,
obrazki oraz katalog na życzenia użytkowników. baseURL zawarty jest nawet w samych życzeniach. Kiedy zmieniamy
tę wartość, zmieniamy ją dla całej aplikacji – tak po stronie klienta, jak i serwera.
Po co więc w ogóle deklarować zmienną i tablicę już w tym pliku? Oba pliki zawarte w ramkach potrzebują
odpowiednich danych do tworzenia swoich stron podczas ładowania się. Jeśli zmienna greetings zostałaby
zdefiniowana w jednym z tych dwóch plików, mogłaby nie zostać załadowana wraz z kodem JavaScript, który z niej
korzysta – to samo dotyczy zmiennej baseURL. Dzięki takiej konstrukcji, jaką zrealizowaliśmy, unikamy błędów
związanych z różnymi sposobami ładowania aplikacji.
236

Pozostałe dwa dokumenty


Idea strony przedniej (front) i tylnej (back) wywodzi się bezpośrednio z tradycyjnej pocztówki. Z przodu jest adres
i sam tekst (wydaje mi się, że to jest przód), a z tyłu znajduje się jakiś obrazek z plażowiczami. W naszym wypadku
back.html zawiera pokazywany obrazek z wybranymi ikonami. Plik ten jest odpowiedzialny za znaczną część
wstępnego procesu podczas ładowania dokumentu. font.html ułatwia dalsze działanie po załadowaniu dokumentu,
na przykład wpisanie wiadomości oraz wybieranie rodzaju życzeń i ich wysyłanie. Wobec tego rozsądnie będzie
najpierw omówić back.html. Tak się szczęśliwie składa, że większość jego kodu już znamy z poprzednich rozdziałów –
obejrzymy przykład 10.2.

Przykład 10.2. back.html


1 <HTML>
2 <HEAD>
3 <TITLE>Drag-n-Drop E-mail</TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6
7 .Greeting
8 {
9 font-family: Arial;
10 font-size: 48px;
11 font-weight: bold;
12 }
13
14 //-->
15 </STYLE>
16 <SCRIPT LANGUAGE="JavaScript1.2">
17 <!--
18
19 var NN = (document.layers ? true : false);
20 var hideName = (NN ? 'hide' : 'hidden');
21 var showName = (NN ? 'show' : 'visible');
22 var zIdx = -1;
23
24 var iconNum = 4;
25 var startWdh = 25;
26 var imgIdx = 0;
27 var activate = false;
28 var activeLayer = null;
29
30 var backImgs = [];
31 var icons = [
32 'bear', 'cowprod', 'dragon', 'judo',
33 'robot', 'seniorexec', 'dude', 'juicemoose',
34 'logo1', 'logo2', 'logo3','tree',

Przykład 10.2. back.html (ciąg dalszy)


35 'sun', 'gator', 'tornado', 'cactus'
36 ];
37
38 function genLayout() {
39
40 for (var i = 0; i <= 7; i++) {
41 backImgs[i] = new Image();
42 backImgs[i].src = parent.Front.baseURL +
43 '/images/background' + i + '.jpg';
44 }
45
46 genLayer("Back", 10, 250, backImgs[1].width, backImgs[1].height,
47 showName, '<IMG NAME="background" SRC="' + parent.Front.baseURL +
48 '/images/background0.jpg">');
49
50 for (var j = 0; j < parent.greetings.length; j++) {
51 genLayer("greeting" + j, 50, 275, 500, 100, hideName,
52 '<SPAN CLASS="Greeting">' + parent.greetings[j] + '</SPAN>');
53 }
54
55 for (var i = 0; i < icons.length; i++) {
56 if (i % iconNum == 0) { startWdh = 25; }
57 else { startWdh += 110; }
58 genLayer(icons[i], startWdh, 15, 100, 100, (i < iconNum ? showName :
59 hideName), '<A HREF="javascript: changeAction(\'' + icons[i] +
237 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

60 '\',' + (i + 1) + ');">' + '<IMG SRC="' + parent.Front.baseURL +


61 '/images/' + icons[i] + '.gif" BORDER="0"></A>');
62 }
63 startWdh = 25;
64 }
65
66 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
67 if (NN) {
68 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
69 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
70 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + (++zIdx) + '>' +
71 copy + '</LAYER>');
72 }
73 else {
74 document.writeln('<DIV ID="' + sName +
75 '" STYLE="position:absolute; overflow:none; left:' +
76 sLeft + 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' +
77 sHgt + 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) +
78 '">' + copy + '</DIV>'
79 );
80 }
81 }
82
83 function hideSlide(name) {
84 refSlide(name).visibility = hideName;
85 }
86
87 function showSlide(name) {
88 refSlide(name).visibility = showName;
89 }
90
91 function refSlide(name) {
92 if (NN) { return document.layers[name]; }
93 else { return eval('document.all.' + name + '.style'); }
94 }
95
96 function motionListener() {
97 if (NN) {
98 window.captureEvents(Event.MOUSEMOVE);
99 window.onmousemove = grabXY;

Przykład 10.2. back.html (dokończenie)


100 }
101 else {
102 document.onmousemove = grabXY;
103 }
104 }
105
106 function grabXY(ev) {
107 if (activate) {
108 if(NN) {
109 var itemWdh = refSlide(activeLayer).document.images[0].width;
110 var itemHgt = refSlide(activeLayer).document.images[0].height;
111 refSlide(activeLayer).left = ev.pageX - parseInt(itemWdh / 2);
112 refSlide(activeLayer).top = ev.pageY - parseInt(itemHgt / 2);
113 }
114 else {
115 var itemWdh = document.images[imgIdx].width;
116 var itemHgt = document.images[imgIdx].height;
117 refSlide(activeLayer).left = event.x - parseInt(itemWdh / 2);
118 refSlide(activeLayer).top = event.y - parseInt(itemHgt / 2);
119 }
120 }
121 }
122
123 function changeAction(name, MSIERef) {
124 activate = !activate;
125 activeLayer = name;
126 imgIdx = MSIERef;
127 }
128
129 //-->
130 </SCRIPT>
131 </HEAD>
132 <BODY onLoad="motionListener();">
133
238

134 <SCRIPT LANGUAGE="JavaScript1.2">


135 <!--
136
137 genLayout();
138
139 //-->
140 </SCRIPT>
141
142 </BODY>
143 </HTML>

Zanim nadawca będzie mógł stworzyć pozdrowienia, kilka funkcji musi wygenerować mnóstwo warstw i określić
położenie wskaźnika myszy względem dokumentu. Podobne funkcje omawialiśmy już w rozdziałach 3. i 8., zresztą
część funkcji pochodzi bezpośrednio z tych rozdziałów. Będziemy odnosić się do nich w miarę omawiania rozdziału.
Teraz przyjrzyjmy się najważniejszym zmiennym zadeklarowanym w wierszach 19–36:
var NN = (document.layers ? true : false);
var hideName = (NN ? 'hide' : 'hidden');
var showName = (NN ? 'show' : 'visible');
var zIdx = -1;

var iconNum = 4;
var startWdh = 25;
var imgIdx = 0;
var activate = false;
var activeLayer = null;

var backImgs = [];


var icons = [
'bear', 'cowprod', 'dragon', 'judo',
'robot', 'seniorexec', 'dude', 'juicemoose',
'logo1', 'logo2', 'logo3','tree',
'sun', 'gator', 'tornado', 'cactus'
];

Pierwsze cztery zmienne używane już były w poprzednich skryptach. NN pomaga określić typ używanej przeglądarki,
showName i hideName to napisy pokazujące i ukrywające warstwy w sposób zależny od przeglądarki, a zIdx jest
liczbą całkowitą używaną do określania współrzędnej z (czyli wysokości) poszczególnych warstw. Zmienna iconNum
to liczba całkowita określająca liczbę ikon wyświetlanych na obrazku jednocześnie. Zaczniemy od czterech. startWdh
wstępnie pozycjonuje ikony, a wkrótce ją ujrzymy w funkcji genLayout().
Zmienna imgIdx śledzi obrazki. activate to wartość logiczna, decydująca, czy warstwa ma być przenoszona myszką.
activeLayer określa, nad którą warstwą użytkownik klika właśnie myszką. Jeśli to nie wystarczy, mamy jeszcze dwie
zmienne tablicowe. backImgs początkowo jest pustą tablicą. Wkrótce wstawimy do niej obiekty Image, przy czym każdy
z nich będzie zawierał jeden obrazek tła. Obrazki te mają nazwy background0.jpg, background1.jpg, background2.jpg i tak
dalej.
Z kolei icons to tablica napisów identyfikujących poprzez nazwy poszczególne ikony. Oznacza to, że każda ikona
będzie tworzona na warstwie o nazwie opisanej elementem tablicy icons. Używany obrazek ikony ma też taką samą
nazwę. Na przykład warstwa o nazwie bear będzie zawierała obrazek bear.gif. Warto zauważyć, że: wszystkie obrazki
ikon są obrazkami GIF z przezroczystym tłem – biały jest kolorem przezroczystym. Jako że zwykle tłem obrazków jest
właśnie biel, możemy umieszczać ikony jedna nad drugą i widzieć „aż do dna”, do tła naszej kartki.

Co już wiemy
Jeśli ktoś śledził uważnie poprzednie rozdziały tej książki, ucieszy się zapewne wiadomością, że jego dotychczasowa
ciężka praca zostanie nagrodzona. Nowe funkcje tej aplikacji były już wielokrotnie używane, więc nie musimy teraz
znów się dokładnie zastanawiać nad ich działaniem. Nieraz już tak się zdarzało we wcześniejszych rozdziałach, ale tym
razem mamy wyjątkowy powód do zadowolenia.
W tabeli 10.1 zestawiono w celu przypomnienia funkcje, z którymi mieliśmy już do czynienia.
Tabela 10.1. Funkcja obsługująca warstwy
Funkcja Zastosowanie Rozdział(y)
genLayer() tworzenie warstw w Netscape Navigatorze 3, 4, 6, 9, 11
i Internet Explorerze
hideSlide() ukrywanie warstw o zadanej nazwie 3, 4, 6, 9, 11
ShowSlide pokazywanie warstw o zadanej nazwie 3, 4, 6, 9, 11
239 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

refSlide() odwołanie się do warstwy o zadanej nazwie 3, 4, 6, 9, 11


motionListener() śledzenie ruchów myszy 11
grabXY() określenie współrzędnych x i y elementu 11

Pierwsze cztery funkcje już dokładnie znamy z wcześniejszych rozdziałów. Jeśli ich jeszcze nie rozumiesz, zajrzyj
do rozdziału 3. Jednak motionListener() jest nieco zmodyfikowana i warto ją omówić. grabXY() zostanie
przedstawiona w rozdziale 11.; ona też została znacząco zmodyfikowana. Oto reszta funkcji, których używamy w tej
aplikacji.

Proszę zająć miejsca!


Kiedy aplikacja się ładuje, back.html ciężko pracuje nad wstępnym załadowaniem wszystkich obrazków, stworzeniem
i umieszczeniem na swoich miejscach warstw, po czym warstwy te w miarę potrzeb są pokazywane lub ukrywane.
Funkcja genLayout() wszystkie te działania koordynuje – znajdziemy ją w wierszach 38–64:
function genLayout() {

for (var i = 0; i <= 7; i++) {


backImgs[i] = new Image();
backImgs[i].src = parent.Front.baseURL +
'/images/background' + i + '.jpg';
}
genLayer("Back", 10, 250, backImgs[1].width, backImgs[1].height,
showName, '<IMG NAME="background" SRC="' + parent.Front.baseURL +
'/images/background0.jpg">');

for (var j = 0; j < parent.greetings.length; j++) {


genLayer("greeting" + j, 50, 275, 500, 100, hideName,
'<SPAN CLASS="Greeting">' + parent.greetings[j] + '</SPAN>');
}

for (var i = 0; i < icons.length; i++) {


if (i % iconNum == 0) { startWdh = 25; }
else { startWdh += 110; }
genLayer(icons[i], startWdh, 15, 100, 100, (i < iconNum ? showName :
hideName), '<A HREF="javascript: changeAction(\'' + icons[i] +
'\',' + (i + 1) + ');">' + '<IMG SRC="' + parent.Front.baseURL +
'/images/' + icons[i] + '.gif" BORDER="0"></A>');
}
startWdh = 25;
}

Funkcja genLayout()najpierw ładuje obrazki tła. Użytkownik zapewne zechce te obrazki obejrzeć, zanim coś wybierze,
więc wstępne ich załadowanie jest dobrym pomysłem. Używając backImgs, funkcja tworzy obiekt Image dla każdego
elementu i przypisuje mu plik źródłowy, używając top. baseURL (zadeklarowaliśmy taką zmienną wcześniej, w pliku
index.html), napisu background, wartości i i napisu .jpg:
for (var i = 0; i <= 7; i++) {
backImgs[i] = new Image();
backImgs[i].src = top.baseURL +
'/images/background' + i + '.jpg';
}

Po załadowaniu wszystkich obrazków przystępujemy do załadowania domyślnego tła. Można wybrać dowolne z nich,
ale dla ułatwienia warto zdecydować się na background0.jpg, po czym wstawić je do warstwy Back. Szerokość
i wysokość tej warstwy są ustawiane na odpowiednie wymiary obrazka. Staje się to ważne później,
przy pozycjonowaniu ikon:
genLayer("Back", 10, 250, backImgs[1].width, backImgs[1].height,
showName, '<IMG NAME="background" SRC="' + parent.Front.baseURL +
'/images/background0.jpg">');

Teraz warstwa tła i domyślny obrazek tła są już na swoich miejscach. Następnie, trzeba, umożliwić wybranie rodzaju
życzeń, chodzi po prostu o duże napisy opisujące wybrany rodzaj życzeń. Wszystkie możliwe rodzaje znajdują się
w tablicy greetings, zadeklarowanej w pliku index. html. Oto wiersze 50–53:
for (var j = 0; j < parent.greetings.length; j++) {
genLayer("greeting" + j, 50, 275, 500, 100, hideName,
'<SPAN CLASS="Greeting">' + parent.greetings[j] + '</SPAN>');
}
240

Znaczy to, że będzie parent.greetings.length pozdrowień, a wszystkie będą miały takie samo położenie: 50
i 275. Użytkownik nie może ich przemieszczać, ale wszystkie one umieszczone są w lewym górnym rogu obszaru
wyświetlania. Każde pozdrowienie ma swoją własną warstwę, która zawiera szereg znaczników SPAN, używających
definicji klasy arkusza stylów o nazwie Greeting, co widać na górze dokumentu.
Kiedy na miejscu są już tło i pozdrowienie, pozostaje tylko porozmieszczać ikony – wiersze 55–62:
for (var i = 0; i < icons.length; i++) {
if (i % iconNum == 0) { startWdh = 25; }
else { startWdh += 110; }
genLayer(icons[i], startWdh, 15, 100, 100, (i < iconNum ? showName :
hideName), '<A HREF="javascript: changeAction(\'' + icons[i] +
'\',' + (i + 1) + ');">' + '<IMG SRC="' + parent.Front.baseURL +
'/images/' + icons[i] + '.gif" BORDER="0"></A>');
}

Każdy element tablicy icons będzie reprezentował warstwę ikony. Zmienna iconNum decyduje o tym, że jednorazowo
pokazywane będą cztery ikony. Każdy obrazek będzie miał 100 pikseli szerokości, wysokość będzie się zmieniała.
Zmienna startWdh zaczyna wartość od 25. Wartość określać będzie położenie lewego piksela tworzonych warstw.
Wybrałem z góry odstępy szerokości 10 pikseli między poszczególnymi ikonami. Zatem poczynając od 25 pikseli
w prawo od lewego marginesu – nowe ikony są układane co 110 pikseli (100 to szerokość ikony, 10 –odstęp między
nimi). Kiedy zostanie utworzone iconNum ikon, znów proces zaczyna się od tego samego odstępu 25 pikseli.
Umożliwiają to dwie techniki programistyczne: jedna to instrukcja if-else, wywoływana przed utworzeniem
w genLayer() kolejnej warstwy, druga to użycie operatora modulo (%). Przyjrzyjmy się temu dokładniej:
if (i % iconNum == 0) { startWdh = 25; }
else { startWdh += 110; }

Kiedy wykonywana jest pętla for, wartość i stale wzrasta – za każdym razem, kiedy zmienna ta jest mnożona
przez iconNum (u nas 4), należy zacząć nową grupę ikon z pierwszą ikoną umieszczoną 25 pikseli od lewego
marginesu. startWdh otrzymuje wartość 25. Zatem następną grupę ikon zaczyna się, kiedy i ma wartości kolejno 4, 8,
12, 16 i 20. Jeśli i jest dowolną inną wartością, oznacza to, że następna ikona ma być przesunięta względem
poprzedniej o 110 pikseli – dlatego właśnie startWdh zwiększamy o 110. Operator modulo zwraca liczbę całkowitą
będącą resztą z dzielenia. Jeśli resztą jest 0, i jest wielokrotnością iconNum.
Określenie położenia lewego brzegu warstwy jest trudnym zadaniem. Teraz genLayout() może skończyć realizację
swoich zadań, tworząc warstwę dla każdej ikony dzięki odpowiedniemu wywołaniu genLayer():
genLayer(icons[i], startWdh, 15, 100, 100, (i < iconNum ? showName :
hideName), '<A HREF="javascript: changeAction(\'' + icons[i] +
'\',' + (i + 1) + ');">' + '<IMG SRC="' + parent.Front.baseURL +
'/images/' + icons[i] + '.gif" BORDER="0"></A>');

Warstwa każdej ikony zawiera pojedynczy znacznik IMG otoczony znacznikami zakładki (anchor, A). Zwróćmy uwagę,
że drugi i trzeci argument, przekazywany do genLayer(), są wartościami lewego i górnego brzegu warstwy.
startWdh zawsze oznacza brzeg lewy, brzeg górny stale jest ustawiony na 15 pikseli. Szósty parametr określa, czy
ikona ma być widoczna, czy ukryta. Domyślnie pokazywany jest tylko pierwszy zestaw utworzonych ikon. W naszym
wypadku są to pierwsze cztery warstwy. Wobec tego operator warunkowy szóstego parametru jest taki, że jeśli i jest
mniejsze od iconNum (czyli u nas ma wartość 0, 1, 2 lub 3), warstwa ma być widoczna, natomiast wszystkie pozostałe
warstwy mają być ukryte. Jeśli ikona ma być widoczna, przekazuje się zmienną showName; w przeciwnym wypadku
przekazuje się hideName.
W tym miejscu zostało nam jeszcze omówienie natury znacznika zakładki. Zastanówmy się: kiedy tylko użytkownik
przesuwa wskaźnik myszki nad ikoną i klika tam po raz pierwszy, chce oczywiście „wziąć” ikonę i ją gdzieś
przeciągnąć. Aby było to możliwe, w protokole javascript: w atrybucie HREF wywołujemy funkcję
changeAction(), którą zresztą zaraz omówimy. Wszystkie atrybuty HREF powodują to samo wywołanie, ale łącze
każdej ikony musi przekazać funkcji changeAction() dane o sobie.
Najpierw changeAction() musi znać nazwę ikony, która ma być obsługiwana. To łatwe – przekazujemy icons[i],
gdzie zapisano odpowiedni tekst. Następnie trzeba przekazać liczbę całkowitą odpowiadającą ikonie w obiektowym
modelu dokumentu Internet Explorera. Oznacza to, że aby można było zastosować technikę „przeciągnij i upuść”,
Internet Explorer będzie musiał wiedzieć, o jaki obrazek chodzi. Pamiętajmy, że pierwszy obrazek na stronie był tłem –
document. images[0]. Pierwsza ikona to document.images[1], chodzi o wszystkie pozostałe ikony to
document.images[i+1], dlatego właśnie przekazujemy wartość (i+1). Stanie się to jaśniejsze, kiedy zajrzymy
do changeAction() i grabXY().
Sporo tego wyjaśniania, jak na 27 wierszy tej funkcji. Ustawiamy startWdh na 25 i idziemy dalej.
241 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

Śledzenie położenia myszy


Funkcja motionListener() umożliwia JavaScriptowi przechwytywać ruchy myszki w ramach obsługi zdarzenia
onmousemove. Łatwo taką obsługę ustawić, a jedyna różnica między przeglądarkami polega na tym, że w Netscape
Navigatorze należy wywołać metodę captureEvents() obsługującą okno, tymczasem w Internet Explorerze
obsługiwany jest dokument. Oto wiersze 96–104:
function motionListener() {
if (NN) {
window.captureEvents(Event.MOUSEMOVE);
window.onmousemove = grabXY;
}
else {
document.onmousemove = grabXY;
}
}

Kiedy tylko użytkownik przesunie myszkę, wywoływana jest funkcja grabXY(). Pamiętajmy, że w tym wypadku
w wywołaniu nawiasy są zbędne. onmousemove po prostu używa funkcji grabXY(), ale nie wywołuje jej standardowo.
Zdarzenie onLoad wywołuje funkcję motionListener() – wiersz 132. Funkcja ta wywoływana jest tylko raz, więc
mysz jest śledzona przez cały czas działania aplikacji.

Wywoływanie wszystkich ikon


Kiedy użytkownik klika ikonę, wywołanie changeAction() tę ikonę ożywia, umożliwiając jej przenoszenie. Oto
szczegóły z wierszy 123–127:
function changeAction(name, MSIERef) {
activate = !activate;
activeLayer = name;
imgIdx = MSIERef;
}

Pamiętamy zmienne activate i activeLayer? Zostały zadeklarowane dawno temu na początku dokumentu.
activate początkowo miała wartość false, co oznacza nieprzenoszenie warstw przy ruchu myszki – okaże się to już
wkrótce, podczas omawiania funkcji grabXY(). Za pierwszym razem, przy wywołaniu changeAction(), activate
uzyskuje wartość true, uruchamiając przez to grabXY(). Warstwa będzie teraz przenoszona tam, gdzie przeniesiony
zostanie wskaźnik myszy. Jedyny sposób przerwania tego to powtórne kliknięcie myszką, kiedy to zmienna activate
znów zmienia swój stan na przeciwny, czyli tym razem false. W ten sposób przeciąganie się skończyło.
Warto przypomnieć, że changeAction() miała otrzymywać dwa parametry. Jeden z nich to nazwa warstwy, na której
wszystko ma się odbywać, przypisywana zmiennej name. Drugi parametr to indeks obrazka, pozwalający się odwołać
do tablicy document.images w Internet Explorerze. Wartość ta przypisywana jest do właściwości
MSIERef.activeLayer, co jest z kolei używane do ustawienia name, natomiast imgIdx uzyskuje wartość MSIERef.
Tak właśnie musimy ikony przeciągać niezależnie od użytej przeglądarki.

Przenoszenie ikon
Funkcja motionListener() skonstruowana jest tak, że przy każdym ruchu wskaźnika myszy wywoływana jest
grabXY() – oto wiersze 106–121:
function grabXY(ev) {
if (activate) {
if(NN) {
var itemWdh = refSlide(activeLayer).document.images[0].width;
var itemHgt = refSlide(activeLayer).document.images[0].height;
refSlide(activeLayer).left = ev.pageX - parseInt(itemWdh / 2);
refSlide(activeLayer).top = ev.pageY - parseInt(itemHgt / 2);
}
else {
var itemWdh = document.images[imgIdx].width;
var itemHgt = document.images[imgIdx].height;
refSlide(activeLayer).left = event.x - parseInt(itemWdh / 2);
refSlide(activeLayer).top = event.y - parseInt(itemHgt / 2);
}
}
}

Wspomniano wyżej o wywoływaniu funkcji grabXY(), ale jej kod jest wykonywany jedynie wtedy, gdy zmienna
activate ma wartość true. Przy pierwszym kliknięciu łącza ikony taka właśnie wartość jest ustawiana i do głosu
242

dochodzi zagnieżdżona instrukcja if-else. Jeśli użytkownik używa przeglądarki Netscape Navigator, wykonywany
jest blok if, w przeciwnym wypadku mamy blok else. W obu wypadkach realizowana jest ta sama funkcja, ale
za każdym razem w innej przeglądarce.
W obu blokach kodu deklaruje się zmienne lokalne itemWdh i itemHgt. Będą one określały położenie lewego
i górnego rogu klikniętej ikony. Dlaczego właściwie nie ustawić ich wartości na współrzędne bieżącego położenia
myszki? W końcu przecież w ten sposób mamy śledzić położenie kursora.
Tak, można byłoby tak zrobić, ale jest tu pewna pułapka. Jeśli tak postąpimy, będzie to znaczyło, że kursor myszki
znajdzie się w lewym górnym rogu ikony podczas tego przeciągania. Wygląda to dość dziwnie, a co gorsza, użytkownik
może poruszać myszką na tyle szybko, aby „uciec przeciąganiu” i kliknąć, kiedy myszka nie jest nad ikoną. Użytkownik
może kliknąć kilka razy, aby ikonę zwolnić.
Rozwiązaniem jest takie umieszczenie ikony, aby kursor myszki był zawsze pośrodku tej ikony. Niezależnie
od stosowanej przeglądarki – itemWdh i itemHgt reprezentują odpowiednio szerokość i wysokość obrazka ikony
klikniętej przez użytkownika. Wartości te musimy jednak pobierać różnie, w zależności od stosowanej przeglądarki.
W Netscape Navigatorze wygląda to tak:
var itemWdh = refSlide(activeLayer).document.images[0].width;
var itemHgt = refSlide(activeLayer).document.images[0].height;

Aby dostać się do obrazka, musimy odwołać się do odpowiedniej warstwy, następnie dokumentu, w końcu
do images[0] (na warstwie jest tylko jeden obrazek). Inaczej jest w Internet Explorerze:
var itemWdh = document.images[imgIdx].width;
var itemHgt = document.images[imgIdx].height;

W drugim przypadku nie ma tablicy warstw. Do obrazków możemy sięgać bezpośrednio przez tablicę images, jednak
musimy wiedzieć, o który obrazek nam chodzi – stąd użycie imgIdx. Pamiętajmy, że ustawiamy tę zmienną przy każdym
wywołaniu changeAction(). W razie jakiś wątpliwości przejrzyjmy jeszcze raz tę funkcję.
Mamy teraz szerokość i wysokość potrzebnego obrazka, więc wystarczy jedynie proste wyliczenie matematyczne
umożliwiające ustawienie kursora myszy nad środkiem obrazka.
Teraz pora na przykład. Załóżmy, że użytkownik kliknął ikonę, którą chce przeciągać. W momencie kliknięcia kursor
myszy był 100 pikseli w prawo od lewego brzegu dokumentu i 100 pikseli poniżej od górnej krawędzi dokumentu
(nie ekranu). Załóżmy, że ikona jest szeroka na 100 pikseli i na 150 pikseli wysoka. Jeśli ustawimy właściwości left
i top ikony na: 100, 100, lewy górny róg ikony zostanie umieszczony dokładnie pod kursorem myszki. Dobrze, ale kursor
miał być pośrodku ikony.
Aby uzyskać taki efekt, od położenia lewego brzegu należy odjąć połowę szerokości i od górnego brzegu odjąć połowę
wysokości. Wiemy, że itemWdh równe jest 100, itemHgt równe jest 150. Oto sposób wyliczania nowych pozycji:
Ikona.left = kursor myszki w poziomie (x) - (100/2) = 100-(50) = 50
Ikona.top = kursor myszki w pionie (y) - (150/2) = 100-(75) = 25

Tak więc left i top uzyskują ostatecznie wartości 50 i 25, a nie 100 i 100. W ten sposób umieszczamy strzałkę
kursora myszy pośrodku ikony. Aby się upewnić, że z dzielenia przez dwa uzyskamy liczbę całkowitą, użyjemy funkcji
parseInt(), zwracającą część całkowitą swojego argumentu. Spójrzmy jeszcze raz na kod funkcji grabXY() – oto
implementacja w Netscape Navigatorze:
refSlide(activeLayer).left = ev.pageX - parseInt(itemWdh / 2);
refSlide(activeLayer).top = ev.pageY - parseInt(itemHgt / 2);

W modelu zdarzeń w Netscape Navigatorze używa się obiektów zdarzeń tworzonych w biegu, czego
odzwierciedleniem jest tutaj zmienna lokalna ev. Właściwości pageX i pageY tego obiektu zdarzenia zawierają
wartości współrzędnych x i y aktywnej warstwy. Z kolei w Internet Explorerze istnieją globalne obiekty zdarzeń,
z których można sięgać do współrzędnych:
refSlide(activeLayer).left = event.x - parseInt(itemWdh / 2);
refSlide(activeLayer).top = event.y - parseInt(itemHgt / 2);

Odpowiednie wartości znajdują się we właściwościach x i y. Teraz można już ikonę przeciągać i kłaść na kartce
z życzeniami.

Kiedy dokumenty już się załadują


Tak naprawdę zaczynamy działać w pliku front.html, którego kod pokazano w przykładzie 10.3. Pierwszych kilkanaście
wierszy zawiera właściwości arkusza stylów, następnych kilkaset to zmienne i funkcje JavaScriptu odpowiedzialne
za przechwytywanie informacji umożliwiających tworzenie, testowanie i w końcu wysyłanie życzeń.
243 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

Przykład 10.3. front.html


1 <HTML>
2 <HEAD>
3 <TITLE></TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6
7 TD
8 {
9 font-family: Arial;
10 }
11
12 .Front
13 {
14 position: absolute;
15 left: 25;
16 top: 25;
17 width: 325;
18 border: 1px solid;
19 background: #ffffee;
20 }
21
22 //-->
23 </STYLE>
24 <SCRIPT LANGUAGE="JavaScript1.2">
25 <!--
26
27 var curGreet = iconIdx = 0;
28 var backgroundIdx = 0;
29 var baseURL = ".";
30 var bRef = parent.Back;
31
32 function showGreeting(selIdx) {
33 if (selIdx > 0) {
34 bRef.hideSlide("greeting" + curGreet);
35 bRef.showSlide("greeting" + selIdx);
36 curGreet = selIdx;
37 }
38 }
39
40 function nextBackground() {
41 backgroundIdx = (backgroundIdx == bRef.backImgs.length - 1 ?
42 backgroundIdx = 0 : backgroundIdx + 1);
43 if(document.all) {
44 bRef.document.background.src = bRef.backImgs[backgroundIdx].src;
45 }
46 else {
47 bRef.document.layers["Back"].document.images[0].src =
48 bRef.backImgs[backgroundIdx].src;
49 }
50 }
51
52 function nextIcons() {
53 for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
54 bRef.iconNum; i++) {

Przykład 10.3. front.html (ciąg dalszy)


55 if (i < bRef.icons.length && !onCard(i)) {
56 bRef.hideSlide(bRef.icons[i]);
57 }
58 }
59 iconIdx = (iconIdx >= (bRef.icons.length / bRef.iconNum) - 1 ? 0 :
60 iconIdx + 1);
61 for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
62 bRef.iconNum; i++) {
63 if (i < bRef.icons.length) {
64 bRef.showSlide(bRef.icons[i]);
65 }
66 else { break; }
67 }
68 }
69
70 function resetForm() {
71 if (document.all) {
72 bRef.hideSlide("greeting" +
73 document.EntryForm.Greetings.selectedIndex);
244

74 document.EntryForm.reset();
75 }
76 else {
77 bRef.hideSlide("greeting" +
78 document.layers["SetupForm"].document.EntryForm.Greetings.selectedIndex);
79 document.layers["SetupForm"].document.EntryForm.reset();
80 }
81 }
82
83 function onCard(iconRef) {
84 var ref = bRef.refSlide(bRef.icons[iconRef]);
85 var ref2 = bRef.refSlide("Back");
86 if(document.all) {
87 if((parseInt(ref.left) >= parseInt(ref2.left)) &&
88 (parseInt(ref.top) >= parseInt(ref2.top)) &&
89 (parseInt(ref.left) + parseInt(ref.width) <= parseInt(ref2.left) +
90 parseInt(ref2.width)) &&
91 (parseInt(ref.top) + parseInt(ref.height) <= parseInt(ref2.top) +
92 parseInt(ref2.height))) {
93 return true;
94 }
95 }
96 else {
97 if((ref.left >= ref2.left) &&
98 (ref.top >= ref2.top) &&
99 (ref.left + ref.document.images[0].width <= ref2.left +
100 ref2.document.images[0].width) &&
101 (ref.top + ref.document.images[0].height <= ref2.top +
102 ref2.document.images[0].height)) {
103 return true;
104 }
105 }
106 ref.left = ((iconRef % bRef.iconNum) * 110) + bRef.startWdh;
107 ref.top = 15;
108 return false;
109 }
110
111 function shipGreeting(fObj) {
112 if (fObj.Recipient.value == "") {
113 alert('Musisz podać adres e-mail!');
114 return false;
115 }
116 else if (fObj.Message.value == "") {
117 alert("Musisz wpisać wiadomość.");
118 return false;
119 }

Przykład 10.3. front.html (ciąg dalszy)


120 else if (fObj.Greetings.selectedIndex == 0) {
121 alert('Musisz wybrać rodzaj życzeń.');
122 return false;
123 }
124
125 fObj.EntireMessage.value = genGreeting(fObj);
126 fObj.UniqueID.value = Math.round(Math.random() * 1000000);
127 fObj.BaseURL.value = baseURL;
128 return true;
129 }
130
131 function testGreeting(fObj) {
132 var msgStr = '<HTML><TITLE>Cyber Greeting Test Page</TITLE>' +
133 genGreeting(fObj) + '<TABLE ALIGN="CENTER"><TR><TD><FORM>' +
134 '<INPUT TYPE=BUTTON VALUE=" OK " onClick="self.close();">' +
135 '</FORM></TD></TR></TABLE></HTML>';
136 newWin = open('', '', 'width=' + (
137 bRef.backImgs[backgroundIdx].width + 50) +
138 ',height=600,scrollbars=yes');
139 with(newWin.document) {
140 open();
141 writeln(msgStr);
142 close();
143 }
144 newWin.focus();
145 }
146
147 function genGreeting(fObj) {
245 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

148 var greetingIdx = fObj.Greetings.selectedIndex;


149 var msg = fObj.Message.value;
150
151 msg = msg.replace(/\r+/g, "");
152 msg = msg.replace(/\n+/g, "<BR><BR>");
153
154 var msgStr = '<TABLE BORDER=0><TR><TD COLSPAN=2><FONT FACE=Arial>' +
155 '<H2>Twoje elektroniczne życzenia</H2>Do: ' + fObj.Recipient.value +
156 '<BR><BR></TD></TR>' + '<TR><TD VALIGN=TOP><IMG SRC="' +
157 baseURL + '/images/background' + backgroundIdx + '.jpg">' +
158 '<DIV STYLE="position:relative;left:40;top:-255;' +
159 'font-family:Arial;font-size:48px;font-weight:bold;">' +
160 parent.greetings[greetingIdx] + '</DIV>';
161
162 var iconStr = '';
163 for (var i = 0; i < bRef.icons.length; i++) {
164 if(onCard(i)) {
165 iconStr += '<DIV STYLE="position:absolute;left:' +
166 bRef.refSlide(bRef.icons[i]).left + ';top:' +
167 (parseInt(bRef.refSlide(bRef.icons[i]).top) -
168 (document.all ? 140 : 150)) + ';"><IMG SRC="' +
169 baseURL + '/images/' + bRef.icons[i] + '.gif"></DIV>';
170 }
171 }
172
173 msgStr += iconStr + '</TD></TR><TR><TD WIDTH=' +
174 bRef.backImgs[backgroundIdx].width + '><FONT FACE=Arial>' +
175 msg + '</TD></TR></TABLE>';
176 return msgStr;
177 }
178
179 //-->
180 </SCRIPT>
181
182 </HEAD>
183 <BODY onLoad="resetForm();">

Przykład 10.3. front.html (ciąg dalszy)


184 <DIV ID="SetupForm" CLASS="Front">
185 <FORM NAME="EntryForm"
186 ACTION=http://www.your_domain.com/cgi-bin/greetings/greet.pl"
187 METHOD="POST" TARGET="_top" OnSubmit="return shipGreeting(this);">
188 <INPUT TYPE=HIDDEN NAME="EntireMessage">
189 <INPUT TYPE=HIDDEN NAME="UniqueID">
190 <INPUT TYPE=HIDDEN NAME="BaseURL">
191 <TABLE CELLSPACING="0" CELLPADDING="5" WIDTH="375">
192 <TR>
193 <TD COLSPAN="3"><CENTER><H2>Cyber-życzenia</H2></CENTER></TD>
194 </TR>
195 <TR>
196 <TD HEIGHT="40" VALIGN="TOP">
197 Do:
198 </TD>
199 <TD COLSPAN="2" VALIGN="TOP">
200 <INPUT TYPE=TEXT NAME="Recipient" SIZE="25">
201 </TD>
202 </TR>
203 <TR>
204 <TD HEIGHT="80" VALIGN="TOP">Wiadomość: </TD>
205 <TD COLSPAN="2" VALIGN="TOP">
206 <TEXTAREA ROWS="7" COLS="25" NAME="Message" WRAP="PHYSICAL">
207 </TEXTAREA>
208 </TD>
209 </TR>
210 <TR>
211 <TD>Obrazki:</TD>
212 <TD HEIGHT="40" COLSPAN="2">
213 <INPUT TYPE=BUTTON VALUE=" Ikony - > " onClick="nextIcons();">
214 &nbsp;&nbsp;&nbsp;
215 <INPUT TYPE=BUTTON VALUE=" Tła - > "
216 onClick="nextBackground();">
217 </TD>
218 </TR>
219 <TR>
220 <TD>Życzenia:</TD>
221 <TD HEIGHT="40" COLSPAN="2">
246

222 <SCRIPT LANGUAGE="JavaScript1.2">


223 <!--
224
225 var sel = '<SELECT NAME="Greetings"
226 onChange="showGreeting(this.selectedIndex);">';
227 for (var i = 0; i < parent.greetings.length; i++) {
228 sel += '<OPTION>' + parent.greetings[i];
229 }
230 sel += '</SELECT>';
231 document.writeln(sel);
232
233 //-->
234 </SCRIPT>
235 </TD>
236 </TR>
237 <TR>
238 <TD VALIGN=TOP>Wysyłanie: </TD>
239 <TD HEIGHT="40" ALIGN="CENTER">
240 <INPUT TYPE=BUTTON VALUE=" Test "
241 onClick="testGreeting(this.form);">
242 &nbsp;&nbsp;&nbsp;&nbsp;
243 <INPUT TYPE=BUTTON VALUE=" Wyczyść " onClick="resetForm();">
244 &nbsp;&nbsp;&nbsp;&nbsp;
245 <INPUT TYPE=SUBMIT VALUE=" Wyślij ">
246 </FORM>
247 </TD>
248 </TR>

Przykład 10.3. front.html (dokończenie)


249 </TABLE>
250 </FORM>
251 </DIV>
252 </BODY>
253 </HTML>

Poznaj zmienne
Choć plik font.html nie zawiera tylu zmiennych, co back.html, to kilka znajdziemy w wierszach 28–30:
var curGreet = iconIdx = 0;
var backgroundIdx = 0;
var bRef = parent.Back;

Zmienna curGreet zawiera indeks listy wyboru rodzaju życzeń. Początkowo ma wartość 0. iconIdx jest zmienną
używaną do śledzenia ikon według indeksu i początkowo też ma wartość 0. Ostatnia zmienna to bRef, która jest
po prostu odnośnikiem do obiektu skryptu i okna w ramce o nazwie Back. Ułatwi to nam życie.

Techniki języka JavaScript:


różnicowanie kodu obsługi sieci
W aplikacji z tego rozdziału, w przeciwieństwie do aplikacji z innych rozdziałów, uży-
wamy dużo statycznego kodu HTML. Warto różne kody pisać tak, aby wyglądały inaczej.
Na przykład w przypadku kodu działającego po stronie klienta zawsze HTML można za-
pisywać wielkimi literami, natomiast liter takich nie używać w kodzie JavaScriptu. Jeśli
ktoś kiedyś widział te dwa języki, jest w stanie je rozróżnić, ale wspomniany wyżej spo-
sób sprawia, że różnica jest jeszcze bardziej uwidaczniana.
Może nie wydawać się to specjalnie ważną rzeczą. Jednak zwyczaj ten podpatrzyłem
u pewnego programisty, który używał dużo kodu języka Cold Fusion Markup Language
(CFML), popularnego języka skryptowego działającego po stronie serwera. Cały jego kod
zawiera HTML, CFML, JavaScript i SQL (Strukturalny Język Zapytań do obsługi baz da-
nych). Są to cztery różne języki w jednym skrypcie. Załóżmy, że używamy Active Server
Pages – otwiera nam to drzwi do języków HTML, VBScript, JavaScript, JScript i SQL.
Ilu akronimów potrzebujemy?
Nie trzeba chyba wspominać, że szybko opracowałem własną strategię kodowania.
247 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

Wyświetlanie życzeń
Teraz, kiedy lista wyboru typu życzeń jest już ustawiona, użytkownik może wyświetlić wybrane życzenia. Procedura obsługi
zdarzenia onChange listy wyboru wywołuje funkcję showGreeting():
function showGreeting(selIdx) {
if (selIdx > 0) {
bRef.hideSlide("greeting" + curGreet);
bRef.showSlide("greeting" + selIdx);
curGreet = selIdx;
}
}
248

Techniki języka JavaScript:


komunikacja między ramkami
W rozdziale 1., że używaliśmy zmiennej docObj umożliwiającej nam proste odwoływa-
nie się do obiektu dokumentu (parent.frames[1]). Z taką sytuacją mamy do czynienia
teraz, ale odwołujemy się do okna Back. W pliku back.html zadeklarowane są zmienne
używane też w pliku font.html. Użycie zmiennej, odnoszącej się do parent.Back,
upraszcza nieco pisanie kodu (zamiast parent.Back wystarczy napisać bRef) i umożli-
wia łatwe używanie danych z innych ramek. Możemy sobie wyświadczyć przysługę, jeśli
stworzymy taką zmienną i do niej będziemy się odwoływać. Przyjrzyjmy się funkcji on-
Card() z pliku front.html. Nie tylko używamy w niej bRef, ale tworzymy też dwie inne
zmienne podobnego typu, ref i ref2, umożliwiające odwoływanie się do wybranych
warstw. Zobaczmy, jak wygląda sama funkcja:
function onCard(iconRef) {
var ref = bRef.refSlide(bRef.icons[iconRef]);
var ref2 = bRef.refSlide("Back");
if(document.all) {
if((parseInt(ref.left) >= parseInt(ref2.left)) &&
(parseInt(ref.top) >= parseInt(ref2.top)) &&
(parseInt(ref.left) + parseInt(ref.width) <= parseInt(ref2.left) +
parseInt(ref2.width)) &&
(parseInt(ref.top) + parseInt(ref.height) <= parseInt(ref2.top) +
parseInt(ref2.height))) {
return true;
}
}
else {
if((ref.left >= ref2.left) &&
(ref.top >= ref2.top) &&
(ref.left + ref.document.images[0].width <= ref2.left +
ref2.document.images[0].width) &&
(ref.top + ref.document.images[0].height <= ref2.top +
ref2.document.images[0].height)) {
return true;
}
}
ref.left = ((iconRef % bRef.iconNum) * 110) + bRef.startWdh;
ref.top = 15;
return false;
}

Nie jest to najdłuższa funkcja, jaką zdarzyło mi się napisać, ale zastanówmy się, co by się
stało, gdyby nie używać w niej zmiennych bRef, ref i ref2 – byłaby dłuższa i trudniej-
sza do zrozumienia:
function onCard(iconRef) {
if(document.all) {
if((parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).left) >=
parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).left)) &&
(parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).top) >=
parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).top)) &&
(parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).left) +
parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).width) <=
parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).left) +
parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).width)) &&

– dokończenie na następnej stronie–

parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).top) +
parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).height)))
{
return true;
}
}
else {
if((parent.Back.refSlide(parent.Back.icons[iconRef]).left >=
parent.Back.refSlide(parent.Back.icons[iconRef]).left) &&
(parent.Back.refSlide(parent.Back.icons[iconRef]).top >=
parent.Back.refSlide(parent.Back.icons[iconRef]).top) &&
(parent.Back.refSlide(parent.Back.icons[iconRef]).left +
parent.Back.refSlide(parent.Back.icons[iconRef]).document.
249 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

images[0].width <=
parent.Back.refSlide(parent.Back.icons[iconRef]).left +
parent.Back.refSlide(parent.Back.icons[iconRef]).document.
images[0].width) &&
(parent.Back.refSlide(parent.Back.icons[iconRef]).top +
parent.Back.refSlide(parent.Back.icons[iconRef]).document.
images[0].height <=
parent.Back.refSlide(parent.Back.icons[iconRef]).top +
parent.Back.refSlide(parent.Back.icons[iconRef]).document.
images[0].height)) {
return true;
}
}
parent.Back.refSlide(parent.Back.icons[iconRef]).left =
((iconRef % parent.Back.iconNum) * 110) + parent.Back.startWdh;
parent.Back.refSlide(parent.Back.icons[iconRef]).top = 15;
return false;
}

Funkcja showGreeting() spodziewa się jednego parametru, selectIndex z listy wyboru Greetings. O ile tylko
selIdx nie jest zerem (co oznacza polecenie wyboru typu życzeń), showGreeting() ukrywa aktualnie widoczną
warstwę pozdrowień i wyświetla warstwę związaną z typem życzeń wybranych przez użytkownika. Następnie selIdx
staje się bieżącą wartością warstwy widzialnej, co przygotowuje nas do następnego wywołania.

Obrazki po kolei
Aby przejrzeć dostępne obrazki tła, użytkownik po prostu klika przycisk Tła ->, póki nie znajdzie takiego, który mu będzie
odpowiadał. Kliknięcie tego przycisku wywołuje funkcję nextBackground() z wiersza 40–50:
function nextBackground() {
backgroundIdx = (backgroundIdx == bRef.backImgs.length - 1 ?
backgroundIdx = 0 : backgroundIdx + 1);
if(document.all) {
bRef.document.background.src = bRef.backImgs[backgroundIdx].src;
}
else {
bRef.document.layers["Back"].document.images[0].src =
bRef.backImgs[backgroundIdx].src;
}
}

Obrazki tła wstępnie ładowane są w wierszach 40–44 pliku back.html. Jako że każdy z tych obrazków ma nazwę typu
background0.jpg, background1.jpg, background2.jpg i tak dalej, możemy użyć liczby całkowitej backgroundIdx,
którą będziemy łączyć z napisem, i w ten sposób uzyskamy kolejne obrazki. Kiedy dokument jest ładowany, zmienna
backgroundIdx jest ustawiana na 0. Przy każdym kliknięciu przycisku Tła -> wartość ta jest zwiększana o 1, aż
dojdziemy do ostatniego obrazka. Kiedy backgroundIdx osiąga wartość top.Back.backImgs.length-1,
ponownie jest zerowana, dzięki czemu możemy zaczynać od początku.
Teraz przyszedł czas na wykorzystanie tej świeżo uzyskanej wartości do zmiany właściwości src odpowiedniego
obiektu Image. Jako że obrazek tła był umieszczony w warstwie w celu dokładniejszego pozycjonowania, musimy
różnie odnosić się do modelu DOM Netscape Navigatora i Internet Explorera.
W przypadku Internet Explorera obrazek jest uważany za właściwość obiektu dokumentu:
top.Back.document.background.src

Z kolei w przypadku Netscape Navigatora odwołujemy się do obiektu dokumentu w warstwie. Ponieważ warstwa
nazywa się Back, dostanie się do odpowiedniego obiektu Image wygląda tak:
top.Back.document.layers["Back"].document.images[0].src

Kiedy odpowiednia składnia zostanie już określona, możemy ustawić ścieżkę we właściwości src obrazka backImgs,
stosując backgroundIdx. Warto dodać, nie po raz pierwszy używamy takiej iteracji. Podobne przykłady znajdziemy
w rozdziałach 3. i 8. Teraz użytkownik może już cyklicznie przeglądać obrazki tła – potrzebujemy podobnego
rozwiązania dla ikon. Tutaj właśnie użyjemy funkcji nextIcons() z wierszy 52–68:
function nextIcons() {
for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
bRef.iconNum; i++) {
if (i < bRef.icons.length && !onCard(i)) {
bRef.hideSlide(bRef.icons[i]);
}
250

}
iconIdx = (iconIdx >= (bRef.icons.length / bRef.iconNum) - 1 ? 0 :
iconIdx + 1);
for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
bRef.iconNum; i++) {
if (i < bRef.icons.length) {
bRef.showSlide(bRef.icons[i]);
}
else { break; }
}
}

Użytkownik przegląda ikony tak jak wcześniej tła, ale tym razem chodzi o coś więcej, niż tylko zmienianie właściwości
src pojedynczego obrazka. Zamiast tego każda ikona jest osobnym obrazkiem na osobnej warstwie. Wobec tego
kliknięcie przycisku Ikony -> powoduje nieco bardziej złożoną akcję. Nie tylko musimy ukryć wszystkie warstwy
obecnie widoczne, ale też zdecydować, które warstwy pokazać, przy czym musimy to wszystko robić też grupami.
Nie powinno być tak, żeby użytkownik musiał klikać 20 razy w celu zobaczenia 20 kolejnych ikon. Może to być
nużącej, a przy tym będziemy marnować dostępną przestrzeń okna przeglądarki. Jak na obrazkach na początku tego
rozdziału widać, zdecydowano się wyświetlać ikony w czteroelementowych grupach. Jakąkolwiek liczbę wybierzemy,
będzie ona zapisywana w zmiennej iconNum ustawianej w wierszu 24 pliku back.html. Tym razem jesteśmy w pliku
font.html, więc odwołanie ma postać top.Back.iconNum. Chodzi o wyświetlenie iconNum ikon przy każdym
kliknięciu przycisku Ikony ->. Jeśli mamy 20 ikon, użytkownik będzie oczekiwał pięciu grup ikon. Oczywiście chcemy
też ułatwić dodawanie i odejmowanie ikon. Jeśli usuwamy jedną ikonę, użytkownik zobaczy cztery grupy
czteroikonowe i jedną grupę trójelementową. Nie musimy dokonywać natomiast żadnych zmian w funkcji
nextIcons().

Rzecz jest całkiem łatwa. Zaczynamy od pierwszej czwórki, potem ją ukrywamy i wyświetlamy następną, aż nam
zbraknie ikon. Wtedy zaczynamy znów od początku. Można wyjaśnić to jeszcze prościej: ukrywamy cztery poprzednie
ikony, pokazujemy cztery następne. Przyjrzyjmy się teraz sformułowaniu tego zdania w JavaScripcie. Do identyfikacji
poszczególnych grup używamy zmiennej iconIdx, początkowo ustawionej na 0. Pierwsza grupa związana jest właśnie
z wartością 0, druga z wartością 1, i tak dalej.
Kiedy użytkownik klika Ikony ->, musimy ukryć wszystkie ikony z grupy związanej z bieżącą wartością iconIdx:
for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
bRef.iconNum; i++) {
if (i < bRef.icons.length && !onCard(i)) {
bRef.hideSlide(bRef.icons[i]);
}
}

Zmienna i ustawiana jest na iconNum * iconIdx. i, będzie zwiększana o 1, póki nie przekroczy wartości (iconNum
* iconIdx) + iconNum. Jeśli ktoś ma wątpliwości, niech zastanowi się, co dzieje się po zakończeniu ładowania
dokumentu. iconNum równa jest 4, iconIdx równa jest 0. Znaczy to, że przy pierwszym wywołaniu funkcji i –
przybierze wartości 0, 1, 2 i 3. Następnym razem iconIdx równa będzie 1, więc i przybierze wartości 4, 5, 6 i 7. I tak
dalej.
Zmienna i jest liczbą całkowitą, która będzie używana do sięgania do elementu z tablicy icons. Dlaczego? Każda ikona
ma przecież własną warstwę. Kod zawarty w pliku back.html nazywa wszystkie warstwy zgodnie z elementami tablicy
icons. Na przykład icons[0] odnosi się do warstwy bear.

Pozostaje teraz ukryć warstwy 0, 1, 2 i 3 – chyba że użytkownik przeciągnął którąś z nich na kartkę z życzeniami.
Zajmuje się tym funkcja onCard(), którą wkrótce omówimy. Załóżmy na razie, że nie zostały przeniesione jeszcze
żadne ikony, bo uprości nam to dalszą analizę funkcji. Wywołujemy po prostu funkcję hideSlide z pliku back.html
i przekazujemy jej nazwę odpowiedniej warstwy, którą identyfikujemy przez i:
bRef.hideSlide(bRef.icons[i]);

Ikon poprzednich już nie ma, teraz musimy pokazać następną grupę. Zanim jednak to zrobimy, upewnimy się, że
nie jesteśmy już przy ostatniej grupie. Jeśli tak, ustawiamy iconIdx ponownie na 0. W przeciwnym wypadku
powiększamy iconIdx o 1. Oto wiersze 59–60:
iconIdx = (iconIdx >= (bRef.icons.length / bRef.iconNum) - 1 ? 0 :
iconIdx + 1);

Jeszcze jedna iteracja i widoczna będzie następna grupa. Wiersze 61–67 zawierają pętlę for, która zajmuje się
pokazaniem ikon:
for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
bRef.iconNum; i++) {
251 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

if (i < bRef.icons.length) {
bRef.showSlide(bRef.icons[i]);
}
else { break; }
}

Zamierzamy zrobić iconNum iteracji i pokazać następną grupę ikon. Poprzednio powiększyliśmy lub wyzerowaliśmy
w wierszach 59–60 zmienną iconIdx, więc teraz wystarczy tylko wykonać prawie to samo w pętli for, co poprzednio
robiliśmy, ukrywając grupę poprzednią. Tym razem jednak użyjemy funkcji showSlide(). Jest tu jednak pewna
pułapka. Pamiętajmy, że chcemy zrobić iconNum iteracji, ale co się stanie, jeśli jest to ostatnia grupa i nie ma już
w niej iconNum ikon? Jeśli mamy 20 ikon i chcemy je pokazywać czwórkami, będzie pięć takich czwórek. Jeśli jednak
mamy 19 ikon i też chcemy je wyświetlać czwórkami, nadal jest pięć grup, ale ostatnia z nich zawiera tylko cztery ikony.
Dlatego właśnie potrzebujemy dodatkowej instrukcji if-else, która będzie sprawdzać, czy nie mamy do dyspozycji
mniej ikon, niż wynikałoby to z indeksów. Jeśli tak, nextIcons() uwidacznia ikony. Jeśli nie, nie ma już w tej grupie
ikon i pętla jest przerywana instrukcją break.

Utrzymanie ikon na miejscu


Jak można się było dowiedzieć, iteracja przez ikony obejmuje ukrywanie starych ikon i pokazywanie nowych. Działa to
nieźle, póki użytkownik nie przeciągnie jakichś ikon na kartkę z życzeniami. Takie ikony chcemy zostawić tam, gdzie
się znajdują. Funkcja onCard() ciężko pracuje nad określeniem, czy kolejne ikony mają być zostawione tam, gdzie są,
czy mają zostać ukryte. Oto wiersze 83–109 – funkcja onCard() niczego nie ukrywa ani nie pokazuje. Po prostu
zwraca true lub false i na podstawie tego inne funkcje podejmują odpowiednie działania:
function onCard(iconRef) {
var ref = bRef.refSlide(bRef.icons[iconRef]);
var ref2 = bRef.refSlide("Back");
if(document.all) {
if((parseInt(ref.left) >= parseInt(ref2.left)) &&
(parseInt(ref.top) >= parseInt(ref2.top)) &&
(parseInt(ref.left) + parseInt(ref.width) <= parseInt(ref2.left) +
parseInt(ref2.width)) &&
(parseInt(ref.top) + parseInt(ref.height) <= parseInt(ref2.top) +
parseInt(ref2.height))) {
return true;
}
}
else {
if((ref.left >= ref2.left) &&
(ref.top >= ref2.top) &&
(ref.left + ref.document.images[0].width <= ref2.left +
ref2.document.images[0].width) &&
(ref.top + ref.document.images[0].height <= ref2.top +
ref2.document.images[0].height)) {
return true;
}
}
ref.left = ((iconRef % bRef.iconNum) * 110) + bRef.startWdh;
ref.top = 15;
return false;
}

Zanim dokładniej przyjrzymy się funkcji onCard(), musimy wiedzieć, które ikony mają zostać uznane za należące
do kartki. W przypadku najprostszym wszystkie brzegi ikony (choćby niewidoczne) muszą być wewnątrz wszystkich
ograniczeń tła lub na tych granicach. Na rysunku 10.8 pokazano, co zostaje, a co jest usuwane.
Przy założeniu, że biały obszar to tło wyświetlania, mały ludzik po prawej natychmiast zostanie usunięty, bo jest całkiem
na zewnątrz. Niewiele brakuje kaktusowi, ale dwa jego brzegi jednak wychodzą za tło, więc kaktus też musi się z nami
pożegnać. Na widoku zostanie jedynie karateka – i tak to działa.
252

Rysunek 10.8. Poza granicami: zostanie tylko karateka


Wszystko odbywa się względem położenia wyznaczonego pikselami. Jako że tło zawarte jest w jednej warstwie,
możemy użyć DHTML do określenia lewego i górnego brzegu względem brzegów dokumentu. Ponieważ warstwa
zawiera tylko jeden obrazek, możemy użyć właściwości width i height obiektu Image, aby dokładnie określić
szerokość i wysokość warstwy. Ta sama zasada obowiązuje też w przypadku ikon. Użyjemy właściwości left i top
warstwy i właściwości width i height obiektu Image. Funkcja ta ma kilka zagnieżdżonych instrukcji if, ale
instrukcja if-else najbardziej zewnętrzna wykonuje w obu blokach tę samą akcję – raz w przeglądarce Internet
Explorer, raz w Netscape Navigatorze. Oto pierwsza część funkcji onCard() działająca w przeglądarce Internet
Explorer:
if(document.all) {
if((parseInt(ref.left) >= parseInt(ref2.left)) &&
(parseInt(ref.top) >= parseInt(ref2.top)) &&
(parseInt(ref.left) + parseInt(ref.width) <= parseInt(ref2.left) +
parseInt(ref2.width)) &&
(parseInt(ref.top) + parseInt(ref.height) <= parseInt(ref2.top) +
parseInt(ref2.height))) {
return true;
}
}

Obrazek tła ma cztery krawędzie, tak samo wszystkie ikony. Wobec tego musimy przeprowadzić cztery porównania,
aby upewnić się, że żaden brzeg ikony nie przekracza brzegu tła. Oto zapisane słownie instrukcje if z wierszy 87–94:
JEŚLI lewy brzeg ikony dotyka lub przekracza w prawo lewy brzeg tła
ORAZ górny brzeg ikony dotyka lub jest poniżej górnego brzegu tła
ORAZ prawy brzeg ikony dotyka lub przekracza w lewo prawy brzeg tła
ORAZ dolny brzeg ikony dotyka lub jest nad dolnym brzegiem tła, TO
ZWRÓĆ true.
Określenie lewego i górnego brzegu poszczególnych warstw jest nieskomplikowane – po prostu używamy właściwości
left i top poszczególnych warstw. Określenie prawego i dolnego brzegu też nie jest zbyt trudne, gdyż po prostu
dodajemy odpowiednio do lewego brzegu szerokość warstwy, a do górnego brzegu – wysokość warstwy.
Można zauważyć dwie rzeczy. Po pierwsze zmienne ref i ref2 zostały ustawione na warstwy ikony i tła. Zrobiono tak
tylko po to, aby ułatwić czytanie tego kodu. Po drugie wszędzie występuje funkcja parseInt().Internet Explorer
zwraca wartości właściwości left i top jako 250px zamiast zwykłego 250 i właśnie parseInt() przekształca taki
napis na liczbę, dzięki czemu możemy wykonywać swoje obliczenia.
Najbardziej zewnętrzna część else realizuje te same zadania w przeglądarce Netscape Navigatorze. Nie musimy już
używać funkcji parseInt(), gdyż Netscape Navigator zwraca liczby:
else {
if((ref.left >= ref2.left) &&
(ref.top >= ref2.top) &&
(ref.left + ref.document.images[0].width <= ref2.left +
ref2.document.images[0].width) &&
(ref.top + ref.document.images[0].height <= ref2.top +
ref2.document.images[0].height)) {
return true;
253 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

}
}

Jeśli zatem badana ikona przejdzie wszystkie cztery testy, obie przeglądarki zwrócą true; w przeciwnym wypadku
dzieje się rzecz następująca:
ref.left = ((iconRef % bRef.iconNum) * 110) + bRef.startWdh;
ref.top = 15;
return false;

Funkcja onCard() zauważa, że ikony nie mieszczą się na tle i przywraca im pierwotne położenie. Wszystkie ikony są
położone względem górnego brzegu tak samo – 15 pikseli poniżej. Jednak położenie lewego brzegu zależy od numeru
ikony w grupie. Nie ma żadnego problemu. Szybkie przeliczenie z użyciem zmiennych iconRef i iconNum pozwala
określić pierwotne położenie ikony. Okazuje się, że każdy obrazek ma 100 pikseli szerokości, poza tym między
obrazkami jest 10 pikseli odległości w poziomie, zatem pozycjonowanie jest całkiem łatwe. Ostatecznie funkcja zwraca
wartość false.

Sprawdzanie, co otrzymaliśmy
Dobrze byłoby, gdyby nadawca mógł podejrzeć, co ma dostać jego odbiorca. Wybór przycisku Test otworzy w tym celu
osobne okienko – oto wiersze 131–145:
function testGreeting(fObj) {
var msgStr = '<HTML><TITLE>Cyber Greeting Test Page</TITLE>' +
genGreeting(fObj) + '<TABLE ALIGN="CENTER"><TR><TD><FORM>' +
'<INPUT TYPE=BUTTON VALUE=" OK " onClick="self.close();">' +
'</FORM></TD></TR></TABLE></HTML>';
newWin = open('', '', 'width=' + (
bRef.backImgs[backgroundIdx].width + 50) +
',height=600,scrollbars=yes');
with(newWin.document) {
open();
writeln(msgStr);
close();
}
newWin.focus();
}

Funkcja testGreeting() ma tylko dwa zadania: otworzyć okno na tyle szerokie, aby umożliwić wyświetlenie
wiadomości i wpisać do niego treści w postaci dokumentu. Treść tego okna jest przechowywana w zmiennej lokalnej
msgStr. Jest tu nieco statycznego HTML, reszta to treści generowane dynamicznie przez funkcję genGreeting(). msgStr
zawiera też na końcu formularz z przyciskiem pozwalającym zamknąć nasze nowe okno. Kiedy funkcja msgStr zostanie
załadowana całym dobrodziejstwem inwentarza, testGreeting() otwiera okno o 50 pikseli szersze od szerokości
obrazka tła i o stałej wysokości 600 pikseli. Funkcja zapisuje treść w strumieniu dokumentu, nowe okno staje się
aktywne – i to wszystko.

Ostateczne tworzenie kartki


Funkcja testGreeting() udostępnia okno pozwalające podglądnąć okno pozdrowień, ale to funkcja
genGreeting() wykonuje całą pracę.

Oto jej wiersze: 147–177:


function genGreeting(fObj) {
var greetingIdx = fObj.Greetings.selectedIndex;
var msg = fObj.Message.value;

msg = msg.replace(/\r+/g, "");


msg = msg.replace(/\n+/g, "<BR><BR>");

var msgStr = '<TABLE BORDER=0><TR><TD COLSPAN=2><FONT FACE=Arial>' +


'<H2>Twoje elektroniczne życzenia</H2>Do: ' + fObj.Recipient.value +
'<BR><BR></TD></TR>' + '<TR><TD VALIGN=TOP><IMG SRC="' +
baseURL + '/images/background' + backgroundIdx + '.jpg">' +
'<DIV STYLE="position:relative;left:40;top:-255;' +
'font-family:Arial;font-size:48px;font-weight:bold;">' +
parent.greetings[greetingIdx] + '</DIV>';

var iconStr = '';


for (var i = 0; i < bRef.icons.length; i++) {
if(onCard(i)) {
iconStr += '<DIV STYLE="position:absolute;left:' +
254

bRef.refSlide(bRef.icons[i]).left + ';top:' +
(parseInt(bRef.refSlide(bRef.icons[i]).top) -
(document.all ? 140 : 150)) + ';"><IMG SRC="' +
baseURL + '/images/' + bRef.icons[i] + '.gif"></DIV>';
}
}

msgStr += iconStr + '</TD></TR><TR><TD WIDTH=' +


bRef.backImgs[backgroundIdx].width + '><FONT FACE=Arial>' +
msg + '</TD></TR></TABLE>';
return msgStr;
}

Funkcja ta jest nieco trudna w analizie, ale możemy sobie zadanie uprościć, jeśli zastanowimy się, co ma ona właściwie
wykonać. Otóż genGreeting() ma tylko zwrócić kod HTML zawierający:
• tekst wyświetlający adres poczty elektronicznej odbiorcy,
• obrazek tła,
• odpowiednio ułożony tekst życzeń,
• odpowiednio rozmieszczone ikony,
• treść wiadomości.
Nie jest to szczególnie dużo, ale zanim zajmiemy się generowaniem tej treści, musimy zrobić trochę porządku. Oto
wiersze 148-152:
var greetingIdx = fObj.Greetings.selectedIndex;
var msg = fObj.Message.value;
msg = msg.replace(/\r+/g, "");
msg = msg.replace(/\n+/g, "<BR><BR>");

Deklarujemy dwie zmienne lokalne, greetingIdx i msg. Pierwsza z nich to selectedIndex listy wyboru
Greetings. msg to wiadomość wpisana przez nadawcę. Jako że życzenia będą wyświetlane jako HTML, znaki końca
wiersza nie będą właściwie interpretowane, zatem musimy je zastąpić znacznikami <BR>. Teraz możemy zabrać się
za tworzenie pozdrowień. Zacznijmy od góry – oto wiersze 154–160:
var msgStr = '<TABLE BORDER=0><TR><TD COLSPAN=2><FONT FACE=Arial>' +
'<H2>Twoje elektroniczne życzenia</H2>Do: ' + fObj.Recipient.value +
'<BR><BR></TD></TR>' + '<TR><TD VALIGN=TOP><IMG SRC="' +
baseURL + '/images/background' + backgroundIdx + '.jpg">' +
'<DIV STYLE="position:relative;left:40;top:-255;' +
'font-family:Arial;font-size:48px;font-weight:bold;">' +
parent.greetings[greetingIdx] + '</DIV>';

Wszystko jest w tablicy ułatwiającej nam ułożenie wszystkiego. Zmienna lokalna msgStr zawiera początek tablicy.
Pierwszy wiersz zawiera nagłówek i pierwszy z czterech wymaganych elementów – adres poczty elektronicznej
odbiorcy. Adres ten jest wartością pola Recipient formularza EntryForm. Dalej znajduje się obrazek tła. Używając
zmiennych baseURL i backgroundIdx nietrudno jest utworzyć napis zawierający ścieżkę do odpowiedniego obrazka
tła. Pamiętajmy, że jeśli lista wyboru Greetings ma wartość selectedIndex równą 4, to odpowiednim obrazkiem
będzie background4.jpg.
Zwróćmy uwagę, że na tej stronie nie mamy kodu DHTML pozycjonującego obrazek tła na stronie. Nie jest to
konieczne, gdyż nagłówek i życzenia będą zapewne osobno. Wiemy, że obrazek będzie gdzieś w pobliżu górnego
brzegu strony, wyrównany do lewej. W ostatnich kilku wierszach tego kodu wybrane przez nadawcę życzenia są
umieszczane względem obrazka tła, tuż przed nim. Lewy brzeg jest przesunięty o 40 pikseli, górny brzeg ustawiamy
na -255. Ustawienia te zostały tak dobrane, aby uwzględnić położenie obrazka tła w back.html i bieżące jego położenie
w życzeniach. Kiedy dodamy swoje tła, zapewne będziemy musieli te ustawienia zmodyfikować doświadczalnie tak,
aby wszystko do siebie dopasować. Potem nie trzeba będzie się martwić o to, dopóki znowu nie zmienimy czegoś tak
istotnego, jak rozmiar obrazka tła.
Za sobą mamy już trzy obowiązkowe elementy, zostały nam jeszcze dwa. Pierwszy to ikony przeciągnięte
przez użytkownika – obsługują je wiersze 162–171:
var iconStr = '';
for (var i = 0; i < bRef.icons.length; i++) {
if(onCard(i)) {
iconStr += '<DIV STYLE="position:absolute;left:' +
bRef.refSlide(bRef.icons[i]).left + ';top:' +
(parseInt(bRef.refSlide(bRef.icons[i]).top) -
(document.all ? 140 : 150)) + ';"><IMG SRC="' +
255 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

baseURL + '/images/' + bRef.icons[i] + '.gif"></DIV>';


}
}

Zmienna lokalna iconStr początkowo zawiera pusty ciąg, później znajdą się w niej adresy URL i położenia
wszystkich ikon. Procedura jest dość prosta: przeglądamy wszystkie ikony i dla każdej, która znajduje się w obszarze
wyświetlania, tworzony jest kod HTML, pozwalający utworzyć kopię tej ikony (czyli obrazka) na innej stronie w takim
samym położeniu względnym. Pamiętajmy, że funkcja onCard() decyduje o położeniu ikony, a przez to o możliwości
umieszczenia tej ikony na kartce.
Kod generowany dla poszczególnych znaczników IMG otoczony jest znacznikami DIV. Te z kolei mają atrybut STYLE,
oznacza się dla nich pozycję względem brzegów lewego i górnego: minus 140 lub 150 pikseli – w zależności od tego,
czy mamy do czynienia z Internet Explorerem, czy Netscape Navigatorem. Pojawiają się teraz dwa pytania:
1.Dlaczego możemy użyć wyrównania ikony do lewej strony, skoro jeszcze trzeba przesunąć jej górny brzeg
o ponad 100 pikseli?
2.Dlaczego ilości pikseli są różne dla obu przeglądarek?
To na pewno dobre pytania.
Najpierw zastanówmy się, gdzie na ekranie znajduje się obrazek tła w przypadku tworzenia życzeń. Jest mniej więcej
w połowie wysokości strony – zależy to nieco od stosowanej przez nas rozdzielczości. Tymczasem w testowej kartce z
życzeniami obrazek tła znajduje się blisko górnego brzegu okna, zaraz pod nagłówkiem i adresem odbiorcy. Właśnie te
dodatkowe piksele mają to wyrównać. Gdyby obrazek tła był w obu przypadkach w tym samym miejscu, to dodatkowe
pozycjonowanie okazałoby się zbędne. Jeśli chodzi o różnicę między przeglądarkami, to po prostu nieco inaczej
rozmieszczane są warstwy – właśnie o tych 10 pikseli.
Po stworzeniu warstwy każdej wstawianej ikony łączymy iconStr z msgStr, dodajemy jeszcze trochę zamykających
znaczników HTML i msg, i mamy już gotowe życzenia w tablicy:
msgStr += iconStr + '</TD></TR><TR><TD WIDTH=' +
bRef.backImgs[backgroundIdx].width + '><FONT FACE=Arial>' +
msg + '</TD></TR></TABLE>';
return msgStr;

Wartość msg jest wstawiana do komórki danych o takiej samej szerokości, jak obrazek tła, dzięki czemu uzyskujemy
lepszy efekt wizualny. Następnie funkcja zwraca gotowy napis.

Wysyłanie życzeń
Użytkownik już przygotował eleganckie życzenia, kilkakrotnie je przetestował i jest w końcu zadowolony. Wybranie
przycisku Wyślij to ostatnia jego czynność – wywołana zostanie tym samym funkcja shipGreeting() zapisana
w wierszach 111–129:
function shipGreeting(fObj) {
if (fObj.Recipient.value == "") {
alert('Musisz podać adres e-mail!');
return false;
}
else if (fObj.Message.value == "") {
alert("Musisz wpisać wiadomość.");
return false;
}
else if (fObj.Greetings.selectedIndex == 0) {
alert('Musiszy wybrać rodzaj życzeń.');
return false;
}

fObj.EntireMessage.value = genGreeting(fObj);
fObj.UniqueID.value = Math.round(Math.random() * 1000000);
fObj.BaseURL.value = baseURL;
return true;
}

Funkcja ta jest dość krótka, a wywołuje ją procedura obsługi zdarzenia onSubmit z wiersza 187. shipGreeting()
nie tylko przygotowuje formularz do wysyłki do serwera, ale też sprawdza krótko poprawność danych. O ile tylko
użytkownik zastosował się do pewnych prostych zaleceń, wszystko idzie bezproblemowo. Jedyne, czego od nadawcy
wymagamy, to podanie jakiegoś adresu, pewnej treści oraz wybranie typu życzeń z listy. Na razie takie wymagania
wystarczą.
256

Jeśli wprowadzone informacje przejdą naszą skróconą odprawę, shipGreeting() zmienia wartość trzech ukrytych pól
z wierszy 188–190. Początkowo pola te były puste, teraz EntireMessage i BaseURL otrzymują wartości potrzebne
skryptowi działającemu na serwerze. Aby skrypt ten nie musiał tworzyć znów kodu HTML życzeń, funkcja
genGreeting()zwraca wartość do pola EntireMessage.value.

Kolejna informacja, potrzebna skryptowi z serwera, to adres bazowy URL, względem którego wskazywane będą
wszystkie życzenia, obrazki tła i ikony, zatem BaseURL.value otrzymuje wartość top.baseURL. Po tym formularz
jest przesyłany na adres wskazany w atrybucie ACTION, do skryptu greet.pl – zajrzyjmy do wiersza 186.

Techniki języka JavaScript:


optymalizacja funkcji
Skąd wiadomo, kiedy jakaś funkcja zaczyna robić się zbyt długa? Kiedy należy się za-
trzymać i powiedzieć sobie „dobra, resztę wprowadzę do następnej funkcji”? Trudno po-
dać spójny zestaw takich reguł, ale zawsze można uzyskać jak najwięcej z minimalnej ilości
kodu. Przyjrzyjmy się funkcji genGreeting(). Pozwala wygenerować kod zarówno do
podglądu życzeń, jak i kod do gotowej kartki z życzeniami. A co z onCard()? Jest to
całość umożliwiająca określenie pozycji ikon wybranych przez użytkownika, odgrywa też
pewną rolę przy tworzeniu kodu podglądu i kodu gotowej kartki. W większości wypad-
ków lepiej jest pisać krótkie funkcje. Nie zawsze jest to możliwe i wygodne, ale jeśli
tylko się da, to należy się tego trzymać.

Uwaga
Teraz jeszcze jedna uwaga, zanim przejdziemy do strony serwerowej. Funkcja resetForm() z wierszy 70–81 czyści
formularz przy każdym jego załadowaniu – tak pierwszym, jak i powtórnym. Wywoływana jest w ramach obsługi
zdarzenia onLoad w znaczniku BODY. Pamiętając o tym, zobaczmy, co się dzieje, kiedy aplikacja zgłosi się do serwera
sieciowego.

Po stronie serwera
Tak jak w przypadku wirtualnego koszyka z rozdziału 8., również ta aplikacja wymaga pewnych mechanizmów
działających po stronie serwera. Życzenia stworzone przez użytkownika są generowane przez skrypt JavaScript
działający po stronie klienta. Informacje są następnie przesyłane do serwera sieciowego, gdzie używane jest takie
środowisko, jak Active Server Pages, działający po stronie serwera JavaScript czy Cold Fusion.
Owe skrypty serwerowe odczytują dane przesłane przez użytkownika i tworzą plik zawierający życzenia.
Niepowtarzalna nazwa tego pliku jest taka sama, jaką wysyła się odbiorcy życzeń. Plik jest gotów do odczytania
i czeka, aż odbiorca kliknie łącze w wiadomości poczty elektronicznej.
Następnie skrypt pokaże nadawcy potwierdzenie, że wszystko przebiegło prawidłowo, a co ważniejsze, przygotuje
formularz HTML umożliwiający wysłanie wiadomości odbiorcy.
Czytelnika zapewnie interesuje JavaScript, ale tym razem skrypt przygotowano w języku Perl. Przy okazji warto
wspomnieć, że język ten jest względnie prosty i ma duże możliwości, przez co szybko staje się podstawowym językiem
skryptowym w Windows NT. W dodatku C można znaleźć więcej informacji o przygotowywaniu środowiska tego
języka do uruchamiania skryptów, a także wyjaśnienie, jak działają dwa skrypty Perl przygotowane dla aplikacji
pokazywanych w tej książce.

Kierunki rozwoju
Teraz czas na omówienie kilku metod dalszej rozbudowy aplikacji.

Dodanie łącza „wstecz”


Dlaczego nie dodać łącza wstecznego do naszej strony z życzeniami? Wystarczy do funkcji genGreeting() z pliku
front.html dopisać następujący kod:
+ ' <A HREF="' + top.baseURL + '/index.html">Do strony życzeń</A>';
257 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść

Zakładamy przy tym, że pliki index.html, front.html i back.html znajdują się w katalogu wskazywanym przez baseURL.
Jeśli nie, należy zamienić top.baseURL w powyższej instrukcji na jawnie podany adres URL, jakiego chcemy użyć.

Dodanie obrazków tematycznych


W tej aplikacji aż się prosi o tematyczne zestawy obrazków, choćby Boże Narodzenie, Walentynki, Wielkanoc,
wakacje, i tym podobne. Nie musimy zresztą ograniczać się tylko do świąt. Możemy też użyć teł związanych z porami
roku i stosownych ikon, jak również tworzyć zestawy urodzinowe, a także odnoszące się do innych tematów.

Banery reklamowe
Jeśli zamierza udostępniać swoją aplikację za darmo, czemu by sobie tego trochę nie wynagrodzić? W naszej funkcji
shipGreeting() można wstawić kod umożliwiający wybranie banera reklamowego i wstawienie kodu znacznika IMG
na dole życzeń. Jeśli użyjemy wspomnianych przed chwilą zestawów tematycznych, możemy dobierać reklamy
pasujące do danych zestawów.

Życzenia bardziej interaktywne


W tej aplikacji używamy zdarzeń JavaScriptu do stworzenia życzeń zgodnie z żądaniami użytkownika. No tak, ale
nie ma w nich w ogóle życia. Można by wysyłać życzenia z przewijaniem obrazków czy elementami reagującymi
na klikanie. Jeśli mamy jakieś ciekawe aplety języka Java, możemy je dołączyć do życzeń. Ludzie uwielbiają
bezużyteczne gadżety, którymi można się pobawić – właśnie dzięki temu istnieją tysiące stron sieciowych.
Cechy aplikacji:
 Aplikacja pomocy działająca w trybie online
 Dzięki odpowiedniemu kodowi
automatycznie ładowane są treści
związane z bieżącą zawartością ekranu
 Przy ruchu myszy wyświetlane są
dodatkowe informacje
Prezentowane techniki:

11
 Kontrolowanie odrębnych okienek
 Użycie łącz bez klikania
 Warstwy bez znaczników LAYER

Pomoc
kontekstowa

Bez względu na to, jak prosta jest aplikacja i jak dobrze jest naszym zdaniem udokumentowana, na pewno znajdzie się
ktoś, kto będzie miał pytanie bez odpowiedzi. Na niektóre pytania użytkowników bardzo łatwo jest odpowiedzieć,
a niektóre mogą doprawdy zdumieć – mimo że to my jesteśmy autorami aplikacji! W zależności od jakości
dokumentacja może szybko przywrócić użytkownika na właściwą drogę, ale może też zwieść go jeszcze dalej. Pliki
pomocy same w sobie też muszą być proste w użyciu – i tym się zajmiemy w tym rozdziale.
Aplikacja ma być nie tylko prosta w użyciu dla klienta, ale także prosta dla nas w przygotowaniu i utrzymaniu. Jest to
kolejna aplikacja, która sama w sobie niczemu specjalnemu nie służy. Tak jak w przypadku aplikacji z rozdziału 7.,
również tym razem pokazany kod możemy wstawiać do swoich własnych aplikacji.
Nazwałem tę aplikację Listą SELECT w JavaScripcie – wygląd nie jest zbyt efektowny, ale zobaczymy, co można
osiągnąć, łącząc z naszym ulubionym językiem programowania listy wyboru. Na rysunku 11.1 pokazano wygląd
aplikacji po załadowaniu.
W aplikacji tej pokazano, jak lista wyboru może zmieniać kolor tła, ładować dokumenty oraz rozwijać inne listy –
a do tego wszystkiego potrzeba tylko trochę JavaScriptu. W tym wypadku to wszystko służy tylko jako przykład,
wybranie łącza Help otwiera okienko z dokumentacją dotyczącą koloru tła. Spójrzmy na rysunek 11.2.
Być może nie ma tu znowu nic szczególnego. Jednak kliknięcie łącza Katalog URL (ładującego stronę katalogów
URL), później kliknięcie znowu Help załaduje dokumentację dotyczącą procedury ładowania URL. Spójrzmy
na rysunek 11.3 – nieźle. Teraz użytkownicy nie muszą już wyszukiwać interesującego ich pliku pomocy, są przecież
duże szanse, że użytkownicy są zainteresowani pomocą dotyczącą tego, co właśnie oglądają. Pliki pomocy są
pokazywane w zależności od kontekstu.
Wygląda to tak, jakby aplikacja miała w sobie jakąś mądrość: zawsze „wie”, gdzie jest użytkownik. Oprócz tego część
pomocy zawiera hipertekst prowadzący do dalszych objaśnień. Jednak użytkownik nie musi w ogóle klikać łącza, aby
załadować następny dokument – samo umieszczenie kursora myszki nad łączem pokaże informacje w podświetlonej
warstwie. Obejrzyjmy rysunek 11.4.
259 Rozdział 11 - Pomoc kontekstowa

Teraz użytkownik nie musi wracać do dokumentu, z którego uruchomił łącze. Zabranie kursora myszki znad łącza
z powrotem ukryje pokazaną warstwę. Funkcjonalność taką można zastosować niemalże w dowolnej aplikacji, w której
użytkownik może potrzebować pomocy.

Rysunek 11.1. Lista SELECT w JavaScripcie

Rysunek 11.2. Objaśnienie zasad zmiany koloru tła


260

Rysunek 11.3. To samo łącze, inna treść pomocy

Rysunek 11.4. Dodatkowe informacje

Wymagania programu
Będziemy potrzebowali Netscape Navigatora lub Internet Explorera w wersjach co najmniej 4.x, a to z powodu użycia
DHTML i nowego modelu zdarzeń. Upewnijmy się, że mamy monitor o rozdzielczości co najmniej 1024x768. Nie jest
to obowiązkowe, ale w przeciwnym wypadku okienko pomocy może przykrywać zbyt dużą część okna głównego.

Struktura programu
Aplikacja ta zawarta została w zestawie ramek z dużą liczbą plików. Oto szybki przegląd:
index.html
Zestaw ramek najwyższego poziomu i najważniejsze zmienne.
261 Rozdział 11 - Pomoc kontekstowa

top.html
Wyświetla nagłówek aplikacji.
nav.html
Wyświetla stronę z łączami.
background.html
Zmienia kolory tła.
multiselect.html
Rozwija listę wyboru na podstawie ustawień dwóch innych list.
urldirectory.html
Ładuje wyszukiwarki według wybranej opcji.
help/background.html
Dokument pomocy związany z plikiem background.html.
help/multiselect.html
Dokument pomocy związany z plikiem multiselect.html.
help/urldirectory.html
Dokument pomocy związany z plikiem urldirectory.html.
help/help.js
Plik źródłowy JavaScriptu.
Raczej małe są szanse, że ktoś będziesz chciał jakoś szczególnie głęboko studiować logikę background.html,
multiselect.html czy urldirectory.html. Nie ma tam nic szczególnego i nie o to przecież w tym rozdziale chodzi. Jednak
przynajmniej obejrzyj sposób tworzenia list w multiselect.html, a jest dość sprytny. Teraz zajmijmy się właściwym
tematem, gdyż zrobimy to w dwóch krokach:
1.Pomoc kontekstowa: ładowanie prawidłowego dokumentu do okienka pomocy (nav.html).
2.Pokazywanie i ukrywanie dodatkowych informacji w odpowiedzi na ruchy myszki (help/help.js).

Pomoc kontekstowa
Ta część jest całkiem prosta. Wszystko można znaleźć w pliku nav.html pokazanym w przykładzie 11.1.

Przykład 11.1. nav.html


1 <HTML>
2 <HEAD>
3 <TITLE>nav.html</TITLE>
4 </HEAD>
5 <STYLE TYPE="text/css">
6 <!--
7
8 A
9 {
10 text-decoration: none;
11 }
12
13 BODY
14 {
15 font-family: Arial;
16 text-align: center;
17 }
18
19 //-->
20 </STYLE>
21 <SCRIPT>
22 <!--
23 var helpWin;
24
25 function inContext(currFile) {
26 var start = currFile.lastIndexOf('/') + 1;
27 var stop = currFile.lastIndexOf('.');
28 var helpName = currFile.substring(start, stop);
29 if(helpWin == null || helpWin.closed) {
30 helpWin = open('help/' + helpName + '.html', 'helpFile',
31 'width=' + top.wdh + ',height=' + top.hgt +
262

32 ',left=100,top=100,scrollbars=no');
33 }
34 else {
35 helpWin.location.href = 'help/' + helpName + '.html';
36 }
37 helpWin.focus();
38 }
39
40 //-->
41 </SCRIPT>
42 <BODY>
43
44 <A HREF="background.html" TARGET="WorkArea">Kolory tła</A>
45 &nbsp;&nbsp;&nbsp;
46 <A HREF="urldirectory.html" TARGET="WorkArea">Katalog URL</A>
47 &nbsp;&nbsp;&nbsp;
48 <A HREF="multiselect.html" TARGET="WorkArea">Listy wielokrotnego
49 wyboru</A> &nbsp;&nbsp;&nbsp;
50 <A HREF="javascript: inContext(parent.WorkArea.location.href);">Help</A>
51
52 </BODY>
53 </HTML>

Funkcja inContext() zajmuje się jednym: dla każdego dokumentu, dla którego chcemy wyświetlać pomoc, tworzy
dokumentację pomocy w pliku o takiej samej nazwie z rozszerzeniem .html. Zatem plik background.html, zmieniający
kolory tła, ma w podkatalogu help/ odpowiadający mu plik pomocy background.html. Oto wiersze 25–38:
function inContext(currFile) {
var start = currFile.lastIndexOf('/') + 1;
var stop = currFile.lastIndexOf('.');
var helpName = currFile.substring(start, stop);
if(helpWin == null || helpWin.closed) {
helpWin = open('help/' + helpName + '.html', 'helpFile',
'width=' + top.wdh + ',height=' + top.hgt +
',left=100,top=100,scrollbars=no');
}
else {
helpWin.location.href = 'help/' + helpName + '.html';
}
helpWin.focus();
}

Funkcja jako argumentu oczekuje adresu URL. currFile może być adresem URL bezwzględnym, na przykład
http://jakis.serwer.com.pl/gdzies/dokument.html, a może to być też adres względny z zapytaniem, na przykład
dokument.cgi?search=all. Niezależnie od tej nazwy potrzebujemy jedynie nazwy samego pliku, bez komputera czy
katalogów z przodu ani bez rozszerzenia czy zapytania z tyłu. Innymi słowy – potrzebujemy wszystkiego po ostatnim
ukośniku (/), jeśli w ogóle jakiś występuje, aż do ostatniej kropki (choć bez niej – zakładamy, że pliki zawsze mają
rozszerzenie, zatem jakaś kropka się pojawi).
Wobec tego zmienna start daje nam indeks ostatniego ukośnika powiększony o 1. Załóżmy, że w adresie nie ma żadnego
ukośnika – w związku z tym nie ma sprawy. Funkcja lastIndexOf() zwróci w takim wypadku nam -1, po czym
dodajemy 1 i otrzymujemy 0 – tutaj właśnie musimy zacząć. Zmienna stop otrzymuje wartość indeksu ostatniej kropki
w adresie. Teraz metoda substring() z wiersza 28 wyłuskuje potrzebny podciąg z URL i przypisuje go zmiennej
helpName. Przyjrzyjmy się:
var helpName = currFile.substring(start, stop);

Następnych kilka wierszy otwiera okno, używając helpName – zgodnie z konwencją nazewnictwa – dokumentów
pomocy. Pierwszy parametr metody open() w wierszach 30–32 dynamicznie wskazuje odpowiedni plik pomocy:
helpWin = open('help/' + helpName + '.html', 'helpFile',
'width=' + top.wdh + ',height=' + top.hgt +
',left=100,top=100,scrollbars=no');

Zwróćmy uwagę, że szerokość i wysokość okna pomocy określamy w biegu przy pomocy zmiennych top.wdh
i top.hgt, ustawiając obie na 300. Te dwie zmienne znajdują się w pliku index.html, zatem aplikacja odwołuje się do nich
z innych miejsc, ale możemy tutaj skorzystać z odnośnika top. Dlaczego używam tych zmiennych do określenia
rozmiarów okna, okaże się później. Jedyne, czego teraz potrzebujesz, to dobre łącze, które wywoła naszą funkcję– oto
wiersz 50:
<A HREF="javascript: inContext(parent.WorkArea.location.href);">Help</A>
263 Rozdział 11 - Pomoc kontekstowa

Uruchomienie tego łącza wywoła inContext()0 i przekaże adres URL aktualnie załadowanego dokumentu w ramce
o nazwie WorkArea. O ile tylko mamy analogicznie nazwany dokument w katalogu help/, nasz nowy system pomocy
kontekstowej może być rozbudowywany lub ograniczany stosownie do potrzeb dowolnej aplikacji.

Techniki języka JavaScript:


kontrolowanie odrębnych okienek
Ile okienek pomocy tak naprawdę otwiera sobie użytkownik jednocześnie? Jedno to
niezła odpowiedź – jak tego dopilnować, oto sposób na to. Czy zauważyłeś, że zmienna
globalna helpWin ustawiana jest w otwieranym okienku po jej zadeklarowaniu
bez inicjalizacji? Innymi słowy helpWin jest deklarowana, ale nie jest ustawiana na żadną
wartość, czyli ma wartość null. Następnie zmienna ta uzyskuje wartość zwrotną otwierania
okienka pomocy.
Kiedy użytkownik po raz pierwszy klika łącze pomocy, poniższy kod „decyduje”, czy
otworzyć nowe okno, czy skorzystać z okna już istniejącego:
if(helpWin == null || helpWin.closed) {
helpWin = open('help/' + helpName + '.html', 'helpFile',
'width=' + top.wdh + ',height=' + top.hgt +
',left=100,top=100,scrollbars=no');
}
else {
helpWin.location.href = 'help/' + helpName + '.html';
}

Jeśli helpWin ma wartość null, nie została jeszcze przypisana jej wartość zwrócona przez
metodę open(). Następnie inContext() otwiera nowe okienko. Jeśli jednak zmiennej
helpWin już przypisano obiekt, to ponieważ jest to obiekt typu window, ma on właści-
wość closed o wartości true, jeśli okno zostało zamknięte, i o wartości false w prze-
ciwnym wypadku. Jeśli zatem helpWin.closed ma wartość true, użytkownik już otwierał
okienko pomocy i je zamknął, więc też trzeba będzie otworzyć nowe.
Gdy helpWin.closed ma wartość false, okienko pomocy nadal jest otwarte, więc
w wierszu 35 po prostu ładujemy odpowiedni dokument, w ogóle nie wywołując open().
Po co więc całe to zamieszanie? Jeśli użytkownik kliknąłby Help przed zamknięciem po-
przedniej pomocy, otworzone zostałoby drugie okienko pomocy. W przypadku kolejnego
kliknięcia o pomoc otworzyłoby się następne okienko, tym razem już trzecie.
Sprawdzanie wartości null i właściwości closed pozwala się przed tym uchronić. To,
czy użytkownik wcześniej okienko pomocy już otwierał, przestaje mieć znaczenie.

Wykorzystana przez nas metoda określania nazwy pliku między ukośnikiem „/”
a kropką „.” nie jest całkiem odporna na różne sytuacje. Na przykład adres wskazujący
jedynie domyślny plik zawiedzie – na przykład http://web.net.com/ czy ../. Jaki plik ma
być w takiej sytuacji uwzględniony? Upewnijmy się, że zmodyfikujemy swój kod tak,
aby obsłużyć także pliki domyślne, jeśli planujemy korzystać z takich adresów.

Pokazywanie i ukrywanie dodatkowych informacji


Technika pokazywania i ukrywania, którą przed chwilą omówiliśmy, ładuje dokumenty pomocy, których potrzebujemy.
Użycie łącz i przenoszenia wskaźnika myszy nad nimi do wyświetlania dodatkowej pomocy wymaga skorzystania
z magii DHTML – trochę kodu podobnego do tego, jakiego już wcześniej używaliśmy, trochę nowości. Na szczęście
większość tego kodu znajduje się w pliku źródłowym help/help.js, który pokazano jako przykład 11.2.

Przykład 11.2. help/help.js


1 var NN = (document.layers ? true : false);
2 var hideName = (NN ? 'hide' : 'hidden');
3 var showName = (NN ? 'show' : 'visible');
4 var zIdx = -1;
264

5 var helpWdh = 200;


6 var helpHgt = 200;
7 var x, y, totalWidth, totalHeight;
8
9 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
10 if (NN) {
11 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
12 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
13 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + (++zIdx) + '>' +
14 copy + '</LAYER>');
15 }
16 else {
17 document.writeln('<DIV ID="' + sName +
18 '" STYLE="position:absolute; overflow:none; left:' +
19 sLeft + 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' +
20 sHgt + 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) +
21 '">' + copy + '</DIV>'
22 );
23 }
24 }
25
26 function hideSlide(name) {
27 refSlide(name).visibility = hideName;
28 }
29
30 function showSlide(name) {
31 refSlide(name).visibility = showName;
32 }
33
34 function refSlide(name) {
35 if (NN) { return document.layers[name]; }
36 else { return eval('document.all.' + name + '.style'); }
37 }
38
39 function motionListener() {
40 if (NN) {
41 window.captureEvents(Event.MOUSEMOVE);
42 window.onmousemove = grabXY;
43 }
44 else {
45 document.onmousemove = grabXY;
46 }
47 }
48
49 function grabXY(ev) {
50 if(NN) {
51 x = ev.pageX;
52 y = ev.pageY;
53 }
54 else {
55 x = event.x;
56 y = event.y;
57 }
58 }
59
60 function helpDisplay(name, action) {
61 if(action) {
62 totalWidth = x + helpWdh;
63 totalHeight = y + helpHgt;
64 x = (totalWidth > wdh ? x -
65 (totalWidth - wdh + 75) : x);

Przykład 11.2. help/help.js (dokończenie)


66 y = (totalHeight > hgt ? y -
67 (totalHeight - hgt) : y);
68 refSlide(name).left = x - 10;
69 refSlide(name).top = y + 8;
70 showSlide(name);
71 }
72 else { hideSlide(name); }
73 }
74
75 motionListener();

Zajmiemy się zawartymi tu funkcjami w dwóch krokach. Najpierw omówimy tworzenie warstw, gdzie są pewne
dodatkowe informacje. Następnie przyjrzymy się pokazywaniu i ukrywaniu tych warstw.
265 Rozdział 11 - Pomoc kontekstowa

Tworzenie warstw
Jeśli widziałeś którykolwiek z rozdziałów: 3., 4., 6., 7. czy 9. pierwsze dwa tuziny wierszy będzie Ci znajome. Jeśli ich
nie czytałeś, poczytaj w rozdziale 3. o funkcjach genLayer(), hideSlide(), showSlide() i refSlide(). Będziemy
tworzyć warstwy tak, jak robiliśmy to we wcześniejszych rozdziałach. Musimy jednak dodać do tego jeszcze jeden
etap: zmienne helpWdh i helpHgt uzyskują wartości 200 pikseli. Oznaczają one domyślną szerokość i wysokość
poszczególnych warstw. Jest to bardzo ważne, gdyż tych zmiennych wraz z top.wdh i top.hgt będziemy potrze-
bować za chwilę do pozycjonowania warstw.
Omawiane funkcje są tutaj funkcjami narzędziowymi, które posłużą nam do tworzenia warstw. Należy wywołać
genLayer() i przekazać treść, w tym zmienne, wywołanie tej funkcji znajdziemy w każdym pliku pomocy. Jako że
wszystkie pliki pomocy są do siebie bardzo podobne, przyjrzymy się tylko jednemu z nich, help/background.html:
var helpOne = ‘<SPAN CLASS=”helpSet”>Właściwość ta jest napisem’ +
‘ oznaczającym bieżący kolor tła dokumentu.</SPAN>’;
var helpTwo = ‘<SPAN CLASS=”helpSet”>Ta właściwość obiektu ‘ +
‘<TT>window</TT> zawiera hierarchię obiektów ‘ +
‘bieżącej strony sieciowej.</SPAN>’;
genLayer(“bgColor”, 0, 0, helpWdh, helpHgt, hideName, helpOne);
genLayer(“document”, 0, 0, helpWdh, helpHgt, hideName, helpTwo);

Zmienna helpOne zawiera napis, który wyświetli pierwsze dodatkowe łącze (bgColor), a helpTwo podobnie zachowuje
się w odniesieniu do łącza dokumentu. Nie mamy tu jednak do czynienia tylko ze zwykłym tekstem – oba napisy
zawierają parę znaczników SPAN, którym przypisano definicję klasy arkusza stylów .helpSet. Klasa ta jest ujęta
w znaczniki STYLE. Przyjrzyjmy się temu nieco – nie jest to szczególnie dopracowana definicja klasy arkusza stylów,
ale i tak całkiem nieźle sprawdza się przy definiowaniu warstw:
.helpSet
{
background-color: #CCFFCC;
padding: 5px;
border: 2px;
width: 200px;
font: normal 10pt Arial;
text-align: left;
}

Skrypt zawiera dwa wywołania funkcji genLayer(). Zwróćmy uwagę, że zamiast przekazywać każdej warstwie
szerokość i wysokość, przekazujemy zmienne helpWdh i helpHgt. W ten sposób wygodniej będzie nam później
dopracować pozycjonowanie warstw. Każda warstwa początkowo jest ukryta przez ustawienie zmiennej hideName.
Warstwy mamy już gotowe, teraz tylko trzeba umożliwić użytkownikowi wygodne ich wyświetlanie na życzenie.
Zajmują się tym funkcje motionListener(), grabXY() i helpDisplay(). Pierwszą z nich, motionListener(),
znajdziemy w wierszach 39–47:
function motionListener() {
if (NN) {
window.captureEvents(Event.MOUSEMOVE);
window.onmousemove = grabXY;
}
else {
document.onmousemove = grabXY;
}
}

Powinniśmy wyświetlić warstwę, kiedy tylko ktoś dotknie jej łącze na stronie. Aby to zrobić, musimy śledzić
lokalizację myszy na ekranie, aby wiedzieć, kiedy znajduje się nad tym łączem. Funkcja motionListener()
przypisuje wywołanie funkcji grabXY() do zdarzenia onMouseMove. Zarówno Netscape Navigator, jak i Internet
Explorer obsługują to zdarzenie, ale w Netscape Navigatorze dotyczy ono obiektu window, a w Internet Explorerze
obiektu document. Netscape Navigator musi też wywołać metodę captureEvents().
Funkcja grabXY() przypisuje zmiennym x i y odpowiednio poziomą i pionową współrzędną kursora myszki
przy każdym jego ruchu. Oto wiersze 49-58:
function grabXY(ev) {
if(NN) {
x = ev.pageX;
y = ev.pageY;
}
else {
x = event.x;
266

y = event.y;
}
}

Zmienne te inaczej działają w Navigatorze, inaczej w Internet Explorerze. Navigator 4.x tworzy w locie obiekt
zdarzenia dla każdego wywołania obsługi zdarzenia. Obiekt jest parametrem ev. W Internet Explorerze z kolei jest
wbudowany obiekt zdarzenia. Wywołanie grabXY() przy każdym ruchu myszy przypisuje zmiennym x i y bieżące
wartości. Kiedy użytkownik znajdzie się nad łączem, x i y będą zawierać wartości, których można będzie użyć jako
punktu odniesienia do pozycjonowania dodatkowych warstw pomocy.

Techniki języka JavaScript:


Używanie łącz bez klikania
Czasami może zaistnieć potrzeba zrobienia czegoś już wtedy, gdy mysz zostanie
nasunięta nad łącze lub z niego zsunięta, wszystko bez klikania. Oto dwa sposoby
umożliwiające uniknięcie niepożądanych efektów związanych z kliknięciem:
Użyj w atrybucie HREF wywołania javascript: void(0).
Operator void ignoruje wszelkie zwracane wartości, także zdarzenie click. Nie musisz
przy tym używać akurat wartości 0, ale jest to bardzo wygodny argument. Przykład zoba-
czymy tuż przed tą ramką.
Użyj przypisania onClick="return false;".
Zwrócenie false odwoła ładowanie dokumentu wskazanego w atrybucie HREF. Spró-
bujmy czegoś takiego:
<A HREF="" onMouseOver="zrobCos();" onClick="return false;">
Zrób coś</A>

Cokolwiek znajdzie się w atrybucie HREF, dokument ten nie będzie ładowany.

Szerokość i wysokość poszczególnych warstw to 200 pikseli. Tak naprawdę wysokość warstwy rośnie dynamicznie,
stosowanie do ilości danych, tak jak dane tabeli rozpychają komórki. Nadal jednak potrzebujemy jakiegoś odniesienia.
Nie trzeba być profesorem matematyki, aby stwierdzić, jeśli łącze jest dalej niż 100 pikseli na prawo od lewego brzegu,
część wyświetlanej warstwy nie będzie widoczna (tak naprawdę nawet niecałe 100 pikseli, bo zewnętrzny wymiar okna
to 300, ale my dokument wyświetlamy wewnątrz). Aby się tego ustrzec, przed pokazaniem warstwy dokonamy
pewnych wyliczeń.

Działa to tak: jeśli suma współrzędnej poziomej wskaźnika myszy oraz szerokości wyświetlanej warstwy jest większa
od dostępnej szerokości okna, wyświetlamy warstwę bardziej na lewo – oto wiersz 62:
totalWidth = x + helpWdh;

Zmienna totalWidth ma wartość sumy współrzędnej poziomej oraz szerokości warstwy. Teraz widać, czemu
używamy do ustawiania wymiarów zmiennych helpWdh i helpHgt, zamiast liczb, na przykład 200. Teraz przyjrzyjmy
się wierszom 64–65:
x = (totalWidth > wdh ? x -
(totalWidth - wdh + 75) : x);

Jeśli wartość totalWidth jest większa niż szerokość okna (pomniejszona o szerokość ramki), współrzędna pozioma
musi być odpowiednio poprawiona. Po prostu wyrównujemy ją do lewej strony, odejmując różnicę między
totalWidth i szerokością okna pomocy. W ten sposób zapewniamy, że wszystkie warstwy wyświetlane będą
poziomo. To samo dotyczy wysokości – wiersze 63 i 66–67. Może to nie zadziałać, jeśli helpHgt ma wartość dość
niską, a tworzona warstwa ma dużo tekstu.
267 Rozdział 11 - Pomoc kontekstowa

Kierunki rozwoju
Pokazana tu aplikacja pomocy będzie zapewne wystarczająca dla wielkości małych i średnich aplikacji. Kiedy aplikacje
rosną, mogą być potrzebne dodatkowe funkcje pomocy. Zastanówmy się nad poniższym propozycjami.

Techniki języka JavaScript:


warstwy bez znaczników LAYER
Tym razem widzieliśmy warstwy DHTML jako znaczniki DIV w Internet Explorerze
i LAYER w Netscape Navigatorze. Znaczniki LAYER działają poprawnie, ale nie staną się
one standardem. Wszystko w konsorcjum W3C wskazuje, że standaryzowany obiektowy
model dokumentu będzie bardzo podobny do tego zrealizowanego w Internet Explorerze.
Przyjrzyjmy się poniższemu kodowi:
<HEAD>
<TITLE>Warstwa DHTML</TITLE>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
var action = true;
function display(name) {
if (document.all) {
var layerObj = eval("document.all." + name + ".style");
var hide = "hidden";
var show = "visible";
}
else {
var layerObj = eval("document." + name);
var hide = "hide";
var show = "show";
}
layerObj.visibility = (action ? hide : show);
action = !action;
}

//-->
</SCRIPT>
</HEAD>
<BODY>
<DIV ID="dhtml"
STYLE="position:relative;background-color:#FFACEE;width:200;">
Jest to warstwa DHTML.
</DIV>
<BR>
<A HREF="javascript:display('dhtml');">Show/Hide</A>
</BODY>
</HTML>

Jest to plik \ch11\layer.html. Jak widać, nie ma tu żadnego znacznika LAYER, a mimo to
Netscape Navigator i Internet Explorer wszystko, co trzeba, zrozumieją (warstwa tu jest
ukrywana i pokazywana na kliknięcie). Choć Netscape Navigator obecnie nie pozwoli
sięgnąć do większości elementów obiektowego modelu dokumentu, to i tak możliwe jest
pozycjonowanie.
Jak zatem najlepiej postępować? Czemu nie użyliśmy tutaj metody znanej z poprzednich
rozdziałów? Obie metody działają dobrze, ale wolę metodę genLayer() dynamicznie
tworzonych znaczników LAYER w Netscape Navigatorze i DIV w Internet Explorerze.
Ważne jest tak naprawdę to, że dysponujemy inną możliwą metodę postępowania. Wy-
próbuj obie metody i zobacz, która jest bardziej interesująca.

Spis treści
Czasami użytkownik szuka jakiejś dokumentacji, niezwiązanej z bieżącą treścią pokazaną na ekranie. Można wyjść
naprzeciw oczekiwaniom użytkownika, udostępniając mu strony ze spisem treści, którego pozycje będą łączami
do wszystkich dokumentów pomocy. Wystarczy do tego statyczny HTML, a możemy też użyć JavaScriptu
do generowania listy dynamicznie na podstawie tablicy:
268

function showContents() {
var helpDocs = ['background', 'multiselect', 'urldirectory'];
var helpLinks = '<UL>';
for (var i = 0; i < helpDocs.length; i++) {
helpLinks += '<LI><A HREF="' + helpDocs[i] + '.html">' +
helpDocs[i] + '</A>';
}
helpLinks = '</UL>';
document.writeln(helpLinks);
}

Przeszukiwanie plików pomocy


Jeśli użytkownik potrzebuje kilku dokumentów pomocy, czemu nie umożliwić mu ich przeszukiwania przy pomocy
aplikacji z rozdziału 1.? Zawsze jest to elegancka metoda ustąpienia użytkownikowi odrobiny interaktywności.

Pytanie do specjalisty
Czasami użytkownik nie potrafi znaleźć odpowiedzi na swoje pytanie. Jeśli dysponujemy odpowiednim personelem,
zastanówmy się nad dodaniem – opartej na formularzu – wiadomości poczty elektronicznej, aby użytkownik mógł
uzyskać odpowiedzi na swoje pytania od wykwalifikowanego pracownika.

Pomoc telefoniczna
Jeśli chcemy naprawdę porządnie obsłużyć klientów, podajmy listę numerów telefonicznych i adresów e-mail, aby
użytkownicy mogli skontaktować się z wybranymi osobami. Tak jak w przypadku pytań do specjalisty, jest to
rozwiązanie dość zasobożerne. Zanim udostępnimy numery telefonów, upewnijmy się, że ktoś będzie owe telefony
odbierać. Ludzie będą dzwonić. Dzwoniły do mnie różne osoby po wizycie na mojej stronie, a mój numer niełatwo było
tam znaleźć.

You might also like