SPIS TREŚCI OD REDAKCJI..................................................................................................................................9 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 Deklarowanie zmiennych globalnych i tablic na początku........................................................17 Deklarowanie konstruktorów po zmiennych globalnych ...........................................................17 Definiowanie funkcji zgodnie z porządkiem „chronologicznym”..............................................17
2
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 Funkcje.......................................................................................................................................26 validate() ................................................................................................................................27 convertString() .......................................................................................................................27 allowAny() .............................................................................................................................28 requireAll() ............................................................................................................................29 verifyManage() ......................................................................................................................31 noMatch()...............................................................................................................................31 formatResults() ......................................................................................................................32 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” ....................................................................34 prevNextResults() ..................................................................................................................34 Tylko przycisk „Następne” ....................................................................................................34 Przyciski „Następne” i „Poprzednie”.....................................................................................35 Tylko przycisk „Poprzednie” .................................................................................................35 Kod HTML .................................................................................................................................38 TWORZENIE BAZY DANYCH W JĘZYKU JAVASCRIPT ........................................................................38 KIERUNKI ROZWOJU ........................................................................................................................39 Zgodność z językiem JavaScript 1.0...........................................................................................39 NICTJDO ...................................................................................................................................39 Odporność na błędy ...................................................................................................................40 Wyświetlanie reklam ..................................................................................................................40 Rozszerzenie możliwości wyszukiwania .....................................................................................40 Zapytania predefiniowane .........................................................................................................41
2
..................................................................................................................................................42
TEST SPRAWDZANY NA BIEŻĄCO.........................................................................................42 WYMAGANIA PROGRAMU................................................................................................................45 STRUKTURA PROGRAMU .................................................................................................................45 index.html – ramki .....................................................................................................................46
3
Spis treści
question.js – plik źródłowy JavaScript.......................................................................................47 administer.html ..........................................................................................................................49 Treść HTML ..........................................................................................................................51 Zmienne globalne...................................................................................................................52 Funkcje...................................................................................................................................53 itemReset() .........................................................................................................................53 shuffle()..............................................................................................................................53 buildQuestion() ..................................................................................................................54 gradeTest() .........................................................................................................................56 printResults()......................................................................................................................57 chickenOut() ......................................................................................................................59 KIERUNKI ROZWOJU ........................................................................................................................59 Uodpornienie na oszustwa .........................................................................................................59 Usuwanie odpowiedzi z tablicy .............................................................................................59 Usuwanie gradeTest() i modyfikacja buildQuestion()...........................................................60 Modyfikacja printResults() ....................................................................................................60 Przekształcenie na ankietę .........................................................................................................60
3
..................................................................................................................................................61
INTERAKTYWNA PREZENTACJA SLAJDÓW ......................................................................61 WYMAGANIA PROGRAMU................................................................................................................63 STRUKTURA PROGAMU....................................................................................................................63 Zmienne......................................................................................................................................66 Ustawienia domyślne warstwy DHTML ...............................................................................67 Zmienne związane z przeglądarkami.....................................................................................67 Zmienne związane z obrazkami.............................................................................................68 Zmienne automatycznego pokazu..........................................................................................68 Funkcje aplikacji........................................................................................................................68 Funkcje związane z warstwami..............................................................................................69 genLayer()..........................................................................................................................69 slide() .................................................................................................................................69 genScreen() ........................................................................................................................71 Elementy tablicy slideShow...............................................................................................74 Funkcje związane z obsługą obrazków..................................................................................75 preLoadImages() ................................................................................................................75 imageSwap() ......................................................................................................................75 Funkcje nawigacji ..................................................................................................................76 refSlide(), hideSlide(), showSlide(), menuManager() .......................................................76 changeSlide() .....................................................................................................................77 setSlide() ............................................................................................................................78 autoPilot() ..........................................................................................................................78 automate() ..........................................................................................................................79 KIERUNKI ROZWOJU ........................................................................................................................79 Losowy dobór slajdów w trybie automatycznym .......................................................................79 Animowane GIF-y i suwaki slajdów ..........................................................................................80 Animacja samych slajdów..........................................................................................................80
4
4
..................................................................................................................................................81
INTERFEJS MULTIWYSZUKIWARKI .....................................................................................81 WYMAGANIA PROGRAMU................................................................................................................83 STRUKTURA PROGRAMU .................................................................................................................83 Przechadzka Aleją Pamięci........................................................................................................87 Dynamiczne ładowanie obrazków..............................................................................................87 Uruchamianie wyszukiwarek .....................................................................................................88 engineLinks()..............................................................................................................................89 Zarządzanie warstwami..........................................................................................................89 Wstępne ładowanie obrazków ...............................................................................................91 Tworzenie łącza .....................................................................................................................91 imageSwap()...............................................................................................................................92 callSearch()................................................................................................................................93 KIERUNKI ROZWOJU: ZWIĘKSZENIE MOŻLIWOŚCI DECYDOWANIA PRZEZ UŻYTKOWNIKA ...............94 Krok 1. ...................................................................................................................................95 Krok 2. ...................................................................................................................................95 Krok 3. ...................................................................................................................................95 Krok 4. ...................................................................................................................................95 Krok 5. ...................................................................................................................................95
5
..................................................................................................................................................97
IMAGEMACHINE..........................................................................................................................97 WYMAGANIA PROGRAMU................................................................................................................99 STRUKTURA PROGRAMU ...............................................................................................................100 Krok 1. Załadowanie strony.....................................................................................................107 Krok 2. Określenie liczby par obrazków i ustawień domyślnych.............................................107 Krok 3. Określenie nazw plików, atrybutów HREF i tak dalej ................................................108 captureDefaultProfile() ........................................................................................................108 generateEntryForm()............................................................................................................109 genJavaScript() ....................................................................................................................110 Czas na decyzje....................................................................................................................112 Generowanie kodu ...............................................................................................................113 Krok 4. Wybór Podglądu w celu obejrzenia działania kodu ...................................................114 Krok 5. Wybór Zmiany danych w celu zrobienia poprawek ....................................................114 KIERUNKI ROZWOJU: DODANIE ATRYBUTÓW DO SZABLONU .........................................................114 Krok 1. Dodanie pól.................................................................................................................115 Krok 2. Tworzenie tablic w setArrays() ...................................................................................115 Krok 3. Pobieranie nowych ustawień domyślnych ..................................................................115 Krok 4. Dodanie pól tekstowych w generateEntryForm() .......................................................116 Krok 5. Odwołanie się do nowych wartości w genJavaScript() i ich użycie ..........................116 Krok 6. Generacja dodatkowego HTML w genJavaScript()....................................................116
5
Spis treści
6
................................................................................................................................................118
REALIZACJA PLIKÓW ŹRÓDŁOWYCH JAVASCRIPTU .................................................118 ARRAYS.JS .....................................................................................................................................119 COOKIES.JS ....................................................................................................................................122 DHTML.JS ......................................................................................................................................125 EVENTS.JS .....................................................................................................................................126 FRAMES.JS .....................................................................................................................................130 IMAGES.JS .....................................................................................................................................132 NAVBAR.JS ....................................................................................................................................133 NUMBERS.JS ..................................................................................................................................135 OBJECTS.JS ....................................................................................................................................136 STRINGS.JS ....................................................................................................................................139 KIERUNKI ROZWOJU ......................................................................................................................143
7
................................................................................................................................................145
USTAWIENIA UŻYTKOWNIKA OPARTE NA CIASTECZKACH.....................................145 WYMAGANIA PROGRAMU..............................................................................................................146 STRUKTURA PROGRAMU ...............................................................................................................147 prefs.html .................................................................................................................................148 Formularz ustawień użytkownika ........................................................................................154 Ładowanie zapisanych ustawień..........................................................................................155 Składanie obrazków .............................................................................................................157 Wprowadzanie zmian ...............................................................................................................158 Krok 1. Iteracja formObj......................................................................................................159 Krok 2. Zapisanie danych do pliku cookies.........................................................................160 Krok 3. Pokazanie użytkownikowi nowych ustawień .........................................................160 Zerowanie formularza..............................................................................................................161 dive.html...................................................................................................................................161 Analiza ciasteczek................................................................................................................163 Twarzą w twarz z nieznanym...............................................................................................164 KIERUNKI ROZWOJU ......................................................................................................................167 Więcej ustawień wyglądu .........................................................................................................167 Gotowe schematy wyglądu stron .............................................................................................167 Umożliwienie użytkownikom tworzenia własnych łącz............................................................167 Marketing bezpośredni.............................................................................................................168
8
................................................................................................................................................169
6
SHOPPING BAG – WÓZEK SKLEPOWY STWORZONY W JAVASCRIPCIE ..............169 SHOPPING BAG W DWÓCH SŁOWACH ............................................................................................169 Etap 1. Ładowanie aplikacji ....................................................................................................170 Etap 2. Przeglądanie towarów i wybór....................................................................................171 Etap 3: Przeglądanie zamówienia i zmiany w nim ..................................................................173 Etap 4. Płacenie .......................................................................................................................175 WYMAGANIA PROGRAMU..............................................................................................................175 STRUKTURA PROGRAMU ...............................................................................................................176 ETAP 1. ŁADOWANIE APLIKACJI ....................................................................................................178 Elementy najwyższego poziomu ...............................................................................................179 inventory.js...............................................................................................................................180 Cechy produktów .................................................................................................................183 Cechy kategorii produktów..................................................................................................184 Tworzenie produktów i kategorii.........................................................................................185 Tworzenie koszyka na zakupy .............................................................................................186 ETAP 2. POKAZANIE TOWARÓW ....................................................................................................187 manager.html ...........................................................................................................................187 Zmienne ...............................................................................................................................194 display() ...............................................................................................................................195 Wyjątki od reguły ................................................................................................................195 Tworzenie wyświetlanej strony ...........................................................................................196 ETAP 3. POKAZANIE WSZYSTKICH KATEGORII...............................................................................197 Wyświetlenie pierwszego produktu ..........................................................................................197 Gdzie tu jest DHTML? ........................................................................................................198 ETAP 4. DODAWANIE PRODUKTÓW DO KOSZYKA ..........................................................................198 Wyszukiwanie produktów.........................................................................................................199 Odwzorowanie produktów i kategorii......................................................................................199 Przeszukiwanie istniejącej bazy danych ..................................................................................200 Obsługa nawigacji między produktami i kategoriami .............................................................200 Kod w łączach ..........................................................................................................................202 ETAP 5. ZMIANA ZAMÓWIENIA, PŁACENIE ....................................................................................202 Tworzenie list wyboru ..............................................................................................................205 Zapisywanie rachunku .............................................................................................................206 Opakowanie showBag(): pokazywanie podsumowań ..............................................................207 Przycisk Do kasy..................................................................................................................208 Koniec wyświetlania ................................................................................................................209 A po stronie serwera? ..............................................................................................................210 Przycisk Wyzeruj ilości ............................................................................................................210 Przycisk Zmiana koszyka .........................................................................................................210 Zapomniane funkcje .................................................................................................................210 KIERUNKI ROZWOJU ......................................................................................................................211 Inteligentniejsze towary ...........................................................................................................211 Zwiększenie możliwości wyszukiwania ....................................................................................211 Obsługa ciasteczek...................................................................................................................212
9
................................................................................................................................................213
7
Spis treści
SZYFRY W JAVASCRIPCIE......................................................................................................213 JAK DZIAŁAJĄ SZYFRY ..................................................................................................................216 Kilka słów o łamaniu kodów ....................................................................................................216 Szyfr Cezara .............................................................................................................................217 Szyfr Vigenere ..........................................................................................................................217 WYMAGANIA PROGRAMU..............................................................................................................218 STRUKTURA PROGRAMU ...............................................................................................................218 Definiowanie szyfru .................................................................................................................222 Tworzenie szyfru z podstawianiem...........................................................................................224 Podstawowa metoda podstawiania..........................................................................................224 Różne podstawienia do różnych szyfrów..................................................................................225 Algorytm szyfru Cezara .......................................................................................................226 Algorytm szyfru Vigenere ...................................................................................................226 Jak działa shiftIdx ....................................................................................................................227 Obiekty SubstitutionCipher też są obiektami Cipher ...............................................................228 Tworzenie nowych obiektów SubstitutionCipher .....................................................................228 Dobór odpowiedniego szyfru ...................................................................................................230 Na koniec .................................................................................................................................230 KIERUNKI ROZWOJU ......................................................................................................................231
10
......................................................................................................................................232
ELEKTRONICZNE ŻYCZENIA: POCZTA ELEKTRONICZNA METODĄ PRZENIEŚ I UPUŚĆ..........................................................................................................................................232 WYMAGANIA PROGRAMU..............................................................................................................234 STRUKTURA PROGRAMU ...............................................................................................................234 Pozostałe dwa dokumenty ........................................................................................................237 Co już wiemy ............................................................................................................................239 Proszę zająć miejsca!...............................................................................................................240 Śledzenie położenia myszy .......................................................................................................242 Wywoływanie wszystkich ikon..................................................................................................242 Przenoszenie ikon.....................................................................................................................242 Kiedy dokumenty już się załadują ............................................................................................243 Poznaj zmienne ........................................................................................................................247 Wyświetlanie życzeń.................................................................................................................248 Obrazki po kolei .......................................................................................................................250 Utrzymanie ikon na miejscu.....................................................................................................252 Sprawdzanie, co otrzymaliśmy.................................................................................................254 Ostateczne tworzenie kartki .....................................................................................................254 Wysyłanie życzeń......................................................................................................................256 Uwaga ......................................................................................................................................257 PO STRONIE SERWERA ...................................................................................................................257 KIERUNKI ROZWOJU ......................................................................................................................257 Dodanie łącza „wstecz” ..........................................................................................................257 Dodanie obrazków tematycznych.............................................................................................258 Banery reklamowe....................................................................................................................258
8
Życzenia bardziej interaktywne................................................................................................258
11
......................................................................................................................................259
POMOC KONTEKSTOWA.........................................................................................................259 WYMAGANIA PROGRAMU..............................................................................................................261 STRUKTURA PROGRAMU ...............................................................................................................261 Pomoc kontekstowa..................................................................................................................262 Pokazywanie i ukrywanie dodatkowych informacji .................................................................264 Tworzenie warstw ....................................................................................................................266 KIERUNKI ROZWOJU ......................................................................................................................268 Spis treści .................................................................................................................................268 Przeszukiwanie plików pomocy................................................................................................269 Pytanie do specjalisty ..............................................................................................................269 Pomoc telefoniczna ..................................................................................................................269
9
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(/ a , 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">
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 i należy umieszczać możliwie dużo kodu w pojedynczym zestawie <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 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()
1
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).
•
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.
3
Nazwy te pochodzą od oznaczeń operatorów warunkowych (przyp. tłum.).
20
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 Explorer 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 2 3 4 5 6
<TITLE>Wyszukiwanie <SCRIPT LANGUAGE="JavaScript1.1" SRC="records.js"> <SCRIPT LANGUAGE="JavaScript1.1">
Przykład 1.1. Zawartość pliku nav.html (ciąg dalszy) 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
SEARCHANY SEARCHALL SEARCHURL searchType = showMatches currentMatch copyArray = docObj =
= 1; = 2; = 4; ''; = 10; = 0; new Array(); parent.frames[1].document;
function validate(entry) { if (entry.charAt(0) == "+") { entry = entry.substring(1,entry.length); searchType = SEARCHALL; } else if (entry.substring(0,4) == "url:") { entry = entry.substring(5,entry.length); searchType = SEARCHURL; } else { searchType = SEARCHANY; } while (entry.charAt(0) == ' ') { entry = entry.substring(1,entry.length); document.forms[0].query.value = entry; } while (entry.charAt(entry.length - 1) == ' ') { entry = entry.substring(0,entry.length - 1); document.forms[0].query.value = entry; } if (entry.length < 3) { alert("Nie możesz wyszukiwać tak krótkich łańcuchów. Wysil się trochę."); document.forms[0].query.focus(); return; } convertString(entry); }
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 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117
} 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); } function verifyManage(resultSet) { if (resultSet.length == 0) { noMatch(); } else { copyArray = resultSet.sort(); formatResults(copyArray, currentMatch, showMatches); } } function noMatch() { docObj.open(); docObj.writeln('<TITLE>Wyniki wyszukiwania' + '' + '
' + '' + '"' + document.forms[0].query.value + '" - nic nie znaleziono.' + ' |
'); docObj.close(); document.forms[0].query.select(); } function formatResults(results, reference, offset) { var currentRecord = (results.length < reference + offset ?
24 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135
results.length : reference + offset); docObj.open(); docObj.writeln('\n\n<TITLE>Wyniki wyszukiwania \n' + '' + '
' + ' |
' + 'Zapytanie: ' + parent.frames[0].document.forms[0].query.value + ' \n' + 'Wyniki wyszukiwania: ' + (reference + 1) + ' - ' + currentRecord + ' z ' + results.length + '
' + '' + '\n\n\n\n'); prevNextResults(results.length, reference, offset); docObj.writeln('' + ' | \n
\n
\n\n'); docObj.close(); document.forms[0].query.select(); } function prevNextResults(ceiling, reference, offset) { docObj.writeln(''); if(reference > 0) { docObj.writeln(''); } if(reference >= 0 && reference + offset < ceiling) { var trueTop = ((ceiling - (offset + reference) < offset) ? ceiling - (reference + offset) : offset); var howMany = (trueTop > 1 ? "ów" : ""); docObj.writeln(''); } docObj.writeln(''); } //-->
Wyszukiwarka pracująca po stronie klienta | | 25 Rozdział 1 - Wyszukiwanie danych po stronie klienta 192 193 194 195 196 197 198 Pomoc |
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 var var var var var var var
4
SEARCHANY SEARCHALL SEARCHURL searchType = showMatches currentMatch copyArray = docObj =
= 1; = 2; = 4; ''; = 10; = 0; new Array(); parent.frames[1].document;
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 JavaScripcie 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 poszczegó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 pozbę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ą refineElement 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 JavaScript tablice są for malnie rzecz biorąc jednowymiarowe, ale możliwe jest też symulowanie tablicy wielowymiarowej, jak opisano poniżej. Przyjrzyjmy się pięcioelementowej, jednowymiarowej 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 new new new );
liczby = new Array { Array("jeden", "dwa", "trzy", "cztery", "pięć"), Array("uno", "dos", "tres", "cuatro", "cinco"), 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++) { for (var j = 0; j < liczby[i].length; j++) { if (myString == liczby[i][j]) { alert("Znalazłem!"); break; } } }
// pierwsza... // i druga
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('Bezpieczna paleta WWW' + '
'); for (var i = 0; i < hexPairs.length; i++) { // tworzenie wiersza document.writeln(''); 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 += '' + '   | '; }
31 Rozdział 1 - Wyszukiwanie danych po stronie klienta // Wypisz wiersz komórek i "wyzeruj" str document.writeln(str); str = ''; } // Koniec wiersza document.writeln('
'); } // Koniec tablicy document.writeln('
');
Uruchomienie tego kodu w przeglądarce (plik znajduje się w archiwum przykładów pod nazwą \Ch01\websafe.html) da nam tablicę 6×36, czyli z 216 (6×6×6) kolorami, których można bezpiecznie używać w każdej przeglądarce . Trzy pętle for odpowiadają trzem wymiarom. Oczywiście tablicę z paletą można modyfikować na różne sposoby, nam jednak chodzi o pokazanie, jak można sobie radzić z różnymi problemami programistycznymi przy użyciu zagnieżdżonych pętli for.
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('<TITLE>Wyniki wyszukiwania' + '' +
32 '
' + '' + '"' + document.forms[0].query.value + '" - nic nie znaleziono.' + ' |
'); 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.
•
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('\n\n<TITLE>Wyniki wyszukiwania\n ' + '' + '
' + ' |
' + 'Zapytanie: ' + parent.frames[0].document.forms[0].query.value + ' \n' + 'Wyniki wyszukiwania: ' + (reference + 1) + ' - ' + currentRecord + ' z ' + results.length + '
' + '' + '\n\n\n\n'); prevNextResults(results.length, reference, offset); docObj.writeln('' + ' | \n
\n
\n\n'); docObj.close(); document.forms[0].query.select(); }
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(''); if(reference > 0) { docObj.writeln(''); } if(reference >= 0 && reference + offset < ceiling) { var trueTop = ((ceiling - (offset + reference) < offset) ? ceiling - (reference + offset) : offset); var howMany = (trueTop > 1 ? "ów" : ""); docObj.writeln(''); } docObj.writeln(''); }
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.
35 Rozdział 1 - Wyszukiwanie danych po stronie klienta
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.
36
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(). Przekazywany 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 formatResults(): function formatResults(results, reference, offset) { docObj.open(); docObj.writeln('\n\n<TITLE>Wyniki wyszukiwania\n' + '' + '
' + ' |
' + 'Zapytanie: ' + parent.frames[0].document.forms[0].query.value + ' \n' + 'Wyniki wyszukiwania: ' + (reference + 1) + ' - ' + currentRecord + ' z ' + results.length + '
' + '' + '\n\n\n\n\t'); 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('JavaScript On-line Test 4 <SCRIPT LANGUAGE="JavaScript1.1"> 5 9 10 11 12 13 14 47 Rozdział 2 - Test sprawdzany na bieżąco 15 16 Przykład 2.1. Plik index.html (dokończenie) 17 18 19 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. 48 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: W nauce JavaScriptu nie ma wakacji... 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ą sytuacją, 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 atrybutem 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: 49 Rozdział 2 - Test sprawdzany na bieżąco 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 środowiska Deklarowanie i inicjalizacja zmiennych globalnych, przemieszanie zestawów pytanie– odpowiedź. Zmienne: qIdx, correct, howMany, stopOK, nextQ, results, aFrame, qFrame Tablice: keeper, rank, questions, answers Funkcje: itemReset(), shuffle() Zarządzanie testem Ocena testu Pokazanie wyników Wyœwietlanie wyjaœnieñ Czyszczenie œrodowiska Zapisanie zestawu pytanie– odpowiedź w oknie, zapis odpowiedzi użytkownika. Porównanie odpowiedzi badanego z poprawnymi odpowiedziami. Pokazanie odpowiedzi poprawnych i błędne wraz z oceną. Wyœwietlanie i chowanie wyjaśnień w parent.frames[1]. Przywracanie zmiennym ich pierwotnych wartości. Funkcje: buildQuestion(), makeButton(), ewentualnie chickenOut() Funkcja gradeTest() Funkcja printResults() Funkcje explain() i show() Zmienne: qIdx, correct, 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 50 1 2 3 4 5 6 7 8 9 10 11 12 13 <TITLE>On-line JavaScript Test <SCRIPT LANGUAGE="JavaScript1.1" SRC="questions.js"> <SCRIPT LANGUAGE="JavaScript1.1"> 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.', Przykład 2.3. Kod źródłowy administer.html (ciąg dalszy) 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 '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]; 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; } } function itemReset() { qIdx = 0; correct = 0; stopOK = false; keeper = new Array(); shuffle(); } function buildQuestion() { if (qIdx == howMany) { gradeTest(); return; } nextQ = '' + 'Pytanie ' + (qIdx + 1) + ' z ' + howMany + '' + '' + '' + units[qIdx].question + '
' + makeButton("a", units[qIdx].a) + makeButton("b", units[qIdx].b) + makeButton("c", units[qIdx].c) + makeButton("d", units[qIdx].d) + ''; qFrame.location.replace("javascript: parent.frames[0].nextQ"); qIdx++; if(qIdx >= 2 && !stopOK) { stopOK = true; } } function makeButton(optLtr, optAnswer) { return '' + optAnswer + ' '; } function chickenOut() { if(stopOK && confirm('Masz już dość? Tchórzysz?')) { gradeTest(); } } 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]); 51 Rozdział 2 - Test sprawdzany na bieżąco 75 itemReset(); 76 } 77 function printResults(ranking) { 78 results = '' + Przykład 2.3. Kod źródłowy administer.html (dokończenie) 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 '' + 'Odpowiedzi poprawnych: ' + correct + '/' + howMany + '.' + 'Ocena: ' + ranking + ' Ustaw kursor myszki nad czerwonym tekstem, a zobaczysz' + ' poprawki odpowiedzi.' + '
Oto Twoje oceny:
'; for (var i = 0; i < howMany; i++) { results += '\n\r\n\r\n\rPytanie ' + (i + 1) + ' ' + units[i].question + '
\n\r' + 'a. ' + units[i].a + ' ' + 'b. ' + units[i].b + ' ' + 'c. ' + units[i].c + ' ' + 'd. ' + units[i].d + ' '; if (keeper[i] == units[i].answer) { results += '' + 'Na to odopwiedziałeś poprawnie (' + keeper[i] + ').\n\r
'; } else { results += '' + '' + 'Prawidłowa odpowiedź: ' + units[i].answer + '\n\r
'; } } results += '\n\r'; qFrame.location.replace("javascript: parent.frames[0].results"); } function show() { parent.status = ''; } function explain(str) { with (aFrame.document) { open(); writeln('' + str + ''); close(); } } function cleanSlate() { aFrame.location.replace('javascript: parent.dummy1'); qFrame.location.replace('javascript: parent.dummy2'); } 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: 52 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: 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ń. 53 Rozdział 2 - Test sprawdzany na bieżąco 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): 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]). 54 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 = '' + 'Pytanie ' + (qIdx + 1) + ' z ' + howMany + '' + '' + '' + units[qIdx].question + '
' + makeButton("a", units[qIdx].a) + makeButton("b", units[qIdx].b) + makeButton("c", units[qIdx].c) + makeButton("d", units[qIdx].d) + ''; 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, następnie 1, 3, 5, 7, 9), wywołujemy shakeUp(twojaTablica, 2). Jeśli przekażemy 0, domyślny przyrost to 1. Więcej tego typu funkcji znajdziesz w rozdziale 6. 55 Rozdział 2 - Test sprawdzany na bieżąco 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: 'Pytanie ' + (qIdx + 1) + ' z ' + howMany + '' 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: '' + '' + 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 '' + optAnswer + ' '; } 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: ''; 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ć. 56 Techniki języka JavaScript: protokół javascript: Wcześniej już zetknęliśmy się z powyższym protokołem w tej książce. Protokół ten pozwala 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 : Kliknij mnie 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 znajduje 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). 57 Rozdział 2 - Test sprawdzany na bieżąco 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 10–19 20–29 30–39 40–50 0 1 2 3 4 byli lepsi jako-tako dobrze bardzo dobrze 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 = '' + '' + 'Odpowiedzi poprawnych: ' + correct + '/' + howMany + '.' + 'Ocena: ' + ranking + ' Ustaw kursor myszki nad czerwonym tekstem, a zobaczysz' + ' poprawki odpowiedzi.' + '
Oto Twoje oceny:
'; 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\rPytanie ' + (i + 1) + ' ' + 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.). 58 units[i].question + '
\n\r' + 'a. ' + units[i].a + ' ' + 'b. ' + units[i].b + ' ' + 'c. ' + units[i].c + ' ' + 'd. ' + units[i].d + ' '; 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 += '' + 'Na to odopwiedziałeś poprawnie (' + keeper[i] + ').\n\r
'; } else { results += '' + '' + 'Prawidłowa odpowiedź: ' + units[i].answer + '\n\r
'; } } 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('' + str + ''); 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'; qFrame.location.replace("javascript: parent.frames[0].results"); } 59 Rozdział 2 - Test sprawdzany na bieżąco 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; } 60 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: '
Oto Twoje oceny:
'; zmieńmy na: '
Oto jak odpowiadałeś:
' + 'FORM ACTION="URL_twojego_skryptu_serwerowego" METHOD=POST>'; Zastąpmy też wierze 92 do 105 w sposób następujący: results += 'Wybrałeś ' + keeper[i] + '
'; 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'; zmieńmy na: results += ' '; 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 w różnych przeglądarkach Korzyści wynikające ze stosowania prostych konwencji nazewniczych Siła funkcji eval() Użycie setInterval() i clearInterval() 3 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. 62 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 . Strzałki w (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ł. 63 Rozdział 3 - Interaktywna prezentacja slajdów 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <TITLE>Pokaz slajdów <STYLE TYPE="text/css"> #menuConstraint { height: 800; } <SCRIPT LANGUAGE="JavaScript1.2"> 64 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 var imgOut = new Array(); var imgOver = new Array(); var imgPath = 'images/'; var showSpeed = 3500; var tourOn = false; function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) { if (NN) { document.writeln('' + copy + ''); } else { document.writeln('' + copy + ''); } } function slide(imgStr, scientific, copy) { this.name = imgStr; imagePreload(imgStr); this.copy = copy; this.structure = '' + ' | ' + 'Nazwa potoczna:' + Przykład 3.1. index.html (ciąg dalszy) 57 camelCap(imgStr) + 'Nazwa naukowa: ' + 58 scientific + '' + 'Krótki opis: ' + copy + 59 ' | '; 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 ptasia noga.') 99 ); 65 Rozdział 3 - Interaktywna prezentacja slajdów 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 += ' '; } genLayer('automation', sWidPos - 100, 11, 100, 200, true, '' + '' ); genLayer('guide', sWidPos - 100, 30, 100, 200, true, '' + '' + '' + '' + '' + '' ); genLayer('menu', sWidPos - 104, 43, 100, 200, false, '' ); } function refSlide(name) { if (NN) { return document.layers[name]; } else { return eval('document.all.' + name + '.style'); } } function hideSlide(name) { refSlide(name).visibility = hideName; } function showSlide(name) { refSlide(name).visibility = showName; } function menuManager() { if (isVis) { hideLayer('menu'); } else { showLayer('menu'); } isVis = !isVis; } function changeSlide(offset) { hideLayer('slide' + curSlide); curSlide = (curSlide + offset < 0 ? slideShow.length - 1 : (curSlide + offset == slideShow.length ? 0 : curSlide + offset)); showSlide('slide' + curSlide); } function setSlide(ref) { if (tourOn) { return; } 66 174 hideSlide('slide' + curSlide); Przykład 3.1. index.html (dokończenie) 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 curSlide = ref; showSlide('slide' + curSlide); } function imageSwap(imagePrefix, imageIndex, isOver) { if (isOver) { document[imagePrefix].src = imgOver[imageIndex].src; } else { document[imagePrefix].src = imgOut[imageIndex].src; } } function hideStatus() { window.status = ''; } 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; } function automate() { imageSwap(slideShow[curSlide].name, curSlide, false); changeSlide(1); imageSwap(slideShow[curSlide].name, curSlide, true); } //--> Prezentacja królestwa zwierząt <SCRIPT LANGUAGE="JavaScript1.2"> 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; } 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 var var var var dWidLyr = 450; dHgtLyr = 450; curSlide = 0; zIdx = -1; isVis = false; var NN = (document.layers ? true : false); var sWidPos = ((NN ? outerWidth : screen.availWidth) (dWidLyr / 2); / 2) - 67 Rozdział 3 - Interaktywna prezentacja slajdów var sHgtPos (dHgtLyr / var hideName var showName var var var var = ((NN ? outerHeight : screen.availHeight) / 2) 2); = (NN ? 'hide' : 'hidden'); = (NN ? 'show' : 'visible'); img = new Array(); imgOut = new Array(); imgOver = new Array(); 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 (dWidLyr / var sHgtPos (dHgtLyr = ((NN ? outerWidth : screen.availWidth) / 2) 2); = ((NN ? outerHeight : screen.availHeight) / 2) / 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. 68 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 var var var img = new Array(); imgOut = new Array(); imgOver = new Array(); 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 warstwy obrazki warstwy warstwy generuje slajdy konstruktor obiektu slajdu ładuje wstępnie grafikę slajdu i pasek nawigacyjny zmienia pierwszą literę nazwy slajdu na wielką wywołuje genLayer() i pozycjonuje wszystkie warstwy ukrywa warstwy pokazuje warstwy zwraca wskaźnik warstwy w zależności od stosowanej przeglądarki ukrywa i pokazuje menu slajdów zmienia aktualny slajd w przypadku stosowania slide() imagePreLoad() camelCap() genScreen() hideSlide() showSlide() refSlide() menuManager() changeSlide() warstwy warstwy warstwy warstwy warstwy 69 Rozdział 3 - Interaktywna prezentacja slajdów setSlide() warstwy imageSwap() obrazki nawigacja nawigacja nawigacja hideStatus() autoPilot() automate() strzałek polecenia lub autopokazu zmienia bieżący slajd na podstawie zdarzeń obsługi myszy przewija obrazki na podstawie menu ustawia wartość paska stanu na "" steruje trybem autopokazu 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('' + copy + ''); } else { document.writeln('' + copy + ''); } } 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 = '' + ' | ' + 'Nazwa potoczna:' + camelCap(imgStr) + 'Nazwa naukowa: ' + scientific + '' + 'Krótki opis: ' + copy + ' | '; return this; 70 } 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 specyfikacją 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) { // // // } else { { // // // } Mamy do czynienia z MSIE Użyj odpowiedników Jscript np. document.all.styles, itd. 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 = '' + ' | ' + 'Nazwa potoczna:' + camelCap(imgStr) + 'Nazwa naukowa: ' + scientific + '' + 'Krótki opis: ' + copy + ' | '; W celu dynamicznego tworzenia obrazków slajdów łączony jest znacznik HTML 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: 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.). 71 Rozdział 3 - Interaktywna prezentacja slajdów Techniki języka JavaScript: poprawne konwencje nazewnicze Temat konwencji nazewniczych przewija się w całej książce. Przyjrzyjmy się, ile aplikacja pokazująca slajdy może uzyskać dzięki użyciu prostych słów: cow, bird i dog. Oczywiś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 stosowanego języka. Przyjrzyjmy się, jak prosta konwencja nazewnicza użyta została w odniesieniu 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 pig cow snake nazwa zwierza pig cow snake obrazek slajdu pig.gif cow.gif snake.gif obrazek menu pigout.gif cowout.gif snakeout.gif obrazek menu pod kursorem pigover.gif cowover.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 += ' '; } genLayer('automation', sWidPos - 100, 11, 100, 200, true, '' + '' ); genLayer('guide', sWidPos - 100, 30, 100, 200, true, '' + '' + '' + '' + '' + '' ); genLayer('menu', sWidPos - 104, 43, 100, 200, false, '' ); 72 } 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 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 += ' '; } 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. Odległość od lewego brzegu okna w pikselach. Odległość w pikselach od górnego brzegu okna. sWidPos sHgtPos 73 Rozdział 3 - Interaktywna prezentacja slajdów dWidLyr dHgtLyr (i == 0 ? true : false) slideshow[i].structure Domyślna szerokość slajdu, w tym wypadku 450. Domyślna wysokość slajdu, w tym wypadku 450. Sprawdza, czy slajd jest pokazany (true), czy schowany (false). Początkowo schowane są wszystkie slajdy poza pierwszym (kiedy i równe jest 0). 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 += ' '; 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 . 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 z niepowtarzalnymi wartościami atrybutów NAME i SRC. 6. Zamykający znacznik . 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.: Znacznik 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 . Pozycja 6. to znów prosta sprawa: należy dodać na końcu znacznik – i gotowe. 74 Do wartości zmiennej menuStr dodawany jest napis pochodzący ze wspomnianego wcześniej kodu pętli for. 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, '' ); 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 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, '' + '' ); 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 : genLayer('guide', sWidPos - 100, 30, 100, 200, true, '' + '' + '' + '' + '' + '' ); 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 , 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 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"> 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 ' + 75 Rozdział 3 - Interaktywna prezentacja slajdów 'osoby o wątpliwych manierach przy jedzeniu.'), new slide('snake', 'Groovius Dudis', 'Wąż jest śliskim i podstępnym ' + '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 ptasia noga.') ); 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)};' 76 Kiedy wynik zostanie zapisany w dokumencie, a i równe jest 0, rzecz będzie wyglądać tak: 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, programistów. Załóżmy, że chcielibyśmy odwołać się do obiektu, ale nie wiemy, jaki jest jego indeks (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 formularzy 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. 77 Rozdział 3 - Interaktywna prezentacja slajdów 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 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 78 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. 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. 79 Rozdział 3 - Interaktywna prezentacja slajdów 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, setInterval() 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: 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); 80 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 Matematyka a pamięć Użycie funkcji escape() 4 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. 82 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). 83 Rozdział 4 - Interfejs multiwyszukiwarki 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 <TITLE>Witryna multiwyszukiwarki <SCRIPT LANGUAGE="JavaScript1.2"> 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 <TITLE>Multi-Engine Menu <SCRIPT LANGUAGE="JavaScript1.2"> = 'javascript: parent.white'; NN = (document.layers ? true : false); curSlide = 0; hideName = (NN ? 'hide' : 'hidden'); showName = (NN ? 'show' : 'visible'); perLyr = 4; engWdh = 90; engHgt = 20; left = 375; top = 10; zIdx = -1; imgPath = 'images/'; arrayHandles = new Array('out', 'over'); 84 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 87 88 89 90 91 'http://www.savvysearch.com/'), newArray('Discovery Online', 'http://www.discovery.com/cgi-bin/searcher/-?' + 'output=title&exclude=/search&search=', 'http://www.discovery.com/'), newArray('Borders.com', 85 Rozdział 4 - Interfejs multiwyszukiwarki 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 'http://www.borders.com:8080/fcgi-bin/db2www/search/' + 'search.d2w/QResults?doingQuickSearch=1&srchPage=QResults&' + 'mediaType=Book&keyword=', 'http://www.borders.com/'), newArray('Life Magazine', 'http://cgi.pathfinder.com/cgi-bin/life/cg/pshell?' + 'venue=life&pg=q&date=all&x=15&y=16&q=', 'http://www.life.com/') ); engines = engines.sort(); 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'"); } } function engineLinks() { genLayer('sliderule', left - 20, top + 2, 25, engHgt, showName, '' + ' ' + '' + ''); lyrCount = (engines.length % perLyr == 0 ? engines.length / perLyr : Math.ceil(engines.length / perLyr)); for (var i = 0; i < lyrCount; i++) { var engLinkStr = ''; 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 += '' + ' | '; } engLinkStr += ' '; genLayer('slide' + i, left, top, engWdh, engHgt, hideName, engLinkStr); } } function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) { if (NN) { document.writeln('' + copy + ''); } Przykład 4.2. multi.html (ciąg dalszy) 149 else { 150 document.writeln('' + copy + ''); 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; } 86 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 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); } function imageSwap(imagePrefix, imageIndex, arrayIdx) { document[imagePrefix].src = eval(arrayHandles[arrayIdx] + "[" + imageIndex + "].src"); } 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); } } function hideStatus() { window.status = ''; } //--> <SCRIPT LANGUAGE="JavaScript1.2"> | Przykład 4.2. multi.html (dokończenie) 214 215 216 217 218 219 220 221 | 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. 87 Rozdział 4 - Interfejs multiwyszukiwarki 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 var var var var var var var var var var var NN = (document.layers ? true : false); curSlide = 0; hideName = (NN ? 'hide' : 'hidden'); showName = (NN ? 'show' : 'visible'); perLyr = 4; engWdh = 90; engHgt = 20; left = 375; top = 10; zIdx = -1; imgPath = 'images/'; 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('' + copy + ''); } else { document.writeln('' + copy + ''); } } 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' 88 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] out[0].src over[0] over[0].src = = = = new Image(); 'images/infoseekout.jpg'; new Image(); '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=', 89 Rozdział 4 - Interfejs multiwyszukiwarki '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 dobranej 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, '' + ' ' + '' + ''); 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. 90 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żytkownicy będą wdzięczni, jeśli szybko znajdą swoją przeglądarkę. Jeśli zaczęliby wszystko realizować 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 w przeciwnym wypadku równe jest engines.length/perLyr; lyrCount 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 = ''; 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 += '' + ' | '; } 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 91 Rozdział 4 - Interfejs multiwyszukiwarki 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 1 2 3 4 0, 1, 2, 3 4, 5, 6, 7 8, 9, 10, 11 12, 13, 14, 15 16, 17, 18, 19 0, 1, 2, 3 0, 1, 2, 3 0, 1, 2, 3 0, 1, 2, 3 0, 1, 2, 3 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 pewnością można byłoby. Po co jednak alokować dodatkową pamięć na deklarację dodatkowej zmiennej, nawet lokalnej? JavaScript ma już konieczne wartości w i, perLyr i j, które pozwalają wykonać potrzebne 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 += '' + ' | '; 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ść document.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: ''; 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 += ' '; 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. 93 Rozdział 4 - Interfejs multiwyszukiwarki 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 przeszkodzą 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 przesyłamy formularz, kodowaniem zajmuje się przeglądarka. Jako że ta aplikacja nie przewiduje 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. 94 • 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 wartości Przykład HotBot InfoSeek Advanced Search Scientific American Yahoo! DC 10, 25, 50, 100 10, 20, 25, 50 10, 25, 50, 100 10, 20, 50, 100 DC=10 Numberresults Docs N Numberresults=10 Docs=10 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). 5. Dostosuj kod funkcji callSearch(), aby prawidłowo łączone było zapytanie dla poszczególnych wyszukiwarek. 95 Rozdział 4 - Interfejs multiwyszukiwarki 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 25 VALUE="50">50 100 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) { if (searchTxt == "") { parent.frames[2].location.href = engines[idx][2] + escape(searchTxt); } 96 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 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 5 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. 98 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. 99 Rozdział 5 - ImageMachine 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. 100 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 2 3 4 5 6 <TITLE>ImageMachine Rysunek 5.4. Wybierz „Podgląd” lub „Zmiana danych” Przykład 5.1. indeks.html (dokończenie) 7 8 9 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 2 3 4 5 6 7 8 9 10 11 12 13 14 <TITLE>ImageMachine <SCRIPT LANGUAGE="JavaScript1.2"> var platform = navigator.platform; var lb = (platform.indexOf("Win" != -1) ? "\n\r" : (platform.indexOf("Mac" != -1) ? "\r" : "\n")); var fontOpen = ''; var fontClose = ''; function genSelect(name, count, start, select) { var optStr = ""; for (var h = start; h " |
" + 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 (HTML (HTML (HTML (HTML gt + ? fontClose : "") + br + nbsp + "HEIGHT=" + ? fontOpen : ") + imgHgt[j] + ? fontClose : ") + br + nbsp + "BORDER=" + ? fontOpen : "") + imgBdr[j] + ? fontClose : "") + "" + 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 (HTML (HTML (HTML (HTML (HTML (HTML (HTML (HTML gt + ? fontClose : "") + br + nbsp + "HEIGHT=" + ? fontOpen : ") + imgHgt[j] + ? fontClose : ") + br + nbsp + "BORDER=" + ? fontOpen : "") + imgBdr[j] + ? fontClose : "") + br + nbsp + "HSPACE=" + ? fontOpen : ") + imgHSpace[j] + ? fontClose : ") + br + nbsp + "VSPACE=" + ? fontOpen : "") + imgVSpace[j] + ? fontClose : "") + "" + lt + "/A" + gt + br + br + br; 117 Rozdział 5 - ImageMachine 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 ; świetnie do rozbudowy nadaje się też – 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 Formatowanie i poprawianie liczb Tworzenie i badanie obiektów Działania na napisach 6 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. 119 Rozdział 6 - Realizacja plików źródłowych JavaScriptu 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. 120 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 2 3 4 5 6 7 8 <TITLE>Przyk¦ady arrays.js <SCRIPT LANGUAGE="JavaScript1.2" SRC="arrays.js"> <SCRIPT LANGUAGE="JavaScript1.2"> Poprzednia kategoria | Następny produkt | Poprzednia kategoria | Następny produkt | <SELECT onChange='this.form.elements[1].value = this.options[this.selectedIndex].value; 206 parent.frames[1].runningTab(this.form);'> <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'; } } 207 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie 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? Otrzymujemy stukrotność oczekiwanego wyniku, czyli 345, zatem wszystko jest w porządku. Skoro poprawnie działa zatem arytmetyka całkowitoliczbowa, to możemy z niej skorzystać, po czym wynik przekształcimy z typu Number na String, dodając przy tym w odpowiednim 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 napis 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 = ' |
' + 'SubTotal: | ' + ' |
' + '' + ' + 6% Tax: | ' + ' |
' + '2% Shipping: | |
' + '' + ' | ' + ' | ' + ' | ' + 'Suma: | ' + '' + 208 ' |
'; var footer = '
';
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 = '<TITLE>Shopping Bag - płatności' + ''; var intro = 'Shopping Bag - płatności'; var shipInfo = '
' + 'Informacje o wysyłce |
'+ 'Imię | ' + ' | ' + '
Nazwisko | ' + ' |
Firma | ' + ' |
' + 'adres - ulica I | ' + ' |
adres - ulica II | ' + ' |
' + 'Miasto | ' + ' |
' + 'Województwo/region | ' + ' |
' + 'Kraj | ' + ' |
' + 'Kod pocztowy | | ' + '
|
'; var payInfo = '
' + 'Informacje o płatności |
'+ 'Typ karty kredytowej | ' + 'Visa ' + ' + 'Amex ' + ' ' + 'Discover ' + | ' + '
' + 'Numer karty kredytowej | ' + ' |
' + 'Data ważności | | ' + '
| ' + ' | ' + '
' + '
';
209 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie 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 += ''; }
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 = '' + '' + '' + '';
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');
210
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() {
211 Rozdział 8 - Shopping Bag – wózek sklepowy stworzony w JavaScripcie 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>
212 Artykuły AGD Budynki Ubrania Elektronika Jedzenie Urządzenia Muzyka
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 Nazwy oznaczają odpowiednio: szyfr z ukryciem, szyfr przestawny i szyfr z podstawianiem; Cipher oznacza szyfr (przyp. tłum.).
219 Rozdział 9 - Szyfry w JavaScripcie 7 8 9 10
I { font-weight: bold; } //--> <SCRIPT LANGUAGE="JavaScript1.2" SRC="dhtml.js">
Rysunek 9.6. Rozszerzanie struktury klas szyfrów Przykład 9.1. index.html (ciąg dalszy) 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
<SCRIPT LANGUAGE="JavaScript1.2">
Przykład 9.1. index.html (ciąg dalszy) 38 '
. 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,' ');
220 52 cleanText = cleanText.replace(/[^a-z0-9\s]/g,''); 53 if(cleanText.length == 0 || cleanText.match(/^\s+$/) != null) { 54 return false; 55 } 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 ];
221 Rozdział 9 - Szyfry w JavaScripcie 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160
function showCipher(name) { hideSlide(curCipher); showSlide(name); curCipher = name; } function routeCipher(cipherIdx, data, action) { var response = cipherArray[cipherIdx].algorithm(data, action); if(response) { document.forms[0].Data.value = response; } } //-->
|
<SELECT NAME="Ciphers" onChange="showCipher(this.options[this.selectedIndex].value);"> Szyfr Cezara Szyfr VigenÚre | Przykład 9.1. index.html (dokończenie) 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185
|
<SCRIPT LANGUAGE="JavaScript1.2">
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
222 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.
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 definiuje 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 przypisać 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 cipherArray, 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; } 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.)
223 Rozdział 9 - Szyfry w JavaScripcie 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) { 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żywamy 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 wystą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ń regularnych wraz ze znaczeniem znaków specjalnych można znaleźć w opisie nowości JavaScriptu w wersji 1.2, pod adresem http://developer1.netscape.com:80/docs/manuals/ communicator/jsguide/regexp.htm.
224 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: 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.
225 Rozdział 9 - Szyfry w JavaScripcie 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. 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.
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.).
226
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) { 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ę Teraz, kiedy wartość została określona, funkcja do document.layers["caesar"]. shiftIdx 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);
227 Rozdział 9 - Szyfry w JavaScripcie keywordIdx = (keywordIdx == keyword.length - 1 ? 0 : keywordIdx + 1); } return cipherData; }
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;
228 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)
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. SubstitutionCipher dziedziczy wszystkie właściwości Cipher. W ten sposób możemy naprawdę skorzystać z możliwości programowania obiektowego (przynajmniej w rozumieniu 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.
229 Rozdział 9 - Szyfry w JavaScripcie
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:
|
<SELECT NAME="Ciphers" onChange="showCipher(this.options[this.selectedIndex].value);"> Szyfr Cezara Szyfr Vigenére |
|
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);"> Szyfr Cezara Szyfr Vigenére
230 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:
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 lub Pierwszy argument – – pochodzi natomiast wartość true false. cipherIdx 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ę.
231 Rozdział 9 - Szyfry w JavaScripcie
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 , 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.
234
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ń.
235 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść
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.
236
Rysunek 10.7. Logika elektronicznych pozdrowień: jak użytkownik otrzymuje wiadomość Przykład 10.1. index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
<TITLE>Cyber Greetings <SCRIPT LANGUAGE="JavaScript1.2">
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.
237 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
<TITLE>Drag-n-Drop E-mail <STYLE TYPE="text/css"> <SCRIPT LANGUAGE="JavaScript1.2">
NN = (document.layers ? true : false); hideName = (NN ? 'hide' : 'hidden'); showName = (NN ? 'show' : 'visible'); zIdx = -1;
var var var var var
iconNum = 4; startWdh = 25; imgIdx = 0; activate = false; activeLayer = null;
var backImgs = []; var icons = [ 'bear', 'cowprod', 'dragon', 'judo', 'robot', 'seniorexec', 'dude', 'juicemoose', '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
239 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść 134 135 136 137 138 139 140 141 142 143
<SCRIPT LANGUAGE="JavaScript1.2">
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 var var var
NN = (document.layers ? true : false); hideName = (NN ? 'hide' : 'hidden'); showName = (NN ? 'show' : 'visible'); zIdx = -1;
var var var var var
iconNum = 4; startWdh = 25; imgIdx = 0; activate = false; 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 i Internet Explorerze ukrywanie warstw o zadanej nazwie pokazywanie warstw o zadanej nazwie
3, 4, 6, 9, 11
hideSlide() ShowSlide
3, 4, 6, 9, 11 3, 4, 6, 9, 11
240 refSlide() motionListener() grabXY()
odwołanie się do warstwy o zadanej nazwie śledzenie ruchów myszy określenie współrzędnych x i y elementu
3, 4, 6, 9, 11 11 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 <STYLE TYPE="text/css"> <SCRIPT LANGUAGE="JavaScript1.2">
curGreet = iconIdx = 0; backgroundIdx = 0; baseURL = "."; bRef = parent.Back;
function showGreeting(selIdx) { if (selIdx > 0) { bRef.hideSlide("greeting" + curGreet); bRef.showSlide("greeting" + selIdx); curGreet = selIdx; } } 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; } } function nextIcons() { for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) + 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);
245 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść 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) = ref2.top) && 99 (ref.left + ref.document.images[0].width
Przykład 10.3. front.html (ciąg dalszy) 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
" onClick="nextIcons();">
Życzenia: | 247 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 <SCRIPT LANGUAGE="JavaScript1.2"> |
Wysyłanie: | |
Przykład 10.3. front.html (dokończenie) 249 250 251 252 253
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żywamy 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 zapisywać 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 sposó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 danych). 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.
248
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; } }
249 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść
Techniki języka JavaScript: komunikacja między ramkami W rozdziale 1., że używaliśmy zmiennej docObj umożliwiającej nam proste odwoływanie 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żliwia ł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 onCard() 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) = ref2.top) && (ref.left + ref.document.images[0].width = 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) = 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 +
250 parent.Back.refSlide(parent.Back.icons[iconRef]).document. images[0].width 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]);
251 Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść } } 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) iconIdx + 1);
- 1 ? 0 :
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) +
252 bRef.iconNum; i++) { 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) = ref2.top) && (ref.left + ref.document.images[0].width = parseInt(ref2.top)) && (parseInt(ref.left) + parseInt(ref.width) = ref2.top) && (ref.left + ref.document.images[0].width nav.html <STYLE TYPE="text/css"> <SCRIPT>
263 Rozdział 11 - Pomoc kontekstowa 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
',left=100,top=100,scrollbars=no'); } else { helpWin.location.href = 'help/' + helpName + '.html'; } helpWin.focus(); } //--> Kolory tła Katalog URL Listy wielokrotnego wyboru Help
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: Help
264 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ściwość closed o wartości true, jeśli okno zostało zamknięte, i o wartości false w przeciwnym 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 poprzedniej 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 2 3 4 5
var var var var var
NN = (document.layers ? true : false); hideName = (NN ? 'hide' : 'hidden'); showName = (NN ? 'show' : 'visible'); zIdx = -1; helpWdh = 200;
265 Rozdział 11 - Pomoc kontekstowa 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65
var helpHgt = 200; var x, y, totalWidth, totalHeight; function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) { if (NN) { document.writeln('' + copy + ''); } else { document.writeln('' + copy + '' ); } } 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 motionListener() { if (NN) { window.captureEvents(Event.MOUSEMOVE); window.onmousemove = grabXY; } else { document.onmousemove = grabXY; } } function grabXY(ev) { if(NN) { x = ev.pageX; y = ev.pageY; } else { x = event.x; y = event.y; } } function helpDisplay(name, action) { if(action) { totalWidth = x + helpWdh; totalHeight = y + helpHgt; x = (totalWidth > wdh ? x (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.
266
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 potrzebować 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.’; var helpTwo = ‘<SPAN CLASS=”helpSet”>Ta właściwość obiektu ‘ + ‘window zawiera hierarchię obiektów ‘ + ‘bieżącej strony sieciowej.’; 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;
267 Rozdział 11 - Pomoc kontekstowa 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 zobaczymy 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: Zrób coś
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.
268
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: <TITLE>Warstwa DHTML <SCRIPT LANGUAGE="JavaScript1.2"> Jest to warstwa DHTML.
Show/Hide
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. Wypró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:
269 Rozdział 11 - Pomoc kontekstowa function showContents() { var helpDocs = ['background', 'multiselect', 'urldirectory']; var helpLinks = '
'; for (var i = 0; i < helpDocs.length; i++) { helpLinks += '- ' + helpDocs[i] + ''; } helpLinks = '
'; 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źć.