Spis Treści
Od redakcji |
|
Niniejsza książka to gotowy zestaw receptur - podobnie jak książka kucharska. O ”wirtualnym koszyku na zakupy” można myśleć jako o ”ciasteczkach cebulowych z pastą łososiową”. W każdym rozdziale podano kod i dokumentację przydatnej aplikacji zwykle napisanej całkowicie w JavaScripcie. Można wszystkie dania przygotowywać tak, jak to podał autor książki, ale można też sięgnąć do pewnych sugestii, aby wykorzystać je w swoich pomysłach. Na rynku znajdują się książki zawierające proste przepisy, jak zrobić jakieś drobiazgi i jak ozdobić JavaScriptem swoją stronę sieciową, natomiast w tej książce znajdziemy całe aplikacje sieciowe napisane w jedynym języku skryptowym, rozumianym przez przeglądarki.
Skoro tyle już sobie wyjaśniliśmy, zastanówmy się, co należy rozumieć przez książkę z recepturami? Jej zadaniem nie jest na pewno podawanie treści w mało elastycznej formie, ale pomoc w tworzeniu kodu. Zapewne takie pozycje książkowe, zawierające receptury, będzie można spotkać coraz częściej.
Richard Koman, Redaktor
Wstęp |
|
Czegoś dotychczas brakowało. Zgłębiałem stosy książek o JavaScripcie, oglądałem kolejne witryny sieciowe wprost ociekające kodem i pomysłami. Jednak kiedy już poznałem wszelakie nowe rodzaje składni i genialne techniki, nie wiedziałem, co z tą wiedzą mogę zrobić poza pokazywaniem przykładów. Czułem się tak, jakbym był w kuchni pełnej wszelakich składników jadła, ale bez żadnego przepisu. Znałem rozmaite sztuczki języka JavaScriptu i miałem różne kawałki kodu, ale nie potrafiłem tego zastosować do rozwiązania typowych problemów na stronach sieciowych. Oczywiście niektóre książki zawierały aplikacje JavaScript, ale nie były one odpowiednie do stosowania w Sieci. Oczko to świetna gra, arkusz kalkulacyjny to ciekawa aplikacja, ale trudno je zamieszczać na swoich stronach sieciowych.
W tej książce znajduje się szereg przepisów. Nie tylko można się dowiedzieć, jak sprawdzić używaną przeglądarkę czy umożliwić przewijanie obrazków, ale można również znaleźć tu kompletne aplikacje, których będziesz mógł używać na swoich stronach. Aplikacje te nie będą tworzone krok po kroku, od razu zostaną zaprezentowane w całości. Można je kopiować do katalogu serwera sieciowego (lub komputera lokalnego) i natychmiast uruchamiać. Rozdziały tej książki naszpikowane są kodem JavaScript, który ma pomóc użytkownikom w realizowaniu typowych zadań, takich jak przeszukiwanie witryny, sporządzenie spisów treści, umożliwienie przewijania obrazków, oglądanie prezentacji, robienie zakupów i wiele innych. Oczywiście można te przykłady modyfikować tak, aby najlepiej pasowały do naszych potrzeb, ale i tak są one mniej więcej gotowe do użycia. Oprócz tego do każdej aplikacji dołączono dokładne objaśnienie jej działania, więc można sobie sprawdzać, jak zadziała zmiana poszczególnych fragmentów kodu.
Co powinieneś wiedzieć
Nie jest to książka dla początkujących, gdyż nikt nie będzie tu nikogo uczył JavaScriptu, ale będzie można się dowiedzieć się, jak go używać. Nie trzeba być wiarusem JavaScriptu z trzyletnim stażem, jeśli jednak info.replace(/</g, "<"), new Image() czy var itemArray = [] są dla kogoś niejasne, to najlepiej mieć pod ręką opis składni tego języka.
Użycie czcionek
Kursywa
używana jest do podawania nazw plików, ścieżek katalogów, adresów URL.
Pismo o stałej szerokości czcionki
używana do przedstawiania znaczników HTML, fragmentów kodu, funkcji, nazw obiektów i innych odwołań do kodu.
Kursywa o stałej szerokości czcionki
stosowana jest do tekstu wprowadzanego przez użytkownika i tekstu podmienianego.
Pismo o stałej szerokości czcionki, pogrubione
zastosowano do tekstu wyświetlanego na ekranie.
Układ książki
Większość rozdziałów ma podobny, czteroczęściowy układ.
Wymagania programu
Ta krótka część określa środowisko wymagane do uruchomienia aplikacji. Zwykle podawana jest potrzebna wersja przeglądarek Netscape Navigator i Microsoft Internet Explorer. Tutaj także nakreśla się tło, omawiając zagadnienia związane ze skalowaniem i monitorowanie wydajności.
Struktura programu
Kiedy już czytelnikowi znudzi się zabawa z aplikacją i będzie chciał zobaczyć, „co tam siedzi w środku”, powinien przeczytać tę część. Tutaj znajdzie omówienie kodu, zwykle wiersz po wierszu. Jest to część najdłuższa, więc warto usiąść sobie wygodnie, zanim zacznie się ją studiować.
Techniki języka JavaScript
Kiedy będziemy przebijać się przez składnię, pojawią się miejsca, gdzie warto będzie zatrzymać się na chwilę i wskazać techniki, które można dodać do arsenału swoich środków.
Kierunki rozwoju
W tej części sugerowane są metody dalszego rozwijania aplikacji. Czasami są to sugestie, a czasem gotowy kod. Zdarza się, że nie potrafię się powstrzymać i piszę za czytelnika kod znajdujący się w archiwum, które można załadować z Sieci. Tak czy inaczej, warto poczuć powiew twórczej weny, a nie ograniczać się tylko do upartego zapytywania: „Niezłe, jak to wprowadzić na moją stronę?”.
O kodzie
Cała ta książka mówi o aplikacjach. Nie powinno być zatem zaskoczeniem, że znajduje się tutaj mnóstwo kodu. Niektóre aplikacje mają kilkaset wierszy, większość z nich to kolejne strony kodu. W niektórych wypadkach kod jest nawet powtarzany, aby czytelnik nie musiał co chwilę przerzucać kartek między kodem programu a jego omówieniem.
Jedną z wad umieszczania kodu w książce jest... no cóż, właśnie umieszczenie go w książce. Często strona jest zbyt wąska, aby umieścić w jednym wierszu tyle, ile by się chciało. Fragment kodu często kontynuowany jest w następnym wierszu, i jeszcze w następnym... Aby zwiększyć czytelność, pominięto komentarze, choć znajdziemy je w plikach. Osoby odpowiedzialne za skład nieźle się napracowały, aby sformatować kod w miarę czytelnie, ale i tak czasem wygodniej może być czytać tenże kod w edytorze.
Jako że kod powinien być używany, a nie tylko czytany, wszystkie aplikacje dostępne są w pliku ZIP, który można załadować ze strony sieciowej wydawnictwa O'Reilly. Warto udać się pod adres http://www.oreilly.com/catalog/jscook/index.html i odnaleźć łącze Download. W każdym rozdziale będziemy odwoływać się do tego pliku.
Programowanie i testowanie
Poniżej - bez jakiejkolwiek szczególnej kolejności - podałem sprzęt i programy używane podczas tworzenia kodu prezentowanego w tej książce. W większości przypadków wszystko testowano w środowisku Windows, ale w środowiskach Unix i Mac także nie powinno być zbyt wielu problemów, a może nie będzie ich w ogóle.
Sprzęt: IBM ThinkPad 55/P75/16M, Compaq Presario/P233/100M, IBM Aptiva C23/P120/128M, DELL OptiPlex/P2-266/128M, Sun SPARC 20.
Systemy operacyjne: Win95, WinNT Workstation 4.0, WinNT Server 4.0, Solaris 2.5.
Przeglądarki: Netscape Navigator 3.0, 3.04 Gold, 4.0, 4.04, 4.07, 4.08, 4.5; Microsoft Internet Explorer 3.0, 3.02, 4.0, 4.01, 5.00.
Rozdzielczości: 640x480, 800x600, 1024x768, 1152x900, 1280x1024.
Oczywiście nie wszystkie aplikacje były testowane we wszystkich kombinacjach, jednak starałem się kodować jak najbardziej ostrożnie, aby w większości możliwych środowisk wszystko działało poprawnie.
Podziękowania
Moje nazwisko znajduje się na okładce, mam więc obowiązek i przyjemność podziękować w tym miejscu wszystkim, którzy przyczynili się do powstania tej książki.
Kwestie techniczne pomogli mi rozstrzygnąć: Steve Quint, James Chan, Jim Esten, Bill Anderson, Roland Chow, Rodney Myers, Matthew Mastracci, Giorgio Braga, Brock Beauchamp i inni - dlatego im właśnie chciałbym podziękować, zwłaszcza za wspomaganie mnie swoją znajomością JavaScriptu, a także innych zagadnień programowania. Specjalne podziękowania kieruję w stronę Patricka Clarka, którego kod był inspiracją aplikacji obsługi pomocy. Dziękuję redaktorowi Richardowi Komanowi, który był otwarty na moje pomysły i umożliwił przelanie ich na papier, a także Tarze McGoldrick i Robowi Romano za ich pracę ważną, choć niewidoczną.
Serdeczne słowa podziękowania składam mojej żonie, Róndine Bradenbaugh, za to, że wytrzymała ze mną, kiedy przez miesiące wpatrywałem się w monitor i szaleńczo stukałem w klawiaturę, noc w noc. Chciałbym podziękować wreszcie moim rodzicom za ich wsparcie i zachęcanie mnie do rozwijania umiejętności pisarskich.
Chciałbym też podziękować komuś, o kim często się zapomina - czytelnikom. To właśnie czytelnicy zostawiają w księgarni ciężko zarobione pieniądze, aby powstanie książki było w ogóle możliwe. Mam nadzieję, że wybór tej książki okaże się dla czytelnika inwestycją wartą wydanych pieniędzy.
Wprowadzenie |
|
Ta książka jest niezwykła, gdyż mówi o pisaniu w JavaScripcie dużych aplikacji sieciowych. Większość ludzi inaczej widzi zastosowania tego języka. JavaScript zwykle jest (a przynajmniej bywał) stosowany do dodawania do obrazków suwaków przewijania, realizacji liczników gości, określania stosowanej przeglądarki i temu podobnych rzeczy.
Zalety języka JavaScript
Żaden język i żadna technologia nie uzyskały statusu najlepszego rozwiązania tworzenia aplikacji sieciowych. Każde z tych rozwiązań ma zalety i wady. Ostatnie postępy w języku JavaScript i innych upowszechniających się technik, jak DHTML, Java, a nawet Macromedia Flash, umożliwiają JavaScriptowi korzystanie z tych rozwiązań i tworzenie naprawdę złożonych systemów sieciowych. Oto jeszcze kilka innych argumentów przemawiających za tworzeniem aplikacji w JavaScripcie.
Prostota, szybkość i efektywność
Jako że JavaScriptu dość łatwo jest się nauczyć, można od razu zacząć go używać. Jest to idealne rozwiązanie, kiedy chcemy dodać swojej witrynie nieco dodatkowej funkcjonalności. Kiedy masz już za sobą podstawy, do tworzenia naprawdę interesujących aplikacji już niedaleko.
JavaScript jest językiem wysokiego poziomu z dużymi możliwościami. Nie sposób zrobić w tym języku niczego na poziomie maszynowym, ale dostępne są różne właściwości przeglądarki, stron, a czasami także systemu, w którym przeglądarka działa. JavaScript nie musi być kompilowany, jak Java™ czy C, a przeglądarka nie wymaga ładowania maszyny wirtualnej do uruchomienia kodu. Po prostu aplikację się koduje i uruchamia.
JavaScript działa także w architekturze obiektowej, podobnie jak Java i C++. Cechy języka, takie jak konstruktory i dziedziczenie oparte na prototypach, dodają nowy poziom abstrakcji. Dzięki temu możliwe jest wielokrotne wykorzystanie tego kodu w naprawdę dużym stopniu.
Wszechobecność
JavaScript jest zdecydowanie najbardziej popularnym językiem skryptowym w Sieci. Nie tysiące, ale miliony witryn sieciowych zawierają JavaScript. Język ten jest obsługiwany przez większość najpopularniejszych przeglądarek (choć tak naprawdę mówimy o JScript w Internet Explorerze). Zarówno Netscape, jak i Microsoft zdają się stale poszukiwać sposobów rozbudowania tego języka. Takie wsparcie oznacza, że mamy naprawdę duże szanse na to, że przeglądarka naszego gościa będzie ten język obsługiwała.
Redukcja obciążenia serwera
To właśnie była jedna z podstawowych przyczyn tak powszechnego przyjęcia JavaScriptu. Język ten pozwala zrealizować po stronie klienta wiele funkcji, które inaczej musiałyby być wykonywane na serwerze. Jednym z najlepszych przykładów jest sprawdzanie poprawności danych. Programiści starej szkoły mogą pamiętać, jak kilka lat temu jedyną metodą sprawdzania poprawności danych, wprowadzonych w formularzu HTML, było przesłanie tych danych na serwer sieciowy i przekazanie ich skryptowi CGI w celu ich sprawdzenia.
Jeśli dane nie miały żadnych błędów, skrypt CGI działał dalej normalnie. Jeśli znalezione zostały jakieś błędy, skrypt zwracał użytkownikowi komunikat, opisując problem. Zastanówmy się teraz, jakiego obciążenia zasobów to wymaga. Przesłanie formularza wymaga żądania HTTP z serwera. Po podróży danych przez Sieć skrypt CGI powtórnie jest uruchamiany. Za każdym razem, kiedy użytkownik się pomyli, cały proces się powtarza. Użytkownik musi czekać na przesłanie komunikatu o błędzie, zanim dowie się, gdzie się pomylił.
Teraz mamy do dyspozycji JavaScript, dlatego możemy sprawdzać zawartość poszczególnych elementów formularza przed odesłaniem ich na serwer sieciowy. Dzięki temu zmniejsza się liczba transakcji HTTP i zdecydowanie zmniejsza się prawdopodobieństwo, że użytkownik pomyli się przy wprowadzaniu danych. JavaScript może też odczytywać i zapisywać „ciasteczka” (cookies), co dotąd wymagało odpowiedniego użycia nagłówków w serwerze sieciowym.
JavaScript rozwija się
Kiedy pojawił się JavaScript 1.1, nowe właściwości: obiekt Image oraz tablica document.images, pozwalające przewijać obrazki - spowodowały szerokie poruszenie. Później pojawił się JavaScript 1.2. Otworzyły się nowe możliwości: obsługa DHTML, warstwy i mnóstwo innych udoskonaleń zdumiały wielu programistów. Wszystko to było zbyt piękne, by mogło być prawdziwe.
Na tym jednak się nie skończyło. JavaScript stał się standaryzowanym językiem skryptowym powszechnego zastosowania, zgodnym z EMCA-262. Przynajmniej jedna firma stworzyła środowisko mogące uruchamiać JavaScript z wiersza poleceń. Firma Macromedia wstawiła odwołania JavaScript do technologii Flash. Do ColdFusion firmy Allaire włączono JavaScript do technologii opartej na XML Web Distributed Data Exchange (WDDX, Wymiana Danych Rozproszonych przez Sieć). JavaScript ma coraz więcej właściwości, coraz więcej opcji... i coraz więcej pułapek.
Być może nie ma wyboru
Czasami mamy przed sobą tylko jedną możliwą drogę. Załóżmy, że nasz dostawca usług internetowych nie pozwala uruchamiać skryptów CGI. Co teraz, jeśli chcemy dodać do swojej strony wysyłanie poczty elektronicznej z formularza lub pragniemy skorzystać z ciasteczek? Musimy zająć się rozwiązaniami działającymi po stronie klienta. JavaScript spośród tego typu rozwiązań jest zdecydowanie najlepszy.
I wiele innych zalet
Istnieje jeszcze sporo innych zalet stosowania JavaScriptu, a i czytelnik z pewnością może tę listę jeszcze dalej rozszerzyć. Najważniejsze jest to, że mimo szeregu zalet technologii realizowanych po stronie serwera aplikacje JavaScript mają swoje miejsce w Sieci.
Podstawowa strategia programowania w JavaScript
Kiedy budujemy jakąkolwiek aplikację, czy to w JavaScript, czy nie, w naszym dobrze zrozumianym interesie jest mieć jakąś strategię działania. Dzięki temu łatwiej będzie uporządkować swoje pomysły, szybciej także uda się wszystko zakodować i przetestować. Istnieje mnóstwo publikacji opisujących dokładnie tworzenie aplikacji. Czytelnik musi wybrać strategię działania najlepiej mu odpowiadającą, nie sposób zatem tutaj zbytnio w ten temat się zagłębiać. Jeśli jednak będziemy pisać coś między znacznikami <SCRIPT> a </SCRIPT>, to pamiętanie o pewnych zasadach projektowych niejednokrotnie zaoszczędzi nam bólu głowy. Jest to naprawdę proste - trzeba odpowiedzieć sobie po prostu na pytania: co? kto? jak?
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.
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 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ą sieciowi żeglarze. Jeśli planowana aplikacja nie znalazła dostatecznego uzasadnienia swojego zaistnienia, nie poświęcałem jej swojego czasu.
Następną kwestią jest to, czy JavaScript pozwala osiągnąć potrzebną mi funkcjonalność. To było dość łatwe. Jeśli odpowiedź brzmiała „tak”, stawałem do walki. Jeśli nie, to tym gorzej dla JavaScriptu.
Potem przychodziła kolej na edytor tekstowy. Oto niektóre zasady używane w prezentowanych kodach.
Wielokrotne użycie kodu przyszłością narodu
To właśnie tutaj do głosu dochodzą pliki źródłowe JavaScriptu. W aplikacjach tych używane są pliki z kodem, ładowane za pomocą następującej składni:
<SCRIPT LANGUAGE="JavaScript1.1" SRC="jakisPlikJS.js"></SCRIPT>
Plik jakisPlikJS.js zawiera kod, który będzie używany przez różne skrypty. W wielu aplikacjach tej książki używane są pliki źródłowe JavaScriptu. To się sprawdza. Po co wymyślać coś od nowa? Można także użyć plików źródłowych w celu ukrycia kodu źródłowego przed resztą aplikacji. Wygodne może być umieszczenie bardzo dużej tablicy JavaScriptu w pliku źródłowym. Użycie plików źródłowych jest tak ważne, że poświęciłem im cały rozdział 6.
Niektóre aplikacje zawierają kod po prostu wycinany i wklejany z jednego miejsca w inne. Kod taki może być dobrym kandydatem na wydzielenie w plik źródłowy. Wklejanie stosowałem po to, aby zbyt często nie powtarzać „zajrzyj do kodu pliku bibliotecznego trzy rozdziały wcześniej”. W ten sposób cały czas masz kod przed oczami, póki go nie zrozumiesz. Kiedy już aplikacja na stronie będzie działała tak, jak sobie tego życzysz, zastanów się nad wydzieleniem części kodu do plików źródłowych.
Wydzielanie JavaScriptu
Między znacznikami <HEAD> i </HEAD> należy umieszczać możliwie dużo kodu w pojedynczym zestawie <SCRIPT></SCRIPT>.
Deklarowanie zmiennych globalnych i tablic na początku
Nawet jeśli globalne zmienne i tabele mają początkowo wartości pustych ciągów lub wartości nieokreślone, definiowanie ich blisko początku skryptu jest dobrym sposobem poradzenia sobie z nimi, szczególnie jeśli używane są w różnych miejscach skryptu. W ten sposób nie musisz przeglądać zbyt wiele kodu, aby zmienić wartość zmiennej: wiesz, że znajdziesz ją gdzieś na początku.
Deklarowanie konstruktorów po zmiennych globalnych
W zasadzie funkcje tworzące obiekty definiowane przez użytkownika umieszczam blisko początku skryptu. Robię tak po prostu dlatego, że większość obiektów musi powstać na początku działania skryptu.
Definiowanie funkcji zgodnie z porządkiem „chronologicznym”
Innymi słowy - staram się definiować funkcje w takiej kolejności, w jakiej będą wywoływane. Pierwsza funkcja definiowana w skrypcie jest wywoływana na początku, druga funkcja jest wywoływana jako następna, i tak dalej. Czasem może to być trudne lub wręcz niemożliwe, ale dzięki temu przynajmniej poprawia się sposób zorganizowania aplikacji i pojawia się szansa, że funkcje wywoływane po sobie będą w kodzie obok siebie.
Każda funkcja realizuje jedno zadanie
Staram się ograniczyć funkcjonalność poszczególnych funkcji tak, aby każda z nich realizowała dokładnie jedno zadanie: sprawdzała dane od użytkownika, odczytywała lub ustawiała ciasteczka, przeprowadzała pokaz slajdów, pokazywała lub ukrywała warstwy i tak dalej. Teoria jest doskonała, ale czasem trudno ją wcielić w życie. W rozdziale 5. zasadę tę bardzo mocno naruszam. Funkcje tamtejsze realizują pojedyncze zadania, ale są naprawdę długie.
W miarę możliwości używaj zmiennych lokalnych
Stosuję tę zasadę w celu zaoszczędzenia pamięci. Jako że zmienne lokalne JavaScriptu znikają zaraz po zakończeniu realizacji funkcji, w której się znajdują, zajmowana przez nie pamięć jest zwracana do puli systemu. Jeśli zmienna nie musi istnieć przez cały czas działania aplikacji, nie tworzę jej jako globalnej, lecz jako lokalną.
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:
Prezentowane techniki:
|
1
Wyszukiwanie danych |
|
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 umownie 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.
|
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.
|
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.
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 <HTML>
2 <HEAD>
3 <TITLE>Wyszukiwanie</TITLE>
4
5 <SCRIPT LANGUAGE="JavaScript1.1" SRC="records.js"></SCRIPT>
6 <SCRIPT LANGUAGE="JavaScript1.1">
Przykład 1.1. Zawartość pliku nav.html (ciąg dalszy)
7 <!--
8
9 var SEARCHANY = 1;
10 var SEARCHALL = 2;
11 var SEARCHURL = 4;
12 var searchType = '';
13 var showMatches = 10;
14 var currentMatch = 0;
15 var copyArray = new Array();
16 var docObj = parent.frames[1].document;
17
18 function validate(entry) {
19 if (entry.charAt(0) == "+") {
20 entry = entry.substring(1,entry.length);
21 searchType = SEARCHALL;
22 }
23 else if (entry.substring(0,4) == "url:") {
24 entry = entry.substring(5,entry.length);
25 searchType = SEARCHURL;
26 }
27 else { searchType = SEARCHANY; }
28 while (entry.charAt(0) == ' ') {
29 entry = entry.substring(1,entry.length);
30 document.forms[0].query.value = entry;
31 }
32 while (entry.charAt(entry.length - 1) == ' ') {
33 entry = entry.substring(0,entry.length - 1);
34 document.forms[0].query.value = entry;
35 }
36 if (entry.length < 3) {
37 alert("Nie możesz wyszukiwać tak krótkich łańcuchów. Wysil się trochę.");
38 document.forms[0].query.focus();
39 return;
40 }
41 convertString(entry);
42 }
43
44 function convertString(reentry) {
45 var searchArray = reentry.split(" ");
46 if (searchType == (SEARCHANY | SEARCHALL)) { requireAll(searchArray); }
47 else { allowAny(searchArray); }
48 }
49
50 function allowAny(t) {
51 var findings = new Array(0);
52 for (i = 0; i < profiles.length; i++) {
53 var compareElement = profiles[i].toUpperCase();
54 if(searchType == SEARCHANY) {
55 var refineElement =
56 compareElement.substring(0,compareElement.indexOf('|HTTP'));
57 }
58 else {
59 var refineElement =
60 compareElement.substring(compareElement.indexOf('|HTTP'),
61 compareElement.length);
62 }
63 for (j = 0; j < t.length; j++) {
64 var compareString = t[j].toUpperCase();
65 if (refineElement.indexOf(compareString) != -1) {
66 findings[findings.length] = profiles[i];
67 break;
68 }
69 }
70 }
71 verifyManage(findings);
Przykład 1.1. Zawartość pliku nav.html (ciąg dalszy)
72 }
73
74 function requireAll(t) {
75 var findings = new Array();
76 for (i = 0; i < profiles.length; i++) {
77 var allConfirmation = true;
78 var allString = profiles[i].toUpperCase();
79 var refineAllString = allString.substring(0,
80 allString.indexOf('|HTTP'));
81 for (j = 0; j < t.length; j++) {
82 var allElement = t[j].toUpperCase();
83 if (refineAllString.indexOf(allElement) == -1) {
84 allConfirmation = false;
85 continue;
86 }
87 }
88 if (allConfirmation) {
89 findings[findings.length] = profiles[i];
90 }
91 }
92 verifyManage(findings);
93 }
94
95 function verifyManage(resultSet) {
96 if (resultSet.length == 0) { noMatch(); }
97 else {
98 copyArray = resultSet.sort();
99 formatResults(copyArray, currentMatch, showMatches);
100 }
101 }
102
103 function noMatch() {
104 docObj.open();
105 docObj.writeln('<HTML><HEAD><TITLE>Wyniki wyszukiwania</TITLE></HEAD>' +
106 '<BODY BGCOLOR=WHITE TEXT=BLACK>' +
107 '<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER><TR><TD VALIGN=TOP>' +
108 '<FONT FACE=Arial><B><DL>' +
109 '<HR NOSHADE WIDTH=100%>"' + document.forms[0].query.value +
110 '" - nic nie znaleziono.<HR NOSHADE WIDTH=100%>' +
111 '</TD></TR></TABLE></BODY></HTML>');
112 docObj.close();
113 document.forms[0].query.select();
114 }
115
116 function formatResults(results, reference, offset) {
117 var currentRecord = (results.length < reference + offset ?
118 results.length : reference + offset);
119 docObj.open();
120 docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki wyszukiwania</TITLE> \n</HEAD>' +
121 '<BODY BGCOLOR=WHITE TEXT=BLACK>' +
122 '<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' +
123 '<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' +
124 '<FONT FACE=Arial><B>Zapytanie: <I>' +
125 parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
126 'Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ' +
127 currentRecord + ' z ' + results.length + '</I><BR><BR></FONT>' +
128 '<FONT FACE=Arial SIZE=-1><B>' +
129 '\n\n<!-- początek zbioru wynikowego //-->\n\n\t<DL>');
130 if (searchType == SEARCHURL) {
131 for (var i = reference; i < currentRecord; i++) {
132 var divide = results[i].split("|");
133 docObj.writeln('\t<DT>' + '<A HREF="' + divide[2] + '">' +
134 divide[2] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>\n\n');
135 }
Przykład 1.1. Zawartość pliku nav.html (dokończenie)
136 }
137 else {
138 for (var i = reference; i < currentRecord; i++) {
139 var divide = results[i].split('|');
140 docObj.writeln('\n\n\t<DT>' + '<A HREF="' + divide[2] + '">' +
141 divide[0] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>');
142 }
143 }
144 docObj.writeln('\n\t</DL>\n\n<!-- Koniec wyników //-->\n\n');
145 prevNextResults(results.length, reference, offset);
146 docObj.writeln('<HR NOSHADE WIDTH=100%>' +
147 '</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
148 docObj.close();
149 document.forms[0].query.select();
150 }
151
152 function prevNextResults(ceiling, reference, offset) {
153 docObj.writeln('<CENTER><FORM>');
154 if(reference > 0) {
155 docObj.writeln('<INPUT TYPE=BUTTON VALUE="Poprzednie ' + offset +
156 ' wyników" ' +
157 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
158 (reference - offset) + ', ' + offset + ')">');
159 }
160 if(reference >= 0 && reference + offset < ceiling) {
161 var trueTop = ((ceiling - (offset + reference) < offset) ?
162 ceiling - (reference + offset) : offset);
163 var howMany = (trueTop > 1 ? "ów" : "");
164 docObj.writeln('<INPUT TYPE=BUTTON VALUE="Następne ' + trueTop +
165 ' wynik' + howMany + '" ' +
166 'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
167 (reference + offset) + ', ' + offset + ')">');
168 }
169 docObj.writeln('</CENTER>');
170 }
171
172 //-->
173 </SCRIPT>
174 </HEAD>
175 <BODY BGCOLOR="WHITE">
176 <TABLE WIDTH="95%" BORDER="0" ALIGN="CENTER">
177 <TR>
178 <TD VALIGN=MIDDLE>
179 <FONT FACE="Arial">
180 <B>Wyszukiwarka pracująca po stronie klienta</B>
181 </TD>
182
183 <TD VALIGN=ABSMIDDLE>
184 <FORM NAME="search"
185 onsubmit="validate(document.forms[0].query.value); return false;">
186 <INPUT TYPE=TEXT NAME="query" SIZE="33">
187 <INPUT TYPE=HIDDEN NAME="standin" VALUE="">
188 </FORM>
189 </TD>
190
191 <TD VALIGN=ABSMIDDLE>
192 <FONT FACE="Arial">
193 <B><A HREF="main.html" TARGET="main">Pomoc</A></B>
194 </TD>
195 </TR>
196 </TABLE>
197 </BODY>
198 </HTML>
Tekst źródłowy jest dość obszerny. Aby zrozumieć, co się tutaj dzieje, najprościej będzie rozpocząć analizę od początku i stopniowo posuwać się coraz dalej. Na szczęście kod zapisano tak, aby układ poszczególnych funkcji był mniej więcej zgodny z kolejnością ich użycia.
Analizę kodu źródłowego przeprowadzimy w następującej kolejności:
plik źródłowy records.js,
zmienne globalne,
funkcje,
kod w języku HTML.
Plik records.js
Na początek zajmiemy się plikiem źródłowym records.js. Odwołanie do niego umieszczono w znaczniku <SCRIPT> w wierszu 5.
Plik ten zawiera dość długą tablicę o nazwie profiles. Ze względu na spore rozmiary, jego zawartość została w książce pominięta. Po rozpakowaniu pliku ZIP trzeba będzie zatem uruchomić edytor tekstów i otworzyć plik ch01/records.js (uwaga: to baza danych, z której będziemy korzystać!). Każdy element bazy jest trzyczęściowym łańcuchem o postaci np.:
"http://www.serve.com/hotsyte|HotSyte-Zasoby JavaScriptu|Witryna" +
"HotSyte zawiera łącza, samouczki, darmowe skrypty i inne"
Elementy rekordu rozdzielone są znakami kreski pionowej (|). Znaki te zostaną użyte w chwili wyświetlania odszukanych rekordów bazy na ekranie. Drugą część rekordu stanowi tytuł dokumentu (nie mający jednak nic wspólnego z zawartością znacznika TITLE), część trzecia to opis dokumentu, zaś pierwszy element rekordu to adres URL.
Na marginesie - nie ma żadnych przeciwwskazań odnośnie rozdzielania elementów rekordu znakami (lub ciągami znaków) innymi niż „|”. Należy tylko zapewnić, że nie będzie to żaden ze znaków, które użytkownik mógłby wpisać w treści zapytania (mamy do dyspozycji choćby ciągi &^ czy ~[%). Nie należy także stosować lewego ukośnika (\): znak ten interpretowany jest przez JavaScript jako początek sekwencji unikowej i jego użycie może spowodować zwrócenie dziwacznych wyników wyszukiwania lub nawet zawieszenie aplikacji.
Dlaczego wszystkie te dane umieszczono w pliku źródłowym zapisanym w JavaScripcie? Są ku temu dwie przesłanki: modułowość kodu i czystość zapisu. W przypadku witryn zawierających więcej niż kilkaset pojedynczych stron, plik rekordów najwygodniej będzie generować z użyciem programu uruchamianego na serwerze; zapisanie danych w postaci pliku źródłowego w JavaScripcie jest w tym przypadku rozwiązaniem nieco lepszym.
Opisanej tu bazy danych można też użyć w innych aplikacjach wyszukujących, po prostu wstawiając w kodzie odwołanie do pliku records.js. Co więcej, włączenie całego kodu w JavaScripcie do pliku HTML i wyświetlenie go w postaci źródłowej byłoby wysoce niepożądane.
Zmienne globalne
W wierszach 9 do 16 przykładu 1.1 deklarujemy i inicjalizujemy zmienne globalne:
var SEARCHANY = 1;
var SEARCHALL = 2;
var SEARCHURL = 4;
var searchType = '';
var showMatches = 10;
var currentMatch = 0;
var copyArray = new Array();
var docObj = parent.frames[1].document;
|
Techniki języka JavaScript: 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.
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.
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().
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.
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 liczby = new Array { new Array("jeden", "dwa", "trzy", "cztery", "pięć"), new Array("uno", "dos", "tres", "cuatro", "cinco"), new Array("won", "too", "tree", "for", "fife") ); Pojedyncza pętla for tutaj nie wystarczy - trzeba się bardziej przyłożyć. Pierwsza tablica liczby jest jednowymiarowa (1×5), ale jej nowa wersja jest już tablicą wielowymiarową (3×5). Przeanalizowanie piętnastu elementów (3×5) oznacza, że będziemy potrzebować dodatkowej pętli: for (var i = 0; i < liczby.length; i++) { // pierwsza... for (var j = 0; j < liczby[i].length; j++) { // i druga if (myString == liczby[i][j]) { alert("Znalazłem!"); break; } } } Tym razem kolejne odpowiedzi sprawdzamy w dwóch wymiarach. Pójdźmy teraz jeszcze o krok dalej i załóżmy, że chcemy stworzyć tablicę z „bezpieczną” paletą 216 kolorów, których można używać we wszystkich przeglądarkach, po jednym kolorze w komórce? Odpowiedzią są zagnieżdżone pętle for. Tym razem jednak użyjemy tablicy zaledwie jednowymiarowej. W notacji szesnastkowej „bezpieczne” kolory zapisywane są w postaci sześciu cyfr (po dwie na każdą barwę składową) przy czym wszystkie składniki muszą być parami cyfr: 33, 66, 99, AA, CC lub FF. Tablica będzie zatem wyglądała tak: var hexPairs = new Array("33", "66", "99", "AA", "CC", "FF"); „Lipa! Tu jest tylko jedna tablica jednowymiarowa - oddawać pieniądze!” Spokojnie, nie ma co jeszcze biec do księgarni. Będą trzy wymiary, tyle że dla każdego użyjemy tej samej tablicy: var str = '';
// Utworzenie tablicy document.writeln('<H2>Bezpieczna paleta WWW</H2>' + '<TABLE BORDER=1 CELLSPACING=0>'); for (var i = 0; i < hexPairs.length; i++) { // tworzenie wiersza document.writeln('<TR>'); for (var j = 0; j < hexPairs.length; j++) { for (var k = 0; k < hexPairs.length; k++) { // Tworzenie ciągu "pustych" komórek wiersza // Zauważmy, że kolor tła jest tworzony z elementów hexPairs str += '<TD BGCOLOR="' + hexPairs[i] + hexPairs[j] + hexPairs[k] + '">' + '  </TD>'; } // Wypisz wiersz komórek i "wyzeruj" str document.writeln(str); str = ''; } // Koniec wiersza document.writeln('</TR>'); } // Koniec tablicy document.writeln('</TABLE>'); 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('<HTML><HEAD><TITLE>Wyniki wyszukiwania</TITLE></HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER><TR><TD VALIGN=TOP>' +
'<FONT FACE=Arial><B><DL>' +
'<HR NOSHADE WIDTH=100%>"' + document.forms[0].query.value +
'" - nic nie znaleziono.<HR NOSHADE WIDTH=100%>' +
'</TD></TR></TABLE></BODY></HTML>');
docObj.close();
document.forms[0].query.select();
}
formatResults()
Zadaniem tej funkcji jest eleganckie zaprezentowanie użytkownikowi znalezionych wyników. Nie jest ona nadmiernie skomplikowana, ale realizuje sporo zadań. Oto elementy składające się na listę wyników:
Nagłówek, tytuł i treść dokumentu HTML.
Tytuł znalezionego dokumentu, jego opis oraz adres URL dla każdego znalezionego rekordu, wraz z łączem do zawartego w rekordzie adresu URL.
Przyciski „Poprzedni” i „Następny”, służące do wyświetlania rekordów poprzednich i następnych, o ile takowe istnieją.
Nagłówek i tytuł dokumentu HTML
Utworzenie nagłówka i tytułu strony jest proste. Wiersze 116 do 129 drukują nagłówek, tytuł i początek treści dokumentu HTML:
function formatResults(results, reference, offset) {
var currentRecord = (results.length < reference + offset ?
results.length : reference + offset);
docObj.open();
docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki wyszukiwania</TITLE>\n </HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' +
'<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' +
'<FONT FACE=Arial><B>Zapytanie: <I>' +
parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
'Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ' +
currentRecord + ' z ' + results.length + '</I><BR><BR></FONT>' +
'<FONT FACE=Arial SIZE=-1><B>' +
'\n\n<!-- początek zbioru wynikowego //-->\n\n\t<DL>');
Przed wydrukowaniem nagłówka i tytułu trzeba sprawdzić, od którego rekordu należy zacząć wyprowadzanie. Wiadomo, że pierwszy wyświetlany rekord znajduje się w elemencie results [reference]. Należy wyświetlić offset rekordów, chyba że wartość reference + offset jest większa niż liczba rekordów. Aby to sprawdzić, znów używamy operatora trójargumentowego, zaś odpowiednią wartość umieszczamy w zmiennej currentRecord w wierszu 117. Użyjemy jej wkrótce.
Teraz funkcja formatResults() wyprowadza nagłówek i tytuł dokumentu HTML. Treść dokumentu zaczyna się od wyśrodkowanej tabeli i poziomej linii. Wyświetlenie treści zapytania, pobranej z odpowiedniego pola formularza, nie jest żadnym problemem (wiersz 125):
parent.frames[0].document.forms[0].query.value
W wierszu 126 rzecz się trochę komplikuje, zaczyna się bowiem zbiór wynikowy. Najpierw wyświetlana jest informacja o prezentowanym właśnie podzbiorze - jego wielkość i liczba wszystkich wyników, na przykład:
Wyniki wyszukiwania: 1 - 10 z 38
Potrzeba do tego trzech liczb: numeru pierwszego rekordu podzbioru, liczby wyświetlanych jednorazowo rekordów oraz rozmiaru tablicy copyArray, w której znajdują się wybrane rekordy. Przyjrzyjmy się im po kolei, pamiętając, że nie chodzi tutaj o pokazanie samych wyników, ale o poinformowanie użytkownika, ile jest rekordów i od którego zaczynamy. Dzieje się to tak:
Numer bieżącego rekordu przypisujemy zmiennej reference i wyświetlamy ją.
Dodajemy do reference wartość offset, określającą liczbę rekordów wyświetlanych na stronie (tutaj 10).
Jeśli suma reference + offset jest większa niż liczba znalezionych odpowiedzi, wyświetlamy liczbę odpowiedzi, w przeciwnym przypadku - obliczoną sumę (wartość ta została już określona i znalazła odbicie w zmiennej currentRecord).
Wyświetlamy łączną liczbę wyników.
Kroki 1 i 2 są proste. Przypomnijmy sobie kod funkcji verifyManage(), szczególnie wiersz 99:
formatResults(copyArray, currentMatch, showMatches);
Zmienna lokalna results to kopia tablicy copyArray. Zmienna reference otrzymuje wartość currentMatch, więc suma reference + offset jest równa sumie currentMatch i showResults. W pierwszych kilku wierszach kodu (ściśle - 13 i 14) zmiennej showMatches nadajemy wartość 10, a zmiennej currentMatch wartość 0. Wobec tego reference ma na początku wartość 0, zaś suma reference + offset równa jest 10. Krok 1 wykonywany jest bezpośrednio po wyprowadzeniu zmiennej reference; opisana wyżej arytmetyka obsługuje krok 2.
W kroku 3 używamy operatora trójargumentowego (wiersze 117-118) do zdecydowania, czy suma wartości reference i offset jest większa od całkowitej liczby wyników. Innymi słowy, należy sprawdzić, czy po dodaniu offset rekordów do reference uzyskamy liczbę większą od całkowitej liczby wyników. Jeśli reference ma wartość 20, a rekordów jest 38, dodanie wartości 10 do reference da 30 i na ekranie pojawi się:
Wyniki wyszukiwania: 20 - 30 z 38
Jeśli jednak reference ma wartość 30, a rekordów jest 38, dodanie do reference wartości 10 da 40, co dałoby:
Wyniki wyszukiwania: 30 - 40 z 38
a to jest niedopuszczalne. Wyszukiwarka nie może wyświetlić rekordów 39 i 40, bo znalazła ich tylko 38. Oznacza to osiągnięcie końca zbioru wynikowego, a zatem zamiast sumy należy wyświetlić całkowitą liczbę rekordów. W ten sposób dochodzimy do ostatniego, czwartego kroku:
Wyniki wyszukiwania: 30 - 38 z 38
|
|
|
Treść funkcji formatResults() upstrzona jest znakami specjalnymi, jak np. \n i \t. \n to znak nowego wiersza, czyli odpowiednik naciśnięcia klawisza Enter podczas pisania w edytorze tekstów. \t to odpowiednik naciśnięcia klawisza tabulacji. Obecność znaków specjalnych ma poprawić wygląd kodu źródłowego strony wyników wyszukiwania. Wstawiono je tu po to, aby pokazać sposób ich użycia, ale należy pamiętać, że nie są one niezbędne dla programów i że nie mają wpływu na ich działanie. Jeśli ktoś uważa, że zaśmiecają kod, wcale nie musi ich używać. W dalszej części książki będą one stosowane sporadycznie. |
Wyświetlanie tytułów, opisów i adresów URL dokumentów
Teraz, gdy podzbiór rekordów został już określony, czas go wyświetlić. Do głosu dochodzą wiersze 130 do 143:
if (searchType == SEARCHURL) {
for (var i = reference; i < currentRecord; i++) {
var divide = results[i].split("|");
docObj.writeln('\t<DT>' + '<A HREF="' + divide[2] + '">' +
divide[2] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>\n\n');
}
}
else {
for (var i = reference; i < currentRecord; i++) {
var divide = results[i].split('|');
docObj.writeln('\n\n\t<DT>' + '<A HREF="' + divide[2] + '">' +
divide[0] + '</A>' + '\t<DD>' + '<I>' + divide[1] + '</I><P>');
}
}
Wiersze 131 i 138 zawierają pętle for, wykonujące na rekordzie currentRecord tę samą operację - jedyną różnicą jest kolejność wyprowadzania poszczególnych fragmentów. Do głosu ponownie dochodzi zmienna searchType. Jeśli jest ona równa SEARCHURL, jako tekst łącza wyświetlany jest adres URL. W przeciwnym razie, tj. dla wartości SEARCHANY lub SEARCHALL - tytuł dokumentu.
Sposób wyszukiwania jest już określony, ale jak ładnie wyświetlić rekordy? Wystarczy przebiec w pętli przez cały podzbiór rekordów, rozbijając każdy z nich na tytuł, opis oraz adres URL i rozmieszczając uzyskane elementy w odpowiednim porządku. Oto pętla for używana dla wszystkich przypadków:
for (var i = reference; i < lastRecord; i++) {
Teraz zajmijmy się elementami rekordów. Jak pamiętamy z opisu pliku records.js, każdy element tablicy profiles to łańcuch opisujący rekord i składający się z części rozdzielonych znakami |. Rozdzielimy je tak:
var divide = results[i].split('|');
Dla każdego elementu zmiennej lokalnej divide przypisywana jest tablica łańcuchów rozdzielonych znakami |. Pierwszy jej element (divide[0]) to adres URL, drugi (divide[1]) to tytuł dokumentu, trzeci zaś (divide[2]) to jego opis. Każdy z elementów jest podczas wyprowadzania uzupełniany odpowiednim zestawem znaczników HTML (w naszym przypadku <DL>, <DT> i <DD>). Jeśli wyszukiwanie odbywa się według adresów URL, będą one stanowiły treść łączy; w przeciwnym przypadku użyte zostaną tytuły dokumentów.
Dodanie przycisków „Poprzedni” i „Następny”
Ostatnią czynnością jest dodanie przycisków, które pozwolą użytkownikowi oglądać poprzednią i następną porcję wyników. Obsługą tych przycisków zajmuje się funkcja prevNextResults(), którą wkrótce omówimy, ale teraz kilka ostatnich wierszy funkcji formatResults():
docObj.writeln('\n\t</DL>\n\n<!-- Koniec wyników //-->\n\n');
prevNextResults(results.length, reference, offset);
docObj.writeln('<HR NOSHADE WIDTH=100%>' +
'</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
docObj.close();
document.forms[0].query.select();
}
Zacytowany tu kod wywołuje funkcję prevNextResults(), dodaje kilka zamykających znaczników HTML i ustawia kursor w polu treści zapytania.
prevNextResults()
Wszystkich, którym udało dobrnąć się aż tutaj, i ta funkcja nie powinna zanadto zmęczyć. Definicja funkcji prevNextResults() zaczyna się w wierszu 152:
function prevNextResults(ceiling, reference, offset) {
docObj.writeln('<CENTER><FORM>');
if(reference > 0) {
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Poprzednie ' + offset +
' wyników" ' +
'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference - offset) + ', ' + offset + ')">');
}
if(reference >= 0 && reference + offset < ceiling) {
var trueTop = ((ceiling - (offset + reference) < offset) ?
ceiling - (reference + offset) : offset);
var howMany = (trueTop > 1 ? "ów" : "");
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Następne ' + trueTop +
' wynik' + howMany + '" ' +
'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference + offset) + ', ' + offset + ')">');
}
docObj.writeln('</CENTER>');
}
Funkcja ta wyświetla HTML-owy formularz zawierający dwa przyciski - „Następne” i „Poprzednie” - i wyśrodkowany w dolnej części strony wyników. Na rysunku 1.3 pokazano stronę wyników z obydwoma przyciskami. Mogą one pojawić się w trzech kombinacjach:
Tylko przycisk „Następne”. Jest on wyświetlany dla pierwszego podzbioru wyników; nie ma jeszcze żadnych poprzednich rekordów.
Przyciski „Następne” i „Poprzednie”. Wyświetlane są one dla wszystkich stron poza pierwszą i ostatnią; istnieją rekordy poprzednie, są też rekordy dalsze.
Tylko przycisk „Poprzednie”. Pojawia się na ostatniej stronie wyników - dalej nie ma już żadnych rekordów do przeglądania.
Trzy kombinacje, dwa przyciski. Oznacza to, że aplikacja musi wiedzieć, czy dany przycisk ma być wyświetlany, czy też nie. Poniżej opisano warunki określające pojawianie się poszczególnych kombinacji:
Tylko przycisk „Następne”
Kiedy powinien pojawić się przycisk „Następne”? Na wszystkich stronach poza ostatnią, czyli zawsze wtedy, gdy ostatni rekord na stronie (reference + offset) ma numer mniejszy od całkowitej liczby wyników.
Kiedy nie powinien pojawić się przycisk „Poprzednie”? Na pierwszej stronie wyników, czyli kiedy wartość reference uzyskana ze zmiennej currentMatch wynosi 0.
Przyciski „Następne” i „Poprzednie”
Kiedy należy wyświetlić oba przyciski? Skoro przycisk „Następne” powinien znajdować się na wszystkich stronach poza ostatnią, a „Poprzednie” na wszystkich poza pierwszą, przycisku „Poprzednie” będziemy potrzebować, kiedy wartość reference będzie większa od 0, a przycisku „Następne” - gdy suma reference + offset będzie mniejsza od całkowitej liczby wyników.
Tylko przycisk „Poprzednie”
Skoro wiemy, kiedy powinien pojawić się przycisk „Poprzednie”, to kiedy powinien zniknąć „Następne”? Kiedy wyświetlana jest ostatnia strona wyników, czyli gdy suma reference + offset jest większa bądź równa całkowitej liczbie wyników.
Podany tu opis jest nadal dość ogólny, ale przynajmniej wiadomo już, kiedy i które przyciski mają być wyświetlane, zaś obsługą warunków zajmują się instrukcje if z wierszy 154 i 160. Umieszczają one na stronie jeden lub oba przyciski w zależności od aktualnego podzbioru wynikowego i liczby pozostałych do wyprowadzenia wyników.
|
Techniki języka JavaScript: 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('<HTML>\n<HEAD>\n<TITLE>Wyniki wyszukiwania</TITLE>\n</HEAD>' + '<BODY BGCOLOR=WHITE TEXT=BLACK>' + '<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' + '<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>' + '<FONT FACE=Arial><B>Zapytanie: <I>' + parent.frames[0].document.forms[0].query.value + '</I><BR>\n' + 'Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ' + currentRecord + ' z ' + results.length + '</I><BR><BR></FONT>' + '<FONT FACE=Arial SIZE=-1><B>' + '\n\n<!-- początek zbioru wynikowego //-->\n\n\t<DL>'); Jak widać, za pomocą pojedynczego wywołania metody zapisujemy tekst całej strony. Alternatywą jest wielokrotne wywoływanie metody w celu wyprowadzenia pojedynczych wierszy: function formatResults(results, reference, offset) { docObj.open(); docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki wyszukiwania</TITLE>\n</HEAD>'); docObj.writeln('<BODY BGCOLOR=WHITE TEXT=BLACK>'); docObj.writeln('<TABLE WIDTH=90% BORDER=0 ALIGN=CENTER ' + 'CELLPADDING=3><TR><TD>'); docObj.writeln('<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP>'); docObj.writeln('<FONT FACE=Arial><B>Zapytanie: <I>' + parent.frames[0].document.forms[0].query.value + '</I><BR>\n'); docObj.writeln('Wyniki wyszukiwania: <I>' + (reference + 1) + ' - '); docObj.writeln( (reference + offset > results.length ? results.length : reference + offset) + ' z ' + results.length + '</I><BR><BR></FONT>' + '<FONT FACE=Arial SIZE=-1><B>'); docObj.writeln('\n\n<!-- początek zbioru wynikowego //-->\n\n\t<DL>'); Wygląda to może trochę porządniej, ale każde wywołanie oznacza nieco więcej pracy dla interpretera JavaScriptu. Pomyślmy: czy wygodniej jest pięć razy iść do sklepu, za każdym razem kupując jakiś drobiazg, czy iść do sklepu raz i od razu kupić wszystko, czego nam trzeba? Przekazanie długiego łańcucha „sklejonego” znakami + rozwiązuje problem skutecznie i za jednym zamachem. |
|
W wyniku kliknięcia obu przycisków wywoływana jest funkcja formatResults(); jedyna różnica to przekazywane jej parametry, opisujące różne podzbiory wyników. Z technicznego punktu widzenia przyciski są w gruncie rzeczy takie same, różnią się natomiast wyglądem ze względu na różnice w wartościach atrybutu VALUE. Oto początek definicji przycisku „Poprzednie” (wiersze 155-156):
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Poprzednie ' + offset + ' Wyników" ' +
Z kolei przycisk „Następne” definiowany jest w wierszach 164-165:
docObj.writeln('<INPUT TYPE=BUTTON VALUE="Następne ' + trueTop + ' wyników' + howMany
Oba wiersze zawierają atrybuty TYPE i VALUE przycisku oraz wartość określającą liczbę rekordów poprzedzających lub następujących po danym. Jako że liczba rekordów poprzedzających jest zawsze taka sama (wynosi offset), jest ona wyświetlana zawsze (na przykład „Następne 10 wyników”). Jednak liczba rekordów następujących po danym może się zmieniać - może ona być równa wartości offset lub liczbie rekordów pozostałych do wyprowadzenia, mniejszej od offset. Aby obsłużyć te możliwości, używamy zmiennej trueTop.
Zwróćmy uwagę, że przycisk „Poprzednie” zawsze zawiera słowo „wyników”. Wartość zmiennej showMatches jest stała przez cały czas pracy aplikacji i w naszym przypadku wynosi 10. Użytkownik może zawsze liczyć, że zobaczy poprzednich 10 rekordów. Jednak w przypadku rekordów następnych nie zawsze musi tak być. Załóżmy, że następny podzbiór wyników zawiera tylko jeden rekord - wtedy przycisk zawierałby tekst „Następne 1 wyników”. Jest to niedopuszczalne i dlatego właśnie w funkcji prevNextResults() używamy lokalnej zmiennej howMany wraz z operatorem trójargumentowym (wiersz 163):
var howMany = (trueTop > 1 ? "ów" : "");
Jeśli zmienna trueTop jest większa od 1, howMany przyjmuje wartość „ów”. Jeśli trueTop równa jest 1, howMany zawiera łańcuch pusty. Jak widać w wierszu 165, zawartość zmiennej howMany jest wyprowadzana zaraz za słowem „wynik”. Jeśli zatem do pokazania pozostał jeden rekord, użyte zostanie słowo „wynik”; jeśli jest ich więcej, ujrzymy słowo „wyników”.
Ostatni krok to określenie akcji wykonywanej w chwili kliknięcia przycisków. Jak już wcześniej wspomniano, zdarzenia onClick obu przycisków obsługiwane są przez funkcję formatResults(). Jej wywołanie konstruowane jest dynamicznie w wierszach 157-158 i 166-167. Oto pierwsze z wywołań:
'onClick="' + parent.frames[0].formatResults(parent.frames[0].copyArray, ' + (reference - offset) + ', ' + offset + ')">');
Argumenty określane są przy użyciu operatora trójargumentowego i wypisywane „na bieżąco”. Jak widać, przycisk „Poprzednie” zawsze przekazuje funkcji trzy argumenty: copyArray, reference - offset oraz offset. Warto też zwrócić uwagę na zapis odwołań do funkcji formatResults() i zmiennej copyArray
parent.frames[0].formatResults(...);
oraz
parent.frames[0].copyArray
W pierwszej chwili może to się wydawać nieco dziwne, ale należy pamiętać, że odwołanie do funkcji formatResults() nie znajduje się w dokumencie nav.html (czyli w ramce parent. frames[0]). Ma ono miejsce w ramce wyników, parent.frames[1], która nie zawiera funkcji formatResults() ani zmiennej copyArray. Stąd właśnie bierze się taka, a nie inna forma odwołań.
Przycisk „Następne” wykorzystuje podobną procedurę obsługi zdarzenia, ale... czy nie należałoby przypadkiem uwzględnić faktu, że w ostatnim podzbiorze może znajdować się mniej niż offset wyników (tak jak miało to miejsce podczas wywołania formatResults() w celu wyświetlenia wyników)? Otóż nie. To w funkcji formatResults() podejmuje się stosowną decyzję, zatem wystarczy dodać do siebie wartości reference i offset, a sumę przekazać do funkcji. Przyjrzyjmy się wierszom 166-167, zawierającym ostatni fragment wywołania metody document. writeln():
'onClick="parent.frames[0].formatResults(parent.frames[0].copyArray, ' +
(reference + offset) + ', ' + offset + ')">');
Techniki języka JavaScript: operator trójargumentowy Po ostatniej porcji materiału można się już było tego spodziewać. Operator trójargumentowy jest całkiem przydatny, a zatem czas na nieco teorii. Operatory te, używane w naszej aplikacji jako jednowierszowe odpowiedniki instrukcji warunkowej if-else, wymagają trzech argumentów. Składnia operatora trójargumentowego, cytowana z opublikowanego przez firmę Netscape Communications dokumentu JavaScript Guide for Communicator 4.0 (rozdział 9) wygląda następująco: (warunek) ? wartość1 : wartość2 Po zastąpieniu parametrów odpowiednimi wartościami, operator taki spowoduje zwrócenie wartości1 w przypadku prawdziwości warunku i wartości2 w przypadku jego fałszywości. Po co jednak ten cały wykład? Otóż omawianą tu konstrukcję niejednokrotnie łatwiej jest czytać, niż odpowiednie instrukcje if-else (mniej też trzeba pisać). Operator ten jest też szczególnie użyteczny, kiedy trzeba zakodować kilka zagnieżdżonych wyrażeń. Operator trójargumentowy nie jest jednak żadnym panaceum. Jeśli w przypadku spełnienia lub niespełnienia warunku należy wykonać kilka operacji, trzeba będzie odwołać się do konstrukcji if-else. Jeśli nie - warto spróbować operatora trójargumentowego. |
|
Kod HTML
W pliku nav.html jest bardzo niewiele statycznego kodu HTML. Cytujemy go ponownie, zaczynając od wiersza 174:
</HEAD>
<BODY BGCOLOR="WHITE">
<TABLE WIDTH="95%" BORDER="0" ALIGN="CENTER">
<TR>
<TD VALIGN=MIDDLE>
<FONT FACE="Arial">
<B>Wyszukiwarka pracująca po stronie klienta</B>
</TD>
<TD VALIGN=ABSMIDDLE>
<FORM NAME="search"
onsubmit="validate(document.forms[0].query.value); return false;">
<INPUT TYPE=TEXT NAME="query" SIZE="33">
<INPUT TYPE=HIDDEN NAME="standin" VALUE="">
</FORM>
</TD>
<TD VALIGN=ABSMIDDLE>
<FONT FACE="Arial">
<B><A HREF="main.html" TARGET="main">Pomoc</A></B>
</TD>
</TR>
</TABLE>
</BODY>
</HTML>
Nie ma tu żadnych niespodzianek. Formularz rozmieszczony został w komórkach tabeli, zaś jego przesłanie uruchamia omawiany uprzednio kod. Można by co najwyżej zapytać, jak przesyłać dane, skoro nie ma przycisku SUBMIT? Poczynając od specyfikacji HTML 2.0, większość przeglądarek (w tym Netscape Navigator i Internet Explorer) umożliwia uproszczone wysłanie formularza za pomocą pojedynczego pola tekstowego.
Oczywiście wcale nie trzeba postępować właśnie w taki sposób - równie dobrze można uatrakcyjnić formularz, wyposażając go w przycisk lub obrazek (przycisk graficzny).
Tworzenie bazy danych w języku JavaScript
Zawartość opisanej tu przykładowej bazy danych trzeba będzie prędzej czy później zastąpić własnymi danymi. Robi się to w trzech prostych etapach:
Otwórz plik records.js w edytorze tekstów.
Usuń znajdujące się tam rekordy tak, by uzyskać następującą zawartość pliku:
var profiles = new Array(
);
Dodaj kolejne rekordy według poniższego wzoru:
"Tytuł strony|Opis strony|http://adres.url/plik.html",
Pomiędzy nawiasami można zapisać dowolną liczbę rekordów, pamiętając, by na końcu każdego poza ostatnim umieścić przecinek. Należy też pamiętać o rozdzielaniu tytułów stron, ich opisów i adresów URL znakami kreski pionowej. Znaków tych nie wolno używać w danych, gdyż spowoduje to błędy interpretera JavaScriptu. Na koniec nie należy zapominać, że w przypadku konieczności użycia wewnątrz danych znaku cudzysłowu, należy zacytować go w formie sekwencji unikowej (tj. \" zamiast samego ").
Kierunki rozwoju
Nasza wyszukiwarka już w opisanej tu postaci jest całkiem użyteczna, jednak w zmianach i udoskonaleniach można pójść znacznie dalej. Oto wybrane możliwości:
zapewnienie zgodności z językiem JavaScript 1.0,
uodpornienie programu na błędy,
wyświetlanie reklam,
rozszerzenie możliwości wyszukiwania,
użycie zapytań predefiniowanych.
Zgodność z językiem JavaScript 1.0
Wszyscy to wiemy: obie najpopularniejsze wśród użytkowników przeglądarki dostępne są obecnie (tj. w chwili wydawania tej książki - przyp. red.) w późnych wersjach 4.x lub wczesnych 5.x. Obie są bezpłatne. Nadal jednak trafiają się użytkownicy korzystający z takich staroci, jak Internet Explorer 3.02 czy Netscape Navigator 2. Statystyka odwiedzin prowadzonej przez autora witryny HotSyte (http://www.serve.com/hotsyte/) wciąż wskazuje na to, że wersje te są zaskakująco popularne.
Jako że wyszukiwarka jest jedną z najważniejszych usług dostępnych w witrynie, warto pokusić się o zakodowanie jej w sposób zgodny ze specyfikacją JavaScript 1.0. Na szczęście jest to dość proste - wystarczy przejrzeć omawiany kod wiersz po wierszu, sprawdzić, które elementy nie są w wersji 1.0 obsługiwane i wymienić je.
OK. Autor już to zrobił, ale zapewne większość czytelników kusiło, by zabrać się za to samemu (prawda?). Zmodyfikowaną wersję kodu można znaleźć w katalogu /ch01/searchengineMSIE/. Jak miało to miejsce poprzednio, otwórzmy w przeglądarce plik index.html i przyjrzyjmy się szybko zmianom zapewniającym zgodność aplikacji z JavaScriptem 1.0. Jest ich trzy:
brak pliku źródłowego skryptu (problem ten jest specyficzny dla przeglądarki),
brak sortowania tablicy (metodą sort()),
„obejście” braku metody split().
Netscape Navigator 2.x i Microsoft Internet Explorer 3.x nie obsługują plików .js. Można sobie z tym poradzić, włączając tablicę profiles do pliku nav.html. Druga zmiana polega na rezygnacji z wywołania resultSet.sort() w wierszu 90. Oznacza to, że dane nie zostaną posortowane alfabetycznie, lecz pozostaną w kolejności, w jakiej zapisano rekordy w tablicy profiles. Trzecia modyfikacja to usunięcie nieobsługiwanej przez JavaScript 1.0 metody split(). Zastosowane rozwiązanie jest skuteczne, ale niestety pogarsza wydajność kodu.
NICTJDO
Hasło to napisał na tablicy profesor ekonomii, kiedy autor był studentem pierwszego roku na Uniwersytecie Stanu Floryda. Rozwinięcie tego akronimu to „Nie Istnieje Coś Takiego Jak Darmowy Obiad”. W naszym przypadku znaczy to, że dokonanie zmian pozwoli na korzystanie z wyszukiwarki w starszych przeglądarkach, ale odbędzie się to kosztem możliwości funkcjonalnych i struktury kodu.
Bez plików .js trzeba będzie upchnąć tablicę profiles w pliku nav.html. Rozwiązanie to jest mało eleganckie, a jeszcze mniej wygodne, jeśli okaże się, że bazę trzeba będzie wykorzystać gdzie indziej.
Metoda sort() nie jest dla aplikacji niezbędna, ale okazuje się bardzo przydatna. Brak uporządkowania zbioru wynikowego może zmusić użytkownika do żmudnego przeglądania wszystkich odpowiedzi. Co prawda można by zapisywać rekordy w tablicy alfabetycznie, ale i to nie jest raczej wygodnym rozwiązaniem. Sortowanie dla JavaScriptu w wersji 1.0 można również zaprogramować samodzielnie. Brak metody split() jest zdecydowanie najmniejszym problemem. Wersja aplikacji przeznaczona dla JavaScriptu 1.0 zawiera zresztą rozwiązanie, więc nie ma co rozpaczać.
Odporność na błędy
W obecnej postaci aplikacja umożliwia użytkownikowi umieszczenie w zapytaniu znaku kreski pionowej. Warto pokusić się o ulepszenie programu poprzez dodanie kodu usuwającego z łańcucha zapytania znaki wykorzystywane jako separatory danych, co zwiększy odporność wyszukiwarki na błędy.
Wyświetlanie reklam
Jeśli w witrynie panuje ruch jak w południe na Marszałkowskiej, czemu by na tym jeszcze trochę nie zarobić? Świetny pomysł, ale jak? Oto jedna z możliwości. Załóżmy, że chcemy wyświetlać losowo pięć reklam (bez jakiejś określonej kolejności). Po umieszczeniu kilku adresów URL obrazków w tablicy, możemy losowo wybierać jeden z nich do wyświetlenia. Oto przykładowa tablica:
var adImages = new Array("pcAd.gif", "modemAd.gif", "webDevAd.gif");
Losowe wybranie jednego z nich i wyświetlenie go na stronie z wynikami wyglądałoby tak:
document.writeln('<IMG SRC=' + ads[Math.floor(Math.random(ads.length))] +
'>');
Rozszerzenie możliwości wyszukiwania
Ten pomysł może dać ambitniejszym programistom niezłe pole do popisu. Załóżmy na przykład, że użytkownik mógłby wybierać wyszukiwane elementy z tablicy, a dopiero potem zawężałby odpowiednio zakres interesujących go danych.
Jednym z rozwiązań jest wyświetlenie pod polem tekstowym w pliku nav.html następującego zestawu opcji:
<INPUT TYPE=CHECKBOX NAME="group" VALUE="98">Dane z roku 1998<BR>
<INPUT TYPE=CHECKBOX NAME="group" VALUE="99">Dane z roku 1999<BR>
<INPUT TYPE=CHECKBOX NAME="group" VALUE="00">Dane z roku 2000<BR>
Pól tych można użyć do zdecydowania, które tablice rekordów należy przeszukać (w naszym przypadku będą to np. tablice profiles98, profiles99 i profiles00).
Kryteria wyszukiwania można rozszerzać na wiele innych sposobów. Jednym z prostszych jest umożliwienie wyszukiwania z uwzględnieniem wielkości liter i bez jego uwzględniania. Obecnie wielkość liter nie ma znaczenia, ale można by dodać do formularza pole wyboru pozwalające na takie rozróżnienie.
Możliwość tworzenia bardziej złożonych warunków wyszukiwania można też zapewnić, uzupełniając operatory AND i OR funkcjami NOT, ONLY, a nawet LIKE. Oto ogólny opis ich znaczenia:
AND
Rekord musi zawierać oba hasła połączone operatorem AND.
OR
Rekord musi zawierać co najmniej jedno z haseł połączonych operatorem OR.
NOT
Rekord nie może zawierać hasła znajdującego się za operatorem NOT.
ONLY
Wynik musi zawierać tylko zadany rekord.
LIKE
Rekord może zawierać terminy podobne (w pisowni lub wymowie) do podanego hasła.
Zaprogramowanie tych możliwości (szczególnie LIKE) wymaga sporo pracy, ale jej efekty mogą rzucić użytkowników na kolana.
Zapytania predefiniowane
Inną popularną, a przy tym użyteczną techniką wyszukiwania jest tworzenie tzw. zapytań predefiniowanych (ang. cluster set). Zapytanie takie zawiera z góry ustaloną grupę haseł, dla której zdefiniowano gotowy zestaw odpowiedzi. Jeśli na przykład użytkownik umieści w zapytaniu tekst „kredyt obrotowy”, wyszukiwarka może od razu zwrócić gotowy zestaw wyników opisujących wszystkie produkty finansowe firmy. Technika ta wymaga starannego przygotowania danych, ale jej użycie może okazać się niezwykle cenne.
Cechy aplikacji:
Prezentowane techniki:
|
2
Test sprawdzany |
|
Przedstawiony w tym rozdziale interaktywny test należy traktować jako aplikację szablonową, na podstawie której można realizować dowolne tego typu zadania działające poprzez Internet. Elastyczność tej aplikacji polega na tym, że:
Określa się liczbę pytań zadawanych użytkownikowi.
Pytania i odpowiedzi są dobierane losowo przy każdym załadowaniu aplikacji lub rozpoczęciu testu, w efekcie czego użytkownik za każdym razem ma do czynienia
z nowym testem.
Można pytania testowe dodawać i usuwać - aplikacja dostosuje wówczas do tego sposób mieszania pytań, administrację, ocenianie odpowiedzi i ocenę badanego.
Bez problemu można z aplikacji usunąć odpowiedzi, dzięki czemu unika się oszukiwania, a odpowiedzi użytkownika można przesłać do działającej po stronie serwera aplikacji oceniającej.
Aplikację tę załadujemy otwierając w przeglądarce plik ch02/index.html. Na rysunku 2.1 pokazano ekran początkowy. Któż zgadłby tutaj, że pytania działają dzięki JavaScriptowi? Spróbujemy rozwiązać test, składający się z 50 pytań, które zwykle sprawiają użytkownikom sporo kłopotów. Pytania obejmują wiele zagadnień naszego języka: jego rdzeń, skrypty działające po stronie klienta i po stronie serwera, LiveConnect, znane błędy i tak dalej. Zadanie nie jest łatwe, ale o to chodzi (istnieje dokumentacja omawiająca wszystkie pytania i odpowiedzi tego testu, ale w razie odkrycia błędów w pytaniach użytkownik może skontaktować się z autorem).
Po rozpoczęciu tekstu użytkownik na każde pytanie otrzyma cztery możliwe odpowiedzi. Kiedy, zdecyduje się na którąś z nich, aplikacja automatycznie przejdzie do następnego pytania, zatem nie można się cofać. Każde pytanie to jedna próba. Na rysunku 2.2 pokazano postać pytań i odpowiedzi.
|
Rysunek 2.1. Gotów do testu?
|
Rysunek 2.2. Pytania z kilkoma możliwymi odpowiedziami
Po odpowiedzi na ostatnie pytanie, nasze odpowiedzi będą porównane z tymi właściwymi, po czym zostaniemy ocenieni, a wyniki wyświetlą się na ekranie, tak jak pokazano to na rysunku 2.3. Warto zwrócić uwagę, że teraz na ekranie pokazywane są poszczególne pytania ze wszystkimi możliwymi odpowiedziami i z odpowiedzią przez nas wybraną. Jeśli wybór był poprawny, tekst jest zielony; w przeciwnym wypadku ma kolor czerwony.
|
|
Rysunek 2.3. Wyniki testu
Aby lepiej zrozumieć pytania, na które udzieliło się złych odpowiedzi, można przejrzeć prawidłowe propozycje; jeśli najedzie się kursorem myszki na czerwony tekst, na górze po prawej stronie pojawi się objaśnienie - rysunek 2.4.
|
Rysunek 2.4. Objaśnienie błędnie udzielonych odpowiedzi
W porządku, to był pierwszy kontakt z aplikacją. Wszystko wygląda prosto, ale diagram z rysunku 2.5 da czytelnikowi pewne wyobrażenie o sposobie działania aplikacji z punktu widzenia użytkownika. Kreski przerywane wskazują opcjonalne działania użytkownika lub stan oczekiwania na jego reakcję. Całość składa się z pięciu etapów.
|
Rysunek 2.5. Działanie aplikacji z punktu widzenia użytkownika
Oto poszczególne etapy:
Użytkownik wybiera przycisk Zaczynamy. Wypisywane jest pierwsze pytanie i aplikacja czeka na odpowiedź użytkownika lub wciśnięcie klawisza Koniec.
Jeśli użytkownik wybierze odpowiedź, aplikacja zapisuje dokonany wybór, określa, czy test się już skończył, czy też należy pokazać następne pytanie. Jeśli tekst jest skończony (użytkownik odpowiedział na ostatnie pytanie), idziemy do etapu 4. (ocena testu). W przeciwnym wypadku pokazuje się następne pytanie.
Jeśli użytkownik wybierze przycisk Koniec, musi jeszcze swój wybór potwierdzić. W przypadku wybrania OK test jest oceniany (mimo że jeszcze na to za wcześnie). Jeśli użytkownik wybierze Anuluj, test jest kontynuowany.
Kiedy test się skończy (lub zostanie przerwany), odpowiedzi użytkownika są porównywane z prawidłowymi odpowiedziami, a na ekranie pokazują się wyniki.
Kiedy użytkownik przegląda wyniki, może przesunąć mysz nad dowolny czerwony tekst (czyli swoją błędną odpowiedź), a wówczas pokazane zostaną dodatkowe informacje.
Wymagania programu
Mamy tu do czynienia z aplikacją JavaScriptu 1.1, więc Navigator 3.x i MSIE 4.x sobie z nią poradzą. Jeśli chodzi o wielkość aplikacji, obecnie jest 75 pytań. Testowanie przerwałem, kiedy było ich 400. Jako że zapewne nikt nie będzie raczej używał naszej aplikacji do przeprowadzania egzaminu adwokackiego ani magisterskiego, uważam, że 400 to liczba wystarczająca.
Struktura programu
Na rysunku 2.5 pokazano działanie aplikacji od początku do końca. Dobrym sposobem na zrozumienie, co się naprawdę dzieje, jest przeanalizowanie dokładniejszego schematu opisującego działanie JavaScriptu, a później przejrzenie odpowiednich plików z kodem.
Na rysunku 2.6 przedstawiono działanie JavaScriptu. Prostokąty narysowane linią przerywaną wskazują przetwarzanie zachodzące przed testem lub po nim (na przykład podczas ładowania stron). Strzałki kresek oznaczają możliwe czynności użytkownika lub oczekiwanie na jego odpowiedź.
|
Rysunek 2.6. Działanie JavaScriptu
Funkcje związane z poszczególnymi akcjami (czynnościami) zaznaczono kursywą. Porównanie rysunków 2.5 i 2.6, umożliwi szybkie zorientowanie się w problemie. W zasadzie mamy do czynienia z tym samym działaniem, przy czym w drugim przypadku dodano pewne przetwarzanie przed testem i po nim.
index.html - ramki
Aplikacja ta składa się z trzech plików: index.html, administer.html oraz questions.js. Jako że index.html zawiera ramki, zacznijmy od niego - obejrzyjmy przykład 2.1.
Przykład 2.1. Plik index.html
1 <HTML>
2 <HEAD>
3 <TITLE>JavaScript On-line Test</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.1">
5 <!--
6 var dummy1 = '<HTML><BODY BGCOLOR=WHITE></BODY></HTML>';
7 var dummy2 = '<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>W nauce JavaScriptu nie ma wakacji...</BODY></HTML>';
8 //-->
9 </SCRIPT>
10
11 </HEAD>
12 <FRAMESET ROWS="90,*" FRAMEBORDER=0 BORDER=0>
13 <FRAMESET COLS="250,*">
14 <FRAME SRC="administer.html" SCROLLING=NO>
15 <FRAME SRC="javascript: self.dummy1">
16 </FRAMESET>
Przykład 2.1. Plik index.html (dokończenie)
17 <FRAME NAME="questions" SRC="javascript: self.dummy2">
18 </FRAMESET>
19 </HTML>
Jak można zauważyć, nie jest to zestaw ramek spotykany w Sieci. Po pierwsze ramki te są zagnieżdżone - czyli mają postać ramek. Zewnętrzny zestaw w wierszu 12 definiuje dwa wiersze: jeden jest wysoki na 90 pikseli, drugi natomiast zajmuje resztę wysokości okna.
90-pikselowa ramka zawiera znów ramki, tym razem składające się z dwóch kolumn: pierwszej o szerokości 250 pikseli i drugiej zajmującej resztę okna. Na rysunku 2.7 pokazano sposób podzielenia okna na ramki. Podano też atrybuty SRC poszczególnych ramek.
|
|
Rysunek 2.7. Układ ramek zagnieżdżonych w index.html
administer.html w atrybucie SRC znacznika FRAME ma sens, ale co z pozostałymi dwiema ramkami? Dwie zmienne dummy1 i dummy2 definiują niejawne strony HTML - czyli strony bez nazwy pliku. Obie istnieją jedynie w aplikacji. Zmienne te zdefiniowane są w wierszach 7 i 8. Warto zwrócić uwagę, że każda z nich zawiera nieco kodu HTML - niewiele wprawdzie, ale wystarczy. W pliku index.html używa się protokołu javascript: do interpretacji wartości zmiennych dummy1 oraz dummy2, następnie całość jest zwracana jako zawartość adresu URL wskazanego w atrybucie SRC. Więcej informacji można znaleźć w ramce „Techniki języka JavaScript”.
Teraz ramki są już na swoim miejscu. Wszystkie trzy wypełniliśmy, stosując tylko jedną stronę HTML (administer.html). Teraz przejdźmy do sedna rzeczy.
question.js - plik źródłowy JavaScript
Zajmijmy się teraz plikiem źródłowym JavaScriptu questions.js wywoływanym przez administer. html, a pokazanym jako przykład 2.2.
Przykład 2.2. Początek pliku questions.js
1 function question(answer, support, question, a, b, c, d) {
2 this.answer = answer;
3 this.support = support;
4 this.question = question;
5 this.a = a;
6 this.b = b;
7 this.c = c;
8 this.d = d;
9 return this;
10 }
11 var units = new Array(
Przykład 2.2. Początek pliku questions.js (dokończenie)
12 new question("a", "The others are external objects.",
13 "Choose the built-in JavaScript object:", "Image", "mimeType",
14 "Password", "Area"),
15 // i tak dalej...
16 }
Istnieje oczywiście wersja skrócona tego pliku. Tablica units jest znacznie dłuższa (ma 75 elementów), ale każdy jej element opisuje obiekt question (ang. pytanie), którego strukturę pokazano w wierszach 1-10.
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: Ustawienie atrybutu SRC na wartość będącą wynikiem rozwinięcia wyrażenia JavaScript może wyglądać nieco dziwnie, więc zastanówmy się nad tym przez chwilę. Załóżmy, że otwieramy edytor tekstowy i wstawiamy do nowego pliku taki tekst: <BODY BGCOLOR=WHITE> <FONT FACE=Arial> W nauce JavaScriptu nie ma wakacji... </FONT> </BODY> Teraz nadajemy nazwę temu plikowi zdanie.html i ładujemy go do przeglądarki. Łatwo przewidzieć rezultat. W pliku index.html mamy do czynienia właściwie z taką samą 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:
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 |
Zapisanie zestawu pytanie-odpowiedź w oknie, zapis odpowiedzi użytkownika. |
Funkcje: buildQuestion(), makeButton(), ewentualnie chickenOut() |
Ocena testu |
Porównanie odpowiedzi badanego z poprawnymi odpowiedziami. |
Funkcja gradeTest() |
Pokazanie wyników |
Pokazanie odpowiedzi poprawnych i błędne wraz z oceną. |
Funkcja printResults() |
Wyœwietlanie wyjaœnieñ |
Wyœwietlanie i chowanie wyjaśnień w parent.frames[1]. |
Funkcje explain() i show() |
Czyszczenie œrodowiska |
Przywracanie zmiennym ich pierwotnych wartości. |
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
1 <HTML>
2 <HEAD><TITLE>On-line JavaScript Test</TITLE>
3 <SCRIPT LANGUAGE="JavaScript1.1" SRC="questions.js"></SCRIPT>
4 <SCRIPT LANGUAGE="JavaScript1.1">
5 var qIdx = 0;
6 var correct = 0;
7 var howMany = 50;
8 var keeper = new Array();
9 var rank = new Array('Nie obraź się, ale potrzebna Ci pomoc.',
10 'Byli i tacy, co zrobili jeszcze gorzej...',
11 'Cosik tam wiesz. Przynajmniej tego nie zapomnij.',
12 'Zdaje się, że pracujesz nad swoją wiedzą.',
13 'Lepiej od przeciętnego niedźwiedzia.',
Przykład 2.3. Kod źródłowy administer.html (ciąg dalszy)
14 'Jesteś niezłym programistą JavaScriptu.',
15 'Jesteś znawcą JavaScriptu.', 'Jesteś doskonały w JavaScripcie.',
16 'Ogłaszam Cię guru JavaScriptu.'
17 );
18 var stopOK = false;
19 var nextQ = '';
20 var results = '';
21 var aFrame = parent.frames[1];
22 var qFrame = parent.frames[2];
23 function shuffle() {
24 for (var i = 0; i < units.length; i++) {
25 var j = Math.floor(Math.random() * units.length);
26 var tempUnit = units[i];
27 units[i] = units[j];
28 units[j] = tempUnit;
29 }
30 }
31 function itemReset() {
32 qIdx = 0;
33 correct = 0;
34 stopOK = false;
35 keeper = new Array();
36 shuffle();
37 }
38 function buildQuestion() {
39 if (qIdx == howMany) {
40 gradeTest();
41 return;
42 }
43 nextQ = '<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>' +
44 '<H2>Pytanie ' + (qIdx + 1) + ' z ' + howMany + '</H2>' +
45 '<FORM>' + '<B>' + units[qIdx].question + '</B><BR><BR>' +
46 makeButton("a", units[qIdx].a) +
47 makeButton("b", units[qIdx].b) +
48 makeButton("c", units[qIdx].c) +
49 makeButton("d", units[qIdx].d) +
50 '</FORM></BODY></HTML>';
51 qFrame.location.replace("javascript: parent.frames[0].nextQ");
52 qIdx++;
53 if(qIdx >= 2 && !stopOK) { stopOK = true; }
54 }
55 function makeButton(optLtr, optAnswer) {
56 return '<INPUT TYPE=RADIO NAME="answer" VALUE="' + optLtr +
57 '" onClick="parent.frames[0].keeper[parent.frames[0].qIdx - 1] =
58 this.value; parent.frames[0].buildQuestion()">' + optAnswer + '<BR>';
59 }
60 function chickenOut() {
61 if(stopOK &&
62 confirm('Masz już dość? Tchórzysz?')) {
63 gradeTest();
64 }
65 }
66 function gradeTest() {
67 for (var i = 0; i < qIdx; i++) {
68 if (keeper[i] == units[i].answer) {
69 correct++;
70 }
71 }
72 var idx = Math.ceil((correct/howMany) * rank.length - 1) < 0 ? 0 :
73 Math.ceil((correct/howMany) * rank.length - 1);
74 printResults(rank[idx]);
75 itemReset();
76 }
77 function printResults(ranking) {
78 results = '<HTML><BODY BGCOLOR=WHITE LINK=RED VLINK=RED ALINK=RED>' +
Przykład 2.3. Kod źródłowy administer.html (dokończenie)
79 '<FONT FACE=Arial>' +
80 '<H2>Odpowiedzi poprawnych: ' + correct + '/' + howMany + '.</H2>' +
81 '<B>Ocena: <I>' + ranking +
82 '</I><BR>Ustaw kursor myszki nad czerwonym tekstem, a zobaczysz' +
83 ' poprawki odpowiedzi.</B>' +
84 '<BR><BR><FONT SIZE=4>Oto Twoje oceny: </FONT><BR><BR>';
85 for (var i = 0; i < howMany; i++) {
86 results += '\n\r\n\r\n\r<B>Pytanie ' + (i + 1) + '</B><BR>' +
87 units[i].question + '<BR><BR>\n\r<FONT SIZE=-1>' +
88 'a. ' + units[i].a + '<BR>' +
89 'b. ' + units[i].b + '<BR>' +
90 'c. ' + units[i].c + '<BR>' +
91 'd. ' + units[i].d + '<BR></FONT>';
92 if (keeper[i] == units[i].answer) {
93 results += '<B><I><FONT COLOR=GREEN>' +
94 'Na to odopwiedziałeś poprawnie (' +
95 keeper[i] + ').</FONT></I></B>\n\r<BR><BR><BR>';
96 }
97 else {
98 results += '<FONT FACE=Arial><B><I>' +
99 '<A HREF=" " onMouseOver="parent.frames[0].show();' +
100 parent.frames[0].explain(\'' + units[i].support + '\'); ' +
101 'return true"' + ' onMouseOut="parent.frames[0].explain(\' \');"' +
102 'onClick="return false;">' +
103 'Prawidłowa odpowiedź: ' + units[i].answer +
104 '</A></FONT></I></B>\n\r<BR><BR><BR>';
105 }
106 }
107 results += '\n\r</BODY></HTML>';
108 qFrame.location.replace("javascript: parent.frames[0].results");
109 }
110 function show() { parent.status = ''; }
111 function explain(str) {
112 with (aFrame.document) {
113 open();
114 writeln('<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>' + str +
115 '</FONT></BODY></HTML>');
116 close();
117 }
118 }
119 function cleanSlate() {
120 aFrame.location.replace('javascript: parent.dummy1');
121 qFrame.location.replace('javascript: parent.dummy2');
122 }
123 </SCRIPT>
124 </HEAD>
125 <BODY BGCOLOR=WHITE onLoad="cleanSlate();">
126 <FONT FACE="Arial">
127 <FORM>
128 <INPUT TYPE=BUTTON VALUE="Zaczynamy"
129 onClick="itemReset(); buildQuestion();">
130
131 <INPUT TYPE=BUTTON VALUE="Koniec" onClick="chickenOut();">
132 </FORM>
133 </FONT>
134 </BODY>
135 </HTML>
Ten dość długi plik można by podzielić na cztery części. W pierwszej wywoływany jest plik źródłowy questions.js. W następnej definiowane są pewne zmienne globalne, po czym przejdziemy do funkcji. W końcu mamy kilka wierszy HTML i od nich właśnie zacznijmy.
Treść HTML
Kiedy administer.html skończy się ładować, wywoływana jest funkcja cleanSlate() z wiersza 125:
<BODY BGCOLOR=WHITE onLoad="cleanSlate();">
W cleanSlate() używa się metody replace() obiektu location, w ten sposób bieżący adres URL parent.frames[1] (alias aFrame) i parent.frames[2] (alias qFrame) zamieniany jest na zawartość zmiennych dummy1 i dummy2, zdefiniowanych wcześniej w pliku index.html. Spójrzmy na wiersze 119 do 122:
function cleanSlate() {
aFrame.location.replace('javascript: parent.dummy1');
qFrame.location.replace('javascript: parent.dummy2');
}
Właśnie to robiliśmy w pliku index.html, zgadza się? Tym razem zapewniamy, że jeśli z jakiegoś powodu administer.html ulegnie przeładowaniu, górna ramka zawierać będzie naszą sentencję, a istniejący tam ewentualnie tekst pytania zostanie usunięty.
Reszta pliku HTML to po prostu formularz HTML z dwoma przyciskami. To jest już proste. Każdy przycisk - po kliknięciu go - wywołuje inną funkcje. Poniżej przedstawiono kod wierszy 127-132:
<FORM>
<INPUT TYPE=BUTTON VALUE="Zaczynamy"
onClick="itemReset(); buildQuestion();">
<INPUT TYPE=BUTTON VALUE="Koniec" onClick="chickenOut();">
</FORM>
Warto zauważyć, że przycisk Zaczynamy wywołuje funkcje itemReset() i buildQuestion(), natomiast Koniec wywołuje chickenOut(). Wszystkie trzy funkcje będą omówione w sekcji im poświęconej.
Zmienne globalne
Zaraz za instrukcją powodującą włączenie pliku źródłowego JavaScript questions.js w wierszu 5 można znaleźć zmienne globalne używane w aplikacji. Oto wiersze 5 do 22:
var qIdx = 0;
var correct = 0;
var howMany = 50;
var keeper = new Array();
var rank = new Array('Nie obraź się, ale potrzebna Ci pomoc.',
'Byli i tacy, co zrobili jeszcze gorzej...',
'Cosik tam wiesz. Przynajmniej tego nie zapomnij.',
'Zdaje się, że pracujesz nad swoją wiedzą.',
'Lepiej od przeciętnego niedźwiedzia.',
'Jesteś niezłym programistą JavaScriptu.',
'Jesteś znawcą JavaScriptu.', 'Jesteś doskonały w JavaScripcie.',
'Ogłaszam Cię guru JavaScriptu.'
);
var stopOK = false;
var nextQ = '';
var results = '';
var aFrame = parent.frames[1];
var qFrame = parent.frames[2];
Na poniższej liście opisujemy znaczenie poszczególnych zmiennych. Dokładniej przyjrzymy się im przy omawianiu poszczególnych funkcji.
qldx
Zmienna używana do monitorowania bieżącego pytania, wyświetlanego na ekranie.
correct
Zmienna rejestrująca liczbę poprawnych odpowiedzi podczas oceny testu.
howMany
Niezmienna liczba określająca liczbę pytań, na które odpowiadać ma użytkownik.
keeper
Początkowo pusta tablica, zawiera odpowiedzi udzielone przez użytkownika.
rank
Tablica napisów określających poziom umiejętności.
stopOK
Zmienna logiczna określająca, czy przerwać test.
nextQ
Pusty napis, któremu przypisywany jest tekst kolejnych pytań.
results
Początkowo napis pusty, później ocena testu.
aFrame
Prosta metoda odwołania się do drugiej ramki.
qFrame
Prosta metoda odwołania się do trzeciej ramki.
Funkcje
Teraz przechodzimy do funkcji. Zaczniemy od itemReset().
itemReset()
Pierwsza z funkcji wywoływanych w aplikacji to itemReset(). Pojawia się, kiedy użytkownik wciśnie przycisk Zaczynamy (wiersze 128-129):
<INPUT TYPE=BUTTON VALUE="Zaczynamy"
onClick="itemReset(); buildQuestion();">
itemReset() przywraca zmiennym globalnym ich pierwotne wartości i miesza zawartość tablicy obiektów pytań (więcej o mieszaniu już wkrótce). Spójrzmy na wiersze 31-37:
function itemReset() {
qIdx = 0;
correct = 0;
stopOK = false;
keeper = new Array();
shuffle();
}
Warto zwrócić uwagę, że użytkownik nie widział jeszcze pierwszego pytania, a JavaScript już napracował się przy ustawianiu zmiennych globalnych. Po co? Załóżmy, że już test rozwiązaliśmy i tylko na dwa pytania odpowiedzieliśmy poprawnie wtedy wciskamy jeszcze raz przycisk Zaczynamy. Jednak wiele zmiennych globalnych ma już różne nieoczekiwane wartości i tym właśnie zajmuje się funkcja itemReset(): odświeża wartości tych zmiennych.
Zauważmy, że nie dotyczy to zmiennej howMany. Wartość ta pozostaje niezmienna przez cały czas działania aplikacji. Zmienne netQ i results na początku mają ciąg pusty jako wartość, ale ich wartości nie są zerowane. Nie ma po prostu takiej potrzeby. Zajrzyjmy do wierszy 43 i 86, a zobaczymy, jak te zmienne są ustawiane na bieżąco.
Kiedy zmienne są już odpowiednio ustawione, można wywołać w wierszu 36 funkcję shuffle().
shuffle()
Ta mała funkcja daje administratorowi testu ogromną elastyczność - zmienia ona losowo kolejność pytań, dzięki czemu daje prawie pewność, że testowany dostanie za każdym razem inny zestaw. Aby unaocznić wynikające z tego możliwości, przypomnijmy, że liczba możliwych kombinacji (różnych uporządkowań) pytań testu wynosi n(n-1), przy czym n to liczba pytań. Zatem najmniejszy nawet tekst z dziesięciu pytań da 10*(10-1) kombinacji, czyli 90. W przypadku testu z 20 pytań możliwości jest już 380. Z kolei test z 50 pytań oznacza 2 450 możliwych kombinacji. To może być nieciekawa wiadomość dla oszustów.
Test jest także niepowtarzalny dlatego, że choć cała tablica units ma 75 pytań, zmienna howMany ustawiana jest na 50. Kiedy skończy się mieszanie, wybieranych jest 50 pierwszych pytań. Wobec tego istnieje duża szansa, że zestaw następnych 50 pytań jest inny, niż pierwsze 50. Oznacza to, że w teście tym istnieją tysiące możliwych kombinacji pytań. Zdumiewające, jak prosty jest proces mieszania tychże pytań.
Oto wiersze 23 do 30:
function shuffle() {
for (var i = 0; i < units.length; i++) {
var j = Math.floor(Math.random() * units.length);
var tempUnit = units[i];
units[i] = units[j];
units[j] = tempUnit;
}
}
Dla każdego elementu tablicy units:
Wybierana jest przypadkowa liczba między 0 a units.length - 1.
Wartość zmiennej lokalnej tempUnit ustawiana jest na bieżący indeks (units[i]).
Wartość elementu bieżącego indeksu (units[i]) ustawiana jest na wartość elementu o przypadkowym indeksie całkowitym (units[j]).
Wartość elementu o przypadkowym indeksie staje się równa wartości zmiennej lokalnej tempUnit.
Innymi słowy, kolejno przeglądane są wszystkie elementy tablicy i parami są zamieniane wartości z losowo wybranym elementem.
Pytania zostały już zatem losowo wymieszane i czekają na użytkownika.
buildQuestion()
Funkcja ta pełni rolę administratora testu. Jak zapewne łatwo zauważyć na poprzednim schemacie, buildQuestion() jest używana kilkakrotnie. Spoczywa na niej wielka odpowiedzialność. Zaczyna się w wierszu 38, a kończy w wierszu 54:
function buildQuestion() {
if (qIdx == howMany) {
gradeTest();
return;
}
nextQ = '<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>' +
'<H2>Pytanie ' + (qIdx + 1) + ' z ' + howMany + '</H2>' +
'<FORM>' + '<B>' + units[qIdx].question + '</B><BR><BR>' +
makeButton("a", units[qIdx].a) +
makeButton("b", units[qIdx].b) +
makeButton("c", units[qIdx].c) +
makeButton("d", units[qIdx].d) +
'</FORM></BODY></HTML>';
qFrame.location.replace("javascript: parent.frames[0].nextQ");
qIdx++;
if(qIdx >= 2 && !stopOK) { stopOK = true; }
}
Prześledźmy wszystko po kolei: najpierw buildQuestion() sprawdza, czy zmienna qIdx równa jest zmiennej howMany. Jeśli tak, użytkownik odpowiedział na ostatnie pytanie i przyszedł czas na ocenę. Funkcja gradeTest() wywoływana jest w wierszu 40.
Techniki języka JavaScript: 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. |
|
Jeśli test jeszcze się nie skończył, buildQuestion() idzie dalej, co oznacza, że generowana jest treść następnego pytania w postaci strony HTML (wiersze 43 do 50). Jeśli zbada się nextQ, można zauważyć, że strona ta zawiera wskaźnik numeru pytania i całkowitą liczbę pytań testu. Oto wiersz 44:
'<H2>Pytanie ' + (qIdx + 1) + ' z ' + howMany + '</H2>'
Dalej znajdziemy otwierający znacznik FORM, a za nim treść pytania. Warto pamiętać, że treść znajduje się we właściwości question poszczególnych elementów units. Nie należy być zatem zaskoczonym, że wiersz 45 wygląda następująco:
'<FORM>' + '<B>' + units[qIdx].question
W pobliżu elementu formularza FORM znajdują się też same elementy formularza. Tak naprawdę są one całą resztą strony HTML. Ta formatka ma tylko cztery elementy, a wszystkie są opcjami radio. Zamiast kodować wszystko w HTML, funkcja makeButton() te wszystkie opcje (niemalże identyczne) tworzy. Wystarczy jedynie przekazywać jej literę odpowiedzi i tekst tej odpowiedzi, co widać w wierszach 46 do 49. Oto funkcja makeButton() z wierszy 55-59:
function makeButton(optLtr, optAnswer) {
return '<INPUT TYPE=RADIO NAME="answer" VALUE="' + optLtr +
'" onClick="parent.frames[0].keeper[parent.frames[0].qIdx - 1] =
this.value; parent.frames[0].buildQuestion()">' + optAnswer + '<BR>';
}
Funkcja po prostu zwraca tekst opisujący jedną opcję radio z odpowiednio ustawionym atrybutem VALUE (a, b, c lub d) oraz tekst odpowiedzi umieszczany na prawo od opcji. Atrybut VALUE pochodzi z optLtr, a tekst z optAnswer.
Warto pamiętać, że test reaguje na działania użytkownika, więc automatycznie przechodzi dalej, kiedy tylko użytkownik udzieli odpowiedzi. W języku JavaScriptu oznacza to, że ze zdarzeniem onClick opcji musi być związanych kilka kolejnych działań.
Po pierwsze - w tablicy keeper należy zarejestrować odpowiedź użytkownika. Aby sprawdzić, który element należy przypisać wyborowi, używamy poniższego wyrażenia:
parent.frames[0].qIdx - 1
Zmienna qIdx „pamięta” numer aktualnej odpowiedzi, więc świetnie nadaje się do określenia kolejnej odpowiedzi użytkownika.
Następnie JavaScript musi wywołać buildQuestion(), aby pokazać następne pytanie lub ocenić test, jeśli został on zakończony. Zwróćmy uwagę, że do keeper i do buildQuestion() odwoływaliśmy się na początku parent.frames[0]. Informacja ta zostanie zapisana w ramce parent. frames[1], więc będziemy musieli dostać się do górnej ramki.
Kiedy już mamy gotowy formularz, trzeba jeszcze tylko (jeśli chodzi o HTML) pozamykać znaczniki i załadować treść do okna - wiersze 50 i 51:
'</FORM></BODY></HTML>';
qFrame.location.replace("javascript: parent.frames[0].nextQ");
Wartość nextQ ładowana jest do dolnej ramki. Zauważmy, że w aplikacji używana jest metoda replace obiektu location, nie ustawiamy więc tym razem właściwości location.href ani nie stosujemy document.write(). W tej aplikacji jest to istotne. Funkcja replace() ładuje wskazany adres URL do przeglądarki (w naszym przypadku adres URL to napis HTML rozwijany przez użycie protokołu javascript:), przy czym nowa strona zastępuje bieżącą. Dzięki temu użytkownik nie może wrócić do strony wcześniejszej ani zmieniać odpowiedzi. Jeśli użytkownik wciśnie w przeglądarce przycisk Back, załaduje się strona, która była oglądana przed index.html.
Ostatnią rzeczą, którą trzeba zrobić przed opuszczeniem funkcji buildQuestion(), jest drobne uporządkowanie, pokazane w wierszach 52-53:
qIdx++;
if(qIdx >= 2 && !stopOK) { stopOK = true; }
Zwiększenie qIdx o 1 przygotowuje sytuację do następnego wywołania buildQuestion(). Pamiętaj, że w wierszu 39 sprawdzamy, czy qIdx jest większa od liczby pytań w teście (zmienna howMany); jeśli tak, czas na ocenę. Instrukcja if z wiersza 53 sprawdza, czy użytkownik zamierza przerwać test. Kod w obecnej jego postaci wymaga udzielenia odpowiedzi na przynajmniej jedno pytanie. Jeśli użytkownik chce, może to już sam poprawić.
|
Techniki języka 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 <A>: <A HREF="javascript: alert('Znalazłeś ostrzeżenie!');">Kliknij mnie</A> Można też ustawiać w taki sposób atrybuty SRC innych znaczników HTML. Szczegóły znajdziemy w ramce opisującej „oszukane” atrybuty SRC, wcześniej w tym rozdziale. Teraz jeszcze ostrzeżenie: jeśli w zdefiniowanej przez siebie funkcji używamy protokołu javascript:, nie próbujemy ewaluować zmiennych lokalnych funkcji. To nie zadziała. Omawiany protokół ma zasięg globalny, więc „widzi” jedynie globalne zmienne, globalne obiekty i tak dalej. Wiersz 51 to klasyczny przykład ilustrujący tę zasadę: qFrame.location.replace("javascript: parent.frames[0].nextQ"); Zmienna nextQ właściwie mogłaby zostać zdefiniowana jako lokalna, w końcu przecież jest używana jedynie w funkcji buildQuestion(). Jednak, jako że w wierszu 51 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:
Wylicza procent odpowiedzi poprawnych (correct / howMany).
Mnoży uzyskane procenty przez (rank.length - 1).
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:
Poprawne odpowiedzi |
Wyznaczona liczba |
Ocena (rank[int]) |
0-9 |
0 |
byli lepsi |
10-19 |
1 |
jako-tako |
20-29 |
2 |
dobrze |
30-39 |
3 |
bardzo dobrze |
40-50 |
4 |
doskonale |
|
|
|
Mamy zatem mniej więcej howMany / rank.length odpowiedzi na jedną ocenę (poza oceną najwyższą). Nie ma znaczenia, czy pytania są 2, czy jest ich 200 - wszystko działa tak samo.
Zaproponowany system ocen jest skuteczny, ale jest dość zgrubny. Stosowane powszechnie systemy ocen zwykle są bardziej złożone. Większość szkół amerykańskich używa systemu literowego, gdzie A to ponad 90%, B to 80-89%, C to 70-79%, D to 60-69%, a F to mniej niż 60%. Być może ktoś zechce użyć jakiejś podobnej krzywej. W sekcji opisującej możliwe rozszerzenia aplikacji znajdzie się kilka innych pomysłów.
gradeTest() w zasadzie skończyła już swoje zadanie. Zmienna rank[idx] przekazywana jest do funkcji wyświetlającej printResults(), a później wywoływana jest funkcja czyszcząca itemReset().
printResults()
Aplikacja wie, jakie wyniki osiągnął użytkownik, czas więc poinformować go o tym. Służy do tego funkcja printResults() wyświetlająca następujące rzeczy:
Stosunek liczby odpowiedzi poprawnych do ilości wszystkich pytań.
Ocenę użytkownika wyznaczoną przez gradeTest().
Wszystkie pytania testu wraz z kompletem czterech odpowiedzi.
Informację, czy użytkownik wybrał dobrą odpowiedź, czy też nie.
Przyłączony tekst pozwalający użytkownikowi uzyskać dodatkowe informacje o pytaniach, na które nie odpowiedział prawidłowo.
Pierwsze dwa punkty realizowane są w wierszach 77 do 84:
function printResults(ranking) {
results = '<HTML><BODY BGCOLOR=WHITE LINK=RED VLINK=RED ALINK=RED>' +
'<FONT FACE=Arial>' +
'<H2>Odpowiedzi poprawnych: ' + correct + '/' + howMany + '.</H2>' +
'<B>Ocena: <I>' + ranking +
'</I><BR>Ustaw kursor myszki nad czerwonym tekstem, a zobaczysz' +
' poprawki odpowiedzi.</B>' +
'<BR><BR><FONT SIZE=4>Oto Twoje oceny: </FONT><BR><BR>';
Zmienne correct i howMany oznaczają odpowiednio liczbę odpowiedzi poprawnych oraz liczbę zadanych pytań, rank[rankIdx] to zapis zawierający ocenę użytkownika. Jeśli chodzi o wyświetlanie pytań i możliwych czterech odpowiedzi, obejrzyjmy wiersze 85 do 91. Nie powinno nikogo zaskakiwać pojawienie się pętli for:
for (var i = 0; i < howMany; i++) {
results += '\n\r\n\r\n\r<B>Pytanie ' + (i + 1) + '</B><BR>' +
units[i].question + '<BR><BR>\n\r<FONT SIZE=-1>' +
'a. ' + units[i].a + '<BR>' +
'b. ' + units[i].b + '<BR>' +
'c. ' + units[i].c + '<BR>' +
'd. ' + units[i].d + '<BR></FONT>';
W każdej iteracji od 0 do howMany - 1 podawana jest jako tekst liczba (i + 1), tekst pytania (units[i].question) oraz cztery możliwe odpowiedzi (units[i].a, units[i].b, units[i].c, units[i].d). Trochę zamieszania wprowadzają użyte znaczniki HTML.
Ostatni fragment wydruku to wyświetlenie dobrych odpowiedzi użytkownika na zielono i złych - z dodatkowym objaśnieniem - na czerwono. Oto odpowiednie wiersze - 92 do 106:
if (keeper[i] == units[i].answer) {
results += '<B><I><FONT COLOR=GREEN>' +
'Na to odopwiedziałeś poprawnie (' +
keeper[i] + ').</FONT></I></B>\n\r<BR><BR><BR>';
}
else {
results += '<FONT FACE=Arial><B><I>' +
'<A HREF=" " onMouseOver="parent.frames[0].show();' +
parent.frames[0].explain(\'' + units[i].support + '\'); ' +
'return true"' + ' onMouseOut="parent.frames[0].explain(\' \');"' +
'onClick="return false;">' +
'Prawidłowa odpowiedź: ' + units[i].answer +
'</A></FONT></I></B>\n\r<BR><BR><BR>';
}
}
Przetwarzaniu ulegają wszystkie kolejne pytania, niezależnie od trafności odpowiedzi udzielonej przez użytkownika. Nie należy być zatem zaszokowanym, gdy się widzi wewnątrz instrukcję if-else. Jeśli keeper[i] równe jest units[i].answer, użytkownik wybrał dobrze, a odpowiedź wyświetla się na zielono. Jeśli równość nie zachodzi, czerwony tekst wskazuje dostępność dodatkowego objaśnienia pytania w parent.frames[1]. Ramka ta nie była dotąd właściwie używana, więc teraz przyszedł i na nią czas.
Pytania, na które użytkownik udzielił jedynej słusznej odpowiedzi, są zwykłym tekstem, tymczasem pytania ze złymi odpowiedziami są jednak tekstem łącza. Zdarzenie onMouseOver tych łącz wywołuje przed zwróceniem true dwie funkcje: show() oraz explain(). Funkcja show() jest bardzo prosta: pokazuje pusty napis w pasku stanu (aby uniknąć dodatkowego zamieszania związanego z umieszczeniem myszy nad łączem) - wiersz 110:
function show() { parent.status = ''; }
Funkcja explain() jako parametr przyjmuje napis i używa go w metodzie document.write() do wyświetlenia HTML w cierpliwie czekającej parent.frames[1]. Oto wiersze 111-118:
function explain(str) {
with (aFrame.document) {
open();
writeln('<HTML><BODY BGCOLOR=WHITE><FONT FACE=Arial>' + str +
'</FONT></BODY></HTML>');
close();
}
}
Mimo że zatroszczyliśmy się o zdarzenie onMouseOver, explain(), nadal ma jeszcze nieco pracy. Zwróćmy uwagę, że explain() wywoływana jest znów w wierszu 101 w obsłudze zdarzenia onMouseOut. Tym razem jednak funkcji explain() przekazywany jest pusty napis, więc aFrame będzie czyszczona po każdym zdarzeniu onMouseOut.
Jedyne, co nam zostało, to zabezpieczenie się przed jakąkolwiek akcją w przypadku kliknięcia naszego łącza przez użytkownika. W wierszu 102 znajdziesz zapis onClick="return false;". Dzięki temu dokument wskazany w atrybucie HREF nie zostanie załadowany.
Należy pamiętać, że nadal jesteśmy w pętli for. Powyższy proces ma miejsce dla każdej odpowiedzi, od 0 do howMany - 1. Kiedy pętla for zakończy swoje działanie, zmienna results jest długim napisem, zawierającym liczbę prawidłowych odpowiedzi, liczbę wszystkich pytań, tekst pytań z możliwymi odpowiedziami i wyborami zrobionymi przez użytkownika oraz informacją o prawidłowości jego odpowiedzi. W wierszach 107-109 dodajemy jeszcze końcowe znaczniki HTML, ładujemy całość do dolnej ramki i zamykamy funkcję:
results += '\n\r</BODY></HTML>';
qFrame.location.replace("javascript: parent.frames[0].results");
}
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;
}
warto zwrócić uwagę, że usunięto też zmienne answer i support. Teraz, kiedy usunąłeś je z konstruktora, można pozbyć się ich ze wszystkich wywołań operatora new dla każdego elementu units. Innymi słowy, z każdego elementu units należy usunąć pierwsze dwa parametry.
Usuwanie gradeTest() i modyfikacja buildQuestion()
Jako że nie ma już odpowiedzi ani wyjaśnień, nie ma powodu lokalnie oceniać testu czy wyświetlać jego wyników. Oznacza to, że możesz pozbyć się funkcji gradeTest(). Po prostu w pliku administer.html należy usunąć wiersze 66 do 76. Można też pozbyć się wywołania gradeTest() w buildQuestion() w wierszu 40. Warto zastąpić to wywołaniem printResults(), aby użytkownik mógł zobaczyć swoje odpowiedzi w postaci HTML.
Wiersze 39 do 42 zmień z poniższej wartości:
if (qIdx == howMany) {
gradeTest();
return;
}
na:
if (qIdx == howMany) {
printResults();
return;
}
Modyfikacja printResults()
Największe zmiany czekają nas właśnie w funkcji printResults(). Wiersz 84 w administer.html, wyglądający obecnie tak:
'<BR><BR><FONT SIZE=4>Oto Twoje oceny: </FONT><BR><BR>';
zmieńmy na:
'<BR><BR><FoONT SIZE=4>Oto jak odpowiadałeś: </FONT><BR><BR>' +
'FORM ACTION="URL_twojego_skryptu_serwerowego" METHOD=POST>';
Zastąpmy też wierze 92 do 105 w sposób następujący:
results += '<INPUT TYPE=HIDDEN NAME="question' + (i + 1) +
'"VALUE="' + keeper[i] + '"><B><I><FONT COLOR=GREEN>Wybrałeś ' +
keeper[i] + '</I></B></FONT><BR><BR><BR>';
W rezultacie usuwamy z funkcji decyzję, czy użytkownik odpowiedział prawidłowo, i wyświetlanie tekstu zielonego lub czerwonego. W końcu wiersz 107 wyglądający tak:
results += '\n\r</BODY></HTML>';
zmieńmy na:
results += '<INPUT TYPE=SUBMIT VALUE="Wyślij"> </FORM></BODY></HTML>';
Te drobne zmiany spowodowały dodanie początkowego i końcowego znacznika FORM, pola ukrytego z odpowiedzią użytkownika jako wartością oraz przycisku SUBMIT. Znaczniki FORM oraz przycisk wysyłania są statyczne, pole ukryte zawiera coś więcej.
Każdą odpowiedź zapisują się jako wartość pola ukrytego, które nazywane jest stosownie do numeru pytania. Zmienna iteracji i używana jest do stworzenia niepowtarzalnej nazwy każdego pola ukrytego, przypisuje też odpowiedni numer pytania właściwej odpowiedzi użytkownika. W każdym przejściu pętli for wartość zmiennej i zwiększana jest o 1 (i++) i można utworzyć nowe pole ukryte. Pola te nazywają się question1, question2, question3 i tak dalej.
Po tych zmianach printResults() nadal wyświetla pytania, cztery możliwe odpowiedzi oraz odpowiedzi użytkownika. Jednak tym razem test nie jest tu oceniany. Użytkownik wciska przycisk SUBMIT, aby wysłać odpowiedzi do oceny.
Przekształcenie na ankietę
Jako że w ankietach teoretycznie nie ma odpowiedzi dobrych ani złych, przekształcenie naszej aplikacji na ankietę wymaga tych samych zmian, które przed chwilą pokazano, oraz jeszcze jednego - dostosowania treści. Po prostu należy zmienić elementy units tak, aby odpowiadały pytaniom ankiety z możliwymi opcjami wyboru, i gotowe. Dzięki takim zmianom użytkownik może obejrzeć wyniki przed wysłaniem ankiety do analizy.
Cechy aplikacji:
Prezentowane techniki:
|
3
Interaktywna |
|
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.
|
|
Rysunek 3.1. Slajd początkowy
Zwróć uwagę na to, że sam obrazek jest pośrodku, natomiast po lewej stronie na górze znajdują się dwa elementy graficzne: Automate i <Guide>. Strzałki w <Guide> (znaki < i >) pozwalają użytkownikowi przeglądać kolejne slajdy, do przodu lub wstecz.
Użytkownicy mogą też przeglądać slajdy kolejno, klikając samo Guide. Pokazuje się wtedy menu slajdów automatycznie przenoszące użytkownika do slajdu, nad którego nazwą pojawi się kursor. Ponowne kliknięcie Guide znów ukrywa wspomniane menu, pokazane na rysunku 3.2.
W poprzednich dwóch rozdziałach aplikacje działały od początku do końca: zawsze tak samo się zaczynały (na przykład wprowadzeniem tekstu lub odpowiedzeniem na pierwsze pytanie), podobnie również się kończyły (pokazaniem strony z wynikami wyszukiwania lub wyświetleniem wyniku testu). W przypadku slajdów mamy do czynienia z inną sytuacją: użytkownik może dowolnie się przemieszczać i korzystać z aplikacji. Lepiej jest zatem opisywać kod aplikacji w kontekście pełnionych przez niego funkcji, zamiast opisywać go w kolejności zapisu. Tak właśnie skonstruowany jest ten rozdział.
|
|
Rysunek 3.2. Podświetlona nazwa oznacza właśnie pokazywany slajd
Wymagania programu
Kiedy tylko zauważasz literę „D” w DHTML, to już wiesz, że mówisz o MSIE 4.x, Navigatorze 4.x i nowszych. Wszystkie slajdy to encje oparte na DHTML. Mógłbyś umieścić w prezentacji nawet setki obrazków, ale problemem może być wydajność systemu. Aplikacja ta zawczasu ładuje wszystkie obrazki (poza dwoma małymi), więc wątpię, czy będziesz chciał poświęcić czas na ładowanie tylu obrazków.
Struktura progamu
Cały skrypt zawarty jest w jednym pliku, index.html. Można go znaleźć w katalogu ch03 w pliku ZIP. Kod pokazano w przykładzie 3.1.
Przykład 3.1. index.html
1 <HTML>
2 <HEAD>
3 <TITLE>Pokaz slajdów</TITLE>
4
5 <STYLE TYPE="text/css">
6 #menuConstraint { height: 800; }
7 </STYLE>
8
9 <SCRIPT LANGUAGE="JavaScript1.2">
10 <!--
11 var dWidLyr = 450;
12 var dHgtLyr = 450;
13 var curSlide = 0;
14 var zIdx = -1;
15 var isVis = false;
16
17 var NN = (document.layers ? true : false);
18 var sWidPos = ((NN ? outerWidth : screen.availWidth) / 2) -
19 (dWidLyr / 2);
20 var sHgtPos = ((NN ? outerHeight : screen.availHeight) / 2) -
21 (dHgtLyr / 2);
22 var hideName = (NN ? 'hide' : 'hidden');
23 var showName = (NN ? 'show' : 'visible');
24
25 var img = new Array();
26 var imgOut = new Array();
27 var imgOver = new Array();
28 var imgPath = 'images/';
29
30 var showSpeed = 3500;
31 var tourOn = false;
32
33 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
34 if (NN) {
35 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
36 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
37 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + (++zIdx) + '>' +
38 copy + '</LAYER>');
39 }
40 else {
41 document.writeln('<DIV ID="' + sName +
42 '" STYLE="position:absolute; overflow:none; left:' + sLeft +
43 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' + sHgt +
44 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) + '">' +
45 copy + '</DIV>');
46 }
47 }
48
49 function slide(imgStr, scientific, copy) {
50 this.name = imgStr;
51 imagePreload(imgStr);
52 this.copy = copy;
53 this.structure =
54 '<TABLE WIDTH=500 CELLPADDING=10><TR><TD WIDTH=60% VALIGN=TOP>' +
55 '<IMG SRC=' + imgPath + imgStr + '.gif></TD>' +
56 '<TD WIDTH=40% VALIGN=TOP><H2>Nazwa potoczna:</H2><H2><I>' +
Przykład 3.1. index.html (ciąg dalszy)
57 camelCap(imgStr) + '</I></H2><H3>Nazwa naukowa: </H3><H3><I>' +
58 scientific + '</I></H3>' + '<B>Krótki opis:</B><BR>' + copy +
59 '</TD></TR></TABLE>';
60
61 return this;
62 }
63
64 function imagePreLoad(imgStr) {
65 img[img.length] = new Image();
66 img[img.length - 1].src = imgPath + imgStr + '.gif';
67
68 imgOut[imgOut.length] = new Image();
69 imgOut[imgOut.length - 1].src = imgPath + imgStr + 'out.gif';
70
71 imgOver[imgOver.length] = new Image();
72 imgOver[imgOver.length - 1].src = imgPath + imgStr + 'over.gif';
73 }
74
75 var slideShow = new Array(
76 new slide('bird', 'Bomb-zis Car-zes', 'Ptak - to skrzydlate stworzenie ' +
77 znane jest z wyszukiwania i paskudzenia świeżo umytych samochodów.'),
78 new slide('walrus', 'Verius Clueless', 'Tłuścioch mors to niezły rybak, ' +
79 ale mycie zębów to już inna historia.'),
80 new slide('gator', 'Couldbeus Luggajus', 'Aligator to gadzina często będąca ' +
81 maskotką podczas lokalnych zawodów sportowych.'),
82 new slide('dog', 'Makus Messus', 'Pies to najlepszy przyjaciel człowieka? ' +
83 'No to nie dziw, że te ssaczyny mają taką złą reputację.'),
84 new slide('pig', 'Oinkus Lotsus', 'Świnia - za takowe często są uważane ' +
85 'osoby o wątpliwych manierach przy jedzeniu.'),
86 new slide('snake', 'Groovius Dudis', 'Wąż jest śliskim i podstępnym ' +
87 'stworzeniem pilnie dookoła się rozglądającym.'),
88 new slide('reindeer', 'Redius Nosius', 'Renifer - choć jego kompani ' +
89 'zeń się śmieją i go przezywają, to jednak zdobył sobie należny ' +
90 'szacunek.'),
91 new slide('turkey', 'Goosius Is Cooktis', 'Indyk w Ameryce przez cały rok ' +
92 'otaczany powszechną opieką, ale potem podawany na obiad.'),
93 new slide('cow', 'Gotius Milkus', 'Zwierzę o dość wątpliwej reputacji. ' +
94 'Wykorzystuje do cna wszelkie napotkane stworzenia. Wyjątkowo paskudna ' +
95 'postać.'),
96 new slide('crane', 'Whooping It Upus', 'Żurawia nie da się pomylić ' +
97 'z maszyną budowlaną o tej samej nazwie. Mówi się, że jest on źródłem ' +
98 'terminu <I>ptasia noga</I>.')
99 );
100
101 function camelCap(str) {
102 return str.substring(0, 1).toUpperCase() + str.substring(1);
103 }
104
105 function genScreen() {
106 var menuStr = '';
107 for (var i = 0; i < slideShow.length; i++) {
108 genLayer('slide' + i, sWidPos, 45, dWidLyr, dHgtLyr,
109 (i == 0 ? showName : hideName), slideShow[i].structure);
110 menuStr += '<A HREF="" onMouseOver="hideStatus(); if(!tourOn)
Przykład 3.1. index.html (ciąg dalszy)
111 { setSlide(' + i + ');' +
112 ' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};' +
113 ' return true;"' +
114 ' onMouseOut="hideStatus(); if(!tourOn) { setSlide(' + i + ');' +
115 ' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', false)}; ' +
116 ' return true;"' +
117 ' onClick="return false;"><IMG NAME="' + slideShow[i].name +
118 '" SRC="' + imgPath + slideShow[i].name +
119 'out.gif" BORDER=0></A><BR>';
120 }
121
122 genLayer('automation', sWidPos - 100, 11, 100, 200, true,
123 '<A HREF="javascript: autoPilot();" onMouseOver="hideStatus(); ' +
124 'return true;">' +
125 '<IMG SRC="images/automate.gif" BORDER=0></A>'
126 );
127
128 genLayer('guide', sWidPos - 100, 30, 100, 200, true,
129 '<A HREF="javascript: if(!tourOn) { changeSlide(-1); }" ' +
130 'onMouseOver="hideStatus(); return true;">' +
131 '<IMG SRC="images/leftout.gif" BORDER=0></A>' +
132 '<A HREF="javascript: if(!tourOn) { menuManager(); }" ' +
133 'onMouseOver="hideStatus(); return true;">' +
134 '<IMG SRC="images/guideout.gif" BORDER=0></A>' +
135 '<A HREF="javascript: if(!tourOn) { changeSlide(1); }" ' +
136 'onMouseOver="hideStatus(); return true;">' +
137 '<IMG SRC="images/rightout.gif" BORDER=0></A></DIV>'
138 );
139
140 genLayer('menu', sWidPos - 104, 43, 100, 200, false,
141 '<DIV ID="menuConstraint"><TABLE><TD>' +
142 menuStr + '</TD></TABLE></DIV>'
143 );
144 }
145
146 function refSlide(name) {
147 if (NN) { return document.layers[name]; }
148 else { return eval('document.all.' + name + '.style'); }
149 }
150
151 function hideSlide(name) {
152 refSlide(name).visibility = hideName;
153 }
154
155 function showSlide(name) {
156 refSlide(name).visibility = showName;
157 }
158
159 function menuManager() {
160 if (isVis) { hideLayer('menu'); }
161 else { showLayer('menu'); }
162 isVis = !isVis;
163 }
164
165 function changeSlide(offset) {
166 hideLayer('slide' + curSlide);
167 curSlide = (curSlide + offset < 0 ? slideShow.length - 1 :
168 (curSlide + offset == slideShow.length ? 0 : curSlide + offset));
169 showSlide('slide' + curSlide);
170 }
171
172 function setSlide(ref) {
173 if (tourOn) { return; }
174 hideSlide('slide' + curSlide);
Przykład 3.1. index.html (dokończenie)
175 curSlide = ref;
176 showSlide('slide' + curSlide);
177 }
178
179 function imageSwap(imagePrefix, imageIndex, isOver) {
180 if (isOver) { document[imagePrefix].src = imgOver[imageIndex].src; }
181 else { document[imagePrefix].src = imgOut[imageIndex].src; }
182 }
183
184 function hideStatus() { window.status = ''; }
185
186 function autoPilot() {
187 if (tourOn) {
188 clearInterval(auto);
189 imageSwap(slideShow[curSlide].name, curSlide, false);
190 }
191 else {
192 auto = setInterval('automate()', showSpeed);
193 imageSwap(slideShow[curSlide].name, curSlide, true);
194 showSlide('menu');
195 visible = true;
196 }
197 tourOn = !tourOn;
198 }
199
200 function automate() {
201 imageSwap(slideShow[curSlide].name, curSlide, false);
202 changeSlide(1);
203 imageSwap(slideShow[curSlide].name, curSlide, true);
204 }
205
206 //-->
207 </SCRIPT>
208 </HEAD>
209 <BODY BGCOLOR=WHITE>
210 <CENTER>
211 <FONT FACE=Arial>
212 <H2>Prezentacja królestwa zwierząt</H2>
213 </FONT>
214 </CENTER>
215 <SCRIPT LANGUAGE="JavaScript1.2">
216 <!--
217 genScreen();
218 //-->
219 </SCRIPT>
220 </FONT>
221 </BODY>
222 </HTML>
Zmienne
Najpierw przyjrzyjmy się zmiennym i innym szczegółom, później przejdziemy do funkcji. Oto wiersze 5-7:
<STYLE TYPE="text/css">
#menuConstraint { height: 800; }
</STYLE>
W ten sposób definiuje się arkusz stylów o nazwie menuConstraint, z jedną tylko właściwością, określającą wysokość na 800 pikseli. Arkusz ten stosowany jest do wszystkich tworzonych slajdów, dzięki czemu użytkownicy będą mieli na pewno dość miejsca na obejrzenie slajdów. Innymi słowy, jeśli użytkownik ustawił rozdzielczość monitora mniejszą niż 800 pikseli, ten arkusz stylów wymusi dodanie pionowych pasków przewijania. Jest to szczególnie przydatne, gdy nasze obrazki są wysokie lub jeśli mamy ich dużo. Użytkownicy będą przynajmniej mogli zastosować przewijanie do obejrzenia całości. Wiersze 11-31 zawierają zmienne:
var dWidLyr = 450;
var dHgtLyr = 450;
var curSlide = 0;
var zIdx = -1;
var isVis = false;
var NN = (document.layers ? true : false);
var sWidPos = ((NN ? outerWidth : screen.availWidth) / 2) -
(dWidLyr / 2);
var sHgtPos = ((NN ? outerHeight : screen.availHeight) / 2) -
(dHgtLyr / 2);
var hideName = (NN ? 'hide' : 'hidden');
var showName = (NN ? 'show' : 'visible');
var img = new Array();
var imgOut = new Array();
var imgOver = new Array();
var imgPath = 'images/';
var showSpeed = 3500;
var tourOn = false;
Zmienne podzielono na cztery grupy:
właściwości warstw DHTML,
zmienne związane z obsługą poszczególnych przeglądarek,
zmienne związane z obrazkami,
zmienne pokazu.
Ustawienia domyślne warstwy DHTML
Zmienne dWidLyr i dHgtLyr służą do zadeklarowania szerokości i wysokości slajdów. Zmienna curSilde zawsze zawiera indeks bieżącego slajdu w tablicy slajdów. Zmienna zIdx określa wymiar z dla każdej tworzonej warstwy, natomiast isVis zawiera wartość logiczną określającą, czy dana warstwa jest obecnie widoczna.
|
|
|
Do slajdów odwołuję się jako do warstw DHTML lub po prostu warstw. Nie należy mylić ich ze znacznikiem LAYER Netscape Navigatora. Innymi słowy warstwa to LAYER w Netscape Navigatorze, ale nie w Internet Explorerze. |
|
|
Zmienne związane z przeglądarkami
Następnych pięć zmiennych, NN, sWidPos, sHgtPos, showName i hideName to zmienne, których ustawienie zależny od przeglądarki, do której aplikacja jest załadowana. Zmienna NN jest w 17. wierszu ustawiana na true, jeśli istnieje właściwość layers obiektu document - czyli gdy mamy do czynienia z przeglądarką Netscape Navigator w wersji co najmniej 4.x, gdyż model dokumentu tej przeglądarki taką właściwość rozpoznaje:
var NN = (document.layers ? true : false);
W innym przypadku skrypt zakłada, że ma do czynienia z przeglądarką Internet Explorer 4.x i ustawia NN na false. W modelu dokumentu Microsoftu do warstw odwoływać się należy w obiekcie styles obiektu document.all. Zmienne sWidPos i sHgtPos mają wartości współrzędnych x i y położenia lewego górnego rogu warstwy, która zostanie umieszczona pośrodku okna przeglądarki (nie ekranu). Zmienne te nie są ustawiane tylko na podstawie wartości NN, ale korzysta się też ze zmiennych dWidLyr i dHgtLyr. Oto wiersze 18-21:
var sWidPos = ((NN ? outerWidth : screen.availWidth) / 2) -
(dWidLyr / 2);
var sHgtPos = ((NN ? outerHeight : screen.availHeight) / 2) -
(dHgtLyr / 2);
Jak ustala się wartości współrzędnych x i y? Łatwo byłoby określić współrzędne środka, dzieląc odpowiednio szerokość i wysokość okna przeglądarki przez dwa.
Znamy współrzędne środka okna, tymczasem chcemy, żeby były to jednocześnie współrzędne środka poszczególnych warstw. Wystarczy zatem odjąć od współrzędnej x środka połowę wartości dWidLyr, a od współrzędnej y połowę wartości dHgtLyr.
Pozostałe dwie zmienne zawierają odpowiednie napisy oznaczające w danym modelu pokazanie lub ukrycie warstwy - wiersze 22 i 23:
var hideName = (NN ? 'hide' : 'hidden');
var showName = (NN ? 'show' : 'visible');
Zgodnie z DOM Netscape warstwy ukryte mają właściwość visibility ustawioną na hide, natomiast w DOM Microsoftu taka sama właściwość ustawiana jest na hidden. Analogicznie warstwy widoczne oznaczane są przez show i visible.
Zgodnie z DOM Netscape szerokość i wysokość okna (obiekt window) to innerWidth i innerHeight, natomiast Microsoft umieszcza te same wielkości w obiekcie screen, we właściwościach availWidth i availHeight. Jako że do obsłużenia tej sytuacji ustawia się zmienną NN, JavaScript rozpoznaje, do jakich właściwości ma się odwoływać.
Zmienne związane z obrazkami
Następna grupa zmiennych składa się z tablic służących do zarządzania obrazkami. Oto wiersze 25 do 28:
var img = new Array();
var imgOut = new Array();
var imgOver = new Array();
var imgPath = 'images/';
Rzecz jest dość prosta. Obrazki przechowywane w tablicy img to grafika slajdów. Umieszczone w imgOut używane są jako elementy menu slajdów. Obrazki w imgOver stosuje się do menu przewijania obrazków. Dokładniej zajmiemy się tym, kiedy omówimy funkcję swapImage().
Ostatnia zmienna, imgPath, określa ścieżkę dostępu do tych obrazków na serwerze sieciowym. Może to być ścieżka względna lub bezwzględna. Ścieżka bezwzględna zawiera pełną lokalizację plików wraz z nazwą serwera lub adresem IP (na przykład http://www.oreilly.com/), lub napęd lokalny (na przykład C:\), aż do katalogu z obrazkami. Oto dwa przykłady:
var imgPath = 'http://www.serve.com/hotsyte/';
var imgPath = 'C:\\Winnt\\Profiles\\Administrator\\Desktop\\';
W celu wstawienia w systemie Windows lewego ukośnika trzeba używać dwóch takich ukośników (\\). Jeśli tego nie zrobimy, JavaScript zrozumie zapis następująco:
var imgPath = 'C:\Winnt\Profiles\Administrator\Desktop\';
To nie tylko nieprawidłowy adres, ale w ogóle błąd składniowy.
Zmienne automatycznego pokazu
Ostatnie dwie zmienne, showSpeed i tourOn, określają szybkość zmieniania slajdów w przypadku autopokazu i informują, czy w ogóle funkcja ta została włączona. Oto wiersze 30 i 31:
var showSpeed = 3500;
var tourOn = false;
Zmienna showSpeed podawana jest w milisekundach. Można zwiększyć czas pokazywania jednego slajdu na przykład do 10 sekund, ustawiając wartość 10000. Można też zrealizować błyskawiczne przewijanie, ustawiając wartość 10. Kiedy załadowana zostanie pierwsza strona, autopokaz nie jest domyślnie uruchamiany, zatem tourOn ustawiana jest na false.
Funkcje aplikacji
Funkcje naszego pokazu slajdów podzielić można na trzy grupy: tworzenie warstw, obsługę obrazków oraz nawigację/wyświetlanie. W tabeli 3.1 krótko opisano wszystkie funkcje i zaznaczono, do której grupy należą.
Tabela 3.1. Funkcje i ich opis
Nazwa funkcji |
Grupa |
Opis |
genLayer() |
warstwy |
generuje slajdy |
slide() |
warstwy |
konstruktor obiektu slajdu |
imagePreLoad() |
obrazki |
ładuje wstępnie grafikę slajdu i pasek nawigacyjny |
camelCap() |
warstwy |
zmienia pierwszą literę nazwy slajdu na wielką |
genScreen() |
warstwy |
wywołuje genLayer() i pozycjonuje wszystkie warstwy |
hideSlide() |
warstwy |
ukrywa warstwy |
showSlide() |
warstwy |
pokazuje warstwy |
refSlide() |
warstwy |
zwraca wskaźnik warstwy w zależności od stosowanej przeglądarki |
menuManager() |
warstwy |
ukrywa i pokazuje menu slajdów |
changeSlide() |
warstwy |
zmienia aktualny slajd w przypadku stosowania strzałek polecenia <Guide> lub autopokazu |
setSlide() |
warstwy |
zmienia bieżący slajd na podstawie zdarzeń obsługi myszy |
imageSwap() |
obrazki |
przewija obrazki na podstawie menu |
hideStatus() |
nawigacja |
ustawia wartość paska stanu na "" |
autoPilot() |
nawigacja |
steruje trybem autopokazu |
automate() |
nawigacja |
realizuje automatyczną zmianę slajdów |
|
|
|
Funkcje związane z warstwami
Jako że większa część pokazu realizowana jest przez funkcje warstw, warto od nich zacząć.
genLayer()
Funkcja ta stanowi podstawę obsługi DHTML w różnych przeglądarkach. Cokolwiek wyświetlamy w ramach pokazu, niezależnie od tego, jak duże, małe, wielokolorowe czy urozmaicone są pokazywane slajdy - zawsze przechodzi właśnie tędy. Przyjrzyjmy się dokładnie wierszom 33-47:
function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
if (NN) {
document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
' VISIBILITY="' + sVis + '"' + ' z-Index=' + (++zIdx) + '>' +
copy + '</LAYER>');
}
else {
document.writeln('<DIV ID="' + sName +
'" STYLE="position:absolute; overflow:none; left:' + sLeft +
'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' + sHgt +
'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) + '">' +
copy + '</DIV>');
}
}
Funkcja ta w zasadzie zawiera pojedynczą instrukcję if-else. Tak naprawdę genLayer() realizuje w obu gałęziach tej instrukcji to samo, tyle że odpowiedni kod działa albo w przeglądarce Netscape Navigator, albo w Internet Explorerze. Dopóki model dokumentu nie zostanie ujednolicony, tak będziemy musieli działać.
W wierszu 34 zmiennej NN używa się do określenia, czy przeglądarką użytkownika jest Netscape Navigator, czy (zapewne) Microsoft Internet Explorer. Jeśli NN równa jest true, przeglądarką jest Netscape Navigator.
Zwróćmy uwagę na to, jakich argumentów oczekuje się w wierszu 33. S¹ to sName, sLeft, sTop, sWdh, sHgt, sVis i copy. Niezależnie od stosowanej przeglądarki wszystkie one mają takie samo znaczenie. SName to nazwa, jaką chcemy nadać warstwie. sLeft to liczba pikseli od lewego brzegu ekranu do warstwy, a sTop to odległość w pikselach od górnego brzegu. sWdh i sHgt stanowi odpowiednio szerokość i wysokość warstwy. sVis to true lub false, informujące, czy warstwa jest widoczna. copy zawiera napis, który ma być wyświetlony jako zawartość warstwy. Treść jest w zasadzie kodem HTML, ale może mieć również postać zwykłego tekstu.
Niezależnie od tego, jaka przeglądarka jest używana, genLayer() wywołuje metodę document. writeln() i tworzy w Netscape Navigatorze znacznik LAYER, a w Internet Explorerze znacznik DIV.
slide()
Funkcja slide() to konstruktor obiektu. Poszczególne wystąpienia slide() mogą zawierać ważne szczegóły dotyczące poszczególnych slajdów: nazwę zwierzęcia, tekst opisowy i treść HTML. Przyjrzyjmy się wierszom 49 do 62:
function slide(imgStr, scientific, copy) {
this.name = imgStr;
imagePreload(imgStr);
this.copy = copy;
this.structure =
'<TABLE WIDTH=500 CELLPADDING=10><TR><TD WIDTH=60% VALIGN=TOP>' +
'<IMG SRC=' + imgPath + imgStr + '.gif></TD>' +
'<TD WIDTH=40% VALIGN=TOP><H2>Nazwa potoczna:</H2><H2><I>' +
camelCap(imgStr) + '</I></H2><H3>Nazwa naukowa: </H3><H3><I>' +
scientific + '</I></H3>' + '<B>Krótki opis:</B><BR>' + copy +
'</TD></TR></TABLE>';
return this;
}
|
Techniki języka JavaScript: 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) { // Mamy do czynienia z MSIE // Użyj odpowiedników Jscript // np. document.all.styles, itd. } else { { // teraz NN // trzymamy się JavaScriptu // np. document.layers, itd. } |
|
Funkcja slide() pobiera trzy argumenty: imgStr, scientific i copy. imgStr to nazwa „dzikiego” zwierzęcia opisanego na slajdzie, jest to zarazem z wielu punktów widzenia rdzeń slajdu. Teraz jest właśnie dobry moment na omówienie konwencji nazewniczych stosowanych w tej aplikacji. Nazwę związaną z obiektem slide określa się w wierszu 50:
this.name = imgStr;
imgStr pojawia się jeszcze w kilku miejscach. Zajrzyjmy do wierszy 53-59, gdzie ustawiana jest właściwość structure slajdu:
this.structure =
'<TABLE WIDTH=500 CELLPADDING=10><TR><TD WIDTH=60% VALIGN=TOP>' +
'<IMG SRC=' + imgPath + imgStr + '.gif></TD>' +
'<TD WIDTH=40% VALIGN=TOP><H2>Nazwa potoczna:</H2><H2><I>' +
camelCap(imgStr) + '</I></H2><H3>Nazwa naukowa: </H3><H3><I>' +
scientific + '</I></H3>' + '<B>Krótki opis:</B><BR>' + copy +
'</TD></TR></TABLE>';
W celu dynamicznego tworzenia obrazków slajdów łączony jest znacznik HTML <IMG> ze zmiennymi imgPath i imgStr, po czym dopisuje się gif. Jeśli imgStr miała wartość pig, zapis obrazka będzie wyglądał następująco:
<IMG SRC='images/pig.gif'>
Właściwość structure określa treść slajdu jako tabelę HTML z jednym wierszem i dwiema komórkami. Lewa komórka zawiera obrazek, prawa natomiast opis. W wierszu 57 imgStr używana jest znów do określenia potocznej nazwy angielskiej zwierzęcia:
camelCap(imgStr)
Funkcja camelCap() z wierszy 88-90 po prostu zwraca przekazany jej napis z pierwszą literą zmienioną na wielką. Jest to związane z formatowaniem i po prostu poprawia wygląd całości. Warto zwrócić uwagę, że argument scientific zawiera nazwę naukową zwierzęcia. Oczywiście po przeczytaniu tych nazw naukowych możesz dojść do wniosku, że naukowcy nie są zanadto poważnymi ludźmi...
Kiedy wydaje się, że imgStr została już zupełnie wykorzystana, slide() przekazuje ją do funkcji preLoadImages() (wiersz 51). Ta funkcja z kolei ładuje wstępnie wszystkie obrazki slajdów i wkrótce się nią zajmiemy.
|
||||||
Techniki języka JavaScript: 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 |
nazwa zwierza |
obrazek slajdu |
obrazek menu |
obrazek menu pod kursorem |
|
|
pig |
pig |
pig.gif |
pigout.gif |
pigover.gif |
|
|
cow |
cow |
cow.gif |
cowout.gif |
cowover.gif |
|
|
snake |
snake |
snake.gif |
snakeout.gif |
snakeover.gif |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
genScreen()
Funkcja genScreen() korzysta z możliwości tworzenia warstw przez aplikację do pokazywania treści na ekranie. Jest to funkcja ze zdecydowanie największą ilością kodu. genScreen() nie tylko decyduje o tworzeniu slajdów i ich pozycjonowaniu, ale definiuje też nawigację. Oto zawierające ją wiersze 105 do 144:
function genScreen() {
var menuStr = '';
for (var i = 0; i < slideShow.length; i++) {
genLayer('slide' + i, sWidPos, 45, dWidLyr, dHgtLyr,
(i == 0 ? showName : hideName), slideShow[i].structure);
menuStr += '<A HREF="" onMouseOver="hideStatus(); if(!tourOn)
{ setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};' +
' return true;"' +
' onMouseOut="hideStatus(); if(!tourOn) { setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', false)}; ' +
' return true;"' +
' onClick="return false;"><IMG NAME="' + slideShow[i].name +
'" SRC="' + imgPath + slideShow[i].name +
'out.gif" BORDER=0></A><BR>';
}
genLayer('automation', sWidPos - 100, 11, 100, 200, true,
'<A HREF="javascript: autoPilot();" onMouseOver="hideStatus(); ' +
'return true;">' +
'<IMG SRC="images/automate.gif" BORDER=0></A>'
);
genLayer('guide', sWidPos - 100, 30, 100, 200, true,
'<A HREF="javascript: if(!tourOn) { changeSlide(-1); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/leftout.gif" BORDER=0></A>' +
'<A HREF="javascript: if(!tourOn) { menuManager(); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/guideout.gif" BORDER=0></A>' +
'<A HREF="javascript: if(!tourOn) { changeSlide(1); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/rightout.gif" BORDER=0></A></DIV>'
);
genLayer('menu', sWidPos - 104, 43, 100, 200, false,
'<DIV ID="menuConstraint"><TABLE><TD>' +
menuStr + '</TD></TABLE></DIV>'
);
}
Właśnie ta funkcja jest odpowiedzialna za tworzenie wszystkich warstw slajdów i trzech warstw dodatkowych, obsługujących nawigację (jedną dla menu slajdów, jedną dla obrazków <Guide> i jedną dla obrazka Automate). Pętla for z wierszy 106-120 tworzy warstwy i generuje treść warstwy menu:
var menuStr = '';
for (var i = 0; i < slideShow.length; i++) {
genLayer('slide' + i, sWidPos, 45, dWidLyr, dHgtLyr,
(i == 0 ? showName : hideName), slideShow[i].structure);
menuStr += '<A HREF="" onMouseOver="hideStatus(); if(!tourOn)
{ setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};' +
' return true;"' +
' onMouseOut="hideStatus(); if(!tourOn) { setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', false)}; ' +
' return true;"' +
' onClick="return false;"><IMG NAME="' + slideShow[i].name +
'" SRC="' + imgPath + slideShow[i].name +
'out.gif" BORDER=0></A><BR>';
}
Pętla przechodzi kolejno po wszystkich elementach tablicy slideShow, tworząc za każdym razem warstwę slajdu przez wywołanie genLayer(). Przyjrzyjmy się temu dokładniej:
genLayer('slide' + i, sWidPos, 45, dWidLyr, dHgtLyr,
(i == 0 ? showName : hideName), slideShow[i].structure);
Konieczne jest przekazanie całego pakietu parametrów. W tabeli 3.2 zestawiono i opisano wszystkie te parametry.
|
Można mieć wątpliwości do proponowanego przez autora podejścia. O ile słuszne jest stosowanie jednego identyfikatora do odwoływania się do wszystkich spokrewnionych obiektów, to nie najlepszym pomysłem jest prezentowanie użytkownikowi tego identyfikatora jako nazwy opisowej. Dobrym przykładem jest omawiana aplikacja - zmiana angielskich nazw zwierząt na polskie oznaczałaby konieczność zmienienia nazw wszystkich plików graficznych lub przebudowę aplikacji (dodanie funkcji kodującej polską nazwę zwierzęcia na nazwę angielską, na podstawie której dopiero można określać nazwy plików). Oczywiście najprostszym rozwiązaniem byłoby dodanie do obiektu zwierzęcia dodatkowej nazwy i pokazywanie jej zamiast nazwy angielskiej. Nawet przy pisaniu nowej aplikacji stosowanie od razu polskich nazw zwierząt jako identyfikatorów jest nieciekawym pomysłem. Nazwa polska musiałby także być zawarta w nazwie pliku graficznego - na przykład świnia.gif. Pułapka polega na tym, że w systemie Windows używane jest kodowanie polskich liter Windows 1250, we wszelkich Uniksach (łącznie z Linuksem) stosowany jest standard ISO 8859-2, zatem nazwy plików nie są przenośne między tymi systemami (świnia.gif zmieni się na przykład w œwinia.gif). Pamiętajmy, że większość serwerów sieciowych to serwery uniksowe. Jeśli nawet nie przeszkadza komuś taka dziwna nazwa pliku, to i tak jeszcze nie koniec problemów. W Internecie obowiązuje kodowanie polskich znaków zgodnie z ISO 8859-2 (obecnie upowszechniający się standard XML w ogóle nie obsługuje Windows 1250), jeśli zatem chcemy wyświetlić użytkownikowi nazwę zakodowaną inaczej, na ekranie pojawić się mogą „krzaczki”. Jeśli zmienimy kodowanie nazwy, to z kolei nie będzie ona zgodna z nazwami plików graficznych! Nasuwają się więc dwa wnioski:
|
|
|
Tabela 3.2. Parametry genLayer()
Wartość |
Opis |
'slide' + i |
Tworzy niepowtarzalną, indeksowaną nazwę każdego slajdu, na przykład slide0, slide1 i tak dalej. |
sWidPos |
Odległość od lewego brzegu okna w pikselach. |
sHgtPos |
Odległość w pikselach od górnego brzegu okna. |
dWidLyr |
Domyślna szerokość slajdu, w tym wypadku 450. |
dHgtLyr |
Domyślna wysokość slajdu, w tym wypadku 450. |
(i == 0 ? true : false) |
Sprawdza, czy slajd jest pokazany (true), czy schowany (false). Początkowo schowane są wszystkie slajdy poza pierwszym (kiedy i równe jest 0). |
slideshow[i].structure |
Treść slajdu, tekst i grafika, wstawione do tabeli. Pochodzi z konstruktora slajdu (wiersze 54-59). |
|
|
Funkcja genLayer() wywoływana jest tyle razy, ile wynosi wartość wyrażenia slideShow.length - warstwa tworzona jest dla każdego slajdu. Nie ma znaczenia, czy slajdów jest 6, czy 106 - wszystkie obsługiwane są tak samo, w tym jednym wierszu. Co ciekawe, cała reszta kodu genScreen() służy do uzyskania trzech dodatkowych warstw. Zanim jednak przejdziemy dalej, przypatrzmy się jeszcze pętli for:
menuStr += '<A HREF="" onMouseOver="hideStatus(); if(!tourOn)
{ setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};' +
' return true;"' +
' onMouseOut="hideStatus(); if(!tourOn) { setSlide(' + i + ');' +
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', false)}; ' +
' return true;"' +
' onClick="return false;"><IMG NAME="' + slideShow[i].name +
'" SRC="' + imgPath + slideShow[i].name +
'out.gif" BORDER=0></A><BR>';
Tutaj zaczęliśmy od wiersza 110, ale zmienna menuStr wcześniej była zainicjalizowana jako ciąg pusty, a teraz jej wartością będzie HTML z kodem służącym do wyświetlania par obrazków uaktywniających się, kiedy znajduje się nad nimi wskaźnik myszki. Spojrzyjmy na rysunek 3.2, aby sprawdzić, jak działa to menu.
W przypadku każdego slajdu menuStr rozszerza swoją wartość o obrazek odpowiadający danemu slajdowi. Zanim zaczniemy wyszukiwać pojedyncze i podwójne cudzysłowy, zastanówmy się, co jest potrzebne do każdej pary obrazków menu:
Otwierający znacznik <A HREF>.
Kod obsługi zdarzenia onMouseOver, reagujący na najechanie przez użytkownika kursorem myszy nad obrazek.
Kod obsługi zdarzenia onMouseOut, reagujący na opuszczenie przez kursor myszy obrazka.
Kod obsługi zdarzenia onClick, mający zapobiec reakcji programu na kliknięcie obrazka z menu przez użytkownika.
Znacznik <IMG> z niepowtarzalnymi wartościami atrybutów NAME i SRC.
Zamykający znacznik </A>.
Pozycja 1. jest prosta: po prostu należy ją wpisać.
Pozycja 2. jest troszkę trudniejsza. Aby usunąć uciążliwy tekst w pasku stanu, najpierw przypisywana jest mu wartość pusta przez wywołanie funkcji hideStatus(). Tę jednowierszową funkcję znajdziemy w wierszu 184.
Następnie - jeśli użytkownik nie ogląda slajdów w trakcie automatycznego pokazu - wywoływana jest funkcja setSlide() (wkrótce będzie omawiana). Warto zapamiętać, że dostaje ona jako parametr wartość i.
Pozycja 3. wymaga tego, co obsługa zdarzenia onMouseOver, tyle tylko, że nie jest konieczne wywoływanie hideStatus(), gdyż pasek stanu jest już pusty. W końcu do imageSwap() -zamiast true - przekazywana jest wartość false.
Pozycja 4. to rzecz łatwa: po prostu należy dodać onClick="false". W ten sposób unika się wszelkich akcji, które mogłyby wynikać z klikania przez użytkownika.
Oto sposób na zrealizowanie pozycji 5.:
<IMG NAME="' + slideShow[i].name + '" SRC="' + imgPath +
slideShow[i].name + 'out.gif" BORDER=0>
Znacznik <IMG> uzyska niepowtarzalną nazwę z slideShow[i].name. slideShow[i].name jest też używane wraz ze zmienną imgPath i napisem out.gif do określenia nazwy źródła obrazka <IMG>.
Pozycja 6. to znów prosta sprawa: należy dodać na końcu znacznik <BR> - i gotowe.
Do wartości zmiennej menuStr dodawany jest napis pochodzący ze wspomnianego wcześniej kodu pętli for.
Co się teraz dzieje z menuStr? Jako że menuStr zawiera kod HTML i JavaScript opisujący menu slajdów, przekazywana jest jako argument funkcji genLayer() w wierszach 140-143:
genLayer('menu', sWidPos - 104, 43, 100, 200, false,
'<DIV ID="menuConstraint"><TABLE><TD>' +
menuStr + '</TD></TABLE></DIV>'
);
To wywołanie zostawiłem na koniec, gdyż pozostałe dwie warstwy nawigacyjne umieszczone są nad menu i wydawało mi się, że kodowanie w takiej kolejności będzie sensowniejsze. Zwróćmy uwagę na sposób użycia znacznika <DIV> z atrybutem ID ustawionym na menuConstraint. Dzięki temu zagwarantowana jest wysokość pokazu slajdów wynosząca 800 pikseli.
Musimy jeszcze dwa razy odwołać się do genLayer(): pierwszy raz po to, aby wywołać rysunek pozwalający uruchomić i zatrzymać autopokaz, drugi raz w celu umożliwienia przesuwania się strzałkami po slajdach do przodu i do tyłu. Niewiele potrzeba do stworzenia warstwy autopilota - wiersze 122-126:
genLayer('automation', sWidPos - 100, 11, 100, 200, true,
'<A HREF="javascript: autoPilot();" onMouseOver="hideStatus(); '+
'return true;">' +
'<IMG SRC="images/automate.gif" BORDER=0></A>'
);
Właściwie widzieliśmy już niemalże wszystko, co było do pokazania. Do wywołania funkcji autoPilot() w atrybucie HREF użyty zostanie protokół javascript:, procedura obsługi zdarzenia onMouseOver wywołuje hideStatus(). Przyszedł czas na to, aby stanąć przed jakimś trudniejszym wyzwaniem, więc przyjrzyjmy się kodowi ostatniej warstwy. W celu jej utworzenia w wierszach 128-138 wywoływana jest funkcja genLayer(). Zawiera trzy obrazki: dwie strzałki oraz słowo Guide, co daje razem <Guide>:
genLayer('guide', sWidPos - 100, 30, 100, 200, true,
'<A HREF="javascript: if(!tourOn) { changeSlide(-1); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/leftout.gif" BORDER=0></A>' +
'<A HREF="javascript: if(!tourOn) { menuManager(); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/guideout.gif" BORDER=0></A>' +
'<A HREF="javascript: if(!tourOn) { changeSlide(1); }" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/rightout.gif" BORDER=0></A></DIV>'
);
Kod obsługi wszystkich obrazków jest niemalże identyczny. Znów w obsłudze obrazków jest mnóstwo kodu, ale tym razem kliknięcie na lewą i prawą strzałkę warunkowo wywołuje changeSlide(). Przekazanie -1 powoduje przesunięcie do slajdu poprzedniego, tymczasem wskazanie 1 powoduje przesunięcie do slajdu następnego. Samą changeSlide() omówimy wkrótce. Wszystko, co robi obrazek <Guide>, to pokazanie lub ukrycie menu slajdów, czym zajmuje się funkcja menuManager().
Zanim skończymy omawianie genScreen(), zwróćmy uwagę, że całość jest wywoływana między znacznikami <BODY> przed załadowaniem strony. Internet Explorer nie potrafi tworzyć warstw po załadowaniu dokumentu, więc musimy ją uruchomić wcześniej. Oto wiersze 215 do 219:
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genScreen();
//-->
</SCRIPT>
Elementy tablicy slideShow
Być może zauważyłeś już zmienną tablicową slideShow. Każdy jej element zawiera właściwości jednego obiektu slide. Oto tablica slideShow z wierszy 75-99. Mamy tutaj 10 elementów, co odpowiada 10 slajdom zwierząt:
var slideShow = new Array(
new slide('bird', 'Bomb-zis Car-zes', 'Ptak - to skrzydlate stworzenie ' +
znane jest z wyszukiwania i paskudzenia świeżo umytych samochodów.'),
new slide('walrus', 'Verius Clueless', 'Tłuścioch mors to niezły rybak, ' +
ale mycie zębów to już inna historia.'),
new slide('gator', 'Couldbeus Luggajus', 'Aligator to gadzina często będąca ' +
maskotką podczas lokalnych zawodów sportowych.'),
new slide('dog', 'Makus Messus', 'Pies to najlepszy przyjaciel człowieka? ' +
'No to nie dziw, że te ssaczyny mają taką złą reputację.'),
new slide('pig', 'Oinkus Lotsus', 'Świnia - za takowe często są uważane ' +
'osoby o wątpliwych manierach przy jedzeniu.'),
new slide('snake', 'Groovius Dudis', 'Wąż jest śliskim i podstępnym ' +
'stworzeniem pilnie dookoła się rozglądającym.'),
new slide('reindeer', 'Redius Nosius', 'Renifer - choć jego kompani ' +
'zeń się śmieją i go przezywają, to jednak zdobył sobie należny ' +
'szacunek.'),
new slide('turkey', 'Goosius Is Cooktis', 'Indyk w Ameryce przez cały rok ' +
'otaczany powszechną opieką, wkrótce po tym roku podawany na obiad.'),
new slide('cow', 'Gotius Milkus', 'Zwierzę o dość wątpliwej reputacji. ' +
'Wykorzystuje do cna wszelkie napotkane stworzenia. Wyjątkowo paskudna ' +
'postać.'),
new slide('crane', 'Whooping It Upus', 'Żurawia nie da się pomylić ' +
'z maszyną budowlaną o tej samej nazwie. Mówi się, że jest on źródłem ' +
'terminu <I>ptasia noga</I>.')
);
Porównajmy wartości przekazywane przy każdym wywołaniu konstruktora slide z oczekiwanymi argumentami. Pierwszy to nazwa zwierzęcia (i obrazka), drugi to nazwa formalna, a pojawi się w końcu dodatkowy opis. Zwróćmy uwagę na to, że w wierszu 98 do tego opisu dodano znaczniki HTML. Nie ma oczywiście żadnych przeciwwskazań do korzystania z takiego rozwiązania w slajdach (więcej informacji na ten temat znajdzie się w sekcji o ewentualnej rozbudowie aplikacji).
Jeśli nasza lista jest zbyt długa, dobrym pomysłem może być utworzenie osobnego pliku źródłowego JavaScriptu. W moim przykładzie tablica ma tylko 10 pozycji, więc zostawiłem wszystko razem.
Funkcje związane z obsługą obrazków
Teraz, kiedy mamy już gotowe funkcje obsługujące slajdy, zabierzmy się za analizę sposobu obsługi obrazków.
preLoadImages()
Ta funkcja umożliwia dokładnie to, co sugeruje jej nazwa: wstępnie ładuje obrazki. W kodzie znajduje się w wierszach od 64 do 73:
function imagePreLoad(imgStr) {
img[img.length] = new Image();
img[img.length - 1].src = imgPath + imgStr + '.gif';
imgOut[imgOut.length] = new Image();
imgOut[imgOut.length - 1].src = imgPath + imgStr + 'out.gif';
imgOver[imgOver.length] = new Image();
imgOver[imgOver.length - 1].src = imgPath + imgStr + 'over.gif';
}
Funkcja ta tworzy nowe obiekty Image i ładuje ich pliki źródłowe, po trzy naraz. Wprawdzie dzięki temu pokaz slajdów działa szybciej, ale użytkownicy będą za to musieli chwilę poczekać przed jego uruchomieniem.
Zmienne imgPath i imgStr łączone są ze sobą i z końcówkami .gif, out.gif i over.gif, dzięki czemu uzyskujemy pliki potrzebne nam do wybrania obrazków slajdów. Na przykład ładując slajd o nazwie cow, załadujemy obrazki cow.gif, cowout.gif i cowover.gif.
imageSwap()
Ta funkcja realizuje przewijanie obrazków, niezależnie od tego, czy użytkownik wywołuje je poprzez wskazanie myszką, czy dzieje się to podczas automatycznego pokazu. Nie jest to funkcja skomplikowana, a można ją znaleźć w wierszach 179-182:
function imageSwap(imagePrefix, imageIndex, isOver) {
if (isOver) { document[imagePrefix].src = imgOver[imageIndex].src; }
else { document[imagePrefix].src = imgOut[imageIndex].src; }
}
Wiele skryptów obsługujących tego typu przewijanie, ze skryptem na stronie autora włącznie, realizuje swoje zadanie w dwóch funkcjach: jednej - obsługującej zdarzenie onMouseOver, drugiej zdarzenie onMouseOut. Można obie operacje połączyć w jedną funkcję, trzeba tylko użyć dodatkowych parametrów.
Parametry imagePrefix, imageIndex i isOver oznaczają nazwę podstawową (znów imgStr), wskaźnik żądanego obrazka (wartość i z pętli for w funkcji genScreen()) oraz wartość logiczną wskazującą, czy użyć obrazków z tablicy imgOver, czy imgOut.
Aby rzecz nieco wyjaśnić, spójrzmy jeszcze raz na wiersze 105-120 w funkcji genScreen(). Zwróć uwagę, jak dynamiczny skrypt JavaScript generowany jest w wierszu 112:
' imageSwap(\'' + slideShow[i].name + '\', ' + i + ', true)};'
Kiedy wynik zostanie zapisany w dokumencie, a i równe jest 0, rzecz będzie wyglądać tak:
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: 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. |
|
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:
Ukrycia bieżącego slajdu.
Zdecydowania, jaki slajd ma być następny.
Pokazania wybranego w poprzednim kroku slajdu.
Kroki 1. i 3. są niewątpliwie oczywiste, ale krok 2. jest bardziej złożony, niż mogłoby się to wydawać. Istnieją dwie okoliczności, w których można zmienić slajd. Pierwsza sytuacja dotyczy zmiany slajdów w ustalonej kolejności (naprzód lub wstecz), do czego służą strzałki < i >. Natomiast drugi przypadek wiąże się z automatycznym przerzucaniem slajdów. Funkcja changeSlide() została tak napisana, aby potrafiła obsłużyć obie te sytuacje, a znajdziemy ją w wierszach 165-170:
function changeSlide(offset) {
hideLayer('slide' + curSlide);
curSlide = (curSlide + offset < 0 ? slideShow.length - 1 :
(curSlide + offset == slideShow.length ? 0 : curSlide + offset));
showSlide('slide' + curSlide);
}
Najpierw wywoływana jest hideSlide() z wyrażeniem 'slide' + curSlide jako parametrem. curSlide początkowo, w wierszu 13, ustawiona była na 0. Jako że obecnie jest to oglądany slajd, funkcja hideSlide ukryje slide0, czyli ptaka. Wydaje się to jasne. Który slajd ma być teraz pokazany?
Pamiętajmy, że changeSlide() jako parametru oczekuje przesunięcia, czyli liczby 1 lub -1. 1 oznacza przesunięcie wprzód, -1 cofnięcie się o jeden slajd, a w naszym wypadku wyświetlenie z kolei slajdu ostatniego. Ponieważ curSlide to liczba całkowita oznaczająca indeks bieżącego slajdu, dodanie jedynki zmieniać będzie tę wartość na 1, 2 i tak dalej. -1 powoduje pokazanie poprzedniego slajdu ze slideShow. Jeśli był nim slajd czwarty, o indeksie 3, wynikiem będą kolejno 2, 1, potem 0.
Wszystko jest dobrze, póki nie próbujemy ukryć slajdu 'slide' + -1 lub 'slide' + slideShow.length. Takie slajdy nie istnieją i można być pewnym pojawienia się błędów składniowych. Jak zatem uchronić się przed wartością curSlide mniejszą od zera lub większą od slideShow.length-1?
Odpowiedzią są wiersze 167 i 168:
curSlide = (curSlide + offset < 0 ? slideShow.length - 1 :
(curSlide + offset == slideShow.length ? 0 : curSlide + offset));
Wartość curSlide określana jest przy użyciu zagnieżdżonego operatora trójargumentowego. Oto pseudokod:
JEŚLI curSlide + przesunięcie JEST MNIEJSZE OD 0, TO
curSlide STAJE SIĘ RÓWNE slideShow.length - 1
W PRZECIWNYM WYPADKU
JEŚLI curSlide + przesunięcie JEST RÓWNE slideShow.length, TO
curSlide STAJE SIĘ RÓWNE 0
W PRZECIWNYM WYPADKU
curSlide STAJE SIĘ RÓWNE curSlide + przesunięcie
Kiedy wartość curSlide zostanie już określona, można spokojnie wywołać showSlide() w 169 wierszu.
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.
|
Techniki języka JavaScript: 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: <BODY onLoad="overAndOver()";> setInterval() sama realizuje rekursję i pozwala użyć jednego tylko wywołania: y = 50; function overAndOver() { //coś robimy y = Math.log(y); } Obsługa zdarzenia onLoad może także zrealizować ten kod. Należy się tylko upewnić, że operacja wyłączająca zawiera clearInterval(). |
|
automate()
automate() to mała funkcja uruchamiająca pokaz przez wykonanie następujących trzech czynności.
Symulacja zdarzenia onMouseOut powoduje usunięcie podświetlenia bieżącego slajdu w menu. Realizuje to funkcja imageSwap().
Wywołanie changeSlide() zmienia slajd na następny.
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);
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/dhtmlrefs.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:
Prezentowane techniki:
|
4 Interfejs multiwyszukiwarki |
|
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.
|
|
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).
Czytelnik śledzący rozdziały po kolei, może zauważyć, że tym razem postępujemy nieco inaczej niż dotąd, gdyż nie cały kod jest zupełnie nowy. Pokażę tym razem, jak skorzystać z kodu omówionego w rozdziale 3. Przykład ten doskonale pokaże, jak oszczędzać swój czas, wielokrotnie wykorzystując ten sam kod.
Wymagania programu
W tej aplikacji używamy DHTML, więc będą potrzebne przeglądarki Netscape Navigator lub Internet Explorer w wersji co najmniej 4.x. Włączyłem do gry 20 wyszukiwarek, ale można tę liczbę zwiększyć nawet do setek. Jednak typowego użytkownika zadowoli już zapewne liczba 20 wyszukiwarek. Pamiętajmy też, że ta aplikacja może działać na maszynie lokalnej, ale - jak pokaz slajdów - zbyt duża ilość grafiki zwiększy czas potrzebny do jej załadowania w przypadku użytkowników działających za pośrednictwem Internetu.
Struktura programu
Aplikacja ta zawiera dwa pliki: index.html i multi.html. Pierwszy z nich, pokazany jako przykład 4.1, używa zagnieżdżonych ramek w celu uzyskania efektu otaczającego obramowania.
Przykład 4.1. Index.html
1 <HTML>
2 <HEAD>
3 <TITLE>Witryna multiwyszukiwarki</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5 <!--
6 var black = '<BODY BGCOLOR=BLACK></BODY>';
7 var white = '<BODY BGCOLOR=WHITE></BODY>';
8 //-->
9 </SCRIPT>
10 </HEAD>
11 <FRAMESET ROWS="15,*,50" FRAMEBORDER=0 BORDER=0>
12 <FRAME SRC="javascript: parent.black;" SCROLLING=NO>
13 <FRAMESET COLS="15,*,15" FRAMEBORDER=0 BORDER=0>
14 <FRAME SRC="javascript: parent.black;" SCROLLING=NO>
15 <FRAME SRC="javascript: parent.white;" BORDER=0>
16 <FRAME SRC="javascript: parent.black;"
17 SCROLLING=NO>
18 </FRAMESET>
19 <FRAME SRC="multi.html" SCROLLING=NO>
20 </FRAMESET>
21 </HTML>
Dwie zmienne JavaScriptu black i white zdefiniowane w wierszach 6 i 7 zawierają kod HTML, który może zostać użyty jako wartość atrybutu SRC ramek. Zmienne te użyte są w wierszach 12 i 14-16. Omawialiśmy to w ramce opisującej techniki JavaScriptu w rozdziale 2. („Oszukany atrybut SRC”). Jeśli czytelnik śledzi rozdziały po kolei, powinien być teraz kopalnią wiedzy na ten temat. Jedyne ramki realizujące jakieś funkcje, to frames[2], która wyświetla wyniki wyszukiwania, oraz frames[4], która zawiera interfejs wyszukiwarek. Reszta służy tylko do pokazywania. Przejdźmy teraz do pliku multi.html, pokazanego w przykładzie 4.2.
Przykład 4.2. multi.html
1 <HTML>
2 <HEAD>
3 <TITLE>Multi-Engine Menu</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5 <!--
6
7 parent.frames[2].location.href = 'javascript: parent.white';
8
9 var NN = (document.layers ? true : false);
10 var curSlide = 0;
11 var hideName = (NN ? 'hide' : 'hidden');
12 var showName = (NN ? 'show' : 'visible');
13 var perLyr = 4;
14 var engWdh = 90;
15 var engHgt = 20;
16 var left = 375;
17 var top = 10;
18 var zIdx = -1;
19 var imgPath = 'images/';
20 var arrayHandles = new Array('out', 'over');
21
Przykład 4.2. multi.html (ciąg dalszy)
22 for (var i = 0; i < arrayHandles.length; i++) {
23 eval('var ' + arrayHandles[i] + ' = new Array()');
24 }
25
26 var engines = new Array(
27 newArray('HotBot',
28 'http://www.hotbot.com/?MT=',
29 'http://www.hotbot.com/'),
30 newArray('InfoSeek',
31 'http://www.infoseek.com/Titles?col=WW&sv=IS&lk=noframes&qt=',
32 'http://www.infoseek.com/'),
33 newArray('Yahoo',
34 'http://search.yahoo.com/bin/search?p=',
35 'http://www.yahoo.com/'),
36 newArray('AltaVista',
37 'http://www.altavista.com/cgi-bin/query?pg=q&kl=XX&q=',
38 'http://www.altavista.digital.com/'),
39 newArray('Lycos',
40 'http://www.lycos.com/cgi-bin/pursuit?matchmode=and&cat=lycos' +
41 '&query=',
42 'http://www.lycos.com/'),
43 newArray('Money.com',
44 'http://jcgi.pathfinder.com/money/plus/news/searchResults.oft?' +
45 'vcs_sortby=DATE&search=',
46 'http://www.money.com/'),
47 newArray('DejaNews',
48 'http://www.dejanews.com/dnquery.xp?QRY=',
49 'http://www.dejanews.com/'),
50 newArray('Insight',
51 'http://www.insight.com/cgi-bin/bp/870762397/web/result.html?' +
52 'a=s&f=p&t=A&d=',
53 'http://www.insight.com/'),
54 newArray('Scientific American',
55 'http://www.sciam.com/cgi-bin/search.cgi?' +
56 'searchby=strict&groupby=confidence&docs=100&query=',
57 'http://www.sciam.com/cgi-bin/search.cgi'),
58 newArray('Image Surfer',
59 'http://isurf.interpix.com/cgi-bin/isurf/keyword_search.cgi?q=',
60 'http://www.interpix.com/'),
61 newArray('MovieFinder.com',
62 'http://www.moviefinder.com/search/results/1,10,,00.html?' +
63 'simple=true&type=movie&mpos=begin&spat=',
64 'http://www.moviefinder.com/'),
65 newArray('Monster Board',
66 'http://www.monsterboard.com/pf/search/USresult.htm?' +
67 'loc=&EmploymentType=F&KEYWORDS=',
68 'http://www.monsterboard.com/'),
69 newArray('MusicSearch.com',
70 'http://www.musicsearch.com/global/search/search.cgi?QUERY=',
71 'http://www.musicsearch.com/'),
72 newArray('ZD Net',
73 'http://xlink.zdnet.com/cgi-bin/texis/xlink/xlink/search.html?' +
74 'Utext=',
75 'http://www.zdnet.com/'),
76 newArray('Biography.com',
77 'http://www.biography.com/cgi-bin/biomain.cgi?search=FIND&field=',
78 'http://www.biography.com/'),
79 newArray('Entertainment Weekly',
80 'http://cgi.pathfinder.com/cgi-bin/ew/cg/pshell? venue= pathfinder&q=',
81 'http://www.entertainmentweekly.com/'),
82 newArray('SavvySearch',
83 'http://numan.cs.colostate.edu:1969/nph-search?' +
84 'classic=on&Boolean=OR&Hits=10&Mode=MakePlan&df=normal&' +
85 'AutoStep=on&KW=',
Przykład 4.2. multi.html (ciąg dalszy)
86 'http://www.savvysearch.com/'),
87 newArray('Discovery Online',
88 'http://www.discovery.com/cgi-bin/searcher/-?' +
89 'output=title&exclude=/search&search=',
90 'http://www.discovery.com/'),
91 newArray('Borders.com',
92 'http://www.borders.com:8080/fcgi-bin/db2www/search/' +
93 'search.d2w/QResults?doingQuickSearch=1&srchPage=QResults&' +
94 'mediaType=Book&keyword=',
95 'http://www.borders.com/'),
96 newArray('Life Magazine',
97 'http://cgi.pathfinder.com/cgi-bin/life/cg/pshell?' +
98 'venue=life&pg=q&date=all&x=15&y=16&q=',
99 'http://www.life.com/')
100 );
101
102 engines = engines.sort();
103
104 function imagePreLoad(imgName, idx) {
105 for(var j = 0; j < arrayHandles.length; j++) {
106 eval(arrayHandles[j] + "[" + idx + "] = new Image()");
107 eval(arrayHandles[j] + "[" + idx + "].src = '" + imgPath +
108 imgName + arrayHandles[j] + ".jpg'");
109 }
110 }
111
112 function engineLinks() {
113 genLayer('sliderule', left - 20, top + 2, 25, engHgt, showName,
114 '<A HREF="javascript: changeSlide(1);" ' +
115 'onMouseOver="hideStatus(); return true;">' +
116 '<IMG SRC="images/ahead.gif" BORDER=0></A><BR>' +
117 '<A HREF="javascript: changeSlide(-1);" ' +
118 'onMouseOver="hideStatus(); return true;">' +
119 '<IMG SRC="images/back.gif" BORDER=0></A>');
120 lyrCount = (engines.length % perLyr == 0 ?
121 engines.length / perLyr : Math.ceil(engines.length / perLyr));
122 for (var i = 0; i < lyrCount; i++) {
123 var engLinkStr = '<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=0><TR>';
124 for (var j = 0; j < perLyr; j++) {
125 var imgIdx = (i * perLyr) + j;
126 if (imgIdx >= engines.length) { break; }
127 var imgName = nameFormat(engines[imgIdx][0]);
128 imagePreLoad(imgName, imgIdx);
129 engLinkStr += '<TD><A HREF="javascript: ' +
130 'callSearch(document.forms[0].elements[0].value, ' +
131 imgIdx + ');" ' + 'onMouseOver="hideStatus(); imageSwap(\'' +
132 imgName + '\', ' + imgIdx + ', 1); return true" ' +
133 'onMouseOut="imageSwap(\'' + imgName + '\', ' + imgIdx +
134 ', 0);">' + '<IMG NAME="' + imgName + '" SRC="' + imgPath +
135 imgName + "out.jpg" + '" BORDER=0></A></TD>';
136 }
137 engLinkStr += '</TR></TABLE>';
138 genLayer('slide' + i, left, top, engWdh, engHgt, hideName, engLinkStr);
139 }
140 }
141
142 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
143 if (NN) {
144 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
145 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
146 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + zIdx + '>' +
147 copy + '</LAYER>');
148 }
Przykład 4.2. multi.html (ciąg dalszy)
149 else {
150 document.writeln('<DIV ID="' + sName +
151 '" STYLE="position:absolute; overflow:none; left:' +
152 sLeft + 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' +
153 sHgt + 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) +
154 '">' + copy + '</DIV>');
155 }
156 }
157
158 function nameFormat(str) {
159 var tempArray = str.split(' ');
160 return tempArray.join('').toLowerCase();
161 }
162
163 function hideSlide(name) { refSlide(name).visibility = hideName; }
164
165 function showSlide(name) { refSlide(name).visibility = showName; }
166
167 function refSlide(name) {
168 if (NN) { return document.layers[name]; }
169 else { return eval('document.all.' + name + '.style'); }
170 }
171
172 function changeSlide(offset) {
173 hideSlide('slide' + curSlide);
174 curSlide = (curSlide + offset < 0 || curSlide + offset >= lyrCount ?
175 (curSlide + offset < 0 ? lyrCount - 1 : 0) : curSlide + offset);
176 showSlide('slide' + curSlide);
177 }
178
179 function imageSwap(imagePrefix, imageIndex, arrayIdx) {
180 document[imagePrefix].src = eval(arrayHandles[arrayIdx] +
181 "[" + imageIndex + "].src");
182 }
183
184 function callSearch(searchTxt, idx) {
185 if (searchTxt == "") {
186 parent.frames[2].location.href = engines[idx][2] +
187 escape(searchTxt);
188 }
189 else {
190 parent.frames[2].location.href = engines[idx][1] +
191 escape(searchTxt);
192 }
193 }
194
195 function hideStatus() { window.status = ''; }
196
197 //-->
198 </SCRIPT>
199
200 </HEAD>
201 <BODY BGCOLOR="BLACK" onLoad="showSlide('slide0');">
202 <SCRIPT LANGUAGE="JavaScript1.2">
203 <!--
204 engineLinks();
205 //-->
206 </SCRIPT>
207 <FORM onSubmit="return false;">
208 <TABLE CELLPADDING=0>
209 <TR>
210 <TD>
211 <FONT FACE=Arial>
212 <IMG SRC="images/searchtext.jpg">
213 </TD>
Przykład 4.2. multi.html (dokończenie)
214 <TD>
215 <INPUT TYPE=TEXT SIZE=25>
216 </TD>
217 </TR>
218 </TABLE>
219 </FORM>
220 </BODY>
221 </HTML>
Mamy tu ponad 200 wierszy kodu, ale większość tego kodu jest już czytelnikowi znana, nie powinno być więc z niczym problemu. Zacznijmy od wiersza 7:
parent.frames[2].location.href = 'javascript: parent.white';
Jeśli policzymy ramki w index.html, stwierdzimy, że frames[2] znajduje się tam, gdzie mają być wyniki wyszukiwania. Ustawienie w tej ramce właściwości location.href nieco upraszcza obsługę, jeśli zdecydujemy się powtórnie załadować aplikację. Jako dokument wynikowy załadowana zostanie jakaś lokalna strona HTML, więc nie będzie trzeba czekać na ponowne uzyskanie poprzednich wyników wyszukiwania.
Przy okazji warto zaznaczyć, choć we frames[2] masz ładnie pokazane wyniki wyszukiwania, to kiedy klikniesz któreś z łącz wynikowych, jesteś zdany na łaskę i niełaskę projektantów danej wyszukiwarki. Niektóre wyszukiwarki pokażą odpowiednią stronę w tej samej ramce, inne, wśród nich niestety InfoSeek, wymusi otwarcie dokumentu w głównym oknie przeglądarki.
Przechadzka Aleją Pamięci
Przejdźmy się teraz Aleją Pamięci (chodzi o RAM, jak łatwo się domyślić). Jeśli przyjrzymy się poniższym zmiennym, stwierdzimy, że niektóre z nich są nowe, ale część jest uderzająco podobna do tych, z którymi pracowaliśmy w rozdziale 3. Spójrzmy, mamy NN i curSlide! Są też hideName i showName, jak również imagePath i zIdx:
var NN = (document.layers ? true : false);
var curSlide = 0;
var hideName = (NN ? 'hide' : 'hidden');
var showName = (NN ? 'show' : 'visible');
var perLyr = 4;
var engWdh = 90;
var engHgt = 20;
var left = 375;
var top = 10;
var zIdx = -1;
var imgPath = 'images/';
var arrayHandles = new Array('out', 'over');
Zmienne te pełnią taką samą funkcję, jak w rozdziale 3. Jeśli chodzi o nowe zmienne, perLyr określa na przykład liczbę wyszukiwarek, które mają być wyświetlane na warstwie. Zmienne engWdh i engHgt opisują domyślną szerokość i wysokość poszczególnych warstw. Zmienne left i top służą do pozycjonowania warstw. Zmienna arrayHandles zawiera tablicę używaną do wstępnego ładowania obrazków. Będzie jeszcze o tym mowa w dalszej części rozdziału.
Przyjrzyjmy się funkcjom z wierszy 142-156:
function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
if (NN) {
document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
' VISIBILITY="' + sVis + '"' + ' z-Index=' + zIdx + '>' +
copy + '</LAYER>');
}
else {
document.writeln('<DIV ID="' + sName +
'" STYLE="position:absolute; overflow:none; left:' +
sLeft + 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' +
sHgt + 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) +
'">' + copy + '</DIV>');
}
}
oraz z wierszy 163-177:
function hideSlide(name) { refSlide(name).visibility = hideName; }
function showSlide(name) { refSlide(name).visibility = showName; }
function refSlide(name) {
if (NN) { return document.layers[name]; }
else { return eval('document.all.' + name + '.style'); }
}
function changeSlide(offset) {
hideSlide('slide' + curSlide);
curSlide = (curSlide + offset < 0 || curSlide + offset >= lyrCount ?
(curSlide + offset < 0 ? lyrCount - 1 : 0) : curSlide + offset);
showSlide('slide' + curSlide);
}
Jest tu pięć funkcji: genSlide(), refSlide(), hideSlide(), showSlide() i changeSlide(). Wszystkie działają podobnie jak w rozdziale 3.; jeśli czegoś nie pamiętasz, po prostu wróć do tego rozdziału. Istnieją też dwie nowe funkcje: imagePreLoad() i imageSwap(), które mają te same zadania, ale zostały zmodyfikowane na tyle, że zasadne jest ich ponowne omówienie.
Dynamiczne ładowanie obrazków
Jednym z wielkich paradygmatów Sieci jest dynamiczne przeprowadzenie operacji zasadniczo statycznych. Po co robić coś statycznie, skoro można znacznie wygodniej dokonać „w biegu”? Tak właśnie dzieje się zazwyczaj ze wstępnym ładowaniem obrazków. Jak tego wygląda wstępne ładowanie obrazków, których chcesz użyć do przewijanego menu? Może to być kod mniej więcej taki:
var myImage1On = new Image();
myImage1On.src = 'images/myImgOn1.gif'
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:
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.
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.
Metody eval().
Jeśli chodzi o punkt 1., w aplikacji użyjemy dwóch tablic. Jedna nazwana zostanie out i zawierać będzie obiekty Image odpowiadające obrazkom, nad którymi nie ma wskaźnika myszy. Druga tablica - o nazwie - over i będzie zawierać obiekty Image z obrazkami, nad którymi akurat jest wskaźnik myszy. Zmienne te będą od teraz reprezentowane przez wartości tablicy arrayHandles z wiersza 20:
var arrayHandles = new Array('out', 'over');
Kwestię konwencji nazewnictwa rozwiążemy bardzo prosto. Pary obrazków będą miały ten sam początek, za którym znajdzie się out.jpg lub over.jpg, w zależności od tego, o który obrazek z pary chodzi. Na przykład obrazki związane z InfoSeek będą nazywały się infoseekout.jpg i infoseekover.jpg.
Jeśli chodzi o punkt 3., najpierw przejdziemy po wszystkich elementach tablicy arrayHandles i używając funkcji eval(), utworzymy tablice na obiekty Image - oto wiersze 22 do 24:
for (var i = 0; i < arrayHandles.length; i++) {
eval('var ' + arrayHandles[i] + ' = new Array()');
}
Wykonanie powyższej pętli for odpowiada następującym instrukcjom:
var out = new Array();
var over = new Array();
Aby ładowanie obrazków jeszcze trochę dopracować, użyjemy znów funkcji eval() w imagePreLoad() do dynamicznego utworzenia obiektów Image i przypisania im właściwości SRC. Oto funkcja z wierszy 104-110:
function imagePreLoad(imgName, idx) {
for(var j = 0; j < arrayHandles.length; j++) {
eval(arrayHandles[j] + "[" + idx + "] = new Image()");
eval(arrayHandles[j] + "[" + idx + "].src = '" + imgPath +
imgName + arrayHandles[j] + ".jpg'");
}
}
Funkcja imagePreLoad() pobiera dwa parametry, początek nazwy (na przykład Infoseek) oraz liczbę całkowitą będącą indeksem obiektu w tablicy. Znów w pętli for przeglądamy tablicę arrayHandles, za każdym razem używając napisu do uzyskania dostępu do jednej z właśnie utworzonych tablic i przypisania jej niepowtarzalnego identyfikatora. Na przykład wywołanie imagePreLoad('infoseek',0) równoważne jest następującym instrukcjom:
out[0] = new Image();
out[0].src = 'images/infoseekout.jpg';
over[0] = new Image();
over[0].src = 'images/infoseekover.jpg';
Jednak w tym przypadku potrzebowalibyśmy dla każdej pary czterech wierszy kodu, a tego właśnie chcieliśmy uniknąć. Za każdym razem, kiedy chcemy dodać nową parę obrazków, wystarczy wywołać preLoadImages(), więc jest to czysty zysk.
Uruchamianie wyszukiwarek
Zmienna engines z wierszy 26-100 to tablica, której elementy zawierają tablice elementów opisujących poszczególne wyszukiwarki. Zmienna engines ma 20 całkiem długich elementów, więc przyjrzyjmy się tylko pierwszemu przykładowi - z wierszy 27-29:
newArray('HotBot',
'http://www.hotbot.com/?MT=',
'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: 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:
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,
'<A HREF="javascript: changeSlide(1);" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/ahead.gif" BORDER=0></A><BR>' +
'<A HREF="javascript: changeSlide(-1);" ' +
'onMouseOver="hideStatus(); return true;">' +
'<IMG SRC="images/back.gif" BORDER=0></A>');
Wszystko odbywa się dzięki jednemu odwołanie do genLayer(). Nie ma tu nic zaskakującego. Warstwa zawiera dwa aktywne obrazki: strzałkę do przodu i do tyłu. Zwróćmy uwagę na to, że położenie lewego górnego piksela przekazywane jest względem lewego górnego rogu warstwy łącz wyszukiwarek (którą to warstwę utworzymy wkrótce): left - 20 oraz top + 2.
Dalej zmienna lyrCount określa liczbę warstw przycisków wyszukiwarek, które mają być tworzone, a także liczbę wyszukiwarek umieszczonych w tablicy engines. To jest już proste: należy podzielić liczbę wyszukiwarek (engines.length) przez liczbę wyszukiwarek, które mają być umieszczane na jednej warstwie (perLyr). Jeśli reszta jest różna od zera, potrzebna będzie jeszcze jedna warstwa dodatkowa.
Techniki języka JavaScript: 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 engines.length/perLyr; w przeciwnym wypadku lyrCount równe jest Math. ceil(engines.length/perLyr). Poprawne określenie wartości lyrCount jest istotne, gdyż dalej w engineLinks() utworzymy lyrCount warstw - wiersze 122-136:
for (var i = 0; i < lyrCount; i++) {
var engLinkStr = '<TABLE BORDER=0 CELLPADDING=0 CELLSPACING=0><TR>';
for (var j = 0; j < perLyr; j++) {
var imgIdx = (i * perLyr) + j;
if (imgIdx >= engines.length) { break; }
var imgName = nameFormat(engines[imgIdx][0]);
imagePreLoad(imgName, imgIdx);
engLinkStr += '<TD><A HREF="javascript: ' +
'callSearch(document.forms[0].elements[0].value, ' +
imgIdx + ');" ' + 'onMouseOver="hideStatus(); imageSwap(\'' +
imgName + '\', ' + imgIdx + ', 1); return true" ' +
'onMouseOut="imageSwap(\'' + imgName + '\', ' + imgIdx +
', 0);">' + '<IMG NAME="' + imgName + '" SRC="' + imgPath +
imgName + "out.jpg" + '" BORDER=0></A></TD>';
}
Dla każdej warstwy engineLinks() deklaruje lokalną zmienną engLinkStr, która będzie zawierać kod poszczególnych slajdów. Po stworzeniu engLinkStr, która - jak widać w wierszu 123 - zawiera otwarcie tabeli, w zagnieżdżonej pętli for, sterowanej zmienną perLyr, tworzymy komórki tabeli, które będą zawierały poszczególne obrazki.
W każdej iteracji perLyr zmiennej lokalnej imgIdx przypisywana jest wartość (i * perLyr) + j. Wyrażenie to jest po prostu liczbą całkowitą, która w stosunku do zera jest w każdym przejściu pętli zwiększana o jeden. imgIdx zostanie użyta do identyfikacji początku nazwy obrazka (czyli nazwy wyszukiwarki umieszczonej w elemencie 0 każdej tablicy zawartej w engines), następnie ładowany jest obrazek, co omówiono wcześniej. W tabeli 4.1 pokazano sposób działania powyższego mnożenia, jeśli perLyr równa jest 4.
Tabela 4.1. Wyliczanie wyświetlania warstw (perLayer równa jest 4)
Kiedy i równe jest... |
A j równe jest... |
to (i*perLyr)+j zwiększa się o 1 |
0 |
0, 1, 2, 3 |
0, 1, 2, 3 |
1 |
0, 1, 2, 3 |
4, 5, 6, 7 |
2 |
0, 1, 2, 3 |
8, 9, 10, 11 |
3 |
0, 1, 2, 3 |
12, 13, 14, 15 |
4 |
0, 1, 2, 3 |
16, 17, 18, 19 |
|
|
|
Mamy tu 20 liczb całkowitych, od 0 do 19.
Teraz, kiedy znamy wartości imgIdx, musimy upewnić się, że nie pójdziemy za daleko. Odpowiedni kod jest w wierszu 126:
if (imgIdx >= engines.length) { break; }
Jako że wartość imgIdx zwiększa się stale bezwarunkowo, kiedy osiągnięta zostanie liczba engines. length, nie ma już więcej wyszukiwarek do wyświetlenia, więc funkcja w tym momencie przerwie pętlę for.
|
Techniki języka JavaScript: 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 += '<TD><A HREF="javascript: ' +
'callSearch(document.forms[0].elements[0].value, ' +
imgIdx + ');" ' + 'onMouseOver="hideStatus(); imageSwap(\'' +
imgName + '\', ' + imgIdx + ', 1); return true" ' +
'onMouseOut="imageSwap(\'' + imgName + '\', ' + imgIdx +
', 0);">' + '<IMG NAME="' + imgName + '" SRC="' + imgPath +
imgName + "out.jpg" + '" BORDER=0></A></TD>';
Przyjrzyjmy się teraz temu. Każde łącze wyszukiwarki musi spełniać cztery warunki:
zawierać kod wywołujący odpowiednią przeglądarkę przy kliknięciu na obrazku,
zawierać kod obsługi zdarzenia onMouseOver,
zawierać kod obsługi zdarzenia onMouseOut,
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:
'<IMG NAME="' + imgName + '" SRC="' + imgPath +
imgName + "out.jpg" + '" BORDER=0></A></TD>';
Nazwa każdego obrazka ustawiana jest na wartość imgName. W ten sposób będziemy odwoływać się do obrazków w funkcji imageSwap(). Dla atrybutu SRC ustala się z kolei wartość będącą złączeniem imgPath, imgName i out.jpg. Jako że obrazki najpierw będą pokazywane jako nieaktywne, SRC ma początkowo końcówkę out.jpg. Na przykład początkowy obrazek wyszukiwarki HotBot znajduje się w pliku images/hotbotout.jpg.
W wierszach 137 i 138 kończymy:
engLinkStr += '</TR></TABLE>';
genLayer('slide' + i, left, top, engWdh, engHgt, hideName, engLinkStr);
Zatem do engLinkStr dołączamy domknięcie znacznika tabeli HTML i pozostaje tylko wywołać genLayer(), aby utworzyć nową warstwę. Warto zwrócić uwagę na to, że genLayer()jest wywoływana z parametrem false, aby warstwa była niewidoczna - póki strona nie zostanie załadowana. Następnie w obsłudze zdarzenia onLoad w wierszu 201 pokazywany jest slajd slide0.
imageSwap()
Tę funkcję widzieliśmy w rozdziale 3., ale ta wersja jest nieco inna. Obejrzyjmy wiersze 179 do 182:
function imageSwap(imagePrefix, imageIndex, arrayIdx) {
document[imagePrefix].src = eval(arrayHandles[arrayIdx] +
"[" + imageIndex + "].src");
}
Funkcja ta realizuje przewijanie obrazków. Parametr imagePrefix wskazuje, który obrazek ma być włączony. Parametry imageIndex i arrayIdx to liczby całkowite służące do odwołania się do odpowiedniego obiektu Image w tablicy arrayHandles.
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: 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.
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 |
DC |
10, 25, 50, 100 |
DC=10 |
InfoSeek Advanced Search |
Numberresults |
10, 20, 25, 50 |
Numberresults=10 |
Scientific American |
Docs |
10, 25, 50, 100 |
Docs=10 |
Yahoo! |
N |
10, 20, 50, 100 |
n=10 |
|
|
|
|
Wartości te pobierałem z kodu źródłowego stron poszczególnych witryn. Niektóre pola dostępne są tylko w zaawansowanych wersjach wyszukiwania, więc adresy URL podane w tablicy engines mogą nie działać. Także programiści tworzący wyszukiwarkę mogli ustalić stały limit. Jeśli nie widać na stronie żadnej możliwości określenia liczby wyników, można skontaktować się z właścicielami i spytać kogoś, jak zmienić parametry (o ile w ogóle jakieś są dostępne). Jeśli nie, należy u siebie dodać jakieś ustawienie domyślne, które do niektórych wyszukiwarek w ogóle nie będzie przesyłało informacji o oczekiwanej liczbie wyników.
Zwróćmy też uwagę na to, że w różnych wyszukiwarkach mogą być dopuszczalne inne wartości. Należy wówczas dodać odpowiedni kod. Nie jest to trudne; użyj procedury opisanej niżej, a później w analogiczny sposób możesz do swojej aplikacji dodawać nowe funkcje.
Dodaj do ramki zawierającej pole tekstowe listę wyboru.
Dodaj do każdej tablicy zawierającej opis przeglądarki dodatkowy element.
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.
Usuń z tekstu zapytania odpowiednią parę wartości (jeśli para taka jest tam umieszczona).
Dostosuj kod funkcji callSearch(), aby prawidłowo łączone było zapytanie dla poszczególnych wyszukiwarek.
Przejdźmy teraz do przykładowej wyszukiwarki HotBot.
Krok 1.
Dodanie listy wyboru nie powinno stanowić problemu. Rozsądne może być wybranie wartości najczęściej używanych w przeglądarkach, które uwzględnia nasza aplikacja. W przykładzie zdecydowano się na liczby 10, 25, 50 i 100:
<SELECT NAME="docs">
<OPTION VALUE="10">10
<OPTION VALUE="25">25
<OPTION VALUE="50">50
<OPTION VALUE="100">100
</SELECT>
Krok 2.
Każde wywołanie new Array() w tablicy engines opisuje wyszukiwarkę z trzema elementami: nazwą wyszukiwarki, tekstem przekazywanym do wyszukiwania i stroną domową wyszukiwarki. Oto znów kod opisujący HotBot:
newArray('HotBot',
'http://www.hotbot.com/?MT=',
'http://www.hotbot.com/')
Teraz mamy element 3., którego wartością będzie nazwa pola określającego liczbę wyników. Pole to nazywa się - w przypadku HotBot - DC, więc nowy rekord będzie wyglądał tak:
newArray('HotBot',
'http://www.hotbot.com/?MT=',
'http://www.hotbot.com/',
'DC')
Jeśli co najmniej jedna z wyszukiwarek nie ma potrzebnego pola, niech ta wartość pozostanie pusta (null).
Krok 3.
Teraz, kiedy określiliśmy już potrzebną nazwę, dodaliśmy kolejną tablicę zawierającą dostępne wartości. Nowa tablica ma być elementem 4. Teraz opis HotBot będzie wyglądał następująco:
newArray('HotBot',
'http://www.hotbot.com/?MT=',
'http://www.hotbot.com/',
'DC',
new Array(10, 25, 50, 100) )
Krok 4.
Ten krok obowiązuje tylko wtedy, gdy domyślny napis zapytania w elemencie 2. zawiera parę nazwa -wartość, opisującą ustawienia wyniku. Oto odpowiedni zapis HotBot:
http://www.hotbot.com/?MT=
Jako że DC tu nie występuje, możemy krok 4. pominąć. Jednak w ramach przykładu pokażę obsługę wyszukiwarki Scientific American, która zawiera zapis docs=100. Spójrz:
'http://www.sciam.com/cgi-bin/search.cgi?' +
'searchby=strict&groupby=confidence&docs=100&query=',
Musielibyśmy odpowiedni fragment wyciąć, otrzymując następujący zapis:
'http://www.sciam.com/cgi-bin/search.cgi?' +
'searchby=strict&groupby=confidence&query=',
Jeśli co najmniej jedna z przeglądarek nie zawiera liczby wyników, którą można by ustawiać, po prostu nie twórz wartości elementu 4.
Krok 5.
Ostatnią czynnością jest stworzenie zapytania przed przekazaniem go wyszukiwarce. Robi się to w funkcji callSearch(). Oto kod oryginalny:
function callSearch(searchTxt, idx) {
if (searchTxt == "") {
parent.frames[2].location.href = engines[idx][2] +
escape(searchTxt);
}
else {
parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt);
}
}
Jeśli użytkownik nic nie wprowadzi w polu tekstu, aplikacja nadal może przekierować użytkownika na stronę główną wybranej wyszukiwarki, więc blok po if pozostanie bez zmian. Zmienimy tylko blok po else:
else {
if(engines[idx][3] != null) {
for (var i = 0; i < engines[idx][4].length; i++) {
var selRef = parent.frames[4].document.forms[0].docs;
if (selRef.options[selRef.selectedIndex].value =
engines[idx][4][i].toString()) {
parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt) + '&' + engines[idx][3] + '=' +
engines[idx][4][i];
return;
}
}
parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt);
}
Oto wiersz, który dodaje odpowiednią parę nazwa-wartość do tekstu:
parent.frames[2].location.href = engines[idx][1] +
escape(searchTxt) + '&' + engines[idx][3] + '=' +
engines[idx][4][i];
Mamy tutaj adres URL wyszukiwarki z zacytowanym tekstem searchTxt i nazwą pola (engines[idx] [3]l) oraz jego wartością wybraną przez użytkownika. Jednak tak się stanie tylko wtedy, gdy spełnione zostaną dwa warunki. Jeśli nie, wybierana jest strona domyślna, zapisana w engines[idx] [1]. Po pierwsze, wyszukiwarka musi umożliwiać zmianę liczby wyników - wtedy nazwa tego pola podana jest w engines[idx][3]. Jeśli pola takiego brakuje, wartością odpowiedniej komórki tablicy jest null, co było ustawiane w kroku 3. Poniższa instrukcja if sprawdza, czy engines [idx][3] nie jest puste:
if(engines[idx][3] != null) {
Jeśli wspomniana wartość okaże się pusta, nie zostaje spełniony pierwszy warunek i używany jest domyślny adres. Jeżeli engines[idx][3] nie jest pusta, przeglądane są dopuszczalne wartości zapisane w tablicy engines[idx][4]. Gdy wybrana liczba na liście wyboru wartości jest dopuszczalna dla danej wyszukiwarki, JavaScript łączy adres URL i tekst zapytania z odpowiednią parą nazwa-wartość, następnie do ramki frames[2] ładuje wyniki i kończy swoje działanie.
Jeśli pętla przejdzie po wszystkich dopuszczalnych wartościach, nie znajdując odpowiednika wybranej wartości, a nie jest spełniony drugi warunek i znów używany jest domyślny adres.
Cechy aplikacji:
Prezentowane techniki:
|
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:
Zdecyduj, ile par obrazków chcesz mieć do dyspozycji.
Ustaw domyślną szerokość, wysokość i obramowanie wszystkich obrazków (później można zmieniać te ustawienia dla każdego obrazka z osobna).
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.
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.
|
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:
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.
Wprowadź względny lub bezwzględny adres URL w polu tekstowym, związanym z atrybutem HREF każdej grupy obrazków.
W polu Pasek stanu wprowadź tekst, który ma być wyświetlany, kiedy użytkownik przesunie wskaźnik myszki nad łączem.
Dostosuj ewentualnie ustawienia wysokości, szerokości i obramowań poszczególnych obrazków w odpowiednich polach tekstowych.
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.
Struktura programu
Zanim zaczniemy zastanawiać się nad jakimkolwiek kodem, dobrze byłoby z grubsza obejrzeć sposób działania programu. Na rysunku 5.5 pokazano ten sposób od początku do końca. W zasadzie zaczynamy od stworzenia formularza obrazków i ustawienia właściwości, a później można podglądać wyniki, zmieniać ustawienia i znów generować kod.
ImageMachine składa się z trzech plików: zestawu z ramkami i dwóch plików z zawartością tych ramek. Plik główny to index.html, który zawiera nav.html i base.html. Sam index.html nie zawiera żadnego kodu JavaScript ani innych niespodzianek. Poniżej przedstawiono dziewięć wierszy kodu - przykład 5.1.
Przykład 5.1. indeks.html
1 <HTML>
2 <HEAD>
3 <TITLE>ImageMachine</TITLE>
4 </HEAD>
5 <FRAMESET ROWS="105, *" FRAMEBORDER="0" BORDER="0">
6 <FRAME SRC="nav.html" NAME="nav" SCROLLING=NO>
|
|
Rysunek 5.4. Wybierz „Podgląd” lub „Zmiana danych”
Przykład 5.1. indeks.html (dokończenie)
7 <FRAME SRC="base.html" NAME="base">
8 </FRAMESET>
9 </HTML>
Jeśli zajrzymy do base.html, znajdziemy tam znów statyczny kod HTML. Zanim przejdziemy do nav.html z przykładu 5.2, warto zrozumieć kilka rzeczy dotyczących tego kodu. Jest on długi (ponad 400 wierszy) i dość trudny do czytania, ale nie tak znów skomplikowany.
Przykład 5.2. nav.html
1 <HTML>
2 <HEAD>
3 <TITLE>ImageMachine</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5
6 var platform = navigator.platform;
7 var lb = (platform.indexOf("Win" != -1) ? "\n\r" :
8 (platform.indexOf("Mac" != -1) ? "\r" : "\n"));
9 var fontOpen = '<FONT COLOR=BLUE>';
10 var fontClose = '</FONT>';
11
12 function genSelect(name, count, start, select) {
13 var optStr = "";
14 for (var h = start; h <= count; h++) {
15 optStr += "<OPTION VALUE=" + h +
16 (h == select ? " SELECTED" : "") + ">" + h;
17 }
18 document.write("<SELECT NAME=" + name + ">" + optStr + "</SELECT>");
19 }
20
|
Rysunek 5.5.Logika ImageMachine
Przykład 5.2. nav.html (ciąg dalszy)
21 function captureDefaultProfile(formObj) {
22 setArrays();
23 imgDefaults = formObj;
24 var imgQty = (imgDefaults.imgnumber.selectedIndex + 1);
25 var imgHeight = (imgDefaults.pxlheight.selectedIndex);
26 var imgWidth = (imgDefaults.pxlwidth.selectedIndex);
27 var imgBorder = (imgDefaults.defbdr.selectedIndex);
28 for (var i = 0; i < imgQty; i++) {
29 imgPrim[i] = "";
30 imgRoll[i] = "";
31 imgDown[i] = "";
32 imgLink[i] = "";
33 imgText[i] = "";
34 imgWdh[i] = imgWidth;
Przykład 5.2. nav.html (ciąg dalszy)
35 imgHgt[i] = imgHeight;
36 imgBdr[i] = imgBorder;
37 }
38 generateEntryForm();
39 }
40
41 function setArrays() {
42 imgPrim = new Array();
43 imgRoll = new Array();
44 imgDown = new Array();
45 imgLink = new Array();
46 imgText = new Array();
47 imgWdh = new Array();
48 imgHgt = new Array();
49 imgBdr = new Array();
50 }
51
52
53 function generateEntryForm() {
54 with(parent.frames[1].document) {
55 open();
56 writeln("<HTML><BODY BGCOLOR=FFFFEE><FONT FACE=Arial SIZE=2><BLOCKQUOTE>" +
57 "Wybierz lub wpisz nazwy plików z obrazkami. Dodaj plik łącza " +
58 "(np., <FONT FACE=Courier>web_page.html</FONT>) lub tekst protokołu" +
59 " (np., <FONT FACE=Courier>javascript:</FONT>) dla wszystkich " +
60 " atrybutów HREF oraz wpisz komunikat, który chcesz wyświetlać " +
61 " w pasku stanu podczas zdarzenia <FONT FACE=\"Courier\">MouseOver" +
62 " </FONT>. Następnie wybierz <B>Generuj</B>, aby powstał potrzebny" +
63 " Ci kod, lub <B>Podgląd</B>, aby zobaczyć działanie tego kodu." +
64 "</BLOCKQUOTE> <FORM NAME='imgProfile' onSubmit='return false;'>" +
65 "<CENTER><TABLE BORDER=0 ALIGN=CENTER CELLSPACING=5 CELLPADDING=5>" +
66 "<TH ALIGN=LEFT><FONT FACE=Arial>#" +
67 "<TH ALIGN=LEFT><FONT FACE=Arial>Obrazek główny" +
68 "<TH ALIGN=LEFT><FONT FACE=Arial>Obrazek aktywny" +
69 (imgDefaults.mousedown.checked ? "<TH ALIGN=LEFT>" +
70 "<FONT FACE=Arial>MouseDown Path" : "") +
71 "<TR><TD><BR></TD></TR>");
72 }
73
74 for (i = 0; i < imgPrim.length; i++) {
75 with(parent.frames[1].document) {
76 writeln("<TR>" +
77 "<TD><FONT FACE=Arial SIZE=2><CENTER><B>" + (i + 1) +
78 "</B></CENTER><TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2>" +
79 "<INPUT TYPE=FILE NAME='prim" + i + "' VALUE='" + imgPrim[i] +
80 "'><TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2><INPUT TYPE=FILE " +
81 "NAME='seci" + i + "' VALUE='" + imgRoll[i] + "'>" +
82 (imgDefaults.mousedown.checked ? "<TD VALIGN=BOTTOM><FONT " +
83 "FACE=Arial SIZE=2><INPUT TYPE=FILE NAME='down" + i + "' VALUE='" +
84 imgDown[i] + "'>" : "") + "<TR>" +
85 "<TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2> </TD>" +
86 "<TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2><INPUT TYPE=TEXT " +
87 "NAME='href" + i + "' VALUE='" + imgLink[i] + "'> " +
88 "<IMG SRC='images/href.jpg'>" + "<TD VALIGN=BOTTOM><FONT FACE=Arial" +
89 " SIZE=2><INPUT TYPE=TEXT NAME='stat" + i + "' VALUE='" +
90 imgText[i] + "'> <IMG SRC='images/statusbar.jpg'> " +
91 (!imgDefaults.mousedown.checked ?"<TR>" : "") +
92 "<TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2>" +
93 (!imgDefaults.mousedown.checked ?
94 "</TD><TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2>" : "") +
95 "<INPUT TYPE=TEXT NAME='wdh" + i + "' VALUE='" +
96 imgWdh[i] + "' SIZE=3> <IMG SRC='images/wdh.jpg'> " +
Przykład 5.2. nav.html (ciąg dalszy)
97 " <INPUT TYPE=TEXT NAME='hgt" + i + "' VALUE='" +
98 imgHgt[i] + "' SIZE=3> <IMG SRC='images/hgt.jpg'> " +
99 (!imgDefaults.mousedown.checked ?
100 "<TD VALIGN=BOTTOM><FONT FACE=Arial SIZE=2>" : "") +
101 "<INPUT TYPE=TEXT NAME='bdr" + i + "' VALUE='" + imgBdr[i] +
102 "' SIZE=3> <IMG SRC='images/bdr.jpg'>" +
103 "<TR><TD VALIGN=BOTTOM COLSPAN=" +
104 (!imgDefaults.mousedown.checked ? "3" : "4") +
105 "><BR><HR NOSHADE><BR></TD></TR>");
106 }
107 }
108
109 with(parent.frames[1].document) {
110 writeln("</TABLE><CENTER><INPUT TYPE=BUTTON " +
111 "onClick='parent.frames[0].imgValid8(this.form, true);'" +
112 " VALUE='Generuj'><INPUT TYPE=BUTTON " +
113 "onClick='parent.frames[0].imgValid8(this.form, false);' " +
114 "VALUE='Podgląd'> <INPUT TYPE=RESET VALUE=' Wyczyść '>" +
115 "</FORM></BODY></HTML>");
116 close();
117 }
118 }
119
120 function imgValid8(imgTemplate, mimeType) {
121 for (var i = 0; i < imgPrim.length; i++) {
122 if (imgTemplate['prim' + i].value == "" ||
123 imgTemplate['seci' + i].value == "" ||
124 imgTemplate['href' + i].value == "") {
125 alert("Wszystkie obrazki i atrybuty HREF muszą mieć adresy URL");
126 return;
127 }
128 if (imgDefaults.mousedown.checked) {
129 if(imgTemplate['down' + i].value == "") {
130 alert("Wszystkie obrazki i atrybuty HREF muszą mieć adresy URL");
131 return;
132 }
133 }
134 }
135 genJavaScript(imgTemplate, mimeType);
136 }
137
138 function genJavaScript(imgTemplate, mimeType) {
139 imageLinks = '';
140
141 if (mimeType) {
142 lt = "<";
143 gt = ">";
144 br = "<BR>";
145 HTML = true;
146 nbsp = " ";
147 }
148 else {
149 lt = "<";
150 gt = ">";
151 br = lb;
152 HTML = false;
153 nbsp = " ";
154 }
155
156 if(imgTemplate != null) {
157 setArrays();
158 for (var i = 0; i < (imgDefaults.imgnumber.selectedIndex + 1); i++) {
159 imgPrim[i] = purify(imgTemplate['prim' + i].value);
160 imgRoll[i] = purify(imgTemplate['seci' + i].value);
Przykład 5.2. nav.html (ciąg dalszy)
161 if (imgDefaults.mousedown.checked) {
162 imgDown[i] = purify(imgTemplate['down' + i].value);
163 }
164 imgLink[i] = purify(imgTemplate['href' + i].value);
165 imgText[i] = purify(imgTemplate['stat' + i].value);
166 imgWdh[i] = purify(imgTemplate['wdh' + i].value);
167 imgHgt[i] = purify(imgTemplate['hgt' + i].value);
168 imgBdr[i] = purify(imgTemplate['bdr' + i].value);
169 }
170 }
171
172 if (HTML) {
173 primJavaScript = "<HTML><HEAD><TITLE>Image Machine Code</TITLE>" +
174 "</HEAD><BODY BGCOLOR=FFFFEE><FONT FACE=Arial>" +
175 "<I>Wytnij i wklejaj poniższy kod do pliku HTML. Kod niebieski " +
176 "to podane przez Ciebie informacje.</I>" +
177 "<BR><BR></FONT><FONT SIZE=2 FACE=Arial>" +
178 lt + "HTML" + gt + "<BR>" + lt + "HEAD" + gt + "<BR>" +
179 lt + "TITLE" + gt + "Kod z Image Machine" + lt + "/TITLE" + gt;
180 }
181 else {
182 primJavaScript = "<HTML><HEAD><TITLE>Kod z Image Machine</TITLE>";
183 }
184
185 primJavaScript += br + br + lt + "SCRIPT LANGUAGE=\"JavaScript\"" +
186 gt + br + br + "// Definicja zmiennych globalnych w JavaScript 1.0" + br +
187 "var canRollOver = false;" + br + "var canClickDown = false;" + br +
188 br + lt + "/SCR" + "IPT" + gt + br + br + lt +
189 "SCRIPT LANGUAGE =\"JavaScript1.1\"" + gt + br + br +
190 "// W JavaScript 1.1 zmiana canRollOver na true" + br +
191 "canRollOver = true;" + br + br;
192
193 secJavaScript = lt + "SCRIPT LANGUAGE=\"JavaScript1.2\"" + gt + br +
194 br + "// Zmiana w JavaScript 1.2 canClickDown na true" + br +
195 "canClickDown = true;" + br + br;
196
197 for (var j = 0; j < imgPrim.length; j++) {
198 primJavaScript += "// Obrazki podstawowe i aktywne #" +
199 (j + 1) + br +"switch" + (j + 1) + "out = new Image(" +
200 (HTML ? fontOpen : "") + imgWdh[j] +
201 (HTML ? "</FONT>," : ", ") +
202 (HTML ? fontOpen : "") + imgHgt[j] +
203 (HTML ? fontClose : "") + "); " + br + "switch" + (j + 1) +
204 "out.src = '" +
205 (HTML ? fontOpen : "") +
206 (imgPrim[j].indexOf(":\\") != -1 ? pathPrep(imgPrim[j]) :
207 imgPrim[j]) +
208 (HTML ? fontClose : "") + "';" + br + "switch" + (j + 1) +
209 "over = new Image(" +
210 (HTML ? fontOpen : "") + imgWdh[j] +
211 (HTML ? "</FONT>," : ", ") +
212 (HTML ? fontOpen : "") + imgHgt[j] +
213 (HTML ? fontClose : "") + "); " + br + "switch" + (j + 1) +
214 "over.src = '" +
215 (HTML ? fontOpen : "") +
216 (imgRoll[j].indexOf(":\\") != -1 ? pathPrep(imgRoll[j]) :
217 imgRoll[j]) +
218 (HTML ? fontClose : "") + "';" + br + br;
219
220 if (imgDefaults.mousedown.checked) {
221 secJavaScript += "// obrazek MouseDown #" + (j + 1) + br +
222 "switch" + (j + 1) + "down = new Image(" +
223 (HTML ? fontOpen : "") + imgWdh[j] +
224 (HTML ? "</FONT>," : ", ") +
Przykład 5.2. nav.html (ciąg dalszy)
225 (HTML ? fontOpen : "") + imgHgt[j] +
226 (HTML ? fontClose : "") + "); " + br + "switch" +
227 (j + 1) + "down.src = '" +
228 (HTML ? fontOpen : "") +
229 (imgPrim[j].indexOf(":\\") != -1 ? pathPrep(imgDown[j]) :
230 imgDown[j]) +
231 (HTML ? fontClose : "") + "';" + br + br;
232 }
233
234 imageLinks += lt + "!-- <I> Image Link #" + (j + 1) +
235 " </I>//--" + gt + br + lt + "A HREF=\"" +
236 (HTML ? fontOpen : "") + imgLink[j] +
237 (HTML ? fontClose : "") + "\" " + br + nbsp +
238 "onMouseOver=\"imageSwap('switch" + (j + 1) +
239 "', 'over', false); display('" +
240 (HTML ? fontOpen : "") + imgText[j] +
241 (HTML ? fontClose : "") + "'); return true;\"" + br +
242 nbsp + "onMouseOut=\"imageSwap('switch" +
243 (j + 1) + "', 'out', false); display('');\"" +
244 (imgDefaults.mousedown.checked ?
245 br + nbsp + "onMouseDown=\"isDown=!isDown; imageSwap('switch" +
246 (j + 1) + "', 'down', true);\"" : "") +
247 gt + br + lt + "IMG SRC=\"" +
248 (HTML ? fontOpen : "") + pathPrep(imgPrim[j]) +
249 (HTML ? fontClose : "") + "\"" + br + nbsp +
250 "NAME=switch" + (j + 1) + br + nbsp + "WIDTH=" +
251 (HTML ? fontOpen : "") + imgWdh[j] +
252 (HTML ? fontClose : "") + br + nbsp + "HEIGHT=" +
253 (HTML ? fontOpen : "") + imgHgt[j] +
254 (HTML ? fontClose : "") + br + nbsp + "BORDER=" +
255 (HTML ? fontOpen : "") + imgBdr[j] +
256 (HTML ? fontClose : "") +
257 gt + "" + lt + "/A" + gt + br + br + br;
258 }
259
260 scriptClose = br + lt + "/SCR" + "IPT" + gt + br + br;
261
262 swapCode = br + lt + "/SCR" + "IPT" + gt + br + br +
263 lt + "SCRIPT LANGUAGE =\"JavaScript\"" + gt + br + br +
264 (imgDefaults.mousedown.checked ?
265 "var isDown = false;" + br + br : "") +
266 "// Warunkowe wykonanie przewijania w JavaScript 1.0" + br +
267 "function imageSwap(imageName, imageSuffix) {" + br +
268 nbsp + "if (!canRollOver) { return; }" + br + nbsp +
269 (imgDefaults.mousedown.checked ?
270 "if (!isDown) { " + br + nbsp + nbsp : "") +
271 "document[imageName].src = " +
272 "eval(imageName + imageSuffix + \".src\");" + br + nbsp +
273 (imgDefaults.mousedown.checked ? nbsp + "}" + br + nbsp +
274 "else if (canClickDown) {" + br +
275 nbsp + nbsp + "document[imageName].src = " +
276 "eval(imageName + imageSuffix + \".src\");" + br +
277 nbsp + nbsp + "}" + br + nbsp : "") + "}" + br + br +
278 "function display(stuff) { window.status = stuff; }" +
279 br + br + lt + "/SCR" + "IPT" + gt + br;
280
281 primHTML = br + lt + "/HEAD" + gt + br +
282 lt + "BODY BGCOLOR=FFFFEE" +
283 gt + br + br + (HTML ? "<FONT COLOR=RED>" : "") + lt +
284 "!-- <I> Zaczyna się kodowanie obrazków</I> //--" + gt + br +
285 (HTML ? fontClose : "") + br + br;
286
287 secHTML = (HTML ? "<FONT COLOR=RED>" : "") +
288 lt + "!-- <I> Koniec kodowania obrazków</I> //--" + gt +
Przykład 5.2. nav.html (ciąg dalszy)
289 (HTML ? fontClose : "") + br + br +
290 (HTML ? lt + "/BODY" + gt + br + lt + "/HTML" + gt : "") +
291 br + br + "<CENTER><FORM>" + br +
292 "<INPUT TYPE=BUTTON onClick='parent.frames[0].genJavaScript(null, " +
293 (HTML ? "false" : "true") + ");' VALUE='" +
294 (HTML ? 'Podgląd' : 'Generuj') + "'> " +
295 "<INPUT TYPE=BUTTON " +
296 "onClick='parent.frames[0].generateEntryForm();' " +
297 "VALUE='Zmień dane'>" + br + "</FORM></CENTER>" + br + br +
298 "</BODY></HTML>";
299
300 agregate = primJavaScript +
301 (imgDefaults.mousedown.checked ? scriptClose + secJavaScript : "") +
302 swapCode + primHTML + imageLinks + secHTML;
303
304 parent.frames[1].location.href =
305 "javascript: parent.frames[0].agregate";
306 }
307
308 function purify(txt) { return txt.replace(/\'|\"/g, ""); }
309
310 function pathPrep(path) {
311 if (path.indexOf(":\\") != -1) {
312 path = path.replace(/\\/g, "/");
313 path = path.replace(/:\//, "|/");
314 return "file:///" + path;
315 }
316 else { return path; }
317 }
318
319 </SCRIPT>
320 </HEAD>
321 <BODY BGCOLOR=FFFFEE>
322 <FORM>
323 <TABLE BORDER="0">
324 <TR>
325 <TD VALIGN=MIDDLE>
326 <IMG SRC="images/image_machine.gif" WIDTH=275 HEIGHT=56 HSPACE=25>
327 </TD>
328 <TD>
329 <!-- Tworzenie warunków domyślnych //-->
330 <TABLE BORDER="0" ALIGN="CENTER">
331 <TR>
332 <TD VALIGN="TOP">
333 <FONT FACE="Arial" SIZE=2>
334 Par obrazków
335 </TD>
336 <TD VALIGN="TOP">
337 <FONT FACE="Arial" SIZE=2>
338 <SCRIPT LANGUAGE="JavaScript1.2">
339 <!--
340 genSelect("imgnumber", 50, 1, 1);
341 //-->
342 </SCRIPT>
343 </TD>
344 <TD VALIGN="TOP">
345 <FONT FACE="Arial" SIZE=2>
346 Szerokość
347 </TD>
348 <TD VALIGN="TOP">
349 <FONT FACE="Arial" SIZE=2>
350 <SCRIPT LANGUAGE="JavaScript1.2">
351 <!--
352 genSelect("pxlwidth", 250, 0, 90);
Przykład 5.2. nav.html (dokończenie)
353 //-->
354 </SCRIPT>
355 </TD>
356 <TD VALIGN="TOP">
357 <FONT FACE="Arial" SIZE=2>
358 MouseDown
359 </TD>
360 <TD VALIGN="TOP">
361 <FONT FACE="Arial" SIZE=2>
362 <INPUT TYPE=CHECKBOX NAME="mousedown">
363 </TD>
364 </TR>
365 <TR>
366 <TD VALIGN="TOP">
367 <FONT FACE="Arial" SIZE=2>
368 Obramowanie
369 </TD>
370 <TD VALIGN="TOP">
371 <FONT FACE="Arial" SIZE=2>
372 <SCRIPT LANGUAGE="JavaScript1.2">
373 <!--
374 genSelect("defbdr", 10, 0, 0);
375 //-->
376 </SCRIPT>
377 </TD>
378 <TD VALIGN="TOP">
379 <FONT FACE="Arial" SIZE=2>
380 Wysokość
381 </TD>
382 <TD VALIGN="TOP">
383 <FONT FACE="Arial" SIZE=2>
384 <SCRIPT LANGUAGE="JavaScript1.2">
385 <!--
386 genSelect("pxlheight", 250, 0, 50);
387 //-->
388 </SCRIPT>
389 </TD>
390 <TD VALIGN="TOP">
391 <FONT FACE="Arial" SIZE=2>
392 <INPUT TYPE=BUTTON VALUE="Generuj"
393 onClick="captureDefaultProfile(this.form);">
394 </TD>
395 <TD VALIGN="TOP">
396 <FONT FACE="Arial" SIZE=2>
397 <INPUT TYPE=RESET VALUE=" Reset ">
398 </TD>
399 </TR>
400 </TABLE>
401 </TD>
402 </TR>
403 </TABLE>
404 </CENTER>
405 </FORM>
406 </BODY>
407 </HTML>
To jest jak dotąd największy fragment kodu aplikacji. Niektóre części mogą wyglądać dość onieśmielająco, ale nie jest aż tak źle, jak można by sądzić. Aby lepiej zrozumieć działanie aplikacji, zastanówmy się nad czynnościami typowego użytkownika. Rozważmy pięcioetapowy scenariusz:
załadowanie strony,
użytkownik wprowadza liczbę par obrazków i ustawienia domyślne, następnie wybiera Generuj,
użytkownik wypełnia pola na nazwy plików, atrybuty HREF i tak dalej, a w końcu wybiera Generuj, aby obejrzeć kod,
użytkownik wybiera Podgląd, aby zobaczyć działanie kodu,
użytkownik wybiera Zmień dane, aby wprowadzić jakieś poprawki.
Krok 1. Załadowanie strony
Wszystko wygląda całkiem normalnie. Zestaw ramek nazywa się index.html, a mamy w nim dwie ramki: nav.html oraz base.html. Jednak JavaScript wykona pewną pracę jeszcze zanim użytkownik będzie miał szansę cokolwiek zrobić. Zwróćmy uwagę na wiersze 323-403. Znajduje się tam kod tabeli z kilkoma wywołaniami funkcji JavaScriptu w komórkach, na przykład:
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
Pary obrazków
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genSelect("imgnumber", 50, 1, 1);
//-->
</SCRIPT>
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
Szerokość
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genSelect("pxlwidth", 250, 0, 90);
//-->
</SCRIPT>
</TD>
W wywołaniach funkcji genSelect() język JavaScript używany jest do dynamicznego tworzenia list wyboru. Każda lista umożliwia ustawienie domyślnych wartości atrybutów obrazków. Lista wyboru działa lepiej niż pole tekstowe, gdyż nie trzeba się tak martwić o sprawdzanie zawartości pola. Użytkownik nie może wprowadzić nieprawidłowej wartości (na przykład innej niż liczba), opisującej ramkę czy szerokość obrazka. Ale kto chce wpisywać listę z 250 czy 300 pozycjami, po jednej dla każdej liczby z takiego zakresu? Załóżmy, że musimy zmienić liczbę opcji. JavaScript jest tu bardzo przydatny, gdyż umożliwia wywołanie po prostu genSelect() dla każdej tworzonej listy - spójrzmy na wiersze 12-19:
function genSelect(name, count, start, select) {
var optStr = "";
for (var h = start; h <= count; h++) {
optStr += "<OPTION VALUE=" + h +
(h == select ? " SELECTED" : "") + ">" + h;
}
document.write("<SELECT NAME=" + name + ">" + optStr + "</SELECT>");
}
Funkcja genSelect() oczekuje czterech parametrów: napisu z nazwą listy wyboru, liczby oznaczającej największą wartość na liście, liczby początkowej (całkowitej) dalej zwiększanej o 1 i liczby określającej wybraną opcję. genSelect() po prostu tworzy w pętli kolejne znaczniki <OPTION>. Kiedy pętla zakończy swoje działanie, JavaScript zapisuje wynik między znacznikami <SELECT> do dokumentu. Teraz strony są ładowane i gotowe do działania. Zobaczmy, co się dzieje, kiedy użytkownik już wprowadzi ustawienia domyślne.
Krok 2. Określenie liczby par obrazków i ustawień domyślnych
Zwróćmy uwagę na rysunek 5.1, gdzie użytkownik ustawił wartości domyślne w czterech listach wyboru i jednym polu opcji. Najważniejsze jest ustawienie liczby par obrazków. ImageMachine pozwala użyć od 1 do 50 takich par. Wątpię, czy owe 50 będzie kiedykolwiek potrzebne, ale mała nadwyżka mocy nie zaszkodzi.
Użytkownik może wybierać domyślną szerokość i wysokość obrazków w pikselach, przy czym dopuszczalne rozmiary mogą sięgać od 1 do 250. Być może ktoś będzie chciał to w przyszłości zmienić, ale na razie ustawienia są właśnie takie. Domyślna szerokość i wysokość to odpowiednio 90 i 50 - otrzymuje się w ten sposób bardzo ładny prostokąt o rozmiarach typowego przycisku.
Ostatnia lista wyboru pozwala zdefiniować obramowanie, które może mieć grubość od 0 do 10 pikseli. Zwykle ustawia się ją na 0, ale spotyka się też przewijane obrazki z widocznymi ramkami.
Zaznaczenie pola opcji powoduje dodanie obsługi zdarzenia onMouseDown, obsługiwanego w JavaScripcie w wersji 1.2 i w odpowiednich modelach dokumentu w NN i MSIE. Teraz użytkownik musi jeszcze tylko wybrać Generuj, a pojawi się szablon strony z obrazkami, bazujący na wpisanych właśnie danych.
Krok 3. Określenie nazw plików, atrybutów HREF i tak dalej
Kiedy użytkownik kliknie Generuj, ImageMachine wygeneruje odpowiedni szablon obrazków, pokazany na rysunku 5.2. Do generowania tego szablonu używa się trzech funkcji: captureDefaultProfile(), setArrays() oraz generateEntryForm().
captureDefaultProfile()
Funkcja ta wywoływana jest jako pierwsza, a znajduje się w wierszach 21-39:
function captureDefaultProfile(formObj) {
setArrays();
imgDefaults = formObj;
var imgQty = (imgDefaults.imgnumber.selectedIndex + 1);
var imgHeight = (imgDefaults.pxlheight.selectedIndex);
var imgWidth = (imgDefaults.pxlwidth.selectedIndex);
var imgBorder = (imgDefaults.defbdr.selectedIndex);
for (var i = 0; i < imgQty; i++) {
imgPrim[i] = "";
imgRoll[i] = "";
imgDown[i] = "";
imgLink[i] = "";
imgText[i] = "";
imgWdh[i] = imgWidth;
imgHgt[i] = imgHeight;
imgBdr[i] = imgBorder;
}
generateEntryForm();
}
Pierwsze jej zadanie to wywołanie funkcji setArrays(). Ta funkcja z kolei, pokazana poniżej (wiersze 41-50), deklaruje i inicjalizuje 8 tablic. Każda z tych tablic odpowiada jakiemuś atrybutowi wartości poszczególnych grup obrazków. Na przykład imgPrim zawiera nazwy plików z obrazkami podstawowymi, imgRoll nazwy plików z obrazkami pod wskaźnikiem myszy, i tak dalej. Jeśli tablice nie były jeszcze zadeklarowane, zajmie się tym właśnie funkcja setArrays(). Jeżeli tablice już zadeklarowano (użytkownik wygenerował kod), tablice zostaną wyzerowane.
function setArrays() {
imgPrim = new Array();
imgRoll = new Array();
imgDown = new Array();
imgLink = new Array();
imgText = new Array();
imgWdh = new Array();
imgHgt = new Array();
imgBdr = new Array();
}
Następnie captureDefaultProfile() kopiuje obiekt formularza formObj do zmiennej imgDefaults. To ważne: imgDefaults jest globalna, więc nie znika po zakończeniu działania funkcji i pozwala obsłużyć przełączanie się między generacją kodu a poprawianiem ustawień. Jest to jedyne miejsce w całej aplikacji, gdzie imgDefaults jest ustawiana. Oznacza to, że użytkownik może zmienić wartości domyślne, jedynie uruchamiając znów przycisk Generuj.
Kiedy ImageMachine ma już ustawienia domyślne użytkownika, captureDefaultProfile() deklaruje cztery następujące zmienne lokalne:
var imgQty = (imgDefaults.imgnumber.selectedIndex + 1);
var imgHeight = (imgDefaults.pxlheight.selectedIndex);
var imgWidth = (imgDefaults.pxlwidth.selectedIndex);
var imgBorder = (imgDefaults.defbdr.selectedIndex);
Pętla for robi tyle iteracji, ile użytkownik chce grup obrazków, przy czym każda grupa jest opisywana w zadeklarowanych właśnie tablicach. imgPrim zawiera obrazki zdarzenia MouseOut, imgRoll obrazki zdarzenia MouseOver, a imgDown zawiera obrazki zdarzenia MouseDown. imgLink i imgText to wartości atrybutu HREF i treść paska stanu. Jako że użytkownik określi wartości pierwszych pięciu elementów bezpośrednio w poszczególnych okienkach szablonu, odpowiednie wartości początkowo ustawiane są jako puste.
Wszystkie elementy pozostałych trzech tablic ustalane są podobnie. Domyślna szerokość, wysokość i obramowanie dla wszystkich obrazków są takie same, a użytkownik może je później zmieniać.
generateEntryForm()
Ostatnie zadanie naszej funkcji to wywołanie generateEntryForm(). Tutaj naprawdę zaczyna się coś dziać. Funkcja jest odpowiedzialna za utworzenie szablonu HTML, w którym użytkownik będzie mógł wprowadzać dane opisujące poszczególne grupy obrazków. Przyjrzyjmy się wierszom od 53 do 118.
Sześćdziesiąt sześć wierszy na opisanie jednej tylko funkcji! To narusza wszelkie zasady dotyczące wielkości funkcji, ale generateEntryForm() i tak realizuje jedną tylko operację: tworzy szablon obrazków. Jest ona prosta, jeśli tylko podzieli się ją na trzy części HTML: nagłówek tabeli (TH), pola tekstowe formularza oraz przyciski. Cała funkcja tak naprawdę jest szeregiem wywołań document. writeln(). Oto kod zapisujący nagłówki - z wierszy 54-72:
with(parent.frames[1].document) {
open();
writeln("<HTML><BODY BGCOLOR=FFFFEE><FONT FACE=Arial SIZE=2><BLOCKQUOTE>" +
"Wybierz lub wpisz nazwy plików z obrazkami. Dodaj plik łącza " +
"(np., <FONT FACE=Courier>web_page.html</FONT>) lub tekst protokołu" +
" (np., <FONT FACE=Courier>javascript:</FONT>) dla wszystkich " +
" atrybutów HREF oraz wpisz komunikat, który chcesz wyświetlać " +
" w pasku stanu podczas zdarzenia <FONT FACE=\"Courier\">MouseOver" +
" </FONT>. Następnie wybierz <B>Generuj</B>, aby powstał potrzebny" +
" Ci kod, lub <B>Podgląd</B>, aby zobaczyć działanie tego kodu." +
"</BLOCKQUOTE> <FORM NAME='imgProfile' onSubmit='return false;'>" +
"<CENTER><TABLE BORDER=0 ALIGN=CENTER CELLSPACING=5 CELLPADDING=5>" +
"<TH ALIGN=LEFT><FONT FACE=Arial>#" +
"<TH ALIGN=LEFT><FONT FACE=Arial>Obrazek główny" +
"<TH ALIGN=LEFT><FONT FACE=Arial>Obrazek aktywny" +
(imgDefaults.mousedown.checked ? "<TH ALIGN=LEFT>" +
"<FONT FACE=Arial>MouseDown Path" : "") +
"<TR><TD><BR></TD></TR>");
}
Warto zwrócić uwagę na to, że formularz szablonu obrazków jest zawarty w tabeli. Wszystko w tym bloku jest statyczne poza wierszami 69 i 70. Używając operatora trójargumentowego, JavaScript dołącza dodatkowy nagłówek, jeśli użytkownik zaznaczył opcję MouseDown. Przyjrzyjmy się poniższym wierszom:
(imgDefaults.mousedown.checked ? "<TH ALIGN=LEFT>" +
"<FONT FACE=Arial>MouseDown Path" : "") +
O to właśnie tutaj chodzi: jeśli zrozumie się to, to cała funkcja nie będzie miała przed nami żadnych tajemnic, albowiem generateEntryForm() podejmuje wszystkie swoje decyzje na podstawie zaznaczenia bądź niezaznaczenia opcji MouseDown. Przyjrzyjmy się polom tekstowym w wierszach 74-107.
Dla tylu elementów, ile znajduje się w imgPrim (a więc ile jest grup obrazków) dodawany jest nowy znacznik TR, obejmujący TD z dwoma lub trzema polami TD, w których są pola FILE, tekst atrybutu HREF, tekst paska stanu oraz szerokość, wysokość i obramowanie. Jeśli dokładniej przyjrzymy się, zauważymy, że każdy element o indeksie i, deklarowany w setArrays() ma wartość odpowiedniego pola tekstowego.
imgPrim, imgRoll, imgDown, imgLink i imgText pierwotnie były pustymi ciągami. Aż do teraz użytkownik nie miał szans wprowadzenia tam jakiejkolwiek wartości. Jednak szerokość, wysokość i obramowanie miały wartości domyślne, więc dobrze będzie przypisać odpowiednim polom wartości z tablic imgWidth, imgHeight i imgBorder.
Oto powtarzający się fragment kodu:
(imgDefaults.mousedown.checked ?
Za każdym razem dochodzimy do punktu, kiedy na podstawie zaznaczenia bądź niezaznaczenia przez użytkownika opcji MouseDown musimy ewentualnie wygenerować kod obsługi dodatkowego obrazka.
Nagłówki i pola tekstowe formularza już mamy. Pozostało nam wygenerować przyciski Generuj, Podgląd i Wyczyść:
with(parent.frames[1].document) {
writeln("</TABLE><CENTER><INPUT TYPE=BUTTON " +
"onClick='parent.frames[0].imgValid8(this.form, true);'" +
" VALUE='Generuj'><INPUT TYPE=BUTTON " +
"onClick='parent.frames[0].imgValid8(this.form, false);' " +
"VALUE='Podgląd'> <INPUT TYPE=RESET VALUE=' Wyczyść '>" +
"</FORM></BODY></HTML>");
close();
}
Wyczyść to typowy przycisk RESET, więc nie będziemy się tu nim zajmować. Zwróćmy natomiast uwagę na pozostałe dwa przyciski: kliknięcie obu wywołuje tę samą funkcję, imgValid8(). W obydwóch przypadkach przekazywany jest formularz, ale raz z wartością true, a raz z false. Ta właśnie wartość decyduje, czy generowany kod jest pokazywany, czy interpretowany. Teraz tym właśnie się zajmiemy.
Być może zechcesz przejrzeć kolejne wiersze generateEntryForm(), aby zobaczyć, jak wygląda HTML formularza. Można wtedy zobaczyć, jak tworzony jest długi napis z całym formularzem, który po wypełnieniu przez użytkownika pozwala przejść dalej. Funkcja generateEntryForm() wymusza wypełnienie formularza na użytkowniku, który zgodnie z naszą czteroetapową procedurą, omówioną na początku tego rozdziału, wybierze Generuj. Wtedy wywoływane są właśnie funkcje imgValid8() z wierszy 120-136:
function imgValid8(imgTemplate, mimeType) {
for (var i = 0; i < imgPrim.length; i++) {
if (imgTemplate['prim' + i].value == "" ||
imgTemplate['seci' + i].value == "" ||
imgTemplate['href' + i].value == "") {
alert("Wszystkie obrazki i atrybuty HREF muszą mieć adresy URL");
return;
}
if (imgDefaults.mousedown.checked) {
if(imgTemplate['down' + i].value == "") {
alert("Wszystkie obrazki i atrybuty HREF muszą mieć adresy URL");
return;
}
}
}
genJavaScript(imgTemplate, mimeType);
}
Ta funkcja zapewnia, że użytkownik wprowadził odpowiednie wartości we wszystkich polach przeznaczonych do wstawienia informacji o plikach. Wartości przypisywane są imgTemplate. Znów - używając długości imgPrim jako ograniczenia pętli - ImageMachine przechodzi przez wszystkie pola tekstowe opisujące obrazki. Pola zawierające nazwy plików głównych nazwane są prim + i, gdzie i jest liczbą w zakresie od 0 do imgPrim.length - 1. Pola zawierające obrazki ze wskaźnikiem myszy są nazywane podobnie, przy czym zamiast prim używane jest seci. Jeśli użytkownik załącza też obrazki zdarzenia MouseDown, odpowiednie pola tekstowe mają w nazwie down.
genJavaScript()
Jeśli którekolwiek sprawdzane pole okaże się puste, fakt ten jest zgłaszany użytkownikowi, a funkcja kończy swoje działanie. Jeżeli każde pole zawiera jakikolwiek tekst, ImageMachine wywołuje funkcję genJavaScript(), przekazując imgTemplate i niesprawdzoną jeszcze wartość logiczną mimeType. Jak nietrudno zgadnąć, genJavaScript() odpowiedzialna jest za stworzenie kodu JavaScript strony wynikowej. Funkcja ta jest bardzo długa, ale działa podobnie jak generateEntryForm() (można ją znaleźć w wierszach 138-306).
Mogło by się wydawać, że generateEntryForm() jest długa! To nadal jednak działanie tego samego typu - genJavaScript() ma jedno tylko zadanie: wygenerować kod obsługi przewijania obrazków pod wskaźnikiem myszy, czyli głównie kod JavaScript.
genJavaScript() najpierw wyzeruje zmienne globalne imageLinks. Więcej o tych zmiennych globalnych zawierających kod dowiemy się za chwilę. Następnie ustawiane są pomocnicze zmienne globalne - zgodnie z mimeType. Oto one w wierszach 141 do 154:
if (mimeType) {
lt = "<";
gt = ">";
br = "<BR>";
HTML = true;
nbsp = " ";
}
else {
lt = "<";
gt = ">";
br = lb;
HTML = false;
nbsp = " ";
}
|
Techniki języka JavaScript: Nie jest przypadkiem, że znacznik SCRIPT w generowanym kodzie ma kilka różnych wartości atrybutu LANGUAGE. Nieco kodu znajdziemy między znacznikami <SCRIPT LANGUAGE="JavaScript"> a </SCRIPT> w wierszu 185. Inny kod mieści się między <SCRIPT LANGUAGE="JavaScript1.1"> a </SCRIPT> w wierszu 189 i w końcu jeszcze inny między <SCRIPT LANGUAGE="JavaScript1.2"> a </SCRIPT> w wierszu 193. W ten sposób przeglądarki, obsługujące różne wersje JavaScriptu, nie wykonują kodu przez nie nierozumianego, więc unikamy błędów wykonania. Na przykład w wersji 1.0 obiekt Image() nie jest obsługiwany, dlatego w kodzie między znacznikami <SCRIPT LANGUAGE="JavaScript"> a </SCRIPT> żadnych takich obiektów nie znajdziemy, chyba że będą one zagnieżdżone między znacznikami <SCRIPT> z atrybutem LANGUAGE odpowiadającym wersji 1.1 lub wyższej. Stosując kontrolne zmienne globalne, których wartości będą ustawiane w sekcjach <SCRIPT>, można realizować takie ostrożne kodowanie. Kiedy przychodzi do wywołania funkcji, która raczej nie jest w danej wersji obsługiwana, wywołuje się ją tylko wtedy, gdy spełnione są odpowiednie warunki dotyczące wartości zmiennych kontrolnych. Można to sobie obejrzeć na zmiennych canRollOver dla obiektu Image i canClickDown dla obsługi zdarzenia onMouseDown. Takie kodowanie pozwala zmniejszyć obciążenie. Na przykład przeglądarki nie obsługujące JavaScriptu 1.2 nie wykonają żadnego kodu między <SCRIPT LANGUAGE= "JavaScript1.2"> a </SCRIPT>, dzięki czemu przeglądarka nie będzie w ogóle próbowała ściągać obrazków związanych z obsługą zdarzenia onMouseDown. |
|
Jeśli mimeType ma wartość true, zmienne globalne będą miały wartości wymuszające drukowanie kodu. Zmienne lt i gt będą ustawione na < i >. Zmienna br ma wtedy wartość <BR>. HTML to zmienna logiczna, wskazująca, czy użytkownik chce kod interpretować (zamiast pokazywać na ekranie). To zacznie mieć znaczenie zaraz po wygenerowaniu kodu. Zmiennej nbsp przypisywana jest wartość spacji niedzielących w HTML. nbsp jest odpowiednikiem użycia klawisza tabulacji w HTML.
Jeśli mimeType ma wartość false, zmienne globalne będą ustawione dla potrzeb kodu interpretowanego. Zmienne lt i gt przyjmą wartości < i >, a zmienna br będzie miała wartość zmiennej lb. lb z kolei ustawiana jest w wierszach 6-8:
var platform = navigator.platform;
var lb = (platform.indexOf("Win" != -1) ? "\n\r" :
(platform.indexOf("Mac" != -1) ? "\r" : "\n"));
Jak widać, zmienne lb i platform współpracują ze sobą. platform zawiera napis określający system operacyjny, dla którego przeglądarka została skompilowana. lb ustawiana jest stosownie do wartości platform. W Windows (i DOS) jest to koniec wiersza, czyli klawisz ENTER, znaki \n\r. W przypadku Macintosha jest to \r, a w Uniksie \n. Takie potraktowanie kodu zapewni, że kod ten nie będzie miał pięciu kilometrów długości. Nie jest to specjalnie istotne dla samej aplikacji, ale kiedy ImageMachine wygeneruje kod HTML, można będzie go znacznie łatwiej później oglądać.
|
Techniki języka JavaScript: W tej aplikacji korzystamy z możliwości oferowanych przez zmienne globalne. ImageMachine generuje kod interpretowany lub drukowany. Jeden z nich oglądamy, drugi wykonujemy. Oba jego rodzaje są niemalże takie same - poza tym, że w kodzie wykonywanym używa się znaków < i > -zamiast < i >. Zmienne globalne lt i gt ustawiane są stosownie do tego, czy wybierze się przycisk Generuj, czy Podgląd. Analogicznie określa się wartość zmiennych br i nbsp. Na tym właśnie polega siła zmiennych globalnych: po prostu zmieniasz ich wartość i otrzymujesz napisy, które mogą pełnić inne funkcje, będąc nadal równie użytecznymi. |
|
Przejdźmy dalej - HTML ustawiane jest na false. Jest to zmienna globalna informującą, czy użytkownik chce interpretować otrzymany kod. Nabierze to znaczenia zaraz po wygenerowaniu tego kodu. Zmienna nbsp to napis składający się z białych znaków.
Teraz ImageMachine ma już informacje z pól formularza, wie też, co użytkownik pragnie zrobić z otrzymanym kodem. Generowanie JavaScriptu zaczyna się tak naprawdę od wiersza 185 i trwa już do końca funkcji.
Przeglądając kod, kilka razy natkniesz się na wywołanie funkcji pathPrep(). Funkcja ta zmienia formatowanie napisu z nazwą pliku, jeśli nazwa ta wygląda na lokalną w systemie Windows (o ścieżkach nazw więcej powiedziano w rozdziale 3.). Po co całe to zamieszanie? Pamiętajmy, że Windows do rozdzielania poszczególnych katalogów używa lewego ukośnika (\), natomiast przeglądarki (jak i unix) używają ukośnika zwykłego (/). Wobec tego konieczna będzie zmiana wszystkich lewych ukośników na zwykłe, choć niektóre przeglądarki są w stanie taką konwersję zrealizować w biegu.
Problem w tym, że JavaScript interpretuje lewy ukośnik jako część cytowanego znaku, zatem ścieżkę C:\My_Dir\My.File JavaScript odczyta jako C:My_DirMy.File. Funkcja pathPrep(), pokazana w wierszach 310-317, zajmuje się wymaganą konwersją:
function pathPrep(path) {
if (path.indexOf(":\\") != -1) {
path = path.replace(/\\/g, "/");
path = path.replace(/:\//, "|/");
return "file:///" + path;
}
else { return path; }
}
Przeglądarki otwierają też dokumenty lokalne za pośrednictwem protokołu file, zatem będziemy musieli przed URL umieścić file:///, a zamiast dwukropka po nazwie dysku - znak potoku (|).
Czas na decyzje
Wszystko jest już gotowe do generowania kodu, czy to do wydruku, czy do interpretacji. Zanim kod ten zostanie utworzony, aplikacja musi jeszcze wiedzieć, czy ma generować kod na podstawie nowych danych z szablonu obrazków, czy użyć informacji już znajdujących się w tablicach. Zgodnie z naszym cyklem obsługi użytkownika, opisanym wcześniej, dane zostały wprowadzone już z formularza do tablic. Inaczej jest, kiedy kod już został wygenerowany i użytkownik cofnął się w celu poprawienia danych, a następnie znów wcisnął Generuj lub Podgląd. Wkrótce zajmiemy się tym.
Jeśli informacje pochodzą z szablonu obrazków (o tym wypadku na razie mówimy), genJavaScript() czyści wszystkie tablice img, wywołując setArrays(), dzięki czemu informacje z szablonu mogą zostać przypisane. ImageMachine decyduje, czy wywołać setArrays() i ponownie przypisać wartości, określając wartość imgTemplate. genJavaScript() może być wywołana trzema sposobami: przez przycisk Generuj, przycisk Podgląd i z funkcji imgValid8(). Wywołanie funkcji genJavaScript() za pomocą przycisków przekazuje jako imgTemplate wartość pustą (null). Jeśli zatem imgTemplate nie jest pusta, genJavaScript()przyjmuje, że należy wyczyścić tablice i przygotować miejsce na nowe dane. W przeciwnym wypadku elementy tablic img nie będą modyfikowane. Warto uważnie przeanalizować wiersze 156 do 170, aby zobaczyć, jak to działa:
if(imgTemplate != null) {
setArrays();
for (var i = 0; i < (imgDefaults.imgnumber.selectedIndex + 1); i++) {
imgPrim[i] = purify(imgTemplate['prim' + i].value);
imgRoll[i] = purify(imgTemplate['seci' + i].value);
if (imgDefaults.mousedown.checked) {
imgDown[i] = purify(imgTemplate['down' + i].value);
}
imgLink[i] = purify(imgTemplate['href' + i].value);
imgText[i] = purify(imgTemplate['stat' + i].value);
imgWdh[i] = purify(imgTemplate['wdh' + i].value);
imgHgt[i] = purify(imgTemplate['hgt' + i].value);
imgBdr[i] = purify(imgTemplate['bdr' + i].value);
}
}
Jeśli elementy tablicy zostały zmodyfikowane, wartości są im przypisywane przez szybki, aczkolwiek niebezpieczny proces usuwania znaków funkcją purify() w wierszu 308:
function purify(txt) { return txt.replace(/\'|\"/g, ""); }
W ten sposób z wartości usuwa się wszystkie pojedyncze i podwójne cudzysłowy. Znaki te nie są zabronione, ale JavaScript musi użyć obu tych znaków przy generowaniu kodu. Jeśli nie są one odpowiednio zacytowane lewymi ukośnikami, to przy generowaniu muszą się pojawić problemy. purify() usuwa je z przekazanych napisów, a następnie zwraca nowy wiersz.
Generowanie kodu
Kiedy już wszystko jest gotowe, czas na długo oczekiwane generowanie kodu. Dzieje się to w wierszach 185 do 305 przez przypisanie całego generowanego kodu kilku pomocniczym zmiennym. Zmienne są następujące:
primJavaScript
Zawiera znaczniki HTML, jak HTML, HEAD i TITLE. Posiada też wstępny kod JavaScript związany ze zdarzeniami MouseOver i MouseOut.
secJavaScript
Zawiera kod przewijania związany ze zdarzeniem MouseDown w JavaScripcie 1.2.
|
Techniki języka JavaScript: JavaScript 1.2 zawiera szereg nowych, użytecznych funkcji. Jedną z najważniejszych jest możliwość używania wyrażeń regularnych do dopasowywania i podstawiania napisów. Funkcje pathPrep() i purify() dają proste, ale potężne możliwości podstawiania tekstu w JavaScripcie 1.2. Jest to cecha świetna, ale przeglądarka Netscape 3.x nadal powszechnie jest używana. Oto funkcja realizująca podmianę tekstu w JavaScripcie 1.1 przy użyciu metod obiektu Array: function replacev11(str, oldSubStr, newSubStr) { var newStr = str.split(oldSubStr).join(newSubStr); return newStr; } Funkcja ta pobiera napis, tworzy tablicę elementów, stosując split() z użyciem podciągu, który chcemy usunąć (oldSubStr), następnie zwraca używając funkcji - join() - napis składający się z elementów tablicy z nowym podciągiem (newSubStr). Nie jest to rozwiązanie najefektowniejsze, ale działa. |
|
imageLinks
Zawiera kod HTML wyświetlający łącza.
scriptClose
Zawiera zamykający znacznik SCRIPT.
swapCode
Zawiera funkcje JavaScriptu realizujące przewijanie obrazków.
primHTML
Zawiera znacznik BODY i nieco komentarzy HTML.
secHTML
Zawiera zamykające znaczniki HTML oraz przyciski formularza, wyświetlane po wygenerowanym kodzie (Generuj i Zmień dane lub Podgląd i Zmień dane).
aggregate
Zmienna łączy wszystkie inne wymienione tu zmienne.
Pętla for w wierszu 197 jeszcze raz korzysta z licznika imgPrim.length. Za każdym razem zmienne primJavaScript, secJavaScript (jeśli użytkownik zaznaczył opcję MouseDown) i imageLinks dodawane są do kodu odpowiedniej grupy obrazków.
Zmienne scriptClose, swapCode, primHTML i secHTML nie należą do części for. Ich zawartość może być ustawiona tylko raz w operatorze trójargumentowym w połączeniu ze zmienną HTML i imgDefaults.mousedown.checked.
Kiedy pętla for zakończy swoje działanie i ustawione są stosowne zmienne, ostatnią czynnością jest pobranie zawartości na stronę. Dzieje się to w wierszach 300 do 305:
agregate = primJavaScript +
(imgDefaults.mousedown.checked ? scriptClose + secJavaScript : "") +
swapCode + primHTML + imageLinks + secHTML;
parent.frames[1].location.href =
"javascript: parent.frames[0].agregate";
Krok 4. Wybór Podglądu w celu obejrzenia działania kodu
Trudno się dziwić, jeśli ktoś jest już zmęczony. Na szczęście następne dwa kroki są całkiem szybkie i proste. Załóżmy, że użytkownik oglądał wygenerowany kod i teraz chce zobaczyć, jak on działa. Wystarczy kliknąć Podgląd. Pamiętajmy, że wywołana zostanie wtedy funkcja genJavaScript(), ale mimeType ma wartość false, a nie true, jak w przypadku wybrania Generuj. To jest jedyna różnica: zmienne z wierszy 141-154 są po prostu ustawiane tak, aby generowany był kod do interpretacji, a nie do drukowania, jak poprzednio. Wszystko inne działa tak samo - jak po kliknięciu przycisku Generuj.
Krok 5. Wybór Zmiany danych w celu zrobienia poprawek
Widzieliśmy już kod wygenerowany i tenże kod w akcji. Załóżmy, że użytkownik chce coś zmienić. Wybieramy więc przycisk Zmień dane, więc znów pokazuje się szablon obrazków - szerokości, wysokości, tekst paska stanu - wszystko, co wprowadzałeś oprócz adresów URL obrazków. Zaraz - ale dlaczego bez?
Dzieje się tak dlatego, że adresy URL obrazków znajdują się w obiektach FileUpload (czyli <INPUT TYPE=FILE>). Ze względów bezpieczeństwa obiekty takie są przeznaczone tylko do odczytu. Innymi słowy - trzeba te pola ręcznie wypełnić z klawiatury lub wybierając plik myszką w stosownym dialogu. Łatwo jest to zmienić: po prostu trzeba zmienić w generateEntryForm() TYPE=FILE na TYPE=TEXT. Znajdziemy trzy takie ustawienia. Jedyny problem polega na tym, że tracimy możliwość wybierania plików lokalnych myszką przez dialog. Można użyć takiego ustawienia, jeśli bardziej nam odpowiada. Kiedy już wprowadziliśmy potrzebne zmiany, znów możesz kliknąć Generuj lub Podgląd i obejrzeć nowy kod.
Kierunki rozwoju:
dodanie atrybutów do szablonu
Duże aplikacje zawsze można powiększyć. W tej sekcji zobaczymy, jak dodać do szablonu obrazków atrybuty, które zapewnią większą kontrolę nad generowanym kodem. Aby rzecz uprościć, pokażę, jak dodać do znacznika IMG atrybuty HSPACE i VSPACE. Procedura ta składa się z sześciu kroków:
dodania do domyślnego szablonu nowych pól,
utworzenia tablic na nowe wartości w setArrays(),
pobrania nowych wartości domyślnych,
dodania pól tekstowych w szablonie obrazków, w generateEntryForm(),
odwołania się i przypisania nowych wartości atrybutów w genJavaScript(),
generowanie kodu HTML, potrzebnego do wyświetlenia atrybutów w genJavaScript().
Krok 1. Dodanie pól
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
HSpace
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genSelect("hspace", 25, 0, 0);
//-->
</SCRIPT>
</TD>
<TD VALIGN="TOP">
FONT FACE="Arial" SIZE=2>
VSpace
</TD>
<TD VALIGN="TOP">
<FONT FACE="Arial" SIZE=2>
<SCRIPT LANGUAGE="JavaScript1.2">
<!--
genSelect("vspace", 25, 0, 0);
//-->
</SCRIPT>
</TD>
Krok 2. Tworzenie tablic w setArrays()
function setArrays() {
imgPrim = new Array();
imgRoll = new Array();
imgDown = new Array();
imgLink = new Array();
imgText = new Array();
imgWdh = new Array();
imgHgt = new Array();
imgBdr = new Array();
imgHSpace = new Array(); // na HSPACE
imgVSpace = new Array(); // na VSPACE
}
W ten sposób można przygotować sobie miejsce na nowe wartości domyślne, a teraz dochodzimy do następnego kroku - wypełnienia tych tablic.
Krok 3. Pobieranie nowych ustawień domyślnych
W funkcji captureDefaultProfile() dodamy dwie zmienne lokalne imgHspace i imgVspace, a następnie przypiszemy im wartości z szablonu. Teraz captureDefaultProfile() wygląda tak:
function captureDefaultProfile(formObj) {
setArrays();
imgDefaults = formObj;
var imgQty = (imgDefaults.imgnumber.selectedIndex + 1);
var imgHeight = (imgDefaults.pxlheight.selectedIndex);
var imgWidth = (imgDefaults.pxlwidth.selectedIndex);
var imgBorder = (imgDefaults.defbdr.selectedIndex);
var imgHspace = (imgDefaults.hspace.selectedIndex);
var imgVspace = (imgDefaults.vspace.selectedIndex);
for (var i = 0; i < imgQty; i++) {
imgPrim[i] = "";
imgRoll[i] = "";
imgDown[i] = "";
imgLink[i] = "";
imgText[i] = "";
imgWdh[i] = imgWidth;
imgHgt[i] = imgHeight;
imgBdr[i] = imgBorder;
imgHSpace[i] = imgHspace; // HSPACE
imgVSpace[i] = imgVspace; // VSPACE
}
generateEntryForm();
}
Aktualnie ImageMachine może włączyć do szablonu obrazków wartości domyślne HSPACE i VSPACE.
Krok 4. Dodanie pól tekstowych w generateEntryForm()
W generateEntryForm() możesz teraz dodać treści HTML do obsługi dwóch nowych pól tekstowych. Wstawmy je do osobnego wiersza TR, pod istniejącymi dotąd. Później będzie można wszystko sobie poukładać, aby poprawić wygląd. Wiersze 103-106 teraz będą wyglądały następująco:
"<TR><TD VALIGN=BOTTOM COLSPAN=" +
(!imgDefaults.mousedown.checked ? "3" : "4") +
"><BR><HR NOSHADE><BR></TD></TR>");
Dodanie na koniec dwóch pól tekstowych da taki oto wynik:
"<TR><TD VALIGN=BOTTOM><INPUT TYPE=TEXT NAME='hsp " + i +
"' VALUE='" + imgHspace[i] + "' SIZE=3> HSPACE </TD>" +
"<TR><TD VALIGN=BOTTOM><INPUT TYPE=TEXT NAME='vsp" + i +
"' VALUE='" + imgVspace[i] + "' SIZE=3> VSPACE </TD></TR>" +
"<TR><TD VALIGN=BOTTOM COLSPAN=" +
(!imgDefaults.mousedown.checked ? "3" : "4") + ">" +
"<BR><HR NOSHADE><BR></TD></TR>");
Kod ten powoduje dodanie dwóch pól tekstowych do każdej grupy obrazków i wyświetlenie ich wartości domyślnych. Użytkownik będzie mógł później te wartości zmienić, jak i w przypadku innych pól.
Krok 5. Odwołanie się do nowych wartości w genJavaScript()
i ich użycie
Kiedy użytkownik zdecyduje się już wygenerować kod, ImageMachine musi pobrać dane z nowych pól tekstowych w szablonie obrazków. Po prostu dodajmy kod do wierszy 158-169, a uzyskamy następujący wynik:
for (var i = 0; i < (imgDefaults.imgnumber.selectedIndex + 1); i++) {
imgPrim[i] = purify(imgTemplate['prim' + i].value);
imgRoll[i] = purify(imgTemplate['seci' + i].value);
if (imgDefaults.mousedown.checked) {
imgDown[i] = purify(imgTemplate['down' + i].value);
}
imgLink[i] = purify(imgTemplate['href' + i].value);
imgText[i] = purify(imgTemplate['stat' + i].value);
imgWdh[i] = purify(imgTemplate['wdh' + i].value);
imgHgt[i] = purify(imgTemplate['hgt' + i].value);
imgBdr[i] = purify(imgTemplate['bdr' + i].value);
imgHSpace[i] = purify(imgTemplate['hsp' + i].value);
imgVSpace[i] = purify(imgTemplate['vsp' + i].value);
}
Ostatnie dwa wiersze w tym bloku pokazują przypisanie wartości z formularza elementom tablicy imgHSpace i imgVSpace. Już prawie gotowe. Jedyne, co zostało, to upewnić się, że nowe atrybuty zostały dołączone w procesie generacji kodu, czy to drukowanego, czy interpretowanego.
Krok 6. Generacja dodatkowego HTML w genJavaScript()
Do zmiennej imageLinks dodany zostanie nowy kod, którego ostatnich kilka wierszy pokazano niżej:
(HTML ? fontClose : "") + br + nbsp + "HEIGHT=" +
(HTML ? fontOpen : ") + imgHgt[j] +
(HTML ? fontClose : ") + br + nbsp + "BORDER=" +
(HTML ? fontOpen : "") + imgBdr[j] +
(HTML ? fontClose : "") +
gt + "" + lt + "/A" + gt + br + br + br;
Pozostało jedynie skopiowanie kilku wierszy i zmiana HEIGHT na HSPACE, imgHgt na imgHSpace, BORDER na VSPACE oraz imgBdr na imgVSpace. Oto nowa wersja:
(HTML ? fontClose : "") + br + nbsp + "HEIGHT=" +
(HTML ? fontOpen : ") + imgHgt[j] +
(HTML ? fontClose : ") + br + nbsp + "BORDER=" +
(HTML ? fontOpen : "") + imgBdr[j] +
(HTML ? fontClose : "") + br + nbsp + "HSPACE=" +
(HTML ? fontOpen : ") + imgHSpace[j] +
(HTML ? fontClose : ") + br + nbsp + "VSPACE=" +
(HTML ? fontOpen : "") + imgVSpace[j] +
(HTML ? fontClose : "") +
gt + "" + lt + "/A" + gt + br + br + br;
W ten sposób do naszych obrazków dodane zostaną dwa nowe atrybuty. Można też zastanowić się nad dodaniem atrybutu ALT. Nie trzeba się też ograniczać tylko do znacznika <IMG>; świetnie do rozbudowy nadaje się też <A> - można tworzyć mapy obrazkowe, i tak dalej.
Cechy bibliotek:
|
6
Realizacja plików |
|
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.
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.
|
Rysunek 6.1. Prezentacja możliwości arrays.js
jsGrep()
Dopasowuje napisy i podstawienia we wszystkich elementach tablicy.
truncate()
Zwraca kopię tablicy bez ostatniego elementu.
shrink()
Zwraca kopię tablicy bez pierwszego elementu.
integrate()
Łączy elementy z dwóch tablic, zaczynając od wskazanego indeksu.
reorganize()
Zmienia kolejność elementów tablicy, wybierając elementy w grupach o wskazanej wielkości.
Teraz przyjrzyjmy się kodowi arrays.html, pokazanemu w przykładzie 6.1. Nie ma tu zbyt wiele - po prostu wywołanie document.write(). Wyświetlany napis zawiera wyniki wywołania wszystkich funkcji na tablicach przykładowych, someArray() i grepExample().
Przykład 6.1. arrays.html
1 <HTML>
2 <HEAD>
3 <TITLE>Przyk¦ady arrays.js</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2" SRC="arrays.js"></SCRIPT>
5 </HEAD>
6 <BODY>
7 <SCRIPT LANGUAGE="JavaScript1.2">
8 <!--
Przykład 6.1. arrays.html (dokończenie)
9
10 var someArray = new Array(1,2,3,.1098,5,2,3.456,1324.55,-0.76,45,3,47.234,.00060,65.7,1,3,2,4,55);
11 var grepExample = new Array('Monday', 'Tuesday', 'Wednesday',
12 'Thursday', 'Friday');
13 document.write("<B>Tablica wejściowa: " + someArray + "</B><BR>" +
14 "Average: " + avg(someArray) + "<BR>" +
15 "Lowest: " + low(someArray) + "<BR>" +
16 "Highest: " + high(someArray) + "<BR>" +
17 "Truncate (1): " + truncate(someArray) + "<BR>" +
18 "Shrink (1): " + shrink(someArray) + "<BR>" +
19 "Reorganize (4): " + reorganize(someArray, 4) + "<BR>" +
20 "Integrate ('element', 'kolejny', i 'jeszcze jeden', indeks 5): " +
21 integrate(someArray,
22 new Array('element', 'kolejny', 'jeszcze jeden'), 5) + "<BR><BR>" +
23 "<B>Tablica oryginalna grepExample: " + grepExample + "</B><BR>" +
24 "jsGrep(grepExample, /day/, \'day Night\'): " +
25 jsGrep(grepExample, /day/, 'day Night') + "<BR>");
26
27 //-->
28 </SCRIPT>
29 </BODY>
30 </HTML>
Można zauważyć, że oba znaczniki wymagają JavaScriptu 1.2. Jedynym tego powodem jest funkcja jsGrep() używająca obsługi łańcuchów, dostępnej w tej właśnie wersji. Można uruchamiać ten plik w przeglądarkach z JavaScriptem 1.1 po usunięciu (lub przebudowaniu) funkcji jsGrep(). Skoro już widzieliśmy działanie funkcji, to teraz obejrzyjmy je same - przykład 6.2.
Przykład 6.2. arrays.js
1 function avg(arrObj) {
2 var sum = 0;
3 for (var i = 0; i < arrObj.length; i++) {
4 sum += arrObj[i];
5 }
6 return (sum / i);
7 }
8
9 function high(arrObj) {
10 var highest = arrObj[0];
11 for (var i = 1; i < arrObj.length; i++) {
12 highest = (arrObj[i] > highest ? arrObj[i] : highest);
13 }
14 return (highest);
15 }
16
17 function low(arrObj) {
18 var lowest = arrObj[0];
19 for (var i = 1; i < arrObj.length; i++) {
20 lowest = (arrObj[i] < lowest ? arrObj[i] : lowest);
21 }
22 return (lowest);
23 }
24
25 function jsGrep(arrObj, regexp, subStr) {
26 for (var i = 0; i < arrObj.length; i++) {
27 arrObj[i] = arrObj[i].replace(regexp, subStr);
28 }
29 return arrObj;
30 }
31
Przykład 6.2. arrays.js (dokończenie)
32 function truncate(arrObj) {
33 arrObj.length = arrObj.length - 1;
34 return arrObj;
35 }
36
37
38 function shrink(arrObj) {
39 var tempArray = new Array();
40 for(var p = 1; p < arrObj.length; p++) {
41 tempArray[p - 1] = arrObj[p];
42 }
43 return tempArray;
44 }
45
46
47 function integrate(arrObj, elemArray, startIndex) {
48 startIndex = (parseInt(Math.abs(startIndex)) < arrObj.length ?
49 parseInt(Math.abs(startIndex)) : arrObj.length);
50 var tempArray = new Array();
51 for( var p = 0; p < startIndex; p++) {
52 tempArray[p] = arrObj[p];
53 }
54 for( var q = startIndex; q < startIndex + elemArray.length; q++) {
55 tempArray[q] = elemArray[q - startIndex];
56 }
57 for( var r = startIndex + elemArray.length; r < (arrObj.length +
58 elemArray.length); r++) {
59 tempArray[r] = arrObj[r - elemArray.length];
60 }
61 return tempArray;
62 }
63
64 function reorganize(formObj, stepUp) {
65 stepUp = (Math.abs(parseInt(stepUp)) > 0 ? Math.abs(parseInt(stepUp)) : 1);
66 var nextRound = 1;
67 var idx = 0;
68 var tempArray = new Array();
69
70 for (var i = 0; i < formObj.length; i++) {
71 tempArray[i] = formObj[idx];
72 if (idx + stepUp >= formObj.length) {
73 idx = nextRound;
74 nextRound++;
75 }
76 else {
77 idx += stepUp;
78 }
79 }
80 return tempArray;
81 }
Funkcje avg(), high() i low() nie powinny zaskakiwać. avg() dodaje wszystkie wartości, następnie dzieli sumę przez arrObj.length i zwraca wynik. Pozostałe funkcje przeszukują tablicę, porównując elementy ze sobą, aby w końcu określić największy i najmniejszy element.
Funkcja jsGrep() przegląda wszystkie elementy tablicy i realizuje dopasowywanie tekstu lub jego podstawianie. Każdy czytelnik, zaznajomiony z językiem Perl, zapewne używał procedury grep() już wielokrotnie. Funkcja grep() dostępna w Perlu jest znacznie silniejszym narzędziem, ale zasada działania jest taka sama.
Funkcje truncate() i shrink() to w JavaScripcie 1.1 proste odpowiedniki funkcji pop() i shift(), dostępnych w wersji 1.2. pop() i shift() nazywają się analogicznie i tak samo działają, jak podobne procedury Perla.
Funkcja integrate() to także dla wersji 1.1 odpowiednik metody tablic slice(), dostępnej w wersji 1.2.
slice() to również nazwa procedury Perla. Ta funkcja jest dość prosta - choć ma trzy pętle for, to łączna liczba iteracji zawsze wynosi arrObj.length + elemArray.length.
Funkcja reorganize() zmienia kolejność elementów w tablicy o zadaną krotność. Innymi słowy - zmianie kolejności tablicy dziesięcioelementowej 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 co 3 odpowiada tablica 0, 3, 6, 9, 1, 4, 7, 2, 5, 8.
cookies.js
Zastosowania praktyczne:
Indywidualne liczniki odwiedzin, powtarzalne formularze, preferencje użytkownika.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
getCookieVal(), GetCookie(), DeleteCookie(), SetCookie()
Czy chcesz obsługiwać zmieniający się stan klienta? A co z sympatycznymi pozdrowieniami dla starych znajomych? Chcesz ustawić język interfejsu i inne preferencje użytkownika? Ten kod znakomicie ułatwia zapisywanie i odczytywanie informacji o ciasteczkach. Na rysunkach 6.2, 6.3 i 6.4 pokazano plik cookies.html w działaniu. Zwróćmy uwagę, że na rysunku 6.2 przy pierwszym załadowaniu strony użytkownik jest proszony o podanie nazwy. Na rysunku 6.3 pokazano pozdrowienie wyświetlane odwiedzającemu stronę po raz pierwszy, a na rysunku 6.4 zaprezentowano pozdrowienie przeznaczone dla stałych bywalców, z osobistym licznikiem odwiedzin.
Jest to zdecydowanie prosty przykład zastosowania ciasteczek. W rozdziale 7. użyjemy tego samego kodu do zapamiętania preferencji użytkownika. A tak przy okazji - jeśli ciasteczka są dla kogoś ciężkostrawne, polecam Unofficial Cookie FAQ (Nieoficjalny Zestaw Często Zadawanych Pytań o Ciasteczka) dostępny pod adresem: http://www.cookiecentral.com/unofficial_cookie_faq. htm. Strona jest może i nieoficjalna, ale znajdziemy na niej pełne odpowiedzi na wszelkie swoje pytania z tej dziedziny. Więcej szczegółów znajdzie się w rozdziale 7.Plik cookies.html działa następująco: kiedy użytkownik ładuje stronę, wyszukuje ciasteczka o nazwie user_id. Jeśli nazwa nie istnieje (jest pusta - null), prosi użytkownika o przedstawienie się. Następnie ustawiane jest ciasteczko user_id o wartości odpowiadającej nazwie użytkownika i ciasteczko hit_count o wartości 2 (taki numer będzie miała następna wizyta tego gościa).
Jeśli user_id istnieje, pobierana jest jego wartość oraz wartość hit_count. Pojawienie się user_id sugeruje, że, gość już tu był wcześniej, można zatem spokojnie założyć, iż ustawiono też hit_count. Wyświetlana jest nazwa użytkownika oraz numer jego wizyty, następnie hit_count otrzymuje wartość hit_ count+1. Przyjrzyjmy się cookies.js z przykładu 6.3, aby zorientować się, o co w tym wszystkim chodzi.
|
|
Rysunek 6.2. Odwiedzający po raz pierwszy podają swoje imię...
|
Rysunek 6.3. ...są stosownie witani...
|
Rysunek 6.4. ...a później to już prawdziwi znajomi
Przykład 6.3. cookies.js
1 var today = new Date();
2 var expiry = new Date(today.getTime() + 365 * 24 * 60 * 60 * 1000);
3
4 function getCookieVal (offset) {
5 var endstr = document.cookie.indexOf (";", offset);
6 if (endstr == -1) { endstr = document.cookie.length; }
7 return unescape(document.cookie.substring(offset, endstr));
8 }
9
10 function GetCookie (name) {
11 var arg = name + "=";
12 var alen = arg.length;
13 var clen = document.cookie.length;
14 var i = 0;
15 while (i < clen) {
16 var j = i + alen;
17 if (document.cookie.substring(i, j) == arg) {
18 return getCookieVal (j);
19 }
20 i = document.cookie.indexOf(" ", i) + 1;
21 if (i == 0) break;
22 }
23 return null;
24 }
25
26 function DeleteCookie (name,path,domain) {
27 if (GetCookie(name)) {
28 document.cookie = name + "=" +
29 ((path) ? "; path=" + path : "") +
30 ((domain) ? "; domain=" + domain : "") +
31 "; expires=Thu, 01-Jan-70 00:00:01 GMT";
Przykład 6.3. cookies.js(dokończenie)
32 }
33 }
34
35 function SetCookie (name,value,expires,path,domain,secure) {
36 document.cookie = name + "=" + escape (value) +
37 ((expires) ? "; expires=" + expires.toGMTString() : "") +
38 ((path) ? "; path=" + path : "") +
39 ((domain) ? "; domain=" + domain : "") +
40 ((secure) ? "; secure" : "");
41 }
Znajdują się tutaj cztery, funkcje ale tak naprawdę będziemy potrzebować tylko trzech z nich: SetCookie(), GetCookie() oraz DeleteCookie(). getCookieVal() to funkcja wewnętrzna, nigdy niewywoływana bezpośrednio.
Jeśli mamy do dyspozycji funkcję SetCookie(), tworzenie ciasteczek jest proste. Przekazujemy tylko nazwę (której użyjesz później w GetCookie() przechowywaną informację (na przykład nazwę użytkownika lub liczbę odwiedzin) i datę dezaktywacji, wszystkie w takiej właśnie kolejności. Należy podać dwa pierwsze parametry, a trzeci jest określany na podstawie zmiennych today i expiry. Zmienna expiry ustawiana jest na datę o rok dalszą od dnia załadowania strony. Jest to możliwe dzięki przypisaniu zmiennej today nowego obiektu Date i użyciu metody getTime(). Poniżej przedstawiono, jak to działa:
Zmienna today to obiekt typu Date, zatem today.getTime() zwraca czas w milisekundach (od początku 1970 roku, godzina 0:00:00 GMT). W ten sposób mamy już datę w milisekundach, ale termin ważności chcemy określić na jeden rok. Jako że rok ma 365 dni, dzień ma 24 godziny, godzina 60 minut, minuta 60 sekund, a w końcu sekunda ma 1000 milisekund, mnożymy to wszystko przez siebie (wynik to 3,1536*1010 ms) i dodajemy do getTime().
Składnia GetCookie() i DeleteCookie() jest jeszcze prostsza. Wystarczy tylko podać nazwę związaną z żądanym ciasteczkiem. Pierwsza z tych funkcji zwraca wartość (lub null, jeśli ciasteczko nie zostanie znalezione), druga ciasteczko usuwa. Usunięcie oznacza po prostu ustawienie ciasteczka z datą ważności, która już upłynęła.
dhtml.js
Zastosowania praktyczne:
Tworzenie, ukrywanie i pokazywanie warstw DHTML.
Wymagana wersja:
JavaScript 1.2.
Funkcje:
genLayer(), hideSlide(), showSlide(), refSlide()
Ten kod został przedstawiony w dwóch poprzednich rozdziałach (pokaz slajdów i multiwyszukiwarka). Na rysunkach 6.5 i 6.6 pokazano kod tworzący warstwę oraz pozwalający ją ukryć i pokazać na życzenie.
W przykładzie 6.4 pokazano zawartość dhtml.js. Nic nie zostało zmienione, więc szczegółów należy szukać w rozdziałach 3. i 5.
|
|
Rysunek 6.5. Przyciągający wzrok DHTML - to widać
Przykład 6.4. dhtml.js
1 var NN = (document.layers ? true : false);
2 var hideName = (NN ? 'hide' : 'hidden');
3 var showName = (NN ? 'show' : 'visible');
4 var zIdx = -1;
5 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
6 if (NN) {
7 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
8 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
9 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + zIdx + '>' + copy +
10 '</LAYER>');
11 }
12 else {
13 document.writeln('<DIV ID="' + sName + '" STYLE="position:absolute;
14 overflow:none; left:' + sLeft + 'px; top:' + sTop + 'px; width:' +
15 sWdh + 'px; height:' + sHgt + 'px;' + ' visibility:' + sVis +
16 '; z-Index=' + (++zIdx) + '">' + copy + '</DIV>'
17 );
18 }
19 }
20
21 function hideSlide(name) {
22 refSlide(name).visibility = hideName;
23 }
24
25 function showSlide(name) {
26 refSlide(name).visibility = showName;
27 }
28
29 function refSlide(name) {
30 if (NN) { return document.layers[name]; }
31 else { return eval('document.all.' + name + '.style'); }
32 }
|
Rysunek 6.6. Teraz już nie widać
events.js
Zastosowania praktyczne:
Przypisywanie obsługi zdarzeń działające w różnych przeglądarkach, śledzenie ruchu myszy.
Wymagana wersja:
JavaScript 1.2.
Funkcje:
enableEffects(), showXY(), keepKeys(), showKeys()
Jeśli nie eksperymentowałeś dotąd ze skryptami obsługi zdarzeń, działającymi w różnych przeglądarkach, to może przyszedł właśnie czas na pierwsze podejście. Używane są trzy procedury obsługi zdarzenia: onclick, onmousemove i onkeypress. Po kliknięciu gdziekolwiek na powierzchni dokumentu JavaScript przechwyci początkowe położenie myszki. Potem w pasku stanu wyświetlane będą współrzędne w miarę przesuwania się wskaźnika. Ponowne kliknięcie wyłączy śledzenie i wyliczy odległość w pikselach między pierwszym kliknięciem a drugim. Zobaczymy to na rysunkach 6.7 i 6.8.
Niezależnie od myszy można także wciskać klawisze, z których każdy będzie na bieżąco pokazywany w pasku stanu. Kiedy skończysz, wybierz przycisk Pokaż klawisze, a otrzymasz okienko dialogowe JavaScriptu wyświetlające zebrany ciąg wpisanych klawiszy, które pokazano na rysunku 6.9. Po kliknięciu OK można zaczynać zabawę od nowa.
|
Rysunek 6.7. Współrzędne wskaźnika myszki w pasku stanu
|
Rysunek 6.8. Odległość między dwoma punktami w pikselach
|
Rysunek 6.9. Klawisze, które wciskałeś
Teraz znamy już zawiłości kodowania arkuszy stylów działających na różnych przeglądarkach. Warto zapamiętać: w jednej przeglądarce znaczniki LAYER, w drugiej DIV. Szczęśliwie to samo odnosi się do modelu obsługi zdarzeń. Jeśli zajrzymy do kodu źródłowego events.html, znajdziemy następujące dwa wiersze kodu JavaScript:
document.onclick = enableEffects;
document.onkeypress = keepKeys;
Procedura obsługi zdarzenia onclick związana jest z funkcją enableEffects(), natomiast obsługa zdarzenia onkeypress ma związek keepKeys(). Obie funkcje pokazano niżej. Zwróćmy uwagę na to, że nazwy funkcji podawane są bez nawiasów, więc kod nie wygląda tak, jak można by tego oczekiwać:
document.onclick = enableEffects();
document.onkeypress = keepKeys();
Użycie nawiasów wywołałoby każdą z metod w momencie interpretacji danego wiersza. Tego jednak nie chcemy - obsługa zdarzenia związana jest ze wskazaniem na funkcję. Spójrzmy na kod przykładu 6.5.
Przykład 6.5. events.js
1 var keys = '';
2 var change = true;
3 var x1, x2, y1, y2;
4
5 function enableEffects(ev) {
6 if(change) {
7 if(document.layers) {
8 x1 = ev.screenX;
9 y1 = ev.screenY;
10 document.captureEvents(Event.MOUSEMOVE);
11 }
12 else {
13 x1 = event.screenX;
14 y1 = event.screenY;
15 }
16 document.onmousemove = showXY;
17 }
18 else {
19 if (document.layers) {
20 x2 = ev.screenX;
21 y2 = ev.screenY;
22 document.releaseEvents(Event.MOUSEMOVE);
23 }
24 else {
25 x2 = event.screenX;
26 y2 = event.screenY;
27 document.onmousemove = null;
28 }
29 window.status = 'Pocz¦tek: (' + x1 + ',' + y1 +
30 ') Koniec: (' + x2 + ',' + y2 + ') Odleg¦oťŠ: ' +
31 (Math.abs((x2 - x1) + (y2 - y1))) + ' pikseli';
32 }
33 change = !change;
34 }
35
36 function showKeys() {
37 if (keys != '') {
38 alert('Wpisa¦eť klawisze: ' + keys);
39 window.status = keys = '';
40 }
41 else { alert('Najpierw musisz coť powciskaŠ.'); }
42 }
43
44 function showXY(ev) {
45 if (document.all) { ev = event; }
46 window.status = 'X: ' + ev.screenX + ' Y: ' + ev.screenY;
47 }
48
49 function keepKeys(ev) {
50 if (document.layers) {
51 keys += String.fromCharCode(ev.which);
52 window.status = 'Wciťniŕty klawisz: ' + String.fromCharCode(ev.which);
53 }
54 else {
55 keys += String.fromCharCode(event.keyCode);
56 window.status = 'Wciťniŕty klawisz: ' + String.fromCharCode(event.keyCode);
57 }
58 }
Funkcja enableEffects() jest centrum obsługi zdarzeń click i mouseover. Zwróćmy szczególną uwagę na wiersze 6, 18 i 33:
if (change) { ....
else { ....
change = !change;
Zmienna change początkowo ma wartość true, po czym wartość ta zmieniana jest na przeciwną (czyli false, później znów true i tak dalej), przy każdym następnym wywołaniu. Jako że klikanie wywołuje enableEffects(), a change ma za pierwszym razem wartość true, uruchamiane są wiersze 7-15:
if(document.layers) {
x1 = ev.screenX;
y1 = ev.screenY;
document.captureEvents(Event.MOUSEMOVE);
}
else {
x1 = event.screenX;
y1 = event.screenY;
}
Wiersze te pobierają współrzędne x i y oraz uruchamiają obsługę zdarzenia onousemove. Jeśli istnieje document.layers, użytkownik stosuje Netscape Navigatora. Tworzony w biegu obiekt zdarzenia znajduje odbicie w parametrze przekazywanym funkcji o nazwie ev. Zmienne globalne x1 i y1 ustawiane są na wartości odpowiednich współrzędnych pierwszego kliknięcia. Następnie wywołanie metody dokumentu captureEvents() powoduje przejęcie zdarzenia mousemove.
Jeśli document.layers nie istnieje, skrypt zakłada, że użytkownik korzysta z Internet Explorera i podejmuje kroki w zasadzie takie same, jak opisane powyżej. Model zdarzeń Microsoftu jednak definiuje zdarzenie jako obiekt event, tam będą oczekiwały właściwości screenX i screenY. Nie są wymagane żadne dodatkowe wywołania metod, więc mamy tylko wiersz 16:
document.onmousemove = showXY;
Niezależnie od używanej przeglądarki obsługa zdarzenia onousemove jest przypisana przez odwołanie do funkcji showXY():
function showXY(ev) {
if (document.all) { ev = event; }
window.status = 'X: ' + ev.screenX + ' Y: ' + ev.screenY;
}
Wywołanie showXY() przy każdym ruchu myszy wyświetla w pasku stanu współrzędne wskaźnika myszy. Odwołanie się do współrzędnych x i y działa w obu przeglądarkach dzięki zastosowaniu sztuczki podobnej, jak opisana wyżej. showXY() wywoływana jest tak długo, aż użytkownik zdecyduje się znowu kliknąć, co powoduje ponowne wywołanie enableEffects(). Tym razem jednak zmienna change ustawiona jest na false, więc wywoływany jest kod z wierszy 19 do 31:
if (document.layers) {
x2 = ev.screenX;
y2 = ev.screenY;
document.releaseEvents(Event.MOUSEMOVE);
}
else {
x2 = event.screenX;
y2 = event.screenY;
document.onmousemove = null;
}
window.status = 'Początek: (' + x1 + ',' + y1 +
') Koniec: (' + x2 + ',' + y2 + ') Odległość: ' +
(Math.abs((x2 - x1) + (y2 - y1))) + ' pikseli';
Zmienne x1 i y1 zawierają współrzędne pierwszego kliknięcia, a nowe zmienne, x2 i y2, ustawiane są na współrzędne kliknięcia drugiego. Nie trzeba już obsługiwać zdarzenia onmousemove, więc w przypadku Netscape Navigatora wywoływana jest metoda releaseEvents()- ustawiając document.onmousemove na wartość null.
Pozostaje jeszcze tylko wyświetlić odległość między początkowym a końcowym punktem. Pamiętasz wzór na odległość? Odpowiedni wzór w wierszach 29-31.
Teraz zostaje nam jeszcze tylko zająć się zdarzeniem onkeypress. Należy przypomnieć, że document. onkeypress miało wywoływać funkcję keepKeys(). Oto sama keepKeys() z wierszy 49-58:
function keepKeys(ev) {
if (document.layers) {
keys += String.fromCharCode(ev.which);
window.status = 'Wciśnięty klawisz: ' + String.fromCharCode(ev.which);
}
else {
keys += String.fromCharCode(event.keyCode);
window.status = 'Wciśnięty klawisz: ' + String.fromCharCode(event.keyCode);
}
}
Używając tej samej techniki rozpoznawania przeglądarki ciąg pusty ustawiamy jako wartość zmiennej keys oraz napis odpowiadający wciśniętemu klawiszowi. Robimy to, stosując - niezależnie od użytej przeglądarki - String.fromCharCode(). Jednak w JavaScripcie 1.2 klawiszom odpowiadają znaki ISO Latin-1, a JScript używa standardu Unicode. Liczba dla JavaScriptu przechowywana jest we właściwości which obiektu event. Liczba dla JScriptu znajduje się we właściwości event.keyCode. Użytkownik wciska zatem ciąg klawiszy, a następnie wybiera przycisk Pokaż klawisze. Funkcja zgłasza komunikat podający wartość keys, po czym znów tę zmienną zeruje.
frames.js
Zastosowania praktyczne:
Wymuszanie ładowania w ramkach.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
keepIn(), keepOut()
Ten plik zawiera tylko dwie funkcje. Jedna zatrzymuje dokumenty w danym zestawie ramek, druga natomiast trzyma je od ramek z daleka. Aby kod z pliku frames.js mógł zadziałać, wymaga użycia wielu plików HTML. Spróbujemy na przykład załadować do przeglądarki plik ch06\frameset. html. Plik ten zawiera dwie ramki - w jednej z nich znajduje się plik frames.html, który z kolei używa frames.js w celu zapewnienia, że frames.html zawsze zostanie załadowany na górze. Dlatego właśnie załadowanie frameset.html daje komunikat o błędzie (do przeglądarki ładowany jest w końcu frames.html).
|
Słów parę o konkurujących modelach zdarzeń Modele zdarzeń w Netscape Navigatorze 4 i Internet Explorerze 4 mają pewne cechy wspólne, i całe szczęście. Nadal istnieją jednak między nimi znaczące różnice, z których może najważniejszą jest to, że o ile zdarzenia Netscape Navigatora przesuwają się w dół hierarchii (na przykład z window do frame, następnie do document, dalej do form i w końcu do field), to w Internet Explorerze zdarzenia pączkują do góry (na przykład z field do form do document do frame do window). Więcej wiadomości o obu modelach znajdziesz pod podanym niżej adresem URL. Informacje te są niesłychanie istotne, jeśli zamierzasz stosować złożoną, działającą w różnych przeglądarkach, obsługę zdarzeń. Aby dowiedzieć się więcej o Netscape Navigatorze, odwiedź stronę: http://developer.netscape.com/docs/manuals/communicator/jsguiede4/evnt.htm Aby uzyskać więcej o Internet Explorerze, zajrzyj na stronę: http://msdn.microsoft.com/developer/sdk/inetsdk/help/dhtml/doc_object/event_model.htm#dom_event |
|
Jeśli chcemy z kolei upewnić się, że nasz dokument nie zostanie załadowany inaczej niż do wskazanych ramek, też użyj frames.js. Rysunek 6.10 przedstawia, co się stało, kiedy próbowano załadować ch06\frames2.html. Rezultatem jest komunikat o błędzie, informujący, że naruszono zasady użycia ramek, a następnie przeglądarka ładuje odpowiedni zestaw zawierający frames2.html, co widać na rysunku 6.11.
Kod realizujący wszystkie te funkcje jest krótki i przyjemny. Funkcja keepOut() porównuje adres URL dokumentu z górnego okienka z adresem URL bieżącej ramki. Jeśli właściwości location. href nie pasują do siebie, keepOut() protestuje i ładuje dokument we własnym górnym oknie. Funkcja keepIn() dokonuje porównania dokładnie przeciwne i ładuje adres URL z przekazanego argumentu, jeśli porównanie zawiedzie. Plik frames.js pokazano jako przykład 6.6.
Przykład 6.6. frames.js
1 function keepIn(parentHREF) {
2 if (top.location.href == self.location.href) {
3 alert('[Krach]. . . [Grrrrm]. . . Musisz. . . za¦adowaŠ. . .' +
4 'oryginalne. . . ramki.');
5 top.location.href = parentHREF;
6 }
7 }
8
9 function keepOut() {
|
Rysunek 6.10. Naruszenie przyjętej polityki stosowania ramek
Przykład 6.6. frames.js (dokończenie)
10 if (top.location.href != self.location.href) {
11 alert('Ten dokument +adnym ramkom siŕ nie k¦ania.');
12 top.location.href = self.location.href;
13 }
14 }
images.js
Zastosowania praktyczne:
Przewijanie obrazków pod myszką.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
imagePreLoad(), imageSwap(), display()
Tak jak funkcje w dhtml.js, tak i te w images.js były już prezentowane w poprzednich rozdziałach. Rozdziały 3., 4. i 5. zawierają różne wersje kodu z przykładu 6.7. Funkcje te ładują wstępnie obrazki i używają ich jako przewijanych pod myszą elementów.
|
|
Rysunek 6.11. Teraz już lepiej
Przykład 6.7. images.js
1 var imgPath = 'images/';
2 var arrayHandles = new Array('out', 'over');
3
4 for (var i = 0; i < arrayHandles.length; i++) {
5 eval('var ' + arrayHandles[i] + ' = new Array()');
6 }
7
8 for (var i = 0; i < imgNames.length; i++) {
9 imagePreLoad(imgNames[i], i);
10 }
11
12 function imagePreLoad(imgName, idx) {
13 for(var j = 0; j < arrayHandles.length; j++) {
14 eval(arrayHandles[j] + "[" + idx + "] = new Image()");
15 eval(arrayHandles[j] + "[" + idx + "].src = '" + imgPath + imgName +
16 arrayHandles[j] + ".gif'");
17 }
18 }
19
20 function imageSwap(imagePrefix, imageIndex, arrayIdx) {
21 document[imagePrefix].src = eval(arrayHandles[arrayIdx] + "[" +
22 imageIndex + "].src");
23 }
24 function display(stuff) { window.status = stuff; }
Jako że są już znane procedury realizowania przewijania obrazków, nie umieszczono tutaj żadnych rysunków.
navbar.js
Zastosowania praktyczne:
Dynamiczna nawigacja na stronie.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
navbar()
Ten plik źródłowy zawiera jedną tylko funkcję, ale za to jaką! Załóżmy, że nasza witryna zawierająca kilka stron, a na każdej z nich znajduje się pasek nawigacyjny z łączami do stron pozostałych. Czy nie byłoby ciekawie, gdyby JavaScript mógł stworzyć taki pasek, zawierający łącza do wszystkich stron poza bieżącą? Na rysunku 6.12 pokazano ch06\astronomy.html. Pasek nawigacji zawiera łącza do innych stron: o innych dziedzinach nauki, o sporcie, kącik muzyczny i stronę poświęconą różnym ludziom. Na rysunku 6.13 pokazano dokument, jaki jest ładowany po kliknięciu łącza Fajni ludzie. Teraz spójrzmy na pasek nawigacji: dodatkowo znajduje się strona o astronomii, natomiast znikli nasi fajnie ludzie, gdyż właśnie ich oglądamy. Można zrealizować analogiczną funkcję dla dowolnej ilości stron. Jeśli natomiast zmienią się dokumenty, to wystarczy, że zmienimy tylko plik navbar.js. Zaoszczędzi nam to mnóstwo czasu.
|
Rysunek 6.12. Strona o astronomii bez łącza do samej siebie
Realizujący nasze zadanie kod jest zadziwiająco prosty. Rozwijamy tylko tablicę navURLs zawierającą adresy URL stron oraz tablicę linkText treścią łącz. Funkcja navbar() przetwarza kolejno wszystkie nazwy plików i generuje łącza z odpowiednią treścią, pomijając łącze, którego wskaźnik jest równy właściwości location.href bieżącego dokumentu. To proste. Zajrzyjmy do kodu przykładu 6.8.
|
Rysunek 6.13. Strona o fajnych ludziach, znów bez łącza do samej siebie
Przykład 6.8. navbar.js
1 var navURLs = new Array('astronomy.html', 'science.html', 'sports.html',
2 'music.htm', 'people.htm');
3 var linkText = new Array('Astronomia', 'Inne nauki', 'Sport',
4 'Kącik muzyczny', 'Fajni ludzie');
5
6 function navbar() {
7 var navStr= '';
8 for (var i = 0; i < navURLs.length; i++) {
9 if (location.href.indexOf(navURLs[i]) == -1) {
10 navStr += ' <B>[</B><A HREF="' + navURLs[i] + '">' + linkText[i] +
11 '</A><B>]</B> ';
12 }
13 }
14 document.writeln('<BR><BR>' + navStr);
15 }
Można znacząco zwiększyć tę funkcjonalność. Dlaczego nie użyć zamiast zwykłych łącz przewijanych obrazków? Jeśli mamy mnóstwo łącz i nie chcemy ich wszystkich umieszczać w poprzek strony, dlaczego nie zastosować listy wyboru? Wtedy można stworzyć tych łącz naprawdę wiele, oszczędzając przy tym cenne miejsce na ekranie.
numbers.js
Zastosowania praktyczne:
Poprawienie błędu zaokrąglenia i formatowanie liczb na potrzeby programu pełniącego rolę wózka na zakupy.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
twoPlaces(), round(), totals()
JavaScript realizuje obliczenia zmiennoprzecinkowe nieco inaczej niż tego oczekujemy. W rezultacie wiele uzyskiwanych wyników ma charakter zaskakujący. W FAQ grupy dyskusyjnej DevEdge dostępnych pod adresem: http://developer1.netscape.com:80/supprot/faqs/champions/ javascript.html#2-2 podaje się jako przykład mnożenie 0.119 * 100 dające wynik 11.899999. Często też chcemy wyświetlić kwotę w walucie: dolarach i centach lub złotówkach i groszach. Funkcje z pliku numbers.js mają pomóc w obu tych sytuacjach. Wszystkie trzy funkcje oparte są na funkcjach dostępnych na stronie http://www.irt.org/script/number.htm utrzymywanej przez Martina Webba. Na rysunku 6.14 pokazano wynik załadowania pliku ch06\numbers.html.
|
|
Rysunek 6.14. Dzięki JavaScriptowi liczby wyglądają lepiej
Liczby wyświetlane pod nagłówkiem Dwa miejsca dziesiętne pokazują, jak funkcja twoPlaces() formatuje liczby jako waluty. Pozostałe dwa nagłówki wskazują różnicę między wyrażeniem 51,02 - 3,8 z i funkcjami round() i totals() oraz bez nich. Plik numbers.js podano jako przykład 6.9.
Przykład 6.9. numbers.js
1 function twoPlaces(amount) {
2 return (amount == Math.floor(amount)) ? amount + '.00' :
3 ((amount*10 == Math.floor(amount*10)) ? amount + '0' : amount);
4 }
5
6 function round(number,X) {
Przykład 6.9. numbers.js (dokończenie)
7 X = (!X ? 2 : X);
8 return Math.round(number*Math.pow(10,X))/Math.pow(10,X);
9 }
10
11 function totals(num) {
12 return twoPlaces(Math.floor((num - 0) * 100) / 100);
13 }
Funkcja twoPlaces() zwraca napis reprezentujący otrzymaną liczbę z dodanym 0, .00 lub bez niczego, jeśli liczba jest już poprawnie sformatowana. Długie wyrażenie warunkowe przekłada się na polskie następująco:
Jeśli liczba jest równa najmniejszej liczbie całkowitej nie większej od liczby Math.floor(amount), to dodaj do wyniku .00.
W przeciwnym wypadku, jeśli liczba pomnożona przez 10 równa jest największej liczbie całkowitej, nie mniejszej od takiego iloczynu (Math.floor(amount) * 10), zwróć napis z dodanym jednym zerem, 0.
W innym wypadku zwróć tę samą liczbę, która została przekazana.
Jeśli chodzi o błędy w zaokrąglaniu, funkcja round() zwraca liczbę otrzymaną przez zaokrąglenie liczby do x miejsc dziesiętnych. Jeśli nie podamy x, wartością domyślną jest 2.
objects.js
Zastosowania praktyczne:
Tworzenie ogólnych obiektów i ich analiza.
Wymagana wersja:
JavaScript 1.1.
Funkcje:
makeObj(), parseObj(), objProfile()
Teraz czas na obiekty JavaScriptu. Tak wiele można z nimi zrobić, tymczasem tak mało jest czasu na ich przetestowanie. Plik objects.js zawiera dwa narzędzia: jedno to ogólnie konstruktor obiektów, drugie to analizator ich właściwości. Otwórz plik ch06\objects.html, a uzyskasz wynik pokazany na rysunku 6.15.
parseObj() i objProfile() pokazują właściwości dwóch obiektów: jednego -reprezentowanego przez zmienną someObject, drugiego - będącego obiektem location bieżącego okna. Warto przyjrzeć się plikowi objects.html z przykładu 6.10, aby zobaczyć, jak to właściwie działa.
Przykład 6.10. objects.html
1 <HTML>
2 <HEAD>
3 <TITLE>Przykład objects.js</TITLE>
4 <STYLE type="text/css">
5 <!--
6 td { font-family: courier new; font-size: 14}
7 -->
8 </STYLE>
|
Rysunek 6.15. Wyniki działania objects.html
Przykład 6.10. objects.html (dokończenie)
9 <SCRIPT LANGUAGE="JavaScript" SRC="objects.js"></SCRIPT>
10 </HEAD>
11 <BODY>
12 <SCRIPT LANGUAGE="JavaScript">
13 <!--
14
15 function plainOldObject() {
16 this.name = 'nazwa obiektu';
17 this.numba = 1000;
18 this.objInherit = new makeObj('propertyOne', 'thisProperty',
19 'propertyTwo', 'thatProperty', 'propertyThree', 'theOtherProperty');
20 return this;
21 }
22
23 var someObject = new plainOldObject();
24
25 document.write(objProfile('someObject', 'self.location'));
26 //-->
27 </SCRIPT>
28
29 </BODY>
30 </HTML>
Warto zwrócić uwagę, że w wierszu 23 wartość zmiennej someObject ustawiana jest na wartość new plainOldObject(). Konstruktor plainOldObject() ma kilka właściwości, w tym name, numba i objInherit. objInherit reprezentuje obiekt składający się z ogólnego konstruktora obiektu makeObj() z pliku objects.js. Przyjrzyjmy się teraz temu ostatniemu na wydruku 6.11.
Przykład 6.11. objects.js
1 function makeObj() {
2 if (arguments.length % 2 != 0) {
3 arguments[arguments.length] = "";
4 }
Przykład 6.11. objects.js (dokończenie)
5 for ( var i = 0; i < arguments.length; i += 2 ) {
6 this[arguments[i]] = arguments[i + 1] ;
7 }
8 return this;
9 }
10
11 function parseObj(obj) {
12 var objStr = '';
13 for (prop in obj) {
14 objStr += '<TR><TD>W¦aťciwoťŠ: </TD><TD><B>' + prop +
15 '</B></TD><TD>Typ: </TD><TD><B>' + typeof(obj[prop]) +
16 '</B></TD><TD>WartoťŠ: </TD><TD><B>' + obj[prop] +
17 '</B></TD></TR>';
18 if (typeof(obj[prop]) == "object") {
19 objStr += parseObj(obj[prop]);
20 }
21 }
22 return objStr;
23 }
24
25 function objProfile() {
26 var objTable = '<TABLE BORDER=2 CELLSPACING=0><TR><TD><H1>' +
27 'Opis obiektu</H1></TD></TR>';
28 for (var i = 0; i < arguments.length; i++) {
29 objTable += '<TR><TD><BR><BR><H2><TT>' + (i + 1) + ') ' +
30 arguments[i] + '</H2></TD></TR>';
31 objTable += '<TR><TD><TT><TABLE CELLPADDING=5>' +
32 parseObj(eval(arguments[i])) + '</TABLE></TD></TR>';
33 }
34 objTable += '</TABLE><BR><BR><BR>';
35 return objTable;
36 }
Najpierw przyjrzyjmy się makeObj()- oto stosowne wiersze:
function makeObj() {
if (arguments.length % 2 != 0) {
arguments[arguments.length] = "";
}
for ( var i = 0; i < arguments.length; i += 2 ) {
this[arguments[i]] = arguments[i + 1] ;
}
return this;
}
Konstruktor ten tworzy właściwości, zestawiając pary przekazanych argumentów. Jeśli liczba argumentów jest nieparzysta (czyli któryś nie ma pary), tworzony jest dodatkowy element pusty w tablicy arguments. Teraz każdy parametr ma partnera. Następnie makeObj() przeszukuje i grupuje argumenty w pary, przy czym pierwszy element w parze staje się nazwą właściwości, a drugi wartością tej właściwości. Wobec tego wywołanie makeObj('nazwisko', 'Madonna', 'zawod', 'piosenkarka') zwróci wskaźnik na obiekt o następujących właściwościach:
this.nazwisko = 'Madonna';
this.zawod = 'piosenkarka';
Zmienna objInherit odnosi się teraz do obiektu i ma następujące właściwości:
objInherit.propertyOne = 'thisProperty';
objInherit.propertyTwo = 'thatProperty';
objInherit.propertyThree = 'theOtherProperty';
Zwróćmy uwagę, że wszystkie wartości właściwości to napisy. Można oczywiście przekazywać także liczby, obiekty i tak dalej. Funkcja makeObj() doskonale nadaje się do tworzenia wielu obiektów, każdego z innymi właściwościami, bez konieczności definiowania konstruktora dla każdego z nich.
Inny badany obiekt to location. Jest dość prosty, ale jak działa nasza analiza? Funkcje objProfile() i parseObj() wzajemnie się wywołują w celu analizy „w głąb” właściwości obiektu - tworzą przy tym tablicę opisującą obiekt. Każdy wiersz tej tablicy to jedna nazwa właściwości, typ obiektu właściwości oraz przypisana wartość. Zacznijmy od objProfile():
function objProfile() {
var objTable = '<TABLE BORDER=2 CELLSPACING=0><TR><TD><H1>' +
'Opis obiektu</H1></TD></TR>';
for (var i = 0; i < arguments.length; i++) {
objTable += '<TR><TD><BR><BR><H2><TT>' + (i + 1) + ') ' +
arguments[i] + '</H2></TD></TR>';
objTable += '<TR><TD><TT><TABLE CELLPADDING=5>' +
parseObj(eval(arguments[i])) + '</TABLE></TD></TR>';
}
objTable += '</TABLE><BR><BR><BR>';
return objTable;
}
objProfile() to funkcja, którą należy wywołać, a potem przekazać jej parametry. Spójrzmy do wiersza 25 w pliku objects.html:
document.write(objProfile('someObject', 'self.location'));
Przekazywane argumenty absolutnie nie są obiektami, to zwykłe napisy, ale już wkrótce będą elementami obiektu. Przekazanie napisów pozwala JavaScriptowi wyświetlać te obiekty według nazw. Kiedy utworzone zostaną potrzebne znaczniki TR i TD, argumenty są przekazywane do funkcji objProfile(), która w wierszu 32 kolejno analizuje je metodą eval() i przekazuje do parseObj(). Zobaczmy, jak to wygląda:
function parseObj(obj) {
var objStr = '';
for (prop in obj) {
objStr += '<TR><TD>Właściwość: </TD><TD><B>' + prop +
'</B></TD><TD>Typ: </TD><TD><B>' + typeof(obj[prop]) +
'</B></TD><TD>Wartość: </TD><TD><B>' + obj[prop] +
'</B></TD></TR>';
if (typeof(obj[prop]) == "object") {
objStr += parseObj(obj[prop]);
}
}
return objStr;
}
Każdy zanalizowany napis pojawia się jako obiekt i nazywany jest obj. Używając pętli for z instrukcją if, funkcja parseObj() analizuje kolejno wszystkie właściwości obj, zbierając w zmiennej tekstowej wraz z odpowiednimi znacznikami HTML nazwy właściwości, ich typy i wartości. parseObj()określa typ obiektu, stosując operator typeof(). Kiedy już zostaną określone dane właściwości, sprawdza się, czy właściwość ta jest obiektem. Jeśli tak, parseObj() wywołuje sama siebie rekurencyjnie i przekazuje swojej następnej instancji obiekt będący wartością tej właściwości (znów jako obj). Dzięki temu możliwa jest analiza kolejnych poziomów zagnieżdżenia obiektów i pokazanie ich hierarchii.
Kiedy parseObj() nie ma już więcej obiektów do analizy, cały napis z właściwościami, ich typami, wartościami i znacznikami opisującymi tabelę HTML, znajdujący się w zmiennej objStr, jest zwracany do funkcji objProfile(). Ta funkcja z kolei łączy ten napis z resztą znaczników, a w końcu całość, jako objTable, jest dopisywana do strony w wierszu 25 pliku objects.html.
|
Tego typu funkcje analizy obiektów są przeznaczone do obiektów względnie niewielkich, jak na przykład typowe obiekty tworzone przez użytkownika. Zarówno Netscape Navigator, jak i Internet Explorer przeładują się w przypadku skryptu zanadto obfitującego w wywołania rekursywne. Spróbujmy na przykład zmienić wiersz 25 w pliku objects.html z: document.write(objProfile('someObject', 'self.location')); na: document.write(objProfile('document'); Teraz załadujmy tę stronę do Internet Explorer. Na pewno pojawi się komunikat o przepełnieniu stosu. Spróbujmy z kolei w Netscape Navigatorze użyć takiego wiersza 25: document.write(objProfile('window'); Na konsoli JavaScriptu ukaże się taki komunikat: JavaScript Error: too much recursion. |
|
|
strings.js
Zastosowania praktyczne:
Operacje na napisach, sortowanie alfabetyczne, zliczanie wystąpień.
Wymagana wersja:
JavaScript 1.2.
Funkcje:
camelCaps(), prepStr(), wordCount(), reorder()
Funkcje te pokazują, co można zrobić z tekstem, w szczególności z tekstem wprowadzonym przez użytkownika. Po otworzeniu w swojej przeglądarce pliku ch06\string.html, zobaczymy obrazek taki, jak na rysunku 6.16.
Mamy tu trzy formularze służące do demonstracji działania trzech funkcji. Pierwszy formularz zawiera element TEXTAREA służący do wprowadzenia tekstu. Kiedy użytkownik skończy i wciśnie przycisk Zliczaj, funkcja wordCount() wygeneruje nową stronę z tabelą. Tabela zestawia wszystkie użyte słowa z TEXTAREA i podaje liczbę ich wystąpień. Wyniki widać na rysunku 6.17.
Druga formatka także zawiera TEXTAREA do wprowadzania tekstu. Użytkownik może wybrać opcję Upper lub Lower, a wtedy pierwszy znak każdego słowa zmieniony zostanie odpowiednio na wielką lub małą literę. To zadanie realizuje funkcja camelCaps().Może ona przydać się do sprawdzania danych, szczególnie przy wprowadzaniu przez użytkowników nazwisk czy adresów. Wyniki jej działania pokazuje rysunek 6.18.
Trzeci formularz posiada TEXTAREA na słowa użytkownika, które zostaną posortowane alfabetycznie. Rysunek 6.19 przedstawia wyniki. Wielokrotne wybieranie Sortuj powoduje zmianę kolejności sortowania przemiennie na rosnącą i malejącą. Teraz, kiedy już odbył się pokaz lalek, czas sprawdzić, kto pociąga za sznurki.
|
Rysunek 6.16. Trzy formularze do znęcania się nad danymi znakowymi, zaczynamy od zliczania słów
|
Rysunek 6.17. Tablica ze słowami i liczbą ich wystąpień
|
Rysunek 6.18. Zmiana pierwszej litery każdego słowa na wielką
|
Rysunek 6.19. Alfabetyczne sortowanie słów
Przykład 6.12 zawiera kod pliku strings.js.
Przykład 6.12. strings.js
1 function wordCount(str) {
2 var wordArray = new Array();
Przykład 6.12. strings.js (dokończenie)
3 str = prepStr(str);
4 var tempArray = str.split(' ').sort();
5 var count = 1;
6 for (var i = 0; i < tempArray.length; i++) {
7 if (wordArray[tempArray[i]]) {
8 wordArray[tempArray[i]]++;
9 }
10 else { wordArray[tempArray[i]] = 1; }
11 }
12 if (output) { return wordArray; }
13 else {
14 var arrStr = '';
15 for (word in wordArray) {
16 if (word != "") {
17 arrStr += '<TR><TD>' + word + '</TD><TD>' + wordArray[word] +
18 '</TD></TR>';
19 count++;
20 }
21 }
22 return '<TABLE BORDER=0><TR><TD WIDTH=300 VALIGN=TOP ROWSPAN=' +
23 count + '><B>Tekst pierwotny</B><BR><I>' + str +
24 '</I><TD><B>Wyraz</B><TD><B>Liczba</B></TR>' + arrStr +
25 '</TABLE>';
26 }
27 }
28
29 function prepStr(str) {
30 str = str.toLowerCase();
31 str = str.replace(/['"-]/g, "");
32 str = str.replace(/\W/g, " ");
33 str = str.replace(/\s+/g, " ");
34 return str;
35 }
36
37 function camelCaps(str, theCase) {
38 var tempArray = str.split(' ');
39 for (var i = 0; i < tempArray.length; i++) {
40 if (theCase) {
41 tempArray[i] = tempArray[i].charAt(0).toUpperCase() +
42 tempArray[i].substring(1);
43 }
44 else {
45 tempArray[i] = tempArray[i].charAt(0).toLowerCase() +
46 tempArray[i].substring(1);
47 }
48 }
49 return tempArray.join(' ');
50 }
51
52 var order = true;
53
54 function reorder(str) {
55 str = prepStr(str);
56 str = str.replace(/\d/g, "");
57 order = !order;
58 if(!order) { str = str.split(' ').sort().join(' '); }
59 else { str = str.split(' ').sort().reverse().join(' '); }
60 return str.replace(/^\s+/, "");
61 }
Aby zrealizować zliczanie słów z pierwszego formularza, wordCount() wykonuje następujące czynności:
usunięcie wszystkich znaków - poza literami, cyframi, podkreśleniami i spacjami,
utworzenie tablicy na wszystkie słowa z tekstu,
zliczenie wystąpień poszczególnych słów,
wyświetlenie wyników w postaci tabeli.
Pierwszy krok realizuje inna funkcja, prepStr(), znajdująca się w wierszach 29 do 35:
function prepStr(str) {
str = str.toLowerCase();
str = str.replace(/['"-]/g, "");
str = str.replace(/\W/g, " ");
str = str.replace(/\s+/g, " ");
return str;
}
Napis najpierw jest przekształcany na małe litery (nie trzeba rozróżniać „Domu” od „domu”), odbywa się szereg przekształceń tekstu. W wierszu 31 usuwane są cudzysłowy i myślniki, w wierszu 32 wszystkie znaki inne niż litery, cyfry i podkreślenia zamieniane są na pojedyncze spacje. W końcu ciągi sąsiadujących spacji przekształca się na spacje pojedyncze. W ten sposób tekst został oczyszczony i możemy nie obawiać się haseł takich, jak „?” czy „w cudzysłowie”.
Wracamy do wordCount(). prepStr() zwraca napis składający się ze słów porozdzielanych spacjami. Dzięki temu możemy, stosując funkcję split(), zrealizować punkt 2. Punkt 3. wykonywany jest w wierszach 6-11:
for (var i = 0; i < tempArray.length; i++) {
if (wordArray[tempArray[i]]) {
wordArray[tempArray[i]]++;
}
else { wordArray[tempArray[i]] = 1; }
}
W formularzu drugim tekst wpisany przez użytkownika przekazujemy do funkcji camelCaps(). Funkcja ta ma też drugi argument, wartość logiczną, decydującą, czy należy dane zmienić na litery wielkie, czy małe. Oto ta funkcja:
function camelCaps(str, theCase) {
var tempArray = str.split(' ');
for (var i = 0; i < tempArray.length; i++) {
if (theCase) {
tempArray[i] = tempArray[i].charAt(0).toUpperCase() +
tempArray[i].substring(1);
}
else {
tempArray[i] = tempArray[i].charAt(0).toLowerCase() +
tempArray[i].substring(1);
}
}
return tempArray.join(' ');
}
Zmienna lokalna tempArray staje się tablicą ze wszystkimi słowami z tekstu. W naszym przypadku „słowo” to tekst między spacjami. Teraz należy po kolei w każdym słowie zamienić pierwszą literę na wielką lub małą. Kiedy już to zrobimy, funkcja zwróci napis składający się z nowych słów porozdzielanych spacjami. Funkcja camelCaps() zwraca zatem spacje, które wcześniej usunęła, wykonując split().
Jeśli chodzi o trzeci formularz, funkcja reorder() wykonuje albo sort(), albo sort() w odwrotnej kolejności. Zobaczmy poniżej:
var order = true;
function reorder(str) {
str = prepStr(str);
str = str.replace(/\d/g, "");
order = !order;
if(!order) { str = str.split(' ').sort().join(' '); }
else { str = str.split(' ').sort().reverse().join(' '); }
return str.replace(/^\s+/, "");
}
Tak jak w przypadku wordCount(), i tym razem prepStr() przygotowuje najpierw przekazany tekst. Warto zauważyć jednak, że przez wywołanie str.replace(/\d/g, "") usuwane są także cyfry. Zmienna order jest zmieniana na wartość przeciwną niż dotąd, określa ona kolejność sortowania. Teraz zastanówmy się, kroki powinno się podjąć, aby posortować dane, czy to normalnie, czy odwrotnie. W pierwszym przypadku trzeba:
rozbić tekst do tablicy,
posortować elementy tablicy,
złączyć zawartość tablicy w napis.
W drugim wypadku należy:
rozbić tekst do tablicy,
posortować elementy tablicy,
odwrócić kolejność elementów tablicy,
złączyć zawartość tablicy w napis.
W wierszach 58-59 pliku strings.js używamy wartości zmiennej order do podjęcia decyzji, którą metodę wybrać. Następnie napis jest zwracany (ewentualnie pozbawiony wiodących spacji, które mogły pojawić się w wyniku zastosowania join()).
Kierunki rozwoju
Tutaj nie ma żadnych ograniczeń. Można oczywiście dodawać do tych plików świetne funkcje, można ulepszać funkcje również już istniejące. Jednak swoje funkcje w tych samych plikach. Możemy tworzyć grupę stron sieciowych i nazwać plik źródłowy zgodnie z nazwą tych stron, dając mu tylko rozszerzenie .js. Wspaniale. Wstawmy te funkcje, których będziemy potrzebować, i już mamy gotowy komplet własnych narzędzi. Należy tylko pamiętać, aby stosować układ dla siebie najwygodniejszy. Nie wolno pozwolić, aby to pliki .js nami kierowały, to one przecież mają nam służyć.
Cechy aplikacji:
Prezentowane techniki:
|
7
Ustawienia użytkownika oparte na ciasteczkach |
|
W tym rozdziale może się znaleźć aplikacja całkiem bezwartościowa, ale nie warto przerzucać jeszcze kartek. To właśnie tutaj znajdzie się kod, który umożliwi dodanie do naszej witryny niezwykłej wprost funkcjonalności. Chodzi o obsługę ustawień użytkownika. Zastanówmy się nad tym jakie słowo jest najbliższe każdemu sieciowemu wędrowcy?
„Ja”.
Tak, użytkownicy to samolubny typ ludzki, który zawsze myśli o swoich zainteresowaniach i upodobaniach. Cokolwiek ludzie robią, zawsze starają się wyszukiwać rzeczy im odpowiadające. Dlatego właśnie amatorzy DHTML przesiadują w Strefie Dynamicznego HTML (http://www.dhtmlzone. com/), zwolennicy terapii zakupowej odwiedzają strony Shopping.com (http://www.shopping. com/ibuy/), a domorośli astronomowie odwiedzają witrynę Sky & Telescope (http://www.skypub. com/). Użycie fikcyjnej aplikacji daje możliwość personalizacji naszej witryny, nawet jeśli chodzi tylko o zapamiętanie identyfikatora użytkownika. Używając ciasteczek JavaScriptu, goście będą mogli dostosować naszą witrynę do swoich wymagań.
Załóżmy, że tworzymy witrynę dla internetowych inwestorów, którzy mają do wydania nieco gotówki. Goście dostają darmowe członkostwo w fikcyjnym cybercentrum Wall Street o nazwie Take -A-Dive Brokerage $ervices. Nie mogą tu handlować, ale mogą otrzymać specjalizowaną stronę z zestawem łącz do ich ulubionych innych witryn finansowych i elektronicznych centrów informacyjnych. Na rysunku 7.1 pokazano, co się dzieje, kiedy użytkownik po raz pierwszy odwiedza stronę (\ch07\ dive.html). Jest to pierwsza wizyta, zatem trzeba przekierować użytkownika do strony z ustawieniami użytkownika.
Na rysunku 7.2 pokazano stronę, na której użytkownik może ustawić swoje preferencje (\ch07\ prefs.html). Jest to długi formularz umożliwiający użytkownikowi określenie nazwiska, wieku, zawodu i rodzaju strategii inwestycyjnej. Ciąg pól opcji pozwala wybrać, które z dostępnych łącz związanych z finansami (ewentualnie - czy w ogóle jakiekolwiek) mają być umieszczane na specjalizowanej stronie domowej.
|
Rysunek 7.1. Nowi goście przechodzą bezpośrednio na stronę z ustawieniami użytkownika
Formularz zawiera szereg list opcji, za pomocą których użytkownik wybiera obrazek tła, rozmiar czcionki i i jej rodzaj na podstawie pokazanych miniaturek. Kiedy ustalono już wszystko, kliknięcie Zapisz zapisuje dokonane wybory do pliku z ciasteczkami przeglądarki, co pokazano na rysunku 7.3. Potwierdzenie OK przekierowuje użytkownika na stronę dive.html, która zawiera wszystkie ustawienia zgodne z wyborami dokonanymi przez użytkownika.
Dobrze, ale co będzie, jeśli użytkownik zmieni zdanie? Po prostu wybierze łącze Ustawienia i znów będzie na stronie prefs.html. Zwróćmy uwagę na fakt, że zapamiętane zostały bieżące ustawienia użytkownika. Zachowano wszystkie informacje o użytkowniku, jak również zaznaczono wybrane wcześniej pola opcji, a nawet obrazki tła i inne szczegóły. Teraz użytkownik może pobawić się z ustawieniami, aby stwierdzić, co mu najbardziej odpowiada. Na rysunku 7.4 pokazano jedną z wielu możliwych kombinacji.
Wymagania programu
Aplikacja ta została napisana w wersji 1.2 języka JavaScript. Konieczne jest to z uwagi na stosowanie arkuszy stylów i operacje na tekście. Użytkownicy będą musieli użyć wersji 4. lub nowszej Netscape Navigatora czy Internet Explorera. Aplikacja ta może zostać znacznie rozszerzona - używana się wówczas bardzo prostego arkusza stylów. Jedyne ograniczenie to umiejętność posługiwania się DHTML.
Wspomniana aplikacja bazuje na użyciu ciasteczek, zatem elementami ograniczającymi są ich specyfikacje dla obu przeglądarek. Specyfikacje te znajdziemy pod następującymi adresami URL:
|
|
Rysunek 7.2. Ustawianie preferencji użytkownika
dla Netscape Navigatora:
http://developer1.netscape.com:80/docs/manuals/communicator/jsguide4/cookies.htm
dla Internet Explorera:
http://msdn.microsoft.com/msdn-online/workshop/author/dhtml/reference/properties/cookie.asp
Nie martwmy się - w aplikacji dalecy jesteśmy od kresu możliwości ciasteczek w obu przeglądarkach.
Struktura programu
Aplikacja ta składa się z dwóch stron HTML (prefs.html oraz dive.html) i pliku źródłowego JavaScript (cookies.js). Oto krótki opis każdego z tych elementów:
prefs.html
Strona ta używana jest do określenia ustawień strony dive.html. Jej postać też zależy od zewnętrznych informacji zawartych w pliku cookies, gdyż informacje te stosowane są do sprawdzenia, czy użytkownik wprowadził już jakieś ustawienia i do odpowiedniego pokazania formularza.
|
Rysunek 7.3. Po kliknięciu Zapisz użytkownik jest pytany, czy chce obejrzeć stronę dostosowaną do jego wymagań
dive.html
Ta strona z kolei jest tworzona zgodnie z wybranymi przez użytkownika ustawieniami i zawiera samą treść.
cookies.js
Plik ten zawiera funkcje stosowane do zapisywania ustawień użytkownika w ciasteczkach i odczytywania ich stamtąd. Odwołania do cookies.js znajdują się w obu plikach HTML. Funkcje GetCookie() i SetCookie() - wywoływane z kodu - pochodzą właśnie z naszego pliku .js.
Pliki prefs.html i dive.html są nowe, natomiast cookies.js to plik źródłowy z rozdziału 6. - tam też znajdzie się opis zawartych w nim funkcji.
prefs.html
Choć sekwencja zrzutów ekranów sugerować może istnienie prostego, określonego ciągu zdarzeń (kiedy użytkownik zaczyna bez własnych ustawień), załóżmy, że gość na naszej ustawienia już ma swoje ustawienia i do prefs.html wraca, aby coś poprawić. Sądzę, że w takiej sytuacji znacznie łatwiej będzie nam omawiać kod. Przykład 7.1 zawiera plik prefs.html.
Przykład 7.1. prefs.html
1 <HTML>
2 <HEAD>
|
Rysunek 7.4. Przykładowa strona dostosowana do ustawień użytkownika
Przykład 7.1. prefs.html (ciąg dalszy)
3 <TITLE>Ustawienia u+ytkownika Take-A-Dive</TITLE>
4 <STYLE type="text/css">
5 BODY, TD { font-family: Arial; }
6 </STYLE>
7 <SCRIPT LANGAUGE="JavaScript1.2" SRC="cookies.js"></SCRIPT>
8 <SCRIPT LANGUAGE="JavaScript1.2">
9
10 var imagePath = 'images/';
11 var newsNames = new Array(
12 new Array('The Wall Street Journal','http://www.wsj.com/'),
13 new Array('Barron\'s Online','http://www.barrons.com/'),
14 new Array('CNN Interactive','http://www.cnn.com/'),
15 new Array('MSNBC','http://www.msnbc.com/'),
16 new Array('Fox News','http://www.foxnews.com/')
17 );
18
19 var indexNames = new Array(
20 new Array('The New York Stock Exchange','http://www.nyse.com/'),
21 new Array('NASDAQ','http://www.nasdaq.com/'),
22 new Array('Dow Jones Indexes','http://www.dowjones.com/')
23 );
24
25 var strategy = new Array(
26 new Array('Cheap', 'Grywam tylko o małe stawki'),
27 new Array('Stingy', 'Gram ostrożnie'),
28 new Array('Conservative', 'Jestem konserwatystą'),
29 new Array('Moderate', 'Gram typowo'),
30 new Array('Agressive', 'Gram agresywnie'),
Przykład 7.1. prefs.html (ciąg dalszy)
31 new Array('Willing to sell mother', 'Zaprzedałbym własną duszę!')
32 );
33
34 var background = new Array(
35 new Array(imagePath + 'goldthumb.gif', 'Gold Bars'),
36 new Array(imagePath + 'billsthumb.gif', 'Dollar Bills'),
37 new Array(imagePath + 'fistthumb.gif', 'Fist of Cash'),
38 new Array(imagePath + 'currency1thumb.gif', 'Currency 1'),
39 new Array(imagePath + 'currency2thumb.gif', 'Currency 2')
40 );
41
42 var face = new Array(
43 new Array('times', 'Times Roman'),
44 new Array('arial', 'Arial'),
45 new Array('courier', 'Courier New'),
46 new Array('tahoma', 'Tahoma')
47 );
48
49 var size = new Array(
50 new Array('10', 'Small'),
51 new Array('12', 'Medium'),
52 new Array('14', 'Large'),
53 new Array('16', 'X-Large')
54 );
55
56 indexNames = indexNames.sort();
57 newsNames = newsNames.sort();
58
59 var allImages = new Array();
60
61 var imageNames = new Array(
62 'courier10', 'courier12', 'courier14', 'courier16',
63 'arial10', 'arial12', 'arial14', 'arial16',
64 'times10', 'times12', 'times14', 'times16',
65 'tahoma10', 'tahoma12', 'tahoma14', 'tahoma16',
66 'goldthumb', 'billsthumb', 'fistthumb', 'currency1thumb',
67 'currency2thumb', 'blank'
68 );
69
70 for (var i = 0; i < imageNames.length; i++) {
71 allImages[i] = new Image();
72 allImages[i].src = imagePath + imageNames[i] + '.gif';
73 }
74
75 function makePath(formObj) {
76 var fontName = imagePath +
77 formObj.face.options[formObj.face.selectedIndex].value +
78 formObj.size.options[formObj.size.selectedIndex].value + '.gif'
79 swapImage("fontImage", fontName);
80 }
81
82 function swapImage(imageName, imageBase) {
83 document[imageName].src = imageBase;
84 }
85
86 function genSelect(name, select, onChangeStr) {
87 var optStr = "";
88 var arrObj = eval(name);
89 for (var i = 0; i < arrObj.length; i++) {
90 optStr += '<OPTION VALUE="' + arrObj[i][0] +
91 (i == select ? '" SELECTED' : '"') + '>' + arrObj[i][1];
92 }
93 return '<SELECT NAME="' + name + '"' + (onChangeStr ? ' onChange="' +
94 onChangeStr + ';"' : '') + '>' + optStr + '</SELECT>';
95 }
Przykład 7.1. prefs.html (ciąg dalszy)
96
97 function genBoxes(name) {
98 var boxStr = '';
99 for (var i = 0; i < arrObj.length; i++) {
100 boxStr += '<INPUT TYPE=CHECKBOX NAME="' + name + i + '" VALUE="' +
101 arrObj[i][0] + ',' + arrObj[i][1] + '"> ' + arrObj[i][0] + '<BR>'
102 }
103 return boxStr;
104 }
105
106 function getPrefs(formObj) {
107 var prefStr = GetCookie('userPrefs');
108 if (prefStr == null) { return false; }
109 var prefArray = prefStr.split('-->');
110 for (var i = 0; i < prefArray.length; i++) {
111 var currPref = prefArray[i].split('::');
112 if (currPref[1] == "select") {
113 formObj[currPref[0]].selectedIndex = currPref[2];
114 }
115 else if (currPref[1] == "text") {
116 formObj[currPref[0]].value = currPref[2];
117 }
118 else if (currPref[1] == "checkbox") {
119 formObj[currPref[0]].checked = true;
120 }
121 }
122 return true;
123 }
124
125 function setPrefs(formObj) {
126 var prefStr = '';
127 var htmlStr = '';
128 for (var i = 0; i < formObj.length; i++) {
129 if (formObj[i].type == "select-one") {
130 prefStr += formObj[i].name + '::select::' +
131 formObj[i].selectedIndex + '-->';
132 htmlStr += formObj[i].name + '=' +
133 formObj[i].options[formObj[i].selectedIndex].value + '-->';
134 }
135 else if (formObj[i].type == "text") {
136 if (formObj[i].value == '') { formObj[i].value = "Not Provided"; }
137 prefStr += formObj[i].name + '::text::' +
138 safeChars(formObj[i].value) + '-->';
139 htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
140 }
141 else if (formObj[i].type == "checkbox" && formObj[i].checked) {
142 prefStr += formObj[i].name + '::checkbox::' + '-->';
143 htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
144 }
145 }
146 SetCookie('userPrefs', prefStr, expiry);
147 SetCookie('htmlPrefs', htmlStr, expiry);
148 if (confirm('Zmieniono ustawienia. Przełączyć się na dostosowaną stronę?')) {
149 self.location.href = "dive.html";
150 }
151 }
152
153 function safeChars(str) {
154 return str.replace(/::|=|-->/g, ':;');
155 }
156
157 function populateForm(formObj) {
158 if (getPrefs(formObj)) {
Przykład 7.1. prefs.html (ciąg dalszy)
159 makePath(formObj);
160 swapImage('bkgImage',
161 formObj.background.options[formObj.background.selectedIndex].value);
162 }
163 else { resetImage(document.forms[0]); }
164 }
165
166 function resetImage(formObj) {
167 swapImage('bkgImage', formObj.background.options[0].value);
168 swapImage('fontImage', imagePath + formObj.face.options[0].value +
169 formObj.size.options[0].value + '.gif');
170 }
171
172 </SCRIPT>
173 </HEAD>
174 <BODY BGCOLOR=FFFFFF onLoad="populateForm(document.forms[0]);">
175 <DIV ID="setting">
176 <H2>Ustawienia u+ytkownika Take-A-Dive</H2>
177 Wybierz ustawienia najbardziej Ci odpowiadające<BR>
178
179 <UL>
180 <LI><B>Zapisz</B> zapisanie zmian
181 <LI><B>Wyczyść</B> - wyczyszczenie formularza
182 <LI> <B>Cofnij</B> - powrót do strony z łączami
183 </UL>
184
185 <FORM>
186 <TABLE BORDER=1 CELLBORDER=0 CELLPADDING=0 CELLSPACING=1>
187 <TR>
188 <TD COLSPAN=2>
189 <BR>
190 <H3>Dane o inwestorze</H3>
191 </TD>
192 </TR>
193 <TR>
194 <TD>Nazwisko</TD>
195 <TD><INPUT TYPE=TEXT NAME="investor"></TD>
196 </TR>
197 <TR>
198 <TD>Wiek</TD>
199 <TD><INPUT TYPE=TEXT NAME="age"></TD>
200 </TR>
201 <TR>
202 <TD>Strategia</TD>
203 <TD>
204 <SCRIPT LANGUAGE="JavaScript1.2">
205 document.write(genSelect('strategy', 3));
206 </SCRIPT>
207 </TD>
208 </TR>
209 <TR>
210 <TD>Zawód</TD>
211 <TD>
212 <INPUT TYPE=TEXT NAME="occupation">
213 </TD>
214 <TR>
215 <TD COLSPAN=2>
216 <BR>
217 <H3>łącza inwestora</H3>
218 </TD>
219 </TR>
220 <TR>
221 <TD><B>Nowości<B></TD>
222 <TD>
223 <SCRIPT LANUAGE="JavaScript1.2">
Przykład 7.1. prefs.html (ciąg dalszy)
224 document.write(genBoxes('newsNames'));
225 </SCRIPT>
226 </TD>
227 </TR>
228 <TR>
229 <TD><B>Indeksy giełdowe</B></TD>
230 <TD>
231 <SCRIPT LANUAGE="JavaScript1.2">
232 document.write(genBoxes('indexNames'));
233 </SCRIPT>
234 </TD>
235 </TR>
236 <TR>
237 <TD COLSPAN=2>
238 <BR>
239 <H3>Układ ekranu</H3>
240 </TD>
241 </TR>
242 <TR>
243 <TD>
244 <B>Tło</B>
245 <BR>
246 <SCRIPT LANGUAGE="JavaScript1.2">
247 document.write(genSelect('background', 0,
248 "swapImage('bkgImage',
249 this.options[this.selectedIndex].value)"));
250 </SCRIPT>
251 </TD>
252 <TD>
253 <TD><IMG SRC="images/blank.gif"
254 NAME="bkgImage" WIDTH=112 HEIGHT=60>
255 </TD>
256 </TR>
257 <TR>
258 <TD>
259 <B>Rodzaj czcionki</B>
260 <BR>
261 <SCRIPT LANGUAGE="JavaScript1.2">
262 document.write(genSelect('face', 0, "makePath(this.form)"));
263 </SCRIPT>
264 </TD>
265 <TD ROWSPAN=2>
266 <IMG SRC="images/blank.gif" NAME="fontImage"
267 WIDTH=112 HEIGHT=60>
268 </TD>
269
270 </TR>
271 <TR>
272 <TD>
273 <B>Rozmiar czcionki</B>
274 <BR>
275 <SCRIPT LANGUAGE="JavaScript1.2">
276 document.write(genSelect('size', 0, "makePath(this.form)"));
277 </SCRIPT>
278 </TD>
279 </TR>
280 </TABLE>
281 <BR><BR>
282 <INPUT TYPE=BUTTON VALUE="Zapisz" onClick="setPrefs(this.form);">
283 <INPUT TYPE=RESET VALUE="Wyczyść" onClick="resetImage(this.form);">
284 <INPUT TYPE=BUTTON VALUE="Cofnij" onClick="location.href='dive.html';">
285 <!--
286 <INPUT TYPE=BUTTON VALUE="Show"
287 onClick="alert(GetCookie('userPrefs')); alert(GetCookie('htmlPrefs'));">
Przykład 7.1. prefs.html (dokończenie)
288 <INPUT TYPE=BUTTON VALUE="Erase"
289 onClick="DeleteCookie('userPrefs'); DeleteCookie('htmlPrefs');">
290 //-->
291 </FORM>
292 </DIV>
293 </BODY>
294 </HTML>
Wiersze 10-68 to rzecz łatwa. Zmienna w wierszu 10 służy do określenia ścieżki do obrazków, reszta zmiennych służy do definiowania wyglądu dive.html. Wartością wszystkich zmiennych są „wielowymiarowe” tablice, czyli tablice tablic. Oto na przykład tablica jednowymiarowa:
var jedenWymiar = new Array("Ten", "Tamten", "Jeszcze Inny");
Do elementów łatwo się odwołać - wystarczy podać w nawiasach kwadratowych indeks, na przykład jedenWymiar[0]. W tablicy „wielowymiarowej” każdy element jest tablicą z innymi elementami:
var dwaWymiary =
new Array(
new Array(1,2,3),
new Array(4,5,6),
new Array(7,8,9)
);
Teraz dwaWymiary[0] oznacza new Array(1,2,3). Odnosi się zatem do trzech wartości: 1, 2 i 3, a żeby odwołać się do jednej tylko z tych liczb, należy użyć drugiej pary nawiasów:
dwaWymiary[0][0] // odnosi się do 1
dwaWymiary[0][1] // odnosi się do 2
dwaWymiary[0][2] // odnosi się do 3
dwaWymiary[1][0] // odnosi się do 4
dwaWymiary[1][1] // odnosi się do 5
dwaWymiary[1][2] // odnosi się do 6
Terminu „wielowymiarowe” pojawił się w cudzysłowie, albowiem w JavaScripcie formalnie tablice wielowymiarowe nie istnieją, po prostu się je emuluje. Wróćmy teraz do naszego skryptu: każda ze zmiennych deklarowanych w wierszach 11-68 to zestaw tablic z danymi opisanymi w tabeli 7.1.
Jak to sugeruje termin tablica dwuwymiarowa, każdy element tablicy sam jest dwuelementową tablicą. W zasadzie jeden element zawiera wyświetlany tekst, natomiast drugi zawiera tekst, który pełni rolę identyfikatora. Na przykład wartością size[0][0] jest 10 - taka wartość będzie używana do określenia wielkości czcionki. Jednak size[0][1] ma wartość Small - i taki tekst będzie pokazywany na stronie, zatem dla użytkownika czcionka 10-punktowa to czcionka Small. Podobnie zdeklarowano pozostałe tablice.
Wiersze 56-57 zawierają wywołania metody sort() obiektu Array, dzięki czemu łącza do nowinek i notowań są porządnie ułożone. Nie jest to konieczne, ale dwa dodatkowe wiersze kodu nie zaszkodzą. W pliku prefs.html używana jest spora ilość grafiki, więc dobrze odpowiednie obrazki zawczasu załadować. Można to wykonać w wierszach 70-73:
for (var i = 0; i < imageNames.length; i++) {
allImages[i] = new Image();
allImages[i].src = imagePath + imageNames[i] + '.gif';
}
Tabela 7.1. Zmienne zawierające informacje o stronach użytkownika
Nazwa tablicy |
Zawartość |
newsNames |
Nazwy i adresy URL nowinek finansowych. |
indexNames |
Nazwy i adresy URL serwisów z notowaniami giełdowymi. |
strategy |
Nazwy i rodzaje strategii inwestycyjnych, w ramach których mogą się określić użytkownicy. |
background |
Nazwy i adresy URL dostępnych obrazków tła. |
face |
Uchwyty obrazków (więcej na ten temat zaraz) i nazwy dostępnych grup czcionek. |
size |
Uchwyty obrazków i nazwy dostępnych rozmiarów czcionek. |
allImages |
Obecnie pusta tablica, używana później do przechowywania wstępnie załadowanych obrazków w celu ułatwienia dostępu. |
imageNames |
Tablica zawierająca uchwyty obrazków; napisy te pomagają ładować wstępnie obrazki. |
|
|
Każdy element imageNames jest napisem. Jest to uchwyt obrazka, który w połączeniu ze zmienną imagePath i tekstem stałym .gif umożliwi iterację przez elementy imageNames i wstępne ładowanie potrzebnych obrazków. Zapamiętajmy, że uchwyty obrazków (takie jak courier10) nie są nazwami narzuconymi. Przyjęta konwencja nazewnictwa przyda się dalej, co jeszcze będzie omawiane.
Jeśli przejrzy resztę kodu między znacznikami SCRIPT, zauważy, że wszystko inne zdefiniowano jako funkcje. Wobec tego wszelkie fragmenty kodu są wywołane skądś indziej. Ma to miejsce dwukrotnie:
podczas ładowania treści HTML,
w obsłudze zdarzenia onLoad, w znaczniku BODY.
Przyjrzyjmy się zatem kodowi HTML za JavaScriptem (w wierszach 174-294).
Formularz ustawień użytkownika
Interfejsem jest formularz z polami tekstowymi, polami opcji i listami wyboru, w których użytkownik zaznacza preferowane ustawienia. Stworzenie pól tekstowych to kwestia ich zwykłego zakodowania (przynajmniej tym razem) i ustawienie żądanych nazw. Odpowiednio - nazwisko, wiek i zawód znajdziemy w wierszach 195, 199 i 212.
Następne zadanie to określenie preferowanej strategii gry. Kategorie obejmują cały wachlarz zachowań, od bardzo ostrożnych po wyjątkowo agresywne. Tym razem użytkownik nie wypełnia pola tekstowego, ale wybiera jedną z opcji z listy. Zamiast wstawić normalny znacznik OPTION, można użyć funkcji JavaScriptu, która dynamicznie wygeneruje odpowiednią listę. Stosujemy do tego funkcję genSelect() - wiersze 205, 247-249, 262 i 276. Wywołanie z wiersza 205 pozwala wybrać strategię inwestycyjną, tymczasem pozostałe wywołania służą do określenia tła, typu i rozmiaru czcionki. Oto funkcja genSelect() z wierszy 86-95:
function genSelect(name, select, onChangeStr) {
var optStr = "";
var arrObj = eval(name);
for (var i = 0; i < arrObj.length; i++) {
optStr += '<OPTION VALUE="' + arrObj[i][0] +
(i == select ? '" SELECTED' : '"') + '>' + arrObj[i][1];
}
return '<SELECT NAME="' + name + '"' + (onChangeStr ? ' onChange="' +
onChangeStr + ';"' : '') + '>' + optStr + '</SELECT>';
}
Podobną funkcję genSelect() widzieliśmy w rozdziale 5. Funkcja ta generowała tam listę wyboru, pozwalającą wybierać tekst reprezentujący liczbę całkowitą. Tym razem znaczniki OPTION nie są liczbami, ale zawartością tablicy. Spójrz na wywołanie genSelect() w wierszu 205:
document.write(genSelect('strategy', 3));
Funkcja otrzymuje dwa argumenty: napis strategy oraz liczbę 3. „Zaraz - pomyślisz sobie - przecież mamy tablicę o nazwie strategy, po co więc przekazujemy taki napis?”
Zgadza się, mamy tablicę strategy. Może i dobrze byłoby przekazywać jako argument jej zawartość, tyle tylko, że każda lista wyboru musi mieć swoją nazwę, według której będziemy ją identyfikować. Aby sobie uprościć zadanie, każdej liście nadamy nazwę taką samą, jaką ma odpowiadająca jej tablica. Dlatego właśnie przekazujemy taki napis.
Następnie zmiennej arrObj przypisywana jest zinterpretowana wartość napisu; eval(name) oznacza eval('strategy'), co daje nam odwołanie do tablicy strategy. Teraz genSelect() ma zarówno tablicę (arrObj) do wypełnienia znaczników OPTION, jak i nazwę listy wyboru (name).
Drugi przekazany argument jest liczbą całkowitą dostępną jako select. Wartość ta oznacza opcję wybraną domyślnie. Jeśli wartość i równa jest select, odpowiedni znacznik OPTION otrzymuje atrybut SELECTED. Gdy select równe jest 0, domyślnie wybrana będzie pierwsza opcja; jeśli 1, druga, i tak dalej. Odpowiednie wyrażenie znajdziemy w wierszu 91:
(i == select ? '" SELECTED' : '"')
Po pobraniu z tablicy strategy wszystkich elementów i stworzeniu optStr ze znacznikami OPTION, w wierszach 93-94 całość jest sklejana z obejmującymi ją znacznikami SELECT.
No dobrze, a co z trzecim argumentem zdefiniowanym w genSelect()? Nazywa się on onChangeStr i w tym wywołaniu nie został ustawiony, jednak pojawia się w innych miejscach. Oto na przykład wiersze 247-249:
document.write(genSelect('background', 0,
"swapImage('bkgImage',
this.options[this.selectedIndex].value)"));
W tym wywołaniu parametr name jest napisem równym background, select ma wartość 0, a onChangeStr ustawiono na swapImage('bkgImage', this.options[this.selectedIndex].value). Otóż, kiedy genSelect() otrzymuje trzeci argument, jest on doklejany do procedury obsługi zdarzenia onChange tworzonej listy. Jeśli argument ten nie zostanie podany, procedura taka w ogóle nie jest tworzona. Listy wyboru z wierszy 247-249, 262 i 276 używają tego zdarzenia do pokazania obrazków stosownie do wybranych opcji. Kiedy zmienimy wielkość czcionki ze Small na Medium, dalej na Large i X-Large, odpowiednio do tego będzie pokazywany coraz większy obrazek odpowiadający wybranej wielkości.
Listy wyboru nie są jedynymi elementami tworzonymi w tej formatce dynamicznie przez JavaScript. Funkcja genBoxes() generuje dwie grupy pól opcji: jedną dla łącz nowinek, drugą dla łącz notowań giełdowych. Wywołania znajdziesz w wierszach 224 i 232. Oto funkcja genBoxes() z wierszy 97-104:
function genBoxes(name) {
var boxStr = '';
for (var i = 0; i < arrObj.length; i++) {
boxStr += '<INPUT TYPE=CHECKBOX NAME="' + name + i + '" VALUE="' +
arrObj[i][0] + ',' + arrObj[i][1] + '"> ' + arrObj[i][0] + '<BR>'
}
return boxStr;
}
Mamy tutaj do czynienia z sytuacją podobną, jak w genSelect(). Napis odpowiadający żądanej tablicy przekazywany jest i interpretowany w funkcji. Poszczególne elementy tablicy stają się polami opcji, w końcu wynik wstawiany jest do dokumentu.
Ładowanie zapisanych ustawień
Kiedy załadowano już HTML, można do pustego formularza wstawić wcześniejsze ustawienia (o ile były w ogóle wybrane). Procedura obsługi zdarzenia onLoad, umieszczona w znaczniku BODY, wywołuje funkcję populateForm() i przekazuje kopię pustego formularza, na którym mają być przeprowadzane wszystkie operacje. Oto populateForm() z wierszy 157-164:
function populateForm(formObj) {
if (getPrefs(formObj)) {
makePath(formObj);
swapImage('bkgImage',
formObj.background.options[formObj.background.selectedIndex].value);
}
else { resetImage(document.forms[0]); }
}
Funkcja populateForm() tak naprawdę jest tylko nadzorcą i do pracy wykorzystuje inne funkcje. Działa następująco: jeśli ustawiono wcześniej preferencje, odpowiednie pola wypełniane są danymi odczytanymi z ciasteczek. Następnie określane jest tło i obraz czcionki, które odpowiadają ustawieniom odpowiednich list wyboru. W przypadku braku jakichkolwiek ustawień nic nie jest wykonywane. populateForm() do sprawdzenia ewentualnych ustawień użytkownika stosuje funkcję getPrefs() z wierszy 106-123:
function getPrefs(formObj) {
var prefStr = GetCookie('userPrefs');
if (prefStr == null) { return false; }
var prefArray = prefStr.split('-->');
for (var i = 0; i < prefArray.length; i++) {
var currPref = prefArray[i].split('::');
if (currPref[1] == "select") {
formObj[currPref[0]].selectedIndex = currPref[2];
}
else if (currPref[1] == "text") {
formObj[currPref[0]].value = currPref[2];
}
else if (currPref[1] == "checkbox") {
formObj[currPref[0]].checked = true;
}
}
return true;
}
Także ta funkcja ma duże znaczenie. Działa w taki sposób, że pobiera z ciasteczek ustawienia związane z nazwą userPrefs. Jeśli wartość ta jest pusta, zwracana jest wartość false. Oznacza to, że nie ustawiono userPrefs w document.cookie. Jeśli jednak userPrefs nie jest puste, oznacza to, że wprowadzono już jakieś ustawienia. W naszym przykładzie userPrefs jest puste, ale sprawdźmy od razu, co się stanie, jeśli userPrefs już będzie coś zawierać.
Jeśli usePrefs zawiera potrzebne wartości, getPrefs() tworzy tablicę, rozdzielając wartość prefStr według ogranicznika użytego do złączenia poszczególnych ustawień w setPrefs(). Stosuje się tu napis -->. Teraz elementy prefsArray zawierają napisy ograniczone przez ::, określające rodzaj parametru i jego wartość. Przypisanie poszczególnych wartości z odpowiednimi elementami formularza realizowane jest przez iteracyjną analizę elementów prefsArray i przypisywanie ich kolejno - stosownie do ich typu. Lepiej wyjaśnią to wiersze 110-121:
for (var i = 0; i < prefArray.length; i++) {
var currPref = prefArray[i].split('::');
if (currPref[1] == "select") {
formObj[currPref[0]].selectedIndex = currPref[2];
}
else if (currPref[1] == "text") {
formObj[currPref[0]].value = currPref[2];
}
else if (currPref[1] == "checkbox") {
formObj[currPref[0]].checked = true;
}
}
Pamiętajmy, że użytkownik może ustawiać swoje preferencje na trzy sposoby:
wybierając opcję z listy wyboru,
wpisując tekst w polu tekstowym,
zaznaczając pole opcji.
Wobec tego wartości prefsArray zawierają identyfikator typu elementu formularza (text, checkbox lub select-one) oraz napis reprezentujący wartość elementu formularza, a oddzielone są od siebie dwoma dwukropkami, ::. Zaraz to się wyjaśni. Poniżej pokazano kilka przykładów elementów prefsArray.
strategy::select::0
Element formularza strategy jest listą wyboru i wybrano opcję 0 (OPTION 0).
newsNames0::checkbox::Barron's Online,http://www.barrons.com/
Element newsNames0 jest polem opcji i ma wartość Barron's Online,http://www.barrons.com/.
investor::text::Jerry Bradenbaugh
Element investor jest polem tekstowym, a jego wartość to Jerry Bradenbaugh.
Jeśli chodzi o pętlę for z wiersza 110, służy ona do przeglądania elementów tablicy prefsArray, przy czym wartością zmiennej lokalnej currPref staje się tablica powstająca w wyniku rozbicia elementu prefsArray[i] przy każdym wystąpieniu ::. Znaczy to, że currPref będzie miała trzy elementy (dwa dla pól opcji). Jako że currPref[1] zawiera identyfikator typu elementu formularza, jej sprawdzenie pozwoli określić, co getPrefs()ma zrobić z currPref[0] i currPref[2].
Jeśli currPref[1] równe jest select, getPrefs() używa wiersza 113 do przypisania listy wyboru o nazwie - określonej w currPref[0] - opcji związanej z selectedIndex w currPref[2] - faktycznie jest to parseInt(currPref[2]), ale JavaScript „wie”, że napis należy przekształcić na liczbę.
Jeśli currPref[1] równe jest text, getPrefs() w wierszu 116 przypisuje currPref[2] polu tekstowemu o nazwie currPref[0].
Jeśli w końcu currPref[1] równe jest checkbox, getPrefs() w wierszu 119 ustawia właściwość checked pola opcji o nazwie currPref[0] na true. W tym wypadku currPref[2] nie istnieje, a do określenia stanu opcji wystarczy istnienie odpowiedniego wpisu w ciasteczku.
Cały opisany proces zachodzi dla wszystkich elementów prefsArray. Kiedy już cała tablica zostanie przeanalizowana, formularz opisuje wszystkie ustawienia użytkownika. Zatem getPrefs()zrealizowała swoje zadanie i zwraca true, aby poinformować populateForm(), że wszystko się udało.
Składanie obrazków
Zostało jeszcze tylko jedno do zrobienia: synchronizacja obrazków tła i czcionki z opcjami wybranymi w formularzu. Zwróćmy uwagę na to, że oba atrybuty SRC są ustawione w HTML na images/ blank.gif. Jest to po prostu obojętna nazwa użyteczna, póki nie dokonano zmiany zgodnie z ustawieniem użytkownika. Funkcja populateForm() realizuje omawiane zadanie w wierszach 159-161:
makePath(formObj);
swapImage('bkgImage',
formObj.background.options[formObj.background.selectedIndex].value);
Znów populateForm() sama niczego nie dokonuje, tylko wywołuje makePath() i swapImage(), które realizują przewijanie obrazków. Tak naprawdę zresztą realizuje to tylko swapImage(), natomiast makePath() po prostu przetwarza kilka napisów w celu określenia ścieżki plików na jej potrzeby. Przyjrzyjmy się najpierw funkcji, czyli swapImage(). W wierszach 160-161 funkcji tej przekazywane są dwa argumenty, a samą funkcję znajdziemy w wierszach 82-84:
function swapImage(imageName, imageBase) {
document[imageName].src = imageBase;
}
Parametr imageName jest nazwą obiektu Image, który będzie przewijany. Z kolei parametr imageBase to adres URL obrazka. Oto przykład tego, co jest tutaj przekazywane:
formObj.background.options[formObj.background.selectedIndex].value)
Całkiem pokaźny argument, ale jest to po prostu wartość wybranego znacznika OPTION. Jako że getPrefs() ustawiła już listy wyboru zgodnie z wcześniejszymi ustawieniami użytkownika, na pewno znajdzie się jakiś obrazek odpowiadający ustawieniom. Zajrzyjmy do tabeli 7.2, gdzie pokazano wartość znacznika OPTION, tekst opcji (czyli widziany przez użytkownika) oraz argument przekazywany do swapImage().
Wydaje się to dość proste. swapImage() otrzymuje wartość obecnie wybranej opcji, a jednocześnie jest to adres URL obrazka, zatem pobranie obrazka odbywa się natychmiast. Takiego samego kodu, jakiego użyliśmy do obsługi zmiany wartości listy wyboru tła. Oto kod HTML wygenerowany podczas ładowania strony. Zauważmy, że oba argumenty przekazywane do swapImage() są uderzająco podobne do tego, co widzieliśmy w wierszu 161:
<SELECT NAME="background" onChange="swapImage('bkgImage', this.options[this.selectedIndex].value);">
Ustawiliśmy już obrazek tła, zatem przyszedł czas wykonać czcionki. Tutaj rzecz się nieco komplikuje. W przypadku tła wystarczyło zwykłe przewijanie związane z pojedynczą listą wyboru. W przypadku czcionki jest w zasadzie podobnie, ale musimy uwzględnić dwie listy wyboru. Przyjrzyjmy się, jak są nazwane odpowiednie obrazki z czcionką. W tabeli 7.3 pokazano wartości OPTION i odpowiadający im tekst w obu listach wyboru.
Tabela 7.2. Możliwe ustawienia tła
Wartość OPTION |
Tekst opcji |
Argument przekazywany swapImage() |
images/goldthumb.gif |
Gold Bars |
images/goldthumb.gif |
images/billsthumb.gif |
Dollar Bills |
images/billsthumb.gif |
images/fistthumb.gif |
Fist of Cash |
images/fistthumb.gif |
images/currency1thumb.gif |
Currency 1 |
images/currency1thumb.gif |
images/currency2thumb.gif |
Currency 2 |
images/currency2thumb.gif |
|
|
|
Tabela 7.3. Opcje rodzaju czcionki i jej wielkości
Wartość opcji opisującej typ czcionki |
Wartość opcji opisującej wielkość czcionki |
Tekst opisujący typ czcionki |
Tekst opisujący rozmiar czcionki |
Timesroman |
10 |
Times Roman |
Small |
Arial |
12 |
Arial |
Medium |
Courier |
14 |
Courier |
Large |
Tahoma |
16 |
Tahoma |
X-Large |
|
|
|
|
Zobaczmy teraz, co się dzieje, kiedy łączymy ze sobą obie opcje - oto możliwe kombinacje:
timesroman10 |
arial10 |
courier10 |
tahoma10 |
timesroman12 |
arial12 |
courier12 |
tahoma12 |
timesroman14 |
arial14 |
courier14 |
tahoma14 |
timesroman16 |
arial16 |
courier16 |
tahoma16 |
|
|
|
|
Czy nie przypomina to wstępnie ładowanych obrazków tablicy imageNames z wiersza 61? Tak, to rzeczywiście to samo. Okazuje się, że jednak jest to ciekawa aplikacja. Teraz trzeba jeszcze złożyć w całość adresy URL. Musimy wywołać swapImage(), ale najpierw powinniśmy przygotować adresy URL - z pomocą przychodzi nam funkcja makePath() (populateForm() wywołuje ją w wierszu 159). Oto wywoływana funkcja z wierszy 75-80:
function makePath(formObj) {
var fontName = imagePath +
formObj.face.options[formObj.face.selectedIndex].value +
formObj.size.options[formObj.size.selectedIndex].value + '.gif'
swapImage("fontImage", fontName);
}
Jako parametr funkcja makePath() pobiera kopię obiektu formularza. Odpowiednie wartości pobieramy z formObj, a następnie dodajemy końcówkę .gif. Teraz zmienna lokalna fontName zawiera napis określający prawidłowy obrazek. Sprawę kończy wywołanie swapImage() w wierszu 79. Oczywiście wszystko to odbywa się przy założeniu, że wcześniej użytkownik dokonał wyboru swoich ustawień. Jeśli getPrefs() jednak zwróciła false, populateForm() wywołuje resetImage() w wierszu 163, aby ustawić obrazki związane z opcją 0 dla tła i czcionki. Więcej szczegółów na ten temat pojawiły się dalej, kiedy będziemy omawiać czyszczenie formularza.
A teraz chwila przerwy na podsumowanie:
Elementy formularza zostały zapisane na stronie, przy czym niektóre były zapisywane przez wywołania genSelect() i genBoxes().
Informacje z ciasteczek zostały pobrane przez funkcję getPrefs() i użyte do ustawiania poszczególnych elementów formularza.
Obrazki tła i czcionek były synchronizowane - zgodnie z wyborami użytkownika -przez funkcje swapImage() i makePath().
Wprowadzanie zmian
Użytkownik widzi, że strona z preferencjami „zapamiętała” jego ostatnie ustawienia. Teraz zajmijmy się tym, co się dzieje, kiedy użytkownik zdecyduje się na zmiany.
Z punktu widzenia użytkownika wprowadzanie zmian jest proste, gdyż wystarczy wpisać do pól tekstowych nowy tekst, wybrać inne opcje na listach wyboru czy zaznaczyć lub odznaczyć pola opcji. Można następnie wybrać przycisk Zapisz i gotowe. Kiedy jednak skończy się zadanie użytkownika, to nasze dopiero się zaczyna. Przyjrzyjmy się kodowi obsługi przycisku Zapisz z wiersza 282:
<INPUT TYPE=BUTTON VALUE="Zapisz" onClick="setPrefs(this.form);">
Wygląda całkiem typowo. Wywoływana jest funkcja setPrefs(), która dostaje jako parametr kopię formularza. Tak naprawdę ciekawe rzeczy zaczynają się od wierszy 125-151:
function setPrefs(formObj) {
var prefStr = '';
var htmlStr = '';
for (var i = 0; i < formObj.length; i++) {
if (formObj[i].type == "select-one") {
prefStr += formObj[i].name + '::select::' +
formObj[i].selectedIndex + '-->';
htmlStr += formObj[i].name + '=' +
formObj[i].options[formObj[i].selectedIndex].value + '-->';
}
else if (formObj[i].type == "text") {
if (formObj[i].value == '') { formObj[i].value = "Not Provided"; }
prefStr += formObj[i].name + '::text::' +
safeChars(formObj[i].value) + '-->';
htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
}
else if (formObj[i].type == "checkbox" && formObj[i].checked) {
prefStr += formObj[i].name + '::checkbox::' + '-->';
htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
}
}
SetCookie('userPrefs', prefStr, expiry);
SetCookie('htmlPrefs', htmlStr, expiry);
if (confirm('Zmieniono ustawienia. Przełączyć się na dostosowaną stronę?')) {
self.location.href = "dive.html";
}
}
Funkcja setPrefs() generuje dwa napisy: jeden jest przypisywany zmiennej lokalnej prefStr, a drugi zmiennej lokalnej htmlStr. Potrzebujemy dwóch ciasteczek: jednego do wypełnienia formularza na omawianej stronie, drugiego zaś do wygenerowania odpowiedniej postaci strony dive. html. Przechowywane informacje wydają się w obu wypadkach niemalże identyczne, tyle tylko, że w każdym wypadku dane są inaczej zapisane. Zaraz to zobaczymy. Oto podstawowy zarys działania funkcji setPrefs():
iteracja formObj z tworzeniem dwóch tekstów ciasteczek w oparciu o wartości elementów formularza,
zapisanie obu ciasteczek w pliku,
umożliwienie użytkownikowi przejście do strony dive.html, aby mógł obejrzeć efekt wprowadzonych zmian.
Krok 1. Iteracja formObj
To nie powinien być problem. Od samego początku tej książki wiele razy realizowaliśmy taką iterację, a tutaj dzieje się to samo, tyle tylko, że setPrefs() musi wiedzieć, czego szukać.
Spójrzmy jeszcze raz na formularz ustawień. Można zauważyć, że każdy element (poza przyciskami na dole) jest polem tekstowym, listą wyboru lub polem opcji. Wobec tego setPrefs() musi wiedzieć tylko, co ma zrobić, kiedy wartość formObj[i] opisuje poszczególne typy elementów. Oto wiersze 129-144, w których znajdziemy wytyczne:
if (formObj[i].type == "select-one") {
prefStr += formObj[i].name + '::select::' +
formObj[i].selectedIndex + '-->';
htmlStr += formObj[i].name + '=' +
formObj[i].options[formObj[i].selectedIndex].value + '-->';
}
else if (formObj[i].type == "text") {
if (formObj[i].value == '') { formObj[i].value = "Not Provided"; }
prefStr += formObj[i].name + '::text::' +
safeChars(formObj[i].value) + '-->';
htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
}
else if (formObj[i].type == "checkbox" && formObj[i].checked) {
prefStr += formObj[i].name + '::checkbox::' + '-->';
htmlStr += formObj[i].name + '=' + formObj[i].value + '-->';
}
Jedną z bardzo interesujących, ale niedocenianych właściwości elementu formularza jest właściwość type, która zawiera określenie typu elementu. setPrefs() musi być świadoma istnienia tylko trzech typów: select-one, text i checkbox. Złożona instrukcja if - pokazana w powyższym kodzie - powoduje nieco inne działanie dla poszczególnych typów elementów. Funkcja setPrefs() w ten czy inny sposób realizuje następujące funkcje:
Łączy nazwę elementu formularza, napis określający jego typ i ewentualnie wartość lub wybraną pozycję - wszystkie rozdzielone separatorami.
Łączy napis z istniejącymi już wartościami prefStr i htmlStr.
Jeśli element jest listą wyboru, do napisu dołącza się identyfikator wybranej pozycji. Gdy element jest polem opcji, do napisu dodawana jest nazwa tego pola. Jeśli natomiast element jest polem tekstowym, dodawane są jego nazwa i wartość. Zwróćmy uwagę, że funkcja safeChars() przekształca wartości pól tekstowych. Wynika to stąd, że wartości związane z listami wyboru i polami opcji są od razu narzucone i wiadomo, że nie będą zawierały niejasnych znaków. W polu tekstowym użytkownik może wpisać co zechce, a w szczególności np. jeden z ciągów używanych przez nas jako ograniczniki (::, --> i =). To mogłoby kompletnie zdezorganizować działanie naszej aplikacji podczas analizy zawartości ciasteczek. Oto owa funkcja czyszcząca z wierszy 153-155:
function safeChars(str) {
return str.replace(/::|=|-->/g, ':;');
}
Funkcja safeChars() po prostu usuwa wszystkie zarezerwowane napisy ze wszystkich danych wprowadzonych przez użytkownika i zwraca wynik takiej operacji. Wszystkie napisy określające nazwę/ typ, wartość lub wybraną pozycję są ograniczone przez -->. Każdy fragment tekstu w zmiennej prefStr ograniczony jest przez ::, natomiast w htmlStr używa się znaku =. Nie jest konieczne stosowanie akurat takich ograniczników, ale są one dość proste i przez to wygodne. Oto przykład, jak mogą wyglądać te napisy podczas tworzenia ich na podstawie tego samego formularza:
prefStr może wyglądać następująco:
investor::text::Not Provided-->age::text::Not Provided-->strategy::select::3-->occupation::text::Not Provided-->newsNames0::checkbox::-->newsNames1::checkbox::-->newsNames2::checkbox::-->newsNames4::checkbox::-->indexNames0::checkbox::-->indexNames2::checkbox::-->background::select::2-->face::select::3-->size::select::2-->
Natomiast htmlStr może wyglądać tak:
investor=Not Provided-->age=Not Provided-->strategy=Moderate-->occupation=Not Provided-->newsNames0=Barron's Online,http://www.barrons.com/-->newsNames1=CNN Interactive,http://www.cnn.com/-->newsNames2=Fox News,http://www.foxnews.com/-->newsNames4=The Wall Street Journal,http://www.wsj.com/-->indexNames0=Dow Jones Indexes,http://www.dowjones.com/-->indexNames2=The New York Stock Exchange,http://www.nyse.com/-->background=images/fistthumb.gif-->face=tahoma-->size=14-->
Pamiętajmy, że --> rozdziela hasła formularza zarówno w prefStr, jak i w htmlStr, natomiast :: i = rozdzielają poszczególne fragment jednego elementu, każdy w innej zmiennej. Tę informację uzyskałem usuwając komentarze, które dotąd ukrywały wiersze 286-289. Dodatkowe przyciski umożliwiają wyświetlenie wartości zmiennych i usunięcie zawartości ciasteczek. Nasi użytkownicy takich przycisków nie będą potrzebowali, ale nam mogą one pomóc w uruchamianiu aplikacji.
Jeśli powyższy kod wygląda jak nieciekawa zmienna, nie należy się przejmować. Dekodowaniem htmlStr zajmiemy się, kiedy będziemy przygotowywać kod dive.html. Na szczęście już mamy za sobą rozkodowanie prefStr w funkcji getPrefs(). Dobrze byłoby zajrzeć do tej sekcji. Porównanie tego, jak setPrefs() zestawia informacje w ciasteczko i jak getPrefs()odczytuje, z pewnością może ułatwić zrozumienie działania całości.
Krok 2. Zapisanie danych do pliku cookies
Kiedy już prefStr i htmlStr zawierają komplet informacji o ustawieniach użytkownika, wywołanie SetCookie() w wierszach 146-147 zapisuje informacje w pliku ciasteczek.
Netscape Navigator informacje te trzyma w pliku cookies.txt. Oto fragment mojego pliku:
.hotwired.com TRUE / FALSE 2145917529 p_uniqid
2sfurM4NNMfDKAqQ8A
.hotbot.com TRUE / FALSE 946739221 p_uniqid
3MarneJsXGwNqxWbFA
www.allaire.com FALSE / FALSE 2137622729 CFTOKEN 97611446
Z kolei Internet Explorer 4.x oraz 5.x przechowuje poszczególne ciasteczka w osobnych plikach, które są nazywane zgodnie z nazwami domen, skąd dane ciasteczko pochodzi, i nazwą użytkownika, który był zarejestrowany w momencie ustawiania ciasteczka. Oto część moich plików z komputera wyposażonego w Windows NT (loguję się jako administrator):
Cookie:administrator@altavista.com
Cookie:administrator@amazon.com
Cookie:administrator@builder.com
Cookie:administrator@cnn.com
Cookie:administrator@dejanews.com
Cookie:administrator@hotbot.com
Cookie:administrator@infoseek.com
Krok 3. Pokazanie użytkownikowi nowych ustawień
Ostatnie zadanie SetPrefs() to przekierowanie użytkownika na stronę dive.html, aby mógł obejrzeć efekt swojego działania. Oto stosowny kod z wierszy 148-150:
if (confirm('Zmieniono ustawienia. Przełączyć się na dostosowaną stronę?')) {
self.location.href = "dive.html";
}
W ten sposób zakończyliśmy omawianie działania prefs.html. Jest jednak jeszcze jedna jego funkcja, którą w większości wypadków można pominąć, a mianowicie czyszczenie zawartości formularza.
Zerowanie formularza
Czy nie wystarczyłby zwykły przycisk <INPUT TYPE=RESET>? Owszem, taki przycisk czyści zawartość tekstowych okienek edycyjnych, usuwa zaznaczenia z pól opcji i ustawia wszystkie listy wyboru na OPTION 0. Świetnie, ale nie zostaną wtedy usunięte nasze obrazki tła i czcionek. W obu wypadkach muszą one zostać ustawione na odpowiadające OPTION 0, dlatego też wiersz 283 wygląda następująco:
<INPUT TYPE=RESET VALUE="Wyczyść" onClick="resetImage(this.form);">
Nie tylko zerowany jest sam formularz, ale wywoływana jest też funkcja resetImage(), znajdująca się w wierszach 166-170:
function resetImage(formObj) {
swapImage('bkgImage', formObj.background.options[0].value);
swapImage('fontImage', imagePath + formObj.face.options[0].value +
formObj.size.options[0].value + '.gif');
}
Jest to kolejna funkcja wyręczająca się innymi- wywołuje ona dwukrotnie swapImage(). Za pierwszym razem pokazywany jest obrazek tła odpowiadający ustawieniu OPTION 0 (czyli formObj. background.options[0].value). Za drugim razem dzieje się to samo z obrazkiem czcionki. Podobnie, jak w przypadku użycia makePath(), resetImages() tworzy ze zmiennej imagePath i ustawień list czcionki (obie równe są zero, nie używamy tym razem selectedIndex) oraz stałego tekstu .gif nazwę pliku. Oba wywołania ustawiają odpowiednie obrazki.
W ten sposób doszliśmy do końca prefs.html i możemy przejść do opisania dive.html.
dive.html
Zmieniły się ustawienia użytkownika i czas teraz zobaczyć, jaki jest wizualny rezultat. Zadanie nie jest trudne, ale niektóre jego szczegóły mogą przyprawić o ból głowy. Wydaje się oczywiste, że dane będą pochodziły z ciasteczek. Wydobyte stamtąd informacje użyte zostaną na trzy sposoby:
Do określenia adresu URL obrazka tła.
Do określenia adresów i treści łącz.
Jako część arkusza stylów, opisująca rodzaj i rozmiar użytej czcionki.
W trakcie analizy kodu spotkamy się ze wszystkimi zastosowaniami, a na razie zapoznajmy się z plikiem dive.html pokazanym w przykładzie 7.2.
Przykład 7.2. dive.html
1 <HTML>
2 <HEAD>
3 <TITLE>
4 Twoja strona łącz Take-A-Dive
5 </TITLE>
6 <SCRIPT LANGAUGE="JavaScript1.2" SRC="cookies.js"></SCRIPT>
7 <SCRIPT LANGUAGE="JavaScript1.2">
8 <!--
9
10 var newsNames = new Array();
11 var indexNames = new Array();
12
13 function getAttributes() {
14 var htmlStr = GetCookie('htmlPrefs');
15 if (htmlStr == null) {
16 alert('Witaj. Najpierw musisz określiĆ swoje preferencje.' +
17 'Kiedy wciśniesz OK, załadowana zostanie odowiednia strona.');
18 self.location.href = 'prefs.html';
19 }
20 var htmlArray = htmlStr.split('-->');
21 for (var i = 0; i < htmlArray.length; i++) {
22 var tagInfo = htmlArray[i].split('=');
23 if (tagInfo[0] != "") {
24 if (tagInfo[0].indexOf('newsNames') == 0) {
25 newsNames[newsNames.length] = tagInfo[1];
26 }
27 else if (tagInfo[0].indexOf('indexNames') == 0) {
28 indexNames[indexNames.length] = tagInfo[1];
29 }
30 else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
31 }
32 }
33 }
34
35 getAttributes();
36
37 function genLinks(linkArr) {
38 var linkStr = '';
39 for (var i = 0; i < linkArr.length; i++) {
40 var linkParts = linkArr[i].split(',')
41 linkStr += ' - <A HREF="' + linkParts[1] + '"> ' +
42 linkParts[0] + '</A><BR>'
43 }
44 return linkStr;
45 }
46
47 //-->
48 </SCRIPT>
49 <SCRIPT LANGUAGE="JavaScript1.2">
50 document.write('<STYLE type="text/css"> TD
51 { font-family: ' + face + '; font-size: ' + size + 'pt; } </STYLE>');
52 </SCRIPT>
53 </HEAD>
54 <SCRIPT LANGUAGE="JavaScript">
55 document.write('<BODY BACKGROUND="' +
56 background.replace(/thumb/, "") + '">');
57 </SCRIPT>
58 <TABLE BORDER=0>
59 <TR>
60 <TD VALIGN=TOP COLSPAN=4>
Przykład 7.2. dive.html (ciąg dalszy)
61 <H2>Take-A-Dive Brokerage $ervices</H2>
62 </TD>
63 </TR>
64 <TR>
65 <TD VALIGN=TOP COLSPAN=4>
66 Take-A-Dive Brokerage Services ma pomóc TOBIE zainwestować
67 pieniądze. <BR> Nasze motto brzmi "<I>Wchodzimy w to.</I>"
68 Oto Twój profil
69 i wybrane przez Ciebie łącza.
70 <BR><BR>
71 </TD>
72 </TR>
73 <TR>
74 <TD VALIGN=TOP>
75 Nazwisko:</TD>
76 <TD VALIGN=TOP>
77 <SCRIPT LANGUAGE="JavaScript1.2">document.write(investor);</SCRIPT>
78 </TD>
79 <TD VALIGN=TOP>
80 Wiek:</TD>
81 <TD VALIGN=TOP>
82 <SCRIPT LANGUAGE="JavaScript1.2">document.write(age);</SCRIPT>
83 </TD>
84 </TR>
85 <TR>
86 <TD VALIGN=TOP>
87 Strategia:
88 </TD>
89 <TD VALIGN=TOP>
90 <SCRIPT LANGUAGE="JavaScript1.2">
91 document.write(strategy);
92 </SCRIPT>
93 </TD>
94 <TD VALIGN=TOP>
95 Zawód:
96 </TD>
97 <TD VALIGN=TOP>
98 <SCRIPT LANGUAGE="JavaScript1.2">
99 document.write(occupation);
100 </SCRIPT>
101 </TD>
102 </TR>
103 <TR>
104 <TD VALIGN=TOP COLSPAN=2>
105 <BR><BR>
106 łcza do stron z nowinkami<BR>
107 <SCRIPT LANGUAGE="JavaScript1.2">
108 document.writeln(genLinks(newsNames));
109 </SCRIPT>
110 <TD VALIGN=TOP COLSPAN=2>
111 <BR><BR>
112 łącza do stron z notowaniami giełdowymi <BR>
113 <SCRIPT LANGUAGE="JavaScript1.2">
114 document.write(genLinks(indexNames));
115 </SCRIPT>
116 </TD>
117 </TR>
118 <TR>
119 <TD VALIGN=TOP COLSPAN=2>
120 <BR><BR>
121 [ <A HREF="prefs.html">Ustawienia</A> ]
122 </TD>
123 </TR>
124 </TABLE>
125 </BODY>
126 </HTML>
Analiza ciasteczek
Przyjrzyjmy się znacznikom SCRIPT. Już na podstawie ich rozmieszczenia nietrudno zgadnąć, że ta strona jest naprawdę tylko szablonem, który będzie w biegu wypełniany właściwą treścią. Pierwszym krokiem ku temu musi być przeanalizowanie zapisów z ciasteczek - wywoływana jest funkcja getAttributes(). Aby móc dynamicznie określić wygląd strony, będziemy tych informacji potrzebować szybko, jeszcze przed załadowaniem strony. Dlatego właśnie getAttributes() wywoływana jest w wierszu 35, czyli zaledwie dwa wiersze po jej zdefiniowaniu. Oto wiersze 13-33:
function getAttributes() {
var htmlStr = GetCookie('htmlPrefs');
if (htmlStr == null) {
alert('Witaj. Najpierw musisz określić swoje preferencje.' +
'Kiedy wciśniesz OK, załadowana zostanie odowiedniednia strona.');
self.location.href = 'prefs.html';
}
var htmlArray = htmlStr.split('-->');
for (var i = 0; i < htmlArray.length; i++) {
var tagInfo = htmlArray[i].split('=');
if (tagInfo[0] != "") {
if (tagInfo[0].indexOf('newsNames') == 0) {
newsNames[newsNames.length] = tagInfo[1];
}
else if (tagInfo[0].indexOf('indexNames') == 0) {
indexNames[indexNames.length] = tagInfo[1];
}
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
}
}
}
Zmienna lokalna otrzymuje wartość zwracaną przez funkcję GetCookie(). W pliku prefs.html potrzebne dane o formularzu były zapisane w ciasteczku prefStr, ale w dive.html będziemy potrzebować ciasteczka htmlStr. Jeśli okaże się, że htmlStr jest puste, należy przyjąć, że użytkownik jeszcze nie wprowadził swoich ustawień, więc należy go o tym poinformować i przekierować na stronę prefs.html, aby uzupełnił dane.
Jeśli wartość htmlStr nie jest pusta, rozbijemy ją funkcją split() między każdymi ogranicznikami -->, dzięki czemu otrzymamy lokalną tablicę htmlArray. Pętla for kolejno przypisze wartości wszystkim elementom. Warto zaznaczyć, że jest to niemalże identyczna konstrukcja logiczna, jak getPrefs() w pliku prefs.html. Zajrzyjmy do wierszy 110-120 w prefs.html i porównaj je z wierszami 20-32 w pliku dive.html:
var htmlArray = htmlStr.split('-->');
for (var i = 0; i < htmlArray.length; i++) {
var tagInfo = htmlArray[i].split('=');
if (tagInfo[0] != "") {
if (tagInfo[0].indexOf('newsNames') == 0) {
newsNames[newsNames.length] = tagInfo[1];
}
else if (tagInfo[0].indexOf('indexNames') == 0) {
indexNames[indexNames.length] = tagInfo[1];
}
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
}
}
Twarzą w twarz z nieznanym
Ciekawe getPrefs() w prefs.html wie, że będzie pracować z formularzem opisanym w formObj, przypisuje wartości, pola opcji i wybory stosownie do tego getAttributes() sprawa nie jest tak prosta - trzeba mieć przynajmniej jakieś pojęcie o tym, czego oczekiwać w ciasteczku. Wiemy na przykład, że będą tam pewne informacje o łączach do nowinek inwestycyjnych, jak również łącza do notowań giełdowych. Kto wie, ileż ich tam może być: 0, 10, a może 50? Jako że jest to niewiadoma, obie grupy danych wstawimy do osobnych tablic zdefiniowanych w wierszach 10 i 11:
var newsNames = new Array();
var indexNames = new Array();
Zmienna newsNames będzie miała dane łącz do stron z nowinkami, a indexNames takie same informacje dotyczące notowań giełdowych. Przyjrzyjmy się przykładowej wartości ciasteczka z poprzedniej sekcji. Zwróćmy uwagę na pogrubiony tekst:
investor=Not Provided-->age=Not Provided-->strategy=Moderate-->occupation=Not Provided-->newsNames0=Barron's Online,http://www.barrons.com/-->newsNames1=CNN Interactive,http://www.cnn.com/-->newsNames2=Fox News,http://www.foxnews.com/-->newsNames4=The Wall Street Journal,http://www.wsj.com/-->indexNames0=Dow Jones Indexes,http://www.dowjones.com/-->indexNames2=The New York Stock Exchange,http://www.nyse.com/-->background=images/fistthumb.gif-->face=tahoma-->size=14-->
Pogrubione nazwy oznaczają informacje przeznaczone do umieszczenia we wspomnianych wyżej tablicach. Wiemy teraz, że jakieś zmienne się pojawią, ale o jakich nazwach? Ile ich będzie? Kod w dive.html nie wyjaśni żadnych tajemnic, ale to nie szkodzi. Jeśli tylko wiemy jakie są nazwy zmiennych, nie trzeba wszystkiego kodować w dive.html. Aby to zrozumieć, przyjrzyj się jeszcze raz przykładowej wartości uzyskanej z GetCookies('htmlPrefs'), zwracając znowu uwagę na pogrubiony tekst:
investor=Not Provided-->age=Not Provided-->strategy=Moderate-->occupation=Not Provided-->newsNames0=Barron's Online,http://www.barrons.com/-->newsNames1=CNN Interactive,http://www.cnn.com/-->newsNames2=Fox News,http://www.foxnews.com/-->newsNames4=The Wall Street Journal,http://www.wsj.com/-->indexNames0=Dow Jones Indexes,http://www.dowjones.com/-->indexNames2=The New York Stock Exchange,http://www.nyse.com/-->background=images/fistthumb.gif-->face=tahoma-->size=14-->
Wytłuszczony kod tego przykładu odpowiada nazwom zmiennych, które zaraz zdefiniujemy. Pętla for w getAttributes() jest w stanie obsłużyć zarówno przypisania elementów tablic, jak i deklaracje „nieznanych” zmiennych. Oto wiersze od 22 do 31:
var tagInfo = htmlArray[i].split('=');
if (tagInfo[0] != "") {
if (tagInfo[0].indexOf('newsNames') == 0) {
newsNames[newsNames.length] = tagInfo[1];
}
else if (tagInfo[0].indexOf('indexNames') == 0) {
indexNames[indexNames.length] = tagInfo[1];
}
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
}
Każdy element htmlArray zawiera znak równości (=), rozdzielający identyfikator od naprawdę interesującej nas wartości. W każdej iteracji pętli for htmlArray[i] rozbijana jest funkcją split() na znaku =, a uzyskana tak subtablica umieszczana jest w zmiennej lokalnej tagInfo. Jeśli tagInfo[0] nie jest ciągiem pustym, mamy poprawną parę identyfikator + nazwa. Trzeba sprawdzić, czy uzyskany wynik nie jest napisem równym, a to z uwagi na sposób tworzenia tablicy w funkcji split() w JScripcie.
Każda poprawna para należy do jednej z dwóch kategorii: jest albo elementem tablicy, albo zwykłą zmienną. Jeśli dana para ma należeć do tablicy, znów zostanie przyporządkowana do jednego z dwóch typów: albo opisuje łącze strony z nowinkami, albo z notowaniami giełdowymi. Poniższa instrukcja warunkowa określa, co należy zrobić w zależności od zachodzących okoliczności:
if (tagInfo[0].indexOf('newsNames') == 0) {
newsNames[newsNames.length] = tagInfo[1];
}
else if (tagInfo[0].indexOf('indexNames') == 0) {
indexNames[indexNames.length] = tagInfo[1];
}
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"'); }
Z uwagi na konwencję nazewniczą przyjętą w prefs.html, jeśli tagInfo[0] zawiera napis newNames, musi być związana z łączami do nowinek, a wtedy wartość tagInfo[1] jest przypisywana następnemu wolnemu elementowi w newsNames. Jeśli tagInfo[0] zawiera napis indexNames, musi być związana z łączami notowań giełdowych i wówczas wartość tagInfo[1] przypisywana jest następnemu wolnemu elementowi w indexNames. Gdy tagInfo[0] nie zawiera żadnego z powyższych napisów, musi to być nazwa zmiennej, która ma zostać zadeklarowana, a jej wartością ma być tagInfo[1]. Kod w wierszu 30 będzie wiedział już, co należy zrobić:
else { eval(tagInfo[0] + ' = "' + tagInfo[1] + '"');
Kiedy skończy się działanie pętli for, wygenerowany zostanie następujący kod:
newsNames[0] = 'Barron's Online,http://www.barrons.com/';
newsNames[1] = 'CNN Interactive,http://www.cnn.com/';
newsNames[2] = 'Fox News,http://www.foxnews.com/';
newsNames[3] = 'The Wall Street Journal,http://www.wsj.com/';
są to łącza do nowości dla inwestorów.
indexNames[0] = 'Dow Jones Indexes,http://www.dowjones.com/';
indexNames[1] = 'The New York Stock Exchange,http://www.nyse.com/';
to są z kolei łącza do notowań giełdowych.
var investor = 'Not Provided';
var age = 'Not Provided';
var strategy = 'Moderate';
var occupation = 'Not Provided';
var background = 'images/fistthumb.gif';
var face = 'tahoma';
var size = '14';
a to są zmienne opisujące wygląd strony.
Techniki języka JavaScript: Poruszyliśmy już temat rozsądnych konwencji nazewniczych. Warto teraz może o tym pomyśleć, jak powstają zmienne opisujące wygląd strony oraz elementy łącz do nowości i do notowań. Zaczęło się jeszcze, zanim jakikolwiek kod w dive.html został zinterpretowany, zanim stało się to w prefs.html, po prostu nawet zanim użytkownik po raz pierwszy cokolwiek poprawił w formularzu w prefs.html. Zaczęło się od nazwania pól formularza. Każda ze zmiennych zawierających łącza ma identyfikator (na przykład newsNames0 lub indexNames3) zawierający nazwę listy wyboru w prefs.html. Każda zmienna, opisująca wygląd strony, ma nazwę odpowiadającą jednemu z elementów formularza, na przykład background lub size. Nazwa została wstawiona do treści ciasteczka. Starannie przemyślane konwencje nazewnicze nie tylko ułatwiają pracę, ale niektóre rzeczy w ogóle umożliwiają. Zawsze należy dobrze przemyśleć ich użycie w kodzie. Pamiętajmy, że podane nazwy nigdzie w kodzie bezpośrednio nie występują. Aby sięgnąć do ich wartości, możemy przeglądać wartości newsNames i indexNames, jednak aby dotrzeć do zmiennych, musimy znać z góry ich nazwy. |
|
Mamy już wszystkie dane potrzebne do stworzenia strony zgodnie z wymaganiami użytkownika. Kiedy teraz zapiszemy informacje na stronie, nasza rola będzie skończona. Użyjemy metody document. write(), aby umieścić wszystko na stronie. Metodę tę wywołamy ośmiokrotnie - wszystkie wywołania zestawiono i omówiono w tabeli 7.4.
Tabela 7.4. Tworzenie kodu HTML przy pomocy wywołań document.write()
Wiersze |
Kod |
Opis |
50-51 |
document.write('<STYLE type="text/css"> TD { font-family: ' + face + '; font-size: ' + size + 'pt; } </STYLE>'); |
tworzenie arkusza stylów |
55-56 |
document.write('<BODY BACKGROUND="' + background.replace(/thumb/, "") + '">'); |
określenie adresu URL obrazka tła |
77 |
document.write(investor); |
podanie nazwiska inwestora |
82 |
document.write(age); |
podanie wieku inwestora |
91 |
document.write(strategy); |
określenie strategii inwestowania |
108 |
document.write(genLinks(news)); |
dopisanie łącz do stron z nowościami |
114 |
document.write(genLinks(indexes)); |
dopisanie łącz do stron z notowaniami giełdowymi |
|
|
|
Choć wywołania od trzeciego do szóstego w zasadzie same się objaśniają, to pierwsze dwa i ostatnie dwa są nieco bardziej złożone. Zacznijmy od wierszy 50-51:
document.write('<STYLE type="text/css"> TD { font-family: ' +
face + '; font-size: ' + size + 'pt; } </STYLE>');
To wywołanie dopisuje do strony arkusz stylów, ale wstawimy tu zmienne face i size, określające rodzaj czcionki i jej wielkość.
Techniki języka JavaScript: Dynamiczny DHTML to moja nazwa JavaScriptu generującego DHTML w biegu. Zastanówmy się: możemy użyć document.write() do generowania HTML, a nawet dalszego kodu JavaScriptu. Jeśli połączymy tę funkcjonalność z dodatkowymi możliwościami arkuszy stylów, mamy naprawdę ogromne możliwości formatowania przy zastosowaniu bardzo niewielkiej ilości kodu. Oto przykład tego w omawianej aplikacji: document.write('<STYLE type="text/css"> TD { font-family: ' + face + '; font-size: ' + size + 'pt; } </STYLE>'); Właściwie rzecz polega na wstawienie zmiennych face i size. Teraz typ czcionki i jej rozmiar określone są przez dwie zmienne, których wartość można zawsze zmienić. Nieźle jak na jednowierszowy arkusz stylów. Pomyślmy, jakie możliwości daje nam stworzenie dużego arkusza stylów z opisem nagłówków, elementów formularzy i tak dalej. Arkusze stylów umożliwiają precyzyjne określenie wyglądu dokumentów, natomiast generowanie tych arkuszy przez JavaScript ułatwia dynamiczną realizację tej kontroli. |
|
Skoro już umiemy tworzyć dynamicznie arkusze stylów, przejdźmy do ustawienia właściwego obrazka tła. Oto wiersze 55 i 56:
document.write('<BODY BACKGROUND="' +
background.replace(/thumb/, "") + '">');
Pamiętajmy, że zmienna background zawiera images/fistthumb.gif. Świetnie, tyle tylko, że mamy miniaturę obrazka z tła, a nam potrzebny jest oryginał. Nie ma sprawy, każda miniatura nazwana została tak, jak obrazek pełnoformatowy, z tym, że dodano jej w nazwie słowo thumb. Zatem wystarczy, że z zawartości zmiennej background usuniemy „thumb”, aby otrzymać images/fist.gif - pomoże w tym metoda replace().
W ostatnich dwóch wywołaniach metody document.write() używamy jedynie innej funkcji zdefiniowanej w naszej stronie, genLinks(). Funkcja ta jest podobna do - znanych już nam z prefs .html - genBoxes() i genSelect(), gdyż także tworzy się pętla po wszystkich elementach tablicy, aby wygenerować odpowiedni kod HTML. Jedna różnica polega na tym, że ta funkcja zwraca zestaw łącz, a nie pól opcji i znaczników OPTION. Omawiane kwestie mieszczą się między wierszami 37 a 45:
function genLinks(linkArr) {
var linkStr = '';
for (var i = 0; i < linkArr.length; i++) {
var linkParts = linkArr[i].split(',')
linkStr += ' - <A HREF="' + linkParts[1] + '"> ' +
linkParts[0] + '</A><BR>'
}
return linkStr;
}
Funkcja genLinks() ma - jako jedyny argument - otrzymywać tablicę tekstów. Pierwszy fragment każdego elementu to napis do wyświetlenia jako tekst łącza, drugi natomiast to adres URL do atrybutu HREF. Są one oddzielone od siebie przecinkiem, dzięki czemu użycie metody split() i przypisanie wyników do zmiennej lokalnej linkParts pozwala uzyskać potrzebne fragmenty. Pętla for działa jak zwykle, tworząc tekst z łączami, który będzie zwrócony po zakończeniu.
Kierunki rozwoju
Nawet najmniejszy stopień kreatywności pozwoli nam znaleźć sobie miejsce do poprawek w tej aplikacji. Oto kilka możliwości:
Dodaj pola umożliwiające manipulację kolorem tła komórek tabeli i kolorem czcionki, a także innymi, wybranymi atrybutami wyglądu.
Pozwól użytkownikom na wybranie gotowych schematów wyglądu stron.
Dodaj kilka pól tekstowych, aby użytkownik mógł dodać własne wybrane strony (z nazwami).
Dodawaj bannery reklamowe - stosownie do preferencji użytkownika.
Więcej ustawień wyglądu
Użytkownicy lubią, kiedy mogą wybierać, i wybierać, i wybierać... Wszystko, co tylko umieścimy na stronie użytkownika, może być przedmiotem jego poprawek. Dotyczy to treści i grafiki w warstwach, innych ramkach i osobnych oknach.
Gotowe schematy wyglądu stron
Idea gotowych, tematycznych schematów wyglądu interfejsu pochodzi z Windows 95. Zamiast umożliwiać użytkownikom wybieranie poszczególnych elementów, jak rodzaj czcionki, jej rozmiar i kolor, możemy dać im gotowe schematy, które będą mogli wybrać jednym kliknięciem myszki. Załóżmy, że mamy stronę sieciową związaną z muzyką. Pomyślmy o takiej muzycznej liście wyboru:
<SELECT NAME="tematy" onChange="swapImage('theImage',
this.options[this.selectedIndex].value);">
<OPTION VALUE="brak">Brak
<OPTION VALUE="bigband">Big Band
<OPTION VALUE="rocknroll"> Rock and Roll
<OPTION VALUE="rap">Rap
<OPTION VALUE="country">Country
<OPTION VALUE="reggae">Reggae
<OPTION VALUE="grunge">Grunge
<OPTION VALUE="jazz">Jazz
<OPTION VALUE="club">Muzyka klubowa
</SELECT>
Każda z tych opcji może być związana z jakąś ikoną. Można nawet użyć genSelect() i swapImage() w prefs.html do stworzenia listy i realizacji przewijania. Pamiętajmy jednak, że wybierając jeden z tych rodzajów muzyki, będziemy musieli jakoś wyłączyć poszczególne cechy wyglądu, jak obrazek tła i opis czcionki. Zwróćmy uwagę na to, że pierwsza opcja to brak tematu przewodniego. Warto dodać również znacznik OPTION pozwalający użytkownikom określić w miarę potrzeb własne układy strony.
Umożliwienie użytkownikom tworzenia własnych łącz
Formularz ustawień strony Take-A-Dive umożliwia użytkownikowi wybrać łącza spośród łącz przez nas wcześniej zdefiniowanych. Zawsze można też dodać kilka pól tekstowych, gdzie użytkownik będzie mógł podać swoje ulubione łącza. Poniższa tabela powinna stanowić dobry punkt startowy:
<TABLE>
<TR>
<TD><B>Dodatkowe łącza</B></TD>
<TD>
<INPUT TYPE=BUTTON VALUE=" Dodaj " onClick=addOpt(hits.form);">
<INPUT TYPE=BUTTON VALUE=" Usuń " onClick="deleteOpt(this.form);">
</TD>
</TR>
<TR>
<TD>Nazwa łącza</TD>
<TD><INPUT TYPE=TEXT NAME="linkname" SIZE=20></TD>
</TR>
<TR>
<TD>Adres URL łącza</TD>
<TD><INPUT TYPE=TEXT NAME="linkURL" SIZE=20></TD>
</TR>
</TABLE>
Użytkownicy mogą teraz dodawać i usuwać łącza, wpisując nazwę łącza i jego adres URL, a następnie wybierając Dodaj lub Usuń. Można również wstawiać te zmienne do tablicy, a łącza dodawać i usuwać funkcjami addOpt() i delOpt() wskazanymi w powyższym kodzie. Ambitni programiści mogą, mógłbyś też utworzyć listę wyboru wyświetlającą łącza w miarę ich dodawania i usuwania.
Marketing bezpośredni
Dlaczego nie przeprowadzić własnej kampanii reklamowej zgodnej z profilem zainteresowań użytkownika? W przypadku tej niby-inwestycyjnej strony można by sprawić, żeby użytkownicy uważający się za ostrożnych otrzymywali propozycje inwestycji pewnych i niskodochodowych, jak obligacje. Z kolei inwestorzy gotowi zaprzedać duszę diabłu dostawaliby propozycje bardzo ryzykowne, ale potencjalnie przynoszące duże zyski, oraz propozycje inwestycji zagranicznych.
Cechy aplikacji:
Prezentowane techniki:
|
8
Shopping Bag |
|
Jeśli w tej książce należałoby wskazać pojedynczą aplikację o najbardziej rozbudowanych funkcjach i najsolidniejszą, to byłaby to właśnie aplikacja opisana w tym rozdziale. Wystarczy, że dodamy do niej grafikę i szczegółowy opis produktów, a już będziemy mieć gotowy wózek sklepowy w swoim sieciowym punkcie handlowym. Do wyświetlania danych o swoich produktach nie trzeba tworzyć żadnych dodatkowych plików, zrobi to ta aplikacja właśnie. Nie trzeba też na serwerze wyliczać żadnych podatków ani nic sumować, wszystko to wykona nasza aplikacja. Do dodania czy wyjęcia towaru z koszyka wystarczy jedno lub dwa kliknięcia myszy. W przeciwieństwie do analogicznych aplikacji działających po stronie serwera nie musimy na nic czekać.
Shopping Bag w dwóch słowach
Coś, co ma być dla klienta naprawdę łatwe w użyciu i intuicyjnie rozumiane, zwykle wymaga dodatkowej pracy programisty. Tak jest i tym razem. Jednak to, co dostaniemy w efekcie, jest tej pracy warte. Opis działania aplikacji Shopping Bag i opis kodu oparte są na przykładzie.
Oto czteroetapowy proces działania:
Aplikacja jest ładowana.
Kupujący przegląda produkty według kategorii i je wyszukuje, wybiera przy tym kilka z nich.
Zadowolony klient przegląda dokonane przez siebie wybory i zmienia ilości czy rodzaje towarów.
Ostatecznie usatysfakcjonowany decyduje się potwierdzić zakupy i zapłacić.
Aplikacja zawiera też kilka prostych reguł, które muszą być zachowane przez kupujących. Będą one omówione w tych sekcjach, których dotyczą. Pomóżmy użytkownikowi - niech będzie to pani Daisy Skąpiradło - rozstać się z jego ciężko zarobionymi pieniędzmi.
Etap 1. Ładowanie aplikacji
Otwarcie pliku ch08\index.html spowoduje pojawienie ekranu pokazanego na rysunku 8.1. Jest to po prostu rodzaj ekranu wstępnego. Kiedy Daisy kliknie łącze Zaczynamy, pokaże się ekran taki, jak na rysunku 8.2.
|
|
Rysunek 8.1. Ciepłe przywitanie w naszej aplikacji
Jest to ekran początkowy (i jednocześnie ekran pomocy) ładowany wraz ze znajdującą się pod nim ramką nawigacyjną. Warto się temu przyjrzeć przed podjęciem kolejnych kroków.
Po co dwa okna przeglądarki? Nic nie stoi na przeszkodzie, aby wszystko zrobić w jednym, ale w ten sposób mamy więcej miejsca na prezentację swoich towarów. Poza tym użytkownicy nie będą odciągani od prezentowanych przez nas treści przyciskami takimi, jak Bookmarks czy Search. Oznacza to, że więcej uwagi poświęcą samej ofercie. A skoro już przy tym jesteśmy: warto zauważyć, że można tę stronę wykorzystać do rejestrowania użytkowników, aby odróżnić stałych bywalców od przypadkowych gości.
|
|
Rysunek 8.2. Interfejs Shopping Bag
Etap 2. Przeglądanie towarów i wybór
Dobra, Daisy już weszła. Czas się rozejrzeć. Wybiera Pokaż wszystkie kategorie i otrzymuje listę kategorii z łączami.
Wyświetlona zostanie tablica dostępnych kategorii produktów. No tak: budynki, jedzenie, narzędzia - któż czegoś takiego nie chciałby kupić w Sieci? Kiedy już pani Skąpiradło nieco ochłonie po pierwszym szoku spowodowanym bogactwem oferty, stwierdza, że kończą się jej już zapasy domowe i w związku z tym postanawia sprawdzić, co ma jej do zaoferowania sklep Shopping Bag.
|
|
|
Reguła 1. Kiedy załadowana zostanie aplikacja Shopping Bag, użytkownik musi wybrać jedną z opcji: „Pokaż wszystkie kategorie” lub „Wyszukiwanie produktów” po czym zdecydować się na łącze kategorii, z której produkty chce oglądać. Potem już wybieranie wszystkich przycisków z paska nawigacyjnego będzie działało zgodnie z oczekiwaniami. |
|
|
Uruchamia łącze Buildings, po czym znów jest zaskoczona, ale tym razem bajecznie niskimi cenami towarów takich, jak stodoła, zamek czy wieża. Nie mogąc opanować drżenia rąk, dochodzi do igloo, co pokazano na rysunku 8.3. Wybiera Daj mi to i igloo pojawia się w jej koszyku, jak o tym informuje komunikat.
|
Rysunek 8.3. Wkładanie igloo do koszyka
|
|
|
Reguła 2. Wybór „Daj mi to” powoduje włożenie do koszyka tylko jednego produktu danego rodzaju. Użytkownik może zmieniać ilości, wybierając opcję „Przegląd/korekta koszyka”, tak samo może coś z koszyka usunąć. |
|
|
Nadal polując na okazję, nasza niezmordowana klientka decyduje się użyć opcji wyszukiwania towarów aby zobaczyć, czy znajdzie jeszcze dla siebie coś ciekawego. Wybiera zatem Wyszukiwanie produktów i otrzymuje prosty ekran, pokazany na rysunku 8.4. Wpisuje do okienka edycyjnego 1.15, aby znaleźć towary w takiej właśnie cenie. Przypadkowo trafia - jest pięć takich produktów, co widać na rysunku 8.5. Zwraca uwagę na frytki, ogląda je dokładnie i szybko wkłada do koszyka.
Teraz nasza Daisy kieruje się do zestawienia dostępnych kategorii - klika jeszcze raz Pokaż wszystkie kategorie. Tym razem jej uwagę przyciągają ubrania (clothing). Kiedy klika to łącze,
|
Rysunek 8.4. Tutaj zaczyna się wyszukiwanie towarów
|
Rysunek 8.5. Jedno wyszukiwanie, wiele okazji
trafia na krawat za niewiarygodną wprost cenę 1 dolara 15 centów. Wcześniej to przegapiła, ale teraz już nie daruje - wkłada go do koszyka.
Etap 3: Przeglądanie zamówienia i zmiany w nim
Daisy stwierdza, że na dzisiaj ma już dosyć i wybiera opcję Przegląd/korekta koszyka. Generowany jest odpowiedni ekran, co pokazano na rysunku 8.6. Zwróćmy uwagę, że aplikacja pamiętała o wszystkim, co było wkładane do koszyka, wraz z podatkiem, kosztem wysyłki i sumą zakupów.
|
Rysunek 8.6. Zawartość koszyka Daisy, wraz z cenami
Nadal podekscytowana możliwością posiadania igloo Daisy zwiększa ich ilość na sześć. Mieszka w dość ciepłym klimacie, musi się więc liczyć z dużym zużyciem - lepiej wziąć na zapas. Frytki też nieźle wyglądają, więc bierze drugą ich paczkę. Niestety, okazuje się, że jej portfel nie jest tak zasobny, jak jej się to wydawało, więc musi zrezygnować z krawata. Cóż, przecież to nie ostatnia wizyta.
|
|
|
Reguła 3. Kupujący w celu zapisania zmian w koszyku muszą użyć przycisku „Zmiana koszyka”. Innymi słowy, zmiany nie są dokonywane tylko przez skorygowanie liczby w kolumnie „Ilość” ani przez zaznaczenie pola w kolumnie „Usuń”. |
|
|
Nietrudno zauważyć, jak Daisy zabiera się do zmiany zawartości swojego koszyka. Zmienia ilości odpowiednich towarów w listach wyboru, a produkty do usunięcia zaznacza w kolumnie Usuń. Następnie nasza bohaterka klika przycisk Zmiana koszyka, co powoduje ponowne wyświetlenie zawartości, odwzorowujące dokonane zmiany. Spójrzmy na rysunek 8.7. Jej koszyk zawiera teraz 6 igloo, dwie paczki frytek i jeden krawat. Łączne koszty wraz z opodatkowaniem i wysyłką wynoszą 6 190,75 dolarów.
|
Rysunek 8.7. Daisy zmieniła zawartość swojego koszyka
|
|
|
Reguła 4. Przesłanie zamówienia do Shopping Bag całkowicie wypróżnia koszyk. Jeśli chcemy kupić coś jeszcze, zaczynamy zbieranie towarów od nowa. |
|
|
Etap 4. Płacenie
Usatysfakcjonowana swoimi poczynaniami Daisy wybiera przycisk Do kasy, który powoduje otwarcie formularza pokazanego na rysunku 8.8. Daisy może teraz wpisać informacje związane z jej zamówieniem, przesłać je i niecierpliwie czekać na przesyłkę pocztową.
I tak to właśnie działa... Kolejny zadowolony klient. Przejdźmy teraz dalej, aby zobaczyć, jakiż to kod tak się podoba naszym klientom.
Wymagania programu
W Shopping Bag używany jest JavaScript w wersji 1.2 oraz pewne cechy CSS, więc przeglądarki w wersji 3.x nie wystarczą. Pamiętajmy jednak, że wielu użytkowników nadal ich używa. Można
|
Rysunek 8.8. Formularz zamówienia
bez problemu usunąć wszystkie elementy CSS, dzięki czemu nasza aplikacja zadziała w Netscape Navigatorze i Internet Explorerze w wersjach 3.x.
Jeśli chodzi o obciążenie systemu, warto przewidzieć obciążenie co najmniej 500 pozycji towarowych. W końcu dodanie jednego produktu to dodanie tylko jednego wiersza kodu. Ja testowanie zakończyłem po osiągnięciu blisko 700 pozycji na maszynie z procesorem 120MHz i 128MB pamięci RAM. Jeśli ktoś nie zamierza konkurować z WalMart, Shopping Bag powinien być dla niego w sam raz.
Struktura programu
Teraz przyjrzyjmy się schematowi działania Shopping Bag. Na rysunku 8.9 pokazano, jak użytkownik zaczyna zakupy, przegląda i wybiera produkty, następnie wprowadza ostateczne zmiany w koszyku, wypełnia informacje o płatności i przesyłce oraz wysyła w końcu zamówienie do serwera.
Aplikacja ta składa się z ośmiu plików. Poniższa lista je zestawia i podaje ich znaczenie:
index.html
Strona początkowa, która zawiera obsługę okien.
shopset.html
Zestaw ramek dla całoekranowego okna dodatkowego. Zawiera pliki intro.html i manager.html.
intro.html
Domyślna strona największej z ramek, zawiera też dokument z pomocą, opisujący poszczególne funkcje w pasku nawigacji.
|
Rysunek 8.9. Shopping Bag w skrócie
manager.html
Jest to centrum dowodzenia naszej aplikacji. Tutaj znajdują się najważniejsze funkcje, które przede wszystkim będziemy omawiać w tym rozdziale.
inventory.js
Funkcje, konstruktory i tablice pozwalające stworzyć ofertę towarową naszego sklepu. Znaczna część przetwarzania odbywa się podczas ładowania stron.
search/index.html
Zestaw ramek ładujący aplikację wyszukującą towary. Tak naprawdę jest to zmodyfikowana wersja omawianej w rozdziale 1. wyszukiwarki.
search/main.html
Strona pomocy przeglądarki, zawiera też przykłady.
search/nav.html
„Mózg” wyszukiwarki.
Z powodu wielkości tej aplikacji oraz że o użytym tu kodzie JavaScript do obsługi warstw można przeczytać w innych rozdziałach (,, 4., 6., 9., 10. i 11.), Shopping Bag będziemy omawiać inaczej niż dotychczasowe aplikacje.
Tym razem nie zanalizujemy każdego pliku od początku do końca, a zamiast tego omówimy sposób realizacji przez aplikację pięciu podstawowych funkcji:
ładowanie aplikacji: tworzenie bazy towarowej i przygotowanie do wyświetlenia,
prezentacja produktów: zmiana kategorii i produktów,
dodawanie produktów do koszyka: rejestracja wszystkiego, co znajduje się w koszyku,
wyszukiwanie produktów: wyszukiwanie tekstu w spisie inwentarza,
zmiana zawartości koszyka i obsługa kasowa: zmiany i płacenie.
Jeśli porównamy poprzednie opisy plików z opisami w tym rozdziale, możemy mniej więcej rozpoznać, który z nich za co jest odpowiedzialny. Punkt 1. powyżej związany jest z index.html, shopset.html, inventory.js; punkty 2., 3. i 5. realizuje manager.html; punkt 4. jest w całości realizowany przez funkcje z podkatalogu search.
Kod nadal będziemy omawiali w zasadzie plik po pliku, ale od czasu do czasu zrobimy dygresję, aby jakieś zagadnienie omówić dodatkowo. Każda z pięciu części została zapisana z punktu widzenia działań użytkownika, jak wyszukiwanie, zmiana ilości towaru, uzyskiwanie pomocy i tak dalej. Omówimy też zastosowane techniki programistyczne. Zaczniemy od ładowania naszej aplikacji.
Etap 1. Ładowanie aplikacji
JavaScript i procedury zakodowane w przeglądarkach wykonają za nas większość pracy, choć użytkownik też ma coś do powiedzenia. Zastanówmy się, jak ładowana jest pierwsza strona, index. html. Jej kod znajduje się w przykładzie 8.1.
Przykład 8.1. index.html
1 <HTML>
2 <HEAD>
3 <TITLE>Shopping Bag</TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6 #welcome { text-align: center; margin-top: 150}
7 //-->
8 </STYLE>
9 <SCRIPT LANGUAGE="JavaScript">
10 <!--
11 var shopWin = null;
12 var positionStr = '';
13 function whichBrowser() {
14 if(navigator.appVersion < 4) {
15 alert("Aby użyć Shopping Bag, musisz mieć MSIE lub Navigatora" +
16 " w wersji co najmniej 4.x.")
17 return false;
18 }
19 return true;
Przykład 8.1. index.html (dokończenie)
20 }
21
22 function launch() {
23 if(!whichBrowser()) { return; }
24 if(navigator.appName == "Netscape")
25 { positionStr = ",screenX=0,screenY=0"; }
26 else { positionStr = ",fullscreen=yes"; }
27 if(shopWin == null) {
28 shopWin = open("shopset.html", "", "width=" + screen.width +
29 ",height=" + screen.height + positionStr);
30 }
31 }
32 function closeUpShop() {
33 if (shopWin != null) {
34 if (typeof(shopWin) == "object") {
35 shopWin.close();
36 }
37 }
38 }
39 window.onunload = closeUpShop;
40 //-->
41 </SCRIPT>
42 </HEAD>
43 <BODY>
44 <DIV ID="welcome">
45 <H1>Witaj w aplikacji Shopping Bag!!!</H1>
46 <A HREF="javascript: launch();">Zaczynamy</A>
47 </DIV>
48 </BODY>
49 </HTML>
Może to wyglądać jak mnóstwo kodu JavaScript na stronie wyświetlającej raptem kilka słów na ekranie. Jednak ten dodatkowy kod służy ulepszeniu aplikacji. JavaScript tutaj właśnie definiuje i ustawia obiekty najwyższego poziomu, potrzebne do obsługi aplikacji wielookienkowej, oraz określa rodzaj przeglądarki, co będzie potrzebne w nowo otwartym oknie.
Elementy najwyższego poziomu
Zmienne i funkcje z wierszy 11 i 32-38 wymuszają następującą zależność: jeśli zamknięte zostanie okno główne, zamknąć się ma też jego okno potomne. W przeciwnym wypadku nasza aplikacja mogłaby się załamać, jeśli użytkownik zechciałby po zamknięciu okna głównego na przykład przeładować wspomniane okno potomne.
Wiersz 11:
var shopWin = null;
A oto wiersze 32-38:
function closeUpShop() {
if (shopWin != null) {
if (typeof(shopWin) == "object") {
shopWin.close();
}
}
}
Zmienna shopWin, początkowo ustawiana na wartość null, używana jest później do ustawienia obiektu okna potomnego (zajrzyjmy do wiersza 27). Funkcja closeUpShop() wywoływana jest przy zamknięciu tego okna, a funkcja sprawdza, czy użytkownik nadal ma otwarte okno potomne - jeśli tak, to je zamyka. Jeśli shopWin nie jest równa null i jest typu object, okno potomne musi być otwarte. closeUpShop() zamyka to okno przed skończeniem działania aplikacji.
W tym momencie tylko jedno interesuje użytkownika: jeżeli kliknie Zaczynamy, otworzy się nowe okno. Następnie otwarty zostanie w nim zestaw ramek shopset.html, którego kod znajdziemy w przykładzie 8.2.
Przykład 8.2. shopset.html
1 <HTML>
2 <HEAD>
3 <TITLE>Zestaw ramek Shopping Bag</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5 <!--
6 function resetOpener() {
7 opener.shopWin = null;
8 }
9 //-->
10 </SCRIPT>
11 </HEAD>
12 <FRAMESET ROWS="80%,20%" FRAMEBORDER=0 BORDER=0 onLoad="self.focus();"
13 onUnLoad="resetOpener();">
14 <FRAME SRC="intro.html" NORESIZE>
15 <FRAME SRC="manager.html" NORESIZE>
16 </FRAMESET>
17 </HTML>
Jest to typowy zestaw ramek, z których jedna jest związana z plikiem intro.html, druga z manager. html. Nie ma tutaj zbyt wiele JavaScriptu, ale sprawdźmy przynajmniej, co jest:
function resetOpener() {
opener.shopWin = null;
}
Wiersze 6-8 zawierają funkcję resetOpener(), wywoływaną, kiedy z okna głównego jest usuwany dokument. Gdy ustawimy opener.shopWin na null, resetOpener() umożliwia użytkownikowi zamknięcie okna potomnego Shopping Bag i ponowne otworzenie go za pomocą jednego łącza Zaczynamy.
Może to się wydawać oczywiste, a nawet zbyteczne. Zwróćmy jednak uwagę, że w index.html (wiersz 27) dodatkowe okno jest otwierane tylko wtedy, gdy shopWin równe jest null. Zamknięcie okna nie ustawia shopWin na null, zatem właśnie resetOpener() może tu pomóc. Zwróćmy uwagę również na to, że obsługa zdarzenia onLoad w znaczniku FRAMESET jest ustawiona na self. focus(). Dzięki temu nowe okno nie jest otwierane ani ładowane poza oknem głównym, gdyż w przeciwnym wypadku użytkownik zastanawiałby się, co się właściwie stało.
Przede wszystkim chodzi o ładowanie zestawu ramek. Istnieją jeszcze trzy strony, które trzeba załadować: intro.html, manager.html oraz inventory.js. intro.html to statyczna strona pomocy. Ładowanie manager.html, odbywa się wraz ze stroną do niej włączoną, czyli inventory.js. Warto zatrzymać się dłużej przy manager.html, czym zajmiemy się dalej w tym rozdziale, natomiast inventory.js omówimy już teraz. Jest to plik dość długi, ale przynajmniej zorientujemy się, jak należy tworzyć spis dostępnych towarów.
inventory.js
Plik inventory.js zawiera trzy funkcje. Pierwsze dwie to konstruktory: jedna produktu, druga kategorii. Ostatnia funkcja tworzy tablice obiektów tworzonych przez te konstruktory. Spójrzmy na przykład 8.3.
|
Techniki języka JavaScript: Kiedy pracujemy z aplikacją korzystającą tylko z okna głównego przeglądarki, nie musimy specjalnie przejmować się kwestią okien. Jeśli jednak otwieramy inne okno, musimy być ostrożni. Czy okno ma być zawsze na wierzchu, czy schowane? Czy jego okno macierzyste jest nadal otwarte? Co się dzieje, jeśli jedno z nich zostanie zamknięte? Prawdopodobnie nie będziemy musieli zajmować się wszystkimi tymi kwestiami, ale o tych zagadnieniach trzeba jednak pamiętać. Można nad wszystkim zapanować, jeśli utworzy się zmienne, których wartości będą opisywały stan poszczególnych okien. Na przykład zmienna shopWin ma wartość odpowiadającą obiektowi zdalnego okna lub wartość null, jeśli okno to jest zamknięte. Shopping Bag podejmuje na podstawie tej wartości pewne działania. Podobne zachowania można też zrealizować w ramkach. Zmienne gimmeControl i browseControl mają podobne funkcje - monitorują treść dokumentów. Innymi słowy w zależności od tego, co jest wyświetlone, odpowiednio zachowuje się aplikacja. |
|
Przykład 8.3. inventory.js
1 function product(name, description, price, unit) {
2 this.name = name;
3 this.description = description;
4 this.price = price;
5 this.unit = unit;
6 this.plu = name.substring(0, 3).toUpperCase() +
7 parseInt(price).toString();
8 this.icon = new Image();
9 return this;
10 }
11 function category(name, description) {
12 this.name = name;
13 this.description = description;
14 this.prodLine = eval(name);
15 var imgDir = "images/" + name.toLowerCase() + "/";
16 for (var i = 0; i < this.prodLine.length; i++) {
17 this.prodLine[i].icon.src = imgDir +
18 this.prodLine[i].name.toLowerCase() + ".gif";
19 }
20 return this;
21 }
22 function makeProducts() {
23 Appliances = new Array(
24 new product("Dryer",
25 "Stylowa, pastelowa obudowa z dwuprzyciskową obsługą",
26 263.37 ,
27 "sztuka"),
28 new product("Hairdryer",
29 "Kolorowe, żółte wzornictwo, trwały kabel. Dobry zakup.",
30 1.15,
31 "para"),
32 new product("Oven",
33 "Pochodzący z lat 50-tych XIX wieku piec węglowy momentalnie " +
34 "zwęgli ulubione dania.",
35 865.78,
36 "sztuka"),
37 new product("Radio",
38 "Rewolucyjna technologia jednokanałowa. Generator szumów " +
39 "w zestawie.",
40 15.43,
41 "sztuka"),
42 new product("Toaster",
43 "Toster typu barbecue. Szansa porażenia prądem tylko średnia.",
44 25.78,
45 "sztuka"),
46 new product("Washer",
47 "Wyręcza Cię niemalże we wszystkim.",
48 345.61,
49 "sztuka")
50 );
51
52 Buildings = new Array(
53 new product("Barn",
54 "Kompletne wyposażenie, z przerdzewiałym silosem i gnijącymi " +
55 "drzwiami. Chlew sprzedawany osobno.",
56 6350.57,
57 "sztuka"),
58 new product("Lighthouse",
59 "Zbudowana z cementu, doskonałe żarówki. Zasilana trzema " +
60 "paluszkami (kupowane osobno).",
61 12351.15,
62 "sztuka"),
63 new product("Igloo",
64 "Zbudowane z dobieranych bloków śniegowych, zawiera komin i " +
65 "5-tonowe urządzenie klimatyzacyjne.",
Przykład 8.3. inventory.js (ciąg dalszy)
66 954.76,
67 "sztuka"),
68 new product("City",
69 "Domy, ulice, latarnie, horyzont. Doskonała okazja dla hurtowników.",
70 334165.95,
71 "sztuka"),
72 new product("Castle",
73 "Surowy, średniowieczny projekt, z aligatorami w fosie i mostem" +
74 " zwodzonym z pilotem.",
75 93245.59,
76 "sztuka"),
77 new product("Tower",
78 "Naprawdę wysoka. Doskonale nadaje się do zjednywania " +
79 "przyjaciół i obserwacji lasu.",
80 24345.87,
81 "para")
82 );
83
84 Clothing = new Array(
85 new product("Bowtie",
86 "Gruby, czerwony materiał. Doskonała na bezużteczne prezenty " +
87 "gwiazdkowe i urodzinowe.",
88 5.41,
89 "pięć"),
90 new product("Necktie",
91 "Bądź pierwszym (i zapewne jedynym) w Twoim bloku. Zrobiony " +
92 "z doskonałego płótna żaglowego.",
93 1.15,
94 "sztuka"),
95 new product("Purse",
96 "Interesujące, zielone sukno. Odpędza większość ssaków.",
97 18.97,
98 "sztuka"),
99 new product("Jacket",
100 "Sztuczne futro wzbogacone włóknem szklanym. Można prać w pralce.",
101 180.72,
102 "sztuka"),
103 new product("Glove",
104 "Kryje wszystkie cztery palce plus kciuk. Zmysłowy, lateksowy wzór.",
105 6.59,
106 "trzy"),
107 new product("Dress",
108 "Z ciuchów. Można stosować też jako obrus podczas pikniku.",
109 7.99,
110 "sztuka"),
111 new product("Watch",
112 "Wspaniała replika. Nie podaje czasu, ale bardzo ładnie wygląda.",
113 6.19,
114 "sztuka")
115 );
116
117 Electronics = new Array(
118 new product("Camcorder",
119 "Zasilanie energią słoneczną, darmowy mikrofon - idealny " +
120 "do szantażowania krewnych.",
121 60.45,
122 "sztuka"),
123 new product("Stereo",
124 "Kwadrofoniczny dźwięk ośmiościeżkowy. Opcjonalnie git marynara" +
125 " i glany.",
126 54.91,
127 "sztuka"),
128 new product("Speaker",
129 "Doskonały kawałek śmiecia hi-fi. Najlepiej działa bez podłączania.",
130 1.90,
Przykład 8.3. inventory.js (ciąg dalszy)
131 "sztuka"),
132 new product("Remote",
133 "Dziesiątki przycisków. Steruje wszystkim: telewizją, wideo, " +
134 "kolumną, zwierzętami domowymi i lokalnym samorządem.",
135 465.51,
136 "sztuka"),
137 new product("Cellphone",
138 "Blaszanka, działa nawet do 10 metrów. Gustowny liliowy " +
139 "plastik.",
140 64.33,
141 "sztuka"),
142 new product("Camera",
143 "Robi doskonałe, jednokolorowe zdjęcia. Kompostoodporny.",
144 2.95,
145 "sztuka"),
146 new product("Television",
147 "Model obsługuje dwa kanały UHF. Cudo!",
148 22.57,
149 "sztuka")
150 );
151
152 Food = new Array(
153 new product("Cheese",
154 "Poczekaj, aż go poczujesz. Sery pleśniowe mogą się schowaĆ.",
155 3.05,
156 "gomułka"),
157 new product("Fries",
158 "Więcej oleju niż w garażu. Smak nie do podrobienia.",
159 1.15,
160 "pudełko"),
161 new product("Eggs",
162 "Typowa przystawka śniadaniowa.",
163 1.07,
164 "tuzin"),
165 new product("Drumstick",
166 "Ta noga pterodaktyla niewątpliwie zachwyci.",
167 100.00,
168 "pół tony"),
169 new product("Chips",
170 "Zapach otwartej torebki. Gwarantujemy, gwarantujemy za ich " +
171 "stęchłość lub zwracamy pieniądze.",
172 1.59,
173 "torebka"),
174 new product("Shrimp",
175 "Doskonałe na surowo, serwowaĆ w temperaturze wyższej od pokojowej.",
176 2.95,
177 "sztuka")
178 );
179
180 Hardware = new Array(
181 new product("Chainsaw",
182 "Sam zostań bobrem - ta piła Ci to umożliwi.",
183 226.41,
184 "sztuka"),
185 new product("Cycle",
186 "Zetnij całe pole pszenicy w parę chwil - zupełnie jak " +
187 "Ponury żeniec.",
188 11.15,
189 "sztuka"),
190 new product("Hammer",
191 "Utwardzona główka stalowa, rączka z włókna szklanego. Wiadomo," +
192 " bez młota nie robota.",
193 9.87,
194 "sztuka"),
195 new product("Lawnmower",
Przykład 8.3. inventory.js (dokończenie)
196 "Samojezdna (jeśli tylko ją trochę popchniesz).",
197 165.95,
198 "sztuka"),
199 new product("Pliers",
200 "Doskonałe do radzenia sobie z brwiami i włosami w nosie.",
201 6.59,
202 "sztuka"),
203 new product("Stake",
204 "Dwa w jednym: może służyć jako śledź do namiotu lub jako broń na wampiry.",
205 3.95,
206 "para")
207 );
208
209 Music = new Array(
210 new product("Bongos",
211 "Doskonałe do hałasowania przy różnych okazjach.",
212 35.50,
213 "czynele"),
214 new product("Piano",
215 "Nie jest może zbyt wielkie, ale dla Twojego dziecka " +
216 "całkowicie wystarczy.",
217 1001.40,
218 "sztuka"),
219 new product("Notes",
220 "Masz do wyboru A, B, C, D, E, F i G. Możliwość użycia " +
221 "w dowolnej piosence.",
222 2.97,
223 "nuta"),
224 new product("Guitar",
225 "Brzęk, brzęk. Oto Twoja droga ku chwale i bogactwie.",
226 241.11,
227 "sztuka"),
228 new product("Trumpet",
229 "Solidny, mosiężny korpus, brak wentyli. Dołączono dodatkowy " +
230 "ustnik.",
231 683.59,
232 "sztuka")
233 );
234
235 categorySet = new Array(
236 new category("Appliances", "Sprzęt kuchenny ułatwiający Ci życie"),
237 new category("Buildings", "Budowle, którym nie sposób się oprzeć"),
238 new category("Clothing", "Ciuchy być może modne w 21 wieku"),
239 new category("Electronics", "Szykowne gadżety, które wyczyszczą " +
240 "Ci portfel"),
241 new category("Food", "Najlepsze produkty dostępne kiedykolwiek " +
242 "w Sieci"),
243 new category("Hardware", "Wszelkiej maści narzędzia ogólnego " +
244 "zastosowania"),
245 new category("Music", "Najgorętsze instrumenty z miejsc, " +
246 "o których w życiu nie słyszałeś")
247 );
248 }
Cechy produktów
Przypomnijmy sobie obiekty JavaScriptu, których używaliśmy w poprzednich rozdziałach? Znów mamy z nimi do czynienia. Każdy produkt traktowany jest jako obiekt z kilkoma właściwościami, czyli każdy produkt ma następujące właściwości:
name
Nazwa produktu
description
Krótki opis produktu
price
Cena produktu
unit
Jednostka, w jakiej dany produkt jest sprzedawany, na przykład tuzin, para, sztuka
plu
Numer katalogowy, używany do obsługi magazynowej i przetwarzania zamówień
icon
Obrazek produktu
Aby uzyskać pożądany wynik, definiujemy konstruktor produkcji następująco - wiersze 1-10:
function product(name, description, price, unit) {
this.name = name;
this.description = description;
this.price = price;
this.unit = unit;
this.plu = name.substring(0, 3).toUpperCase() +
parseInt(price).toString();
this.icon = new Image();
return this;
}
Zwróćmy uwagę, że tworzonych jest sześć właściwości, ale oczekuje się tylko czterech parametrów. Liczba właściwości i liczba oczekiwanych argumentów nie muszą iść z sobą w parze, ale pamiętaj też, że każda właściwość otrzymuje jednak jakąś wartość. Pierwsze cztery - name, description, price i unit otrzymują wartości z parametrów.
Inaczej dzieje się z plu. Jest to złożenie właściwości name i price. Bierze się pod uwagę pierwsze trzy litery nazwy produktu przekształcone na wielkie litery oraz cenę. Skoro zatem łódź (boat) kosztuje 5 501 dolarów, to jej kodem jest BOA5501. Pamiętajmy, że jest to kod z góry narzucony. Produkty sprzedawane w naszym sklepie będą miały własne kody. Można tak postąpić, aby uprościć nasze zadanie. Ostatnia właściwość to ikona, której przypisujemy nowy obiekt Image. W tym wypadku parametr też jest zbędny.
Cechy kategorii produktów
Wiemy, że tak naprawdę każdy produkt jest obiektem product. Podobnie każda kategoria produktów jest obiektem category. Jak produkty mają właściwości, tak i kategorie je posiadają. Oto właściwości obiektu category:
name
Nazwa kategorii
description
Krótki opis kategorii
prodLine
Wszystkie produkty (obiekty product) danej kategorii
Konstruktor kategorii znajduje się w wierszach 11-21:
function category(name, description) {
this.name = name;
this.description = description;
this.prodLine = eval(name);
var imgDir = "images/" + name.toLowerCase() + "/";
for (var i = 0; i < this.prodLine.length; i++) {
this.prodLine[i].icon.src = imgDir +
this.prodLine[i].name.toLowerCase() + ".gif";
}
return this;
}
Każda kategoria ma trzy właściwości: napis name, kolejny napis description i tablicę prodLine. Właściwości name i description są proste, ale skąd się bierze tablica i jak użyć do niej eval()? Wątpliwości zaraz się wyjaśnią, ale można już określić podstawową strukturę: niezależnie od nazwy kategorii, należące do niej produkty są tablicą o takiej samej nazwie. Jeśli na przykład nazwiemy kategorię stereos, tablica zawierająca produkty stereo nazywać się będzie stereos. Oznacza to, że prodLine stanowi kopię zmiennej stereo, która jest tablicą różnych produktów typu stereo.
Pamiętajmy, że każdy produkt ma właściwość o nazwie icon, będącą obiektem Image, któremu nie przypisaliśmy jego źródła. Teraz jeszcze skorzystajmy nieco więcej z kategorii. Nie tylko każda kategoria zawiera w tablicy o określonej nazwie produkty danego typu, ale też obrazki tejże kategorii znajdują się w tablicy o takiej samej nazwie.
Wszystkie produkty muzyczne, kategorii music, znajdują się w katalogu music/. Obrazki kategorii hardware mieszczą się w katalogu hardware/, i tak dalej. Wygląda to nawet logicznie. Jeśli mamy taką strukturę katalogów na dysku, to możemy ładować wstępnie obrazki z bieżącej kategorii podczas jej ładowania. Oto wiersze 16-19, które to zadanie realizują:
for (var i = 0; i < this.prodLine.length; i++) {
this.prodLine[i].icon.src = imgDir +
this.prodLine[i].name.toLowerCase() + ".gif";
}
Jeśli obejrzymy dokładnie strukturę podkatalogów ch08, znajdziemy coś takiego:
images/
appliances/
buildings/
clothing/
electronics/
food/
hardware/
music/
W wierszu 17 ustawiamy właściwość SRC poszczególnych ikon (obiektu Image) na tekst images/ z dodaną nazwą kategorii, zapisaną małymi literami, ukośnikiem, nazwą produktu zapisaną małymi literami, i przyrostkiem .gif. Znów wracamy do zagadnienia konwencji nazewniczych, o czym mówiono w kilku poprzednich rozdziałach. Obrazki poszczególnych produktów znajdują się w plikach o analogicznych nazwach, w podkatalogach o nazwach kategorii. Oto odpowiedni wzorzec tworzenia nazwy pliku graficznego:
URL_obrazka = images/kategoria/produkt.gif
Jeśli przejrzymy katalog ch08\images\, zauważymy, że każda nazwa obrazka odpowiada jakiemuś produktowi Shopping Bag w katalogu odnoszącym się do kategorii produktów. Dzięki temu mamy wszystko uporządkowane, możemy dodawać, usuwać i sprawdzać nasze produkty.
|
|
|
Jeśli mamy wiele dużych obrazków, zastanówmy się, czy nie lepiej byłoby ominąć wstępnego ładowania obrazków. Niewątpliwe dobrze jest mieć wszystko na maszynie klienta, kiedy nawigacja nie wymaga żadnych właściwie opóźnień. Jeśli mamy mnóstwo dużych obrazków wysokiej jakości, użytkownik może nie zechce czekać, aż załaduje się na jego komputer 500 kB obrazków. Należy liczyć na swoje własne rozeznanie. |
|
|
Tworzenie produktów i kategorii
Widzieliśmy już konstruktory, teraz czas z nich skorzystać. Najpierw trzeba utworzyć produkty, a potem zabierzemy się za kategorie. Zajmuje się tym wszystkim funkcja makeProducts(). Oto wiersze 22-248. Jako że stale korzystamy z jednego i tego samego konstruktora produktu, to podamy tutaj jej wersję skróconą:
function makeProducts() {
Appliances = new Array(
new product("Dryer",
"Stylowa, pastelowa obudowa z dwuprzyciskową obsługą",
263.37 ,
"sztuka"),
new product("Hairdryer",
"Kolorowe, żółte wzornictwo, trwały kabel. Dobry zakup.",
1.15,
"para"),
new product("Oven",
"Pochodzący z lat 50-tych XIX wieku piec węglowy momentalnie " +
"zwęgli ulubione dania.",
865.78,
"sztuka"),
new product("Radio",
"Rewolucyjna technologia jednokanałowa. Generator szumów " +
"w zestawie.",
15.43,
"sztuka"),
new product("Toaster",
"Toster typu barbecue. Szansa porażenia prądem tylko średnia.",
25.78,
"sztuka"),
new product("Washer",
"Wyręcza Cię niemalże we wszystkim.",
345.61,
"sztuka")
);
...
... i tak dalej ...
...
categorySet = new Array(
new category("Appliances", "Sprzęt kuchenny ułatwiający Ci życie"),
new category("Buildings", "Budowle, którym nie sposób się oprzeć"),
new category("Clothing", "Ciuchy być może modne w 21 wieku"),
new category("Electronics", "Szykowne gadżety, które wyczyszczą " +
"Ci portfel"),
new category("Food", "Najlepsze produkty dostępne kiedykolwiek " +
"w Sieci"),
new category("Hardware", "Wszelkiej maści narzędzia ogólnego " +
"zastosowania"),
new category("Music", "Najgorętsze instrumenty z miejsc, " +
"o których w życiu nie słyszałeś")
);
}
Najpierw produkty. Zmienna Appliances staje się tablicą, której każdy element jest obiektem opisującym produkt. Każde wywołanie product zawiera wszystkie potrzebne argumenty: nazwę, opis, cenę i jednostkę sprzedażną. Dzieje się tak dla wszystkich naszych kategorii.
Teraz pozostaje zająć się kategoriami. Nazwy kategorii mamy już na miejscu (Appliances, Buildings, Clothing i tak dalej); teraz wystarczy poinformować o nich także naszą aplikację, co robimy w wierszach 235-248:
categorySet = new Array(
new category("Appliances", "Sprzęt kuchenny ułatwiający Ci życie"),
new category("Buildings", "Budowle, którym nie sposób się oprzeć"),
new category("Clothing", "Ciuchy być może modne w 21 wieku"),
new category("Electronics", "Szykowne gadżety, które wyczyszczą " +
"Ci portfel"),
new category("Food", "Najlepsze produkty dostępne kiedykolwiek " +
"w Sieci"),
new category("Hardware", "Wszelkiej maści narzędzia ogólnego " +
"zastosowania"),
new category("Music", "Najgorętsze instrumenty z miejsc, " +
"o których w życiu nie słyszłeś")
);
}
Zmienna categorySet także jest tablicą. Każdy element tej tablicy jest tworzony dwuargumentowym konstruktorem. Pierwszy argument otrzymuje właściwość name, drugi właściwość description. Spójrzmy jeszcze raz na 14. wiersz konstruktora obiektu kategorii:
this.prodLine = eval(name);
Właściwość prodLine jako wartość otrzymuje eval(name), zatem wywołanie category() w wierszu 249 oznacza ustawienie prodLine na wartość eval("Appliances"), czyli Appliances. Teraz kategoria o nazwie „Appliances” zna wszystkie swoje produkty (w tablicy prodLine). Każdy element categorySet opisuje jedną kategorię produktów, dzięki czemu proste jest ich dodawanie i usuwanie.
Tworzenie koszyka na zakupy
Mamy już nasze produkty. Pozostaje jeszcze zrobienie koszyka sklepowego. Koszyk ten musi mieć kilka właściwości, które pozwolą obsłużyć płatności, oraz tablicę, w której będziemy zaznaczać wszystkie wybrane przez klienta towary. Konstruktor Bag(), zawarty w pliku manager.html, opisuje jeden tylko koszyk. Oto wiersze 21-31, które pochodzą z przykładu 8.4 pokazanego dalej w tym rozdziale:
function Bag() {
this.taxRate = .06;
this.taxTotal = 0;
this.shipRate = .02;
this.shipTotal = 0;
this.subTotal = 0;
this.bagTotal = 0;
this.things = new Array();
}
shoppingBag = newBag();
Mamy tu dwie wielkości procentowe, taxRate i shipRate. Pierwsza z nich to współczynnik do określenia podatku od sprzedaży, druga to współczynnik pozwalający wyliczyć opłatę za przesyłkę. Podatek będzie trzeba oczywiście zmienić stosownie do swoich potrzeb, ale tutaj przynajmniej widzimy, jak to działa. Trzy pozostałe zmienne, taxTotal, subTotal i shipTotal zawierają sumę podatku, sumę cen wybranych produktów oraz ich ilości, a także całkowitą kwotę do zapłaty. Ostatnia zmienna to tablica things, która będzie zawierać produkty wybrane przez użytkownika wraz z ich ilościami. Zmienna shoppingBag jest następnie ustawiana na new Bag(). Możemy więc już iść na zakupy.
|
Techniki języka JavaScript: Zwróćmy uwagę, że we właściwościach shoppingBag() rejestrowane są wszystkie wybrane produkty i sumy. W ten sposób można stopniowo zbierać dane podczas działania aplikacji. Ogólnie rzecz biorąc, użytkownik może dowolnie zmieniać ilości i wybrane produkty, a właściwości shoppingBag dostosują się do tego. Tak również należy to programować. |
|
Etap 2. Pokazanie towarów
Po załadowaniu aplikacji użytkownik będzie chciał obejrzeć naszą ofertę towarową. W tym celu może przemieszczać się między kategoriami, używając łącz Poprzednia kategoria i Następna kategoria, lub między produktami, wybierając Poprzedni produkt i Następny produkt. Spójrzmy jak to działa. Przypomnijmy sobie wiersze 235-247 pliku inventory.js:
categorySet = new Array(
new category("Appliances", "Sprzęt kuchenny ułatwiający Ci życie"),
new category("Buildings", "Budowle, którym nie sposób się oprzeć"),
new category("Clothing", "Ciuchy być może modne w 21 wieku"),
new category("Electronics", "Szykowne gadżety, które wyczyszczą " +
"Ci portfel"),
new category("Food", "Najlepsze produkty dostępne kiedykolwiek " +
"w Sieci"),
new category("Hardware", "Wszelkiej maści narzędzia ogólnego " +
"zastosowania"),
new category("Music", "Najgorętsze instrumenty z miejsc, " +
"o których w życiu nie słyszałeś")
);
Tablica categorySet zawiera siedem obiektów category. Do pierwszego możemy się odwołać przez categorySet[0], do drugiego przez categorySet[1] i tak dalej. Bez względu na to, jaki produkt użytkownik ogląda, Shopping Bag zna jedynie odpowiedni numer kategorii (0-6). Jeśli użytkownik zdecyduje się cofnąć do kategorii poprzedniej, aplikacja od numeru bieżącej kategorii odejmie jeden i pokaże pierwszy produkt z tej nowej kategorii. Jeśli bieżącą kategorią była kategoria 0 i użytkownik chce się cofnąć, aplikacja zmienia numer na ostatnią kategorię (u nas jest to 6).
Jeśli użytkownik chce zmienić kategorię na następną, do numeru kategorii bieżącej dodawane jest 1. Jeśli kategoria bieżąca jest ostatnia, to kategorią następną jest kategoria o numerze 0.
To samo dotyczy także produktów. Każda kategoria zawiera pewną ilość produktów. Shopping Bag zna numer produktów w każdej kategorii, zatem odwołanie się do produktu następnego lub poprzedniego spowoduje dodanie lub odjęcie jedności od numeru produktu właśnie pokazywanego.
Jeśli użytkownik jest przy ostatnim produkcie danej kategorii i chce przejść do produktu następnego, pokazywany jest produkt o numerze 0 kategorii następnej. Gdy użytkownik jest przy produkcie pierwszym i chce przejść do poprzedniego, pokazywany jest ostatni produkt kategorii poprzedniej.
Jeśli cały ten opis wprowadził nieco zamieszania, to powinien wszystko rozjaśnić diagram z rysunku 8.10, który pokazuje, jak nasza aplikacja wiedzie użytkownika po kategoriach. Tak samo działa nawigacja po produktach. Kiedy dochodzimy do ostatniego produktu w danej kategorii, otrzymujemy produkt pierwszy kategorii następnej.
manager.html
Opisana powyżej nawigacja realizowana jest przez plik manager.html, który pokazano jako przykład 8.4.
Przykład 8.4. manager.html
1 <HTML>
2 <HEAD>
3 <TITLE>Menedżer Shopping Bag</TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6 TD {font-weight: bold; margin-left: 20; margin-right: 20; padding: 10}
7 //-->
8 </STYLE>
9 </HEAD>
10 <BODY onLoad="freshStart(); makeProducts();" LINK=BLUE ALINK=BLUE VLINK=BLUE>
11 <SCRIPT LANGUAGE="JavaScript1.2" SRC="inventory.js"></SCRIPT>
12
13 <SCRIPT LANGUAGE="JavaScript1.2">
14 <!--
15 var gimmeControl = false;
16 var browseControl = false;
17 var curCLoc = -1;
18 var curPLoc = -1;
|
Rysunek 8.10. Nawigacja po kategoriach
Przykład 8.4. manager.html (ciąg dalszy)
19 var infoStr = '';
20 var shoppingBag;
21 function Bag() {
22 this.taxRate = .06;
23 this.taxTotal = 0;
24 this.shipRate = .02;
25 this.shipTotal = 0;
26 this.subTotal = 0;
27 this.bagTotal = 0;
28 this.things = new Array();
29 }
30
31 shoppingBag = new Bag();
32
33 function showStore() {
34 gimmeControl = false;
35 var header = '<HTML><TITLE>Kategoria</TITLE><BODY BGCOLOR=FFFFFF>';
36 var intro = '<H2>Kategorie produktów Shopping Bag</H2><B>';
37 var footer = '</DL></BLOCKQUOTE></BODY></HTML>';
38 var storeStr = '<BLOCKQUOTE><DL>';
39 for (var i = 0; i < categorySet.length; i++) {
40 storeStr += '<DT><A HREF="javascript: parent.frames[1].reCall(' +
41 i + ', 0);">' + categorySet[i].name + '</A>' +
42 '<DD>' + categorySet[i].description + '<BR><BR>';
Przykład 8.4. manager.html (ciąg dalszy)
43 }
44 infoStr = header + intro + storeStr + footer;
45 parent.frames[0].location.replace(
46 "javascript: parent.frames[1].infoStr");
47 }
48
49 function portal() {
50 gimmeControl = false;
51 parent.frames[0].location.href = "search/index.html";
52 }
53 function display(cOffset, pOffset) {
54 if(!browseControl) {
55 alert("Zacznij zakupy od wybrania kategorii produktów lub " +
56 "wyszukując produkty.");
57 return;
58 }
59 gimmeControl = true;
60 if (curPLoc + pOffset < 0 || curPLoc + pOffset ==
61 categorySet[curCLoc].prodLine.length) {
62 if (curPLoc + pOffset < 0) {
63 if (curCLoc - 1 < 0) { curCLoc = categorySet.length - 1; }
64 else { curCLoc--; }
65 curPLoc = categorySet[curCLoc].prodLine.length - 1;
66 }
67 else if (curPLoc + pOffset == categorySet[curCLoc].prodLine.length) {
68 if (curCLoc + 1 == categorySet.length) { curCLoc = 0; }
69 else { curCLoc++; }
70 curPLoc = 0;
71 }
72 }
73 else {
74 if (curCLoc + cOffset < 0 || curCLoc + cOffset ==
75 categorySet.length) {
76 curCLoc = (curCLoc + cOffset < 0 ? categorySet.length - 1 : 0);
77 }
78 else { curCLoc += cOffset; }
79 if (cOffset == -1 || cOffset == 1) { curPLoc = 0; }
80 else if (pOffset == 0) {
81 curPLoc = (curPLoc >= categorySet[curCLoc].prodLine.length ? 0 :
82 curPLoc)
83 }
84 else { curPLoc = curPLoc + pOffset; }
85 }
86 infoStr = '<HTML><HEAD><TITLE>Nazwa produktu</TITLE></HEAD>' +
87 '<BODY><TABLE CELLPADDING=3><TR><TD VALIGN=TOP COLSPAN=2>' +
88 '<FONT FACE=Tahoma><H2>Shopping Bag: <I>' +
89 categorySet[curCLoc].name + '</I></H2><TR>' +
90 '<TD VALIGN=TOP><IMG SRC="' +
91 categorySet[curCLoc].prodLine[curPLoc].icon.src +
92 '"></TD><TD VALIGN=TOP><FONT FACE=Tahoma>' +
93 '<B>Nazwa: </B>' + categorySet[curCLoc].prodLine[curPLoc].name +
94 '<BR><B>Opis: </B>' +
95 categorySet[curCLoc].prodLine[curPLoc].description + '<BR>' +
96 '<B>Cena: </B> $' +
97 numberFormat(categorySet[curCLoc].prodLine[curPLoc].price) + '/' +
98 categorySet[curCLoc].prodLine[curPLoc].unit + '<BR>' +
99 '<B>PLU: </B>' + categorySet[curCLoc].prodLine[curPLoc].plu +
100 '</TD></TR></TABLE></BODY></HTML>';
101 parent.frames[0].location.href =
102 "javascript: parent.frames[1].infoStr";
103 }
104
105 function reCall(cReset, pReset) {
106 browseControl = true;
107 curCLoc = cReset;
Przykład 8.4. manager.html (ciąg dalszy)
108 curPLoc = pReset;
109 display(0, 0);
110 }
111
112 function gimmeOne() {
113 if (!gimmeControl) {
114 alert("Nie ma na ekranie nic, co mógłbyś dostać.");
115 return;
116 }
117 for (var i = 0; i < shoppingBag.things.length; i++) {
118 if (categorySet[curCLoc].prodLine[curPLoc].plu ==
119 shoppingBag.things[i].plu) {
120 alert("Już to masz. Ilość możesz zmienić, wybierając " +
121 "Widok/Zmiana koszyka.");
122 return;
123 }
124 }
125 shoppingBag.things[shoppingBag.things.length] =
126 categorySet[curCLoc].prodLine[curPLoc];
127 shoppingBag.things[shoppingBag.things.length - 1].itemQty = 1;
128 shoppingBag.things[shoppingBag.things.length - 1].category =
129 categorySet[curCLoc].name;
130 alert("W porządku, wkładamy " +
131 shoppingBag.things[shoppingBag.things.length - 1].name +
132 " do koszyka.");
133 }
134
135 function showBag() {
136 if (shoppingBag.things.length == 0) {
137 alert("Twój koszyk jest obecnie pusty. Włóż coś do niego.");
138 return;
139 }
140 gimmeControl = false;
141 var header = '<HTML><HEAD><TITLE>Twój koszyk</TITLE>' +
142 '</HEAD><BODY BGCOLOR=FFFFFF ' +
143 onLoad="parent.frames[1].runningTab(document.forms[0]);">';
144 var intro = '<H2>Twój koszyk!!!</H2>' +
145 '<FORM onReset="' +
146 'setTimeout(\'parent.frames[1].runningTab(document.forms[0])\', ' +
147 '25);">';
148 var tableTop = '<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=5>' +
149 '<TR><TH><B>Index' +
150 '<TH><B>Produkt<TH><B>Kategoria' +
151 '<TH><B>PLU<TH><B>Cena jednostkowa' +
152 '<TH><B>Ilość<TH><B>Kwota' +
153 '<TH><B>Usuń' +
154 '</TR>';
155 var itemStr = '';
156 for (var i = 0; i < shoppingBag.things.length; i++) {
157 itemStr += '<TR>' +
158 '<TD ALIGN=CENTER>' + (i + 1) + '</TD>' +
159 '<TD>' + shoppingBag.things[i].name + '</TD>' +
160 '<TD>' + shoppingBag.things[i].category + '</TD>' +
161 '<TD>' + shoppingBag.things[i].plu + '</TD>' +
162 '<TD ALIGN=RIGHT>$' +
163 parent.frames[1].numberFormat(shoppingBag.things[i].price) +
164 '</TD>' +
165 '<TD ALIGN=CENTER>' +
166 parent.frames[1].genSelect(shoppingBag.things[i].price,
167 shoppingBag.things[i].itemQty, i) + '</TD>' +
168 '<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 VALUE="' +
169 parent.frames[1].numberFormat(shoppingBag.things[i].price *
170 shoppingBag.things[i].itemQty) +
171 '" onFocus="this.blur();"></TD>' +
172 '<TD ALIGN=CENTER><INPUT TYPE=CHECKBOX></TD>' +
Przykład 8.4. manager.html (ciąg dalszy)
173 '</TR>';
174 }
175 var tableBottom = '<TR>' +
176 '<TD ALIGN=RIGHT COLSPAN=6>SubTotal:</TD>' +
177 '<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="subtotal" ' +
178 'onFocus="this.blur();"></TD></TR>' +
179 '<TR>' + '<TD ALIGN=RIGHT COLSPAN=6> + 6% Tax:</TD>' +
180 '<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="tax" ' +
181 'onFocus="this.blur();"></TD></TR><TR><TD ALIGN=RIGHT COLSPAN=6>' +
182 '2% Shipping:</TD><TD ALIGN=CENTER><INPUT TYPE=TEXT ' +
183 'SIZE=10 NAME="ship" onFocus="this.blur();"></TD></TR>' +
184 '<TR>' +
185 '<TD ALIGN=RIGHT COLSPAN=3><INPUT TYPE=BUTTON VALUE="Do kasy" ' +
186 'onClick="parent.frames[1].checkOut(this.form);"></TD>' +
187 '<TD ALIGN=RIGHT><INPUT TYPE=RESET VALUE="Wyzeruj ilości"></TD>' +
188 '<TD ALIGN=RIGHT><INPUT TYPE=BUTTON VALUE="Zmiana koszyka" ' +
189 'onClick="parent.frames[1].changeBag(this.form, true);"></TD>' +
190 '<TD ALIGN=RIGHT>Suma:</TD><TD ALIGN=CENTER>' +
191 '<INPUT TYPE=TEXT NAME="total" SIZE=10 onFocus="this.blur();">' +
192 '</TD></TR>';
193
194 var footer = '</TABLE></FORM></BODY></HTML>';
195 infoStr = header + intro + tableTop + itemStr + tableBottom + footer;
196 parent.frames[0].location.replace(
197 'javascript: parent.frames[1].infoStr');
198 }
199
200 function genSelect(priceAgr, qty, idx) {
201 var selStr = '<SELECT onChange="this.form.elements[' + (idx * 3 + 1) +
202 '].value = this.options[this.selectedIndex].value; ' +
203 'parent.frames[1].runningTab(this.form);">';
204 for (var i = 1; i <= 10; i++) {
205 selStr += '<OPTION VALUE="' + numberFormat(i * priceAgr) + '"' +
206 (i == qty ? ' SELECTED' : '') + '>' + i;
207 }
208 selStr += '</SELECT>';
209 return selStr;
210 }
211
212 function runningTab(formObj) {
213 var subTotal = 0;
214 for (var i = 0; i < shoppingBag.things.length; i++) {
215 subTotal += parseFloat(formObj.elements[(i * 3) + 1].value);
216 }
217 formObj.subtotal.value = numberFormat(subTotal);
218 formObj.tax.value = numberFormat(subTotal * shoppingBag.taxRate);
219 formObj.ship.value = numberFormat(subTotal * shoppingBag.shipRate);
220 formObj.total.value = numberFormat(subTotal +
221 round(subTotal * shoppingBag.taxRate) +
222 round(subTotal * shoppingBag.shipRate));
223 shoppingBag.subTotal = formObj.subtotal.value;
224 shoppingBag.taxTotal = formObj.tax.value;
225 shoppingBag.shipTotal = formObj.ship.value;
226 shoppingBag.bagTotal = formObj.total.value;
227 }
228
229 function numberFormat(amount) {
230 var rawNumStr = round(amount) + '';
231 rawNumStr = (rawNumStr.charAt(0) == '.' ? '0' + rawNumStr : rawNumStr);
232 if (rawNumStr.charAt(rawNumStr.length - 3) == '.') {
233 return rawNumStr
234 }
235 else if (rawNumStr.charAt(rawNumStr.length - 2) == '.') {
236 return rawNumStr + '0';
237 }
Przykład 8.4. manager.html (ciąg dalszy)
238 else { return rawNumStr + '.00'; }
239 }
240 function round(number,decPlace) {
241 decPlace = (!decPlace ? 2 : decPlace);
242 return Math.round(number * Math.pow(10,decPlace)) /
243 Math.pow(10,decPlace);
244 }
245
246 function changeBag(formObj, showAgain) {
247 var tempBagArray = new Array();
248 for (var i = 0; i < shoppingBag.things.length; i++) {
249 if (!formObj.elements[(i * 3) + 2].checked) {
250 tempBagArray[tempBagArray.length] = shoppingBag.things[i];
251 tempBagArray[tempBagArray.length - 1].itemQty =
252 formObj.elements[i * 3].selectedIndex + 1;
253 }
254 }
255 shoppingBag.things = tempBagArray;
256 if(shoppingBag.things.length == 0) {
257 alert("Twój koszyk jest już pusty. Włóż tam coś.");
258 parent.frames[1].showStore();
259 }
260 else { showBag(); }
261 }
262
263 function checkOut(formObj) {
264 gimmeControl = false;
265 if(!confirm("Czy masz już wszystko, czego potrzebujesz, " +
266 "w potrzebnych Ci ilościach? Pamiętaj, że do usunięcia czegoś " +
267 "lub zmiany ilości musisz wybrać przycisk zmiany. Jeśli jesteś " +
268 "gotów, wciśnij OK.")) {
269 return;
270 }
271 if(shoppingBag.things.length == 0) {
272 showStore();
273 return;
274 }
275 var header = '<HTML><TITLE>Shopping Bag - płatności</TITLE>' +
276 '<BODY BGCOLOR=FFFFFF>';
277
278 var intro = '<H2>Shopping Bag - płatności</H2><FORM METHOD=POST ' +
279 'ACTION="http://www.serve.com/hotsyte/cgi-bin/bag.cgi" ' +
280 'onSubmit="return parent.frames[1].cheapCheck(this);">';
281
282 var shipInfo = '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=5>' +
283 '<TR><TD><B>Informacje o wysyłce</TD></TR>'+
284 '<TR><TD>Imię</TD>' + '<TD><INPUT TYPE=TEXT NAME="fname"></TD>' +
285 '</TR><TR><TD>Nazwisko</TD>' +
286 '<TD><INPUT TYPE=TEXT NAME="lname"></TD></TR><TR><TD>Firma</TD>' +
287 '<TD><INPUT TYPE=TEXT NAME="cname"></TD></TR><TR>' +
288 '<TD>adres - ulica I</TD><TD><INPUT TYPE=TEXT NAME="saddress1">' +
289 '</TD></TR><TR><TD>adres - ulica II</TD>' +
290 '<TD><INPUT TYPE=TEXT NAME="saddress2"></TD></TR><TR>' +
291 '<TD>Miasto</TD>' + '<TD><INPUT TYPE=TEXT NAME="city"></TD></TR>' +
292 '<TR><TD>Województwo/region</TD>' +
293 '<TD><INPUT TYPE=TEXT NAME="stpro"></TD></TR><TR>' +
294 '<TD>Kraj</TD>' + '<TD><INPUT TYPE=TEXT NAME="country"></TD></TR>' +
295 '<TR><TD>Kod pocztowy</TD><TD><INPUT TYPE=TEXT NAME="zip"></TD>' +
296 '</TR><TR><TD><BR><BR></TD></TR></TABLE>';
297
298 var payInfo = '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=5>' +
299 '<TR><TD><B>Informacje o płatności</TD></TR>'+
300 '<TR><TD>Typ karty kredytowej </TD>' +
301 '<TD>Visa <INPUT TYPE=RADIO NAME="ctype" VALUE="visa" CHECKED>' +
302 ' +
Przykład 8.4. manager.html (ciąg dalszy)
303 'Amex <INPUT TYPE=RADIO NAME="ctype" VALUE="amex"> ' +
304 ' ' +
305 'Discover <INPUT TYPE=RADIO NAME="ctype" VALUE="disc"> ' +
306 </TD>' + '</TR>' +
307 '<TR><TD>Numer karty kredytowej</TD>' +
308 '<TD><INPUT TYPE=TEXT NAME="cnumb"></TD></TR><TR>' +
309 '<TD>Data ważności</TD><TD><INPUT TYPE=TEXT NAME="edate"></TD>' +
310 '</TR><TR><TD><INPUT TYPE=SUBMIT VALUE="Wyślij zamówienie"></TD>' +
311 '<TD><INPUT TYPE=RESET VALUE="Wyczyść dane"></TD>' + '</TR>' +
312 '</TABLE>';
313
314 var itemInfo = '';
315 for (var i = 0; i < shoppingBag.things.length; i++) {
316 itemInfo += '<INPUT TYPE=HIDDEN NAME="prod' + i +
317 '" VALUE="' + shoppingBag.things[i].plu + '-' +
318 shoppingBag.things[i].itemQty + '">';
319 }
320 var totalInfo = '<INPUT TYPE=HIDDEN NAME="subtotal" VALUE="' +
321 shoppingBag.subTotal + '">' +
322 '<INPUT TYPE=HIDDEN NAME="taxtotal" VALUE="' +
323 shoppingBag.taxTotal + '">' +
324 '<INPUT TYPE=HIDDEN NAME="shiptotal" VALUE="' +
325 shoppingBag.shipTotal + '">' +
326 '<INPUT TYPE=HIDDEN NAME="bagtotal" VALUE="' +
327 shoppingBag.bagTotal + '">';
328
329 var footer = '</FORM></BODY></HTML>';
330
331 infoStr = header + intro + shipInfo + payInfo + itemInfo +
332 totalInfo + footer;
333 parent.frames[0].location.replace(
334 'javascript: parent.frames[1].infoStr');
335 }
336
337 function cheapCheck(formObj) {
338 for (var i = 0; i < formObj.length; i++) {
339 if (formObj[i].type == "text" && formObj.elements[i].value == "") {
340 alert ("Musiszy wypełnić wszystkie pola.");
341 return false;
342 }
343 }
344 if(!confirm("Jeśli wszystko już poprawisz, wybierz OK w celu " +
345 "wysłania zamówienia lub wybierz Anuluj w celu zrobienia zmian.")) {
346 return false;
347 }
348 alert("Dziękujemy. Już niedługo będziemy mogli skorzystać z Twoich pieniążków.");
349 shoppingBag = new Bag();
350 showStore();
351 return true;
352 }
353
354 function help() {
355 gimmeControl = false;
356 parent.frames[0].location.href = "intro.html";
357 }
358
359 function freshStart() {
360 if(parent.frames[0].location.href != "intro.html") { help(); }
361 }
362
363 //-->
364 </SCRIPT>
365 <TABLE ALIGN=CENTER BORDER=0>
366 <TR>
Przykład 8.4. manager.html (dokończenie)
367 <TD>
368 <A HREF="javascript: gimmeOne();">Daj mi to<A>
369 </TD>
370 <TD>
371 <A HREF="javascript: showBag();">Przegląd/korekta koszyka<A>
372 </TD>
373 <TD>
374 <A HREF="javascript: showStore();">Pokaż wszystkie kategorie<A>
375 </TD>
376 <TD>
377 <A HREF="javascript: portal();">Wyszukiwanie produktów<A>
378 </TD>
379 <TD>
380 <A HREF="javascript: help();">Pomoc<A>
381 </TD>
382 </TR>
383 </TABLE>
384 <TABLE ALIGN=CENTER BORDER=0>
385 <TR>
386 <TD> <!-- Cofnięcie się o jedną kategorię //-->
387 <A HREF="javascript: display(-1,0);">Poprzednia kategoria<A>
388 </TD>
389 <TD> <!-- Cofnięcie się o jeden produkt //-->
390 <A HREF="javascript: display(0,-1);">Poprzedni produkt<A>
391 </TD>
392 <TD> <!-- Jeden produkt do przodu //-->
393 <A HREF="javascript: display(0,1);">Następny produkt<A>
394 </TD>
395 <TD> <!-- Jedna kategoria do przodu //-->
396 <A HREF="javascript: display(1,0);">Następna kategoria<A>
397 </TD>
398 </TR>
399 </TABLE>
400 </BODY>
401 </HTML>
Jeszcze tylko jedna krótka uwaga. Zauważmy, ile kodu wstawiono do znacznika BODY? Na początku odbywa się wstępne ładowanie obrazków i tworzenie obiektów. Netscape Navigator w takiej sytuacji może wyświetlić nieciekawe, szare tło okna (lub w naszym wypadku ramki), dopóki wszystko nie zostanie przygotowane. Dopiero wówczas przeglądarka może zabrać się za interpretację dalszego ciągu dokumentu. Wtedy interpretowany jest właśnie atrybut BGCOLOR.
Zmienne
Dalej znajduje się kod, który wyświetla oferowane przez nas produkty. W wierszach 15-18 ustawiane są cztery zmienne, a w wierszach 53-103 mieści się funkcja display(). Zmienna gimmeControl informuje naszą aplikację, czy na ekranie jest jakiś produkt, którego mógłby zażyczyć sobie klient. Zmienna browseControl wymusza przestrzeganie reguły, że przed rozpoczęciem przeglądania oferty użytkownik musi wybrać zestaw wszystkich reguł lub zacząć od wyszukiwania (zobacz: reguła 1.). Obie zmienne będą używane stale w aplikacji, ale najpierw używa ich display(), więc przyjrzyjmy im się:
var gimmeControl = false;
var browseControl = false;
var curCLoc = -1;
var curPLoc = -1;
Zmienne curCLoc i curPLoc zawierają indeksy - odpowiednio - bieżącej kategorii i bieżącego produktu. Są to liczby, o których mówiliśmy w poprzedniej sekcji. Choć obie mają w tej chwili wartość -1, to gdy tylko użytkownik coś wybierze, ich wartości się zmienią. Więcej o nich powiemy za chwilę. Teraz zobaczmy, jak wygląda funkcja display() zawarta w wierszach 53-103:
function display(cOffset, pOffset) {
if(!browseControl) {
alert("Zacznij zakupy od wybrania kategorii produktów lub " +
"wyszukując produkty.");
return;
}
gimmeControl = true;
if (curPLoc + pOffset < 0 || curPLoc + pOffset ==
categorySet[curCLoc].prodLine.length) {
if (curPLoc + pOffset < 0) {
if (curCLoc - 1 < 0) { curCLoc = categorySet.length - 1; }
else { curCLoc--; }
curPLoc = categorySet[curCLoc].prodLine.length - 1;
}
else if (curPLoc + pOffset == categorySet[curCLoc].prodLine.length) {
if (curCLoc + 1 == categorySet.length) { curCLoc = 0; }
else { curCLoc++; }
curPLoc = 0;
}
}
else {
if (curCLoc + cOffset < 0 || curCLoc + cOffset ==
categorySet.length) {
curCLoc = (curCLoc + cOffset < 0 ? categorySet.length - 1 : 0);
}
else { curCLoc += cOffset; }
if (cOffset == -1 || cOffset == 1) { curPLoc = 0; }
else if (pOffset == 0) {
curPLoc = (curPLoc >= categorySet[curCLoc].prodLine.length ? 0 :
curPLoc)
}
else { curPLoc = curPLoc + pOffset; }
}
infoStr = '<HTML><HEAD><TITLE>Nazwa produktu</TITLE></HEAD>' +
'<BODY><TABLE CELLPADDING=3><TR><TD VALIGN=TOP COLSPAN=2>' +
'<FONT FACE=Tahoma><H2>Shopping Bag: <I>' +
categorySet[curCLoc].name + '</I></H2><TR>' +
'<TD VALIGN=TOP><IMG SRC="' +
categorySet[curCLoc].prodLine[curPLoc].icon.src +
'"></TD><TD VALIGN=TOP><FONT FACE=Tahoma>' +
'<B>Nazwa: </B>' + categorySet[curCLoc].prodLine[curPLoc].name +
'<BR><B>Opis: </B>' +
categorySet[curCLoc].prodLine[curPLoc].description + '<BR>' +
'<B>Cena: </B> $' +
numberFormat(categorySet[curCLoc].prodLine[curPLoc].price) + '/' +
categorySet[curCLoc].prodLine[curPLoc].unit + '<BR>' +
'<B>PLU: </B>' + categorySet[curCLoc].prodLine[curPLoc].plu +
'</TD></TR></TABLE></BODY></HTML>';
parent.frames[0].location.href =
"javascript: parent.frames[1].infoStr";
}
display()
Przed funkcją display() stoją trzy zadania:
sprawdzenie, czy można wyświetlić produkt,
sprawdzenie, którą kategorię czy produkt użytkownik chce oglądać,
wyświetlenie informacji o wybranym produkcie.
Zadanie pierwsze jest proste. Jeśli wartością browseControl jest true, odpowiedź na postawione pytanie jest twierdząca. browseControl początkowo ma wartość false. Kiedy użytkownik raz już wybierze produkt za pośrednictwem wyszukiwania lub wybierze jedną z kategorii, wartością browseControl staje się true. Teraz display() może zabrać się za zadania 2. i 3. Jako że wyświetlany będzie produkt, zmienna gimmeControl jest ustawiana na true.
Zauważmy, że funkcja display() oczekuje dwóch argumentów, cOffset i pOffset. Jeden zawiera wartość wskazującą, jak daleko należy przenieść się z bieżącej kategorii, druga, jak daleko trzeba przenieść się względem bieżącego produktu. Oba parametry mogą być liczbami dodatnimi, ujemnymi, mogą też mieć wartość zero. Aby to uprościć, załóżmy, że nasza pani Daisy Skąpiradło spełniła warunek reguły 1. i może teraz używać do nawigacji łącz Poprzedni i Następny. Spójrzmy do kodu obsługującego te łącza, a znajdziesz go w wierszach 386-397:
<TD> <!-- Cofnięcie się o jedną kategorię //-->
<A HREF="javascript: display(-1,0);">Poprzednia kategoria<A>
</TD>
<TD> <!-- Cofnięcie się o jeden produkt //-->
<A HREF="javascript: display(0,-1);">Poprzedni produkt<A>
</TD>
<TD> <!-- Jeden produkt do przodu //-->
<A HREF="javascript: display(0,1);">Następny produkt<A>
</TD>
<TD> <!-- Jedna kategoria do przodu //-->
<A HREF="javascript: display(1,0);">Następna kategoria<A>
</TD>
Każde z tych łączy wywołuje funkcję display()i przekazuje jej parę liczb całkowitych. W tabeli 8.1 wyjaśniono znaczenie poszczególnych zestawów parametrów. Pamiętajmy, że curCLoc to numer kategorii, a curPLoc to numer produktu.
Tabela 8.1. Ustawianie wartości curCLoc i curPLoc
Łącze |
Przekazywane argumenty |
Interpretacja |
Poprzednia kategoria |
-1, 0 |
Od curCLoc odejmij 1, curPLoc bez zmian. |
Poprzedni produkt |
0, -1 |
curCLoc bez zmian, od curPLoc odejmij 1. |
Następny produkt |
0, 1 |
curCLoc bez zmian, do curPLoc dodaj 1. |
Następna kategoria |
1, 0 |
Do curCLoc dodaj 1, curPLoc bez zmian. |
|
|
|
Wyjątki od reguły
To wszystko ma sens. Jeśli chcemy cofnąć się o jedną kategorię, odejmujemy od numeru kategorii bieżącej 1. Jeśli chcemy przejść do następnego produktu, do numeru bieżącego produktu dodajemy 1. Istnieją jednak trzy wyjątki, które wymagają specjalnego potraktowania:
Nie ma kategorii lub produktu o numerze -1. Jeżeli któryś z numerów wynosi 0 i użytkownik chce się jeszcze cofnąć, jesteśmy na krawędzi katastrofy.
Nie istnieje kategoria o numerze categorySet[categorySet.length]. Jako że kategorii jest categorySet.length, najwyższy dostępny numer to categorySet.length-1. Jeśli właśnie taki jest numer kategorii, a użytkownik wybierze następny produkt lub następną kategorię, znowu nie jest najlepiej. To samo rozumowanie odnosi się też do produktów.
Nawigowanie między kategoriami zawsze pokazuje pierwszy produkt kategorii, niezależnie od tego, jaki produkt użytkownik oglądał poprzednio.
W wierszach 60-85 zapisano odpowiedni kod obsługujący te trzy wyjątki. Dość intensywnie używamy tu zagnieżdżonych instrukcji warunkowych, więc przyjrzyjmy się temu dokładnie:
if (curPLoc + pOffset < 0 || curPLoc + pOffset ==
categorySet[curCLoc].prodLine.length) {
if (curPLoc + pOffset < 0) {
if (curCLoc - 1 < 0) { curCLoc = categorySet.length - 1; }
else { curCLoc--; }
curPLoc = categorySet[curCLoc].prodLine.length - 1;
}
else if (curPLoc + pOffset == categorySet[curCLoc].prodLine.length) {
if (curCLoc + 1 == categorySet.length) { curCLoc = 0; }
else { curCLoc++; }
curPLoc = 0;
}
}
else {
if (curCLoc + cOffset < 0 || curCLoc + cOffset ==
categorySet.length) {
curCLoc = (curCLoc + cOffset < 0 ? categorySet.length - 1 : 0);
}
else { curCLoc += cOffset; }
if (cOffset == -1 || cOffset == 1) { curPLoc = 0; }
else if (pOffset == 0) {
curPLoc = (curPLoc >= categorySet[curCLoc].prodLine.length ? 0 :
curPLoc)
}
else { curPLoc = curPLoc + pOffset; }
}
Poniższy pseudokod powinien nieco lepiej wyjaśnić, co się właściwie dzieje. Numery wierszy w nawiasach odpowiadają odpowiednim wierszom naszego pliku:
1 JEŚLI numer produktu będzie zbyt mały lub zbyt duży, TO (73)
2 JEŚLI numer produktu będzie zbyt mały, TO (74)
3 JEŚLI numer kategorii będzie zbyt mały, TO
numer kategorii niech będzie liczbą kategorii -1 (75)
4 INACZEJ numer kategorii zmniejsz o 1 (76)
5 numer produktu niech będzie liczbą produktów
w nowej kategorii minus 1 (77)
6 INACZEJ JEŚLI numer produktu będzie zbyt duży, TO (79)
7 JEŚLI numer kategorii będzie zbyt duży, TO
numer kategorii ustaw na 0 (80)
8 INACZEJ numer kategorii zmniejsz o 1 (81)
9 numer produktu niech będzie równy 0 (82)
10 INACZEJ (85)
11 JEŚLI numer kategorii będzie zbyt duży lub zbyt mały, to (86)
12 JEŚLI numer kategorii jest zbyt mały, TO
zmniejsz numer kategorii o 1 (87)
13 INACZEJ numer kategorii ustaw na 0 (88)
14 INACZEJ numer kategorii zwiększ o zadaną wielkość (89)
15 JEŚLI przesunięcie kategorii równe jest -1 lub 1, TO
ustaw numer produktu na 0 (90)
16 INACZEJ JEŚLI przesunięcie produktu równe jest 0, TO (91)
17 JEŚLI numer produktu jest większy bądź równy liczbie
produktów w kategorii,
TO ustaw numer produktu na 0 (92)
18 INACZEJ produkt powiększ o zadane przesunięcie (94)
Najbardziej zewnętrzny blok warunkowy JEŚLI obsługuje zmienne, jeśli numer produktu spełnia jeden z dwóch pierwszych warunków podanych wyżej. Najbardziej zewnętrzny blok INACZEJ, obsługuje zmienne jeśli jeden z dwóch pierwszych warunków spełnia numer kategorii. Aby obsłużyć warunek trzeci, w wierszu 80 ustawiamy numer produktu na zero, jeśli kategoria zmienia się o jeden do przodu lub wstecz.
Tworzenie wyświetlanej strony
Znając numer kategorii i produktu, nasza aplikacja może stworzyć stronę HTML wyświetlającą odpowiedni produkt. Niemalże cała reszta kodu funkcji display() ma za zadanie umieścić na ekranie wybrany produkt - wiersze 86-102:
infoStr = '<HTML><HEAD><TITLE>Nazwa produktu</TITLE></HEAD>' +
'<BODY><TABLE CELLPADDING=3><TR><TD VALIGN=TOP COLSPAN=2>' +
'<FONT FACE=Tahoma><H2>Shopping Bag: <I>' +
categorySet[curCLoc].name + '</I></H2><TR>' +
'<TD VALIGN=TOP><IMG SRC="' +
categorySet[curCLoc].prodLine[curPLoc].icon.src +
'"></TD><TD VALIGN=TOP><FONT FACE=Tahoma>' +
'<B>Nazwa: </B>' + categorySet[curCLoc].prodLine[curPLoc].name +
'<BR><B>Opis: </B>' +
categorySet[curCLoc].prodLine[curPLoc].description + '<BR>' +
'<B>Cena: </B> $' +
numberFormat(categorySet[curCLoc].prodLine[curPLoc].price) + '/' +
categorySet[curCLoc].prodLine[curPLoc].unit + '<BR>' +
'<B>PLU: </B>' + categorySet[curCLoc].prodLine[curPLoc].plu +
'</TD></TR></TABLE></BODY></HTML>';
parent.frames[0].location.href =
"javascript: parent.frames[1].infoStr";
Jak widzimy, wszystko polega na tworzeniu jednego, długiego napisu HTML w początkowo pustej zmiennej infoStr. Zwróćmy uwagę, jak istotne są zmienne curPLoc i curCLoc przy wybieraniu informacji o produkcie. categorySet[curCLoc] wskazuje właściwą kategorię, a categorySet [curCLoc].prodLine[curPLoc] odnosi się do odpowiedniego produktu. Kiedy wartości curCLoc i curPLoc zostaną raz określone, można wyświetlić wszystkie informacje o produkcie w dowolny już sposób.
Kiedy infoStr zawiera już cały kod potrzebny do wyświetlenia produktu, właściwość href ramki górnej ustawiana jest przy użyciu protokołu javascript: tak, aby jego wartością była wartość tej zmiennej. Pamiętajmy, że z uwagi na zasięg protokołu należy podać pełne odwołanie do zmiennej, czyli parent.frames[1].infoStr zamiast zwykłego infoStr. Szczegóły na ten temat znajdują się w rozdziale 2.
Etap 3. Pokazanie wszystkich kategorii
Wybranie łącza Pokaż wszystkie kategorie to inny sposób przeglądania produktów. Funkcja showStore() z wierszy 33-47 jest do tego zadania odpowiednio przygotowana:
function showStore() {
gimmeControl = false;
var header = '<HTML><TITLE>Kategoria</TITLE><BODY BGCOLOR=FFFFFF>';
var intro = '<H2>Kategorie produktów Shopping Bag</H2><B>';
var footer = '</DL></BLOCKQUOTE></BODY></HTML>';
var storeStr = '<BLOCKQUOTE><DL>';
for (var i = 0; i < categorySet.length; i++) {
storeStr += '<DT><A HREF="javascript: parent.frames[1].reCall(' +
i + ', 0);">' + categorySet[i].name + '</A>' +
'<DD>' + categorySet[i].description + '<BR><BR>';
}
infoStr = header + intro + storeStr + footer;
parent.frames[0].location.replace(
"javascript: parent.frames[1].infoStr");
}
Wyświetlenie pierwszego produktu
Reguła 1. wymaga, aby przy pierwszym (i tylko pierwszym) wyświetlaniu produktów użytkownik użył łącza wszystkich kategorii lub wyszukiwania produktów. Wyszukiwanie zostanie wkrótce omówione, teraz zajmijmy się pokazywaniem wszystkich kategorii. Jest to dość proste. Funkcja showStore() po prostu analizuje poszczególne elementy tablicy categorySet, generując listę łącz z nazwami i opisami tych kategorii. Po dopisaniu ostatniej kategorii zmienna zawierająca tę listę (czyli infoStr) staje się wartością właściwości href ramki górnej. Zwróćmy uwagę, że każdy atrybut HREF równy jest:
javascript: parent.frames[1].reCall(' + i + ', 0)
Kliknięcie łącza którejś z kategorii wywoła funkcję reCall() z pliku manager.html. Znajduje się ona w wierszach 105-110:
function reCall(cReset, pReset) {
browseControl = true;
curCLoc = cReset;
curPLoc = pReset;
display(0, 0);
}
Funkcja reCall() spodziewa się dwóch argumentów, numeru kategorii oznaczonego jako i w wierszu 42 i wartości 0. Wartość i przypisywana jest curCLoc. To określa oczywiście kategorię, którą użytkownik chce obejrzeć. Liczba 0 przypisywana jest zmiennej curPLoc. Przypomnijmy sobie wyjątek od reguł o numerze 3? Zmiana kategorii zawsze pokazuje użytkownikowi pierwszy jej produkt, czyli prodLine[0].
Kiedy już wszystko to zostanie wykonane, wywoływana jest funkcja display(), która jako argumenty dostaje dwa zera. Gdy w wierszach 60-85 sprawdzany jest pierwszy warunek, zakładamy, że użytkownik ogląda produkt lub kategorię o numerze większym bądź mniejszym niż bieżące wartości curCLoc i curPLoc. Jednak funkcja reCall() już te wartości ustawiła, więc nie ma potrzeby nigdzie się już przenosić. Użytkownik chce zobaczyć produkt o parametrach określonych bieżącymi wartościami curCLoc i curPLoc. Przekazanie dwóch zer to właśnie oznacza i kod z wierszy 60-85 z tym się pogodzi.
Gdzie tu jest DHTML?
Zauważmy, że na stronach produktów w ogóle nie pojawia się DHTML. Nie ma warstw, ale czy nie powinny się tu pokazać? Większość stosowanych obecnie przeglądarek używa JavaScriptu w wersji 1.2. Kilka rozdziałów tej książki poświęcono tworzeniu DHTML przenośnego między różnymi przeglądarkami. Dlaczego teraz się z tego wycofywać? Czy kiedy nasza aplikacja się ładuje, nie moglibyśmy tworzyć warstw dla poszczególnych produktów i następnie je ukrywać i pokazywać według potrzeb? No tak, moglibyśmy, ale...
Ładowanie wstępne zbyt dużej liczby obrazków może nie wyjść nikomu na zdrowie. Jak wspomniano już wcześniej, jeśli mamy mnóstwo grafiki, to wszelkiego rodzaju uprzednie jej ładowanie wystawia na próbę cierpliwość odbiorcy. Tworzenie warstw dla poszczególnych produktów oznacza konieczność załadowania wszystkich obrazków, natomiast czysty kod HTML świetnie się ze swojego zadania wywiązuje i bez tego.
Teraz użytkownik może swobodnie poruszać się między kategoriami i produktami. Czas zobaczyć, co się dzieje, kiedy użytkownik zdecyduje się coś kupić, wkładając to do swojego koszyka.
Etap 4. Dodawanie produktów do koszyka
Wkładanie czegokolwiek do koszyka jest proste. Użytkownik klika łącze, nazwane subtelnie Daj mi to. Wywoływana jest funkcja o nazwie gimmeOne() z wierszy 112-133:
function gimmeOne() {
if (!gimmeControl) {
alert("Nie ma na ekranie nic, co mógłbyś dostać.");
return;
}
for (var i = 0; i < shoppingBag.things.length; i++) {
if (categorySet[curCLoc].prodLine[curPLoc].plu ==
shoppingBag.things[i].plu) {
alert("Już to masz. Ilość możesz zmienić, wybierając " +
"Widok/Zmiana koszyka.");
return;
}
}
shoppingBag.things[shoppingBag.things.length] =
categorySet[curCLoc].prodLine[curPLoc];
shoppingBag.things[shoppingBag.things.length - 1].itemQty = 1;
shoppingBag.things[shoppingBag.things.length - 1].category =
categorySet[curCLoc].name;
alert("W porządku, wkładamy " +
shoppingBag.things[shoppingBag.things.length - 1].name +
" do koszyka.");
}
Funkcja ta najpierw upewnia się, że obecnie na ekranie jest coś, co można włożyć do koszyka. Zmienna gimmeControl ma wartość true, jeśli tylko wyświetlony zostanie produkt. W przeciwnym wypadku funkcje wyświetlające na ekranie dane ustawiają tę zmienną na false. W takim wypadku użytkownik dostaje ostrzeżenie i funkcja gimmeOne() kończy swoje działanie. W przeciwnej sytuacji nasza funkcja przegląda elementy tablicy things, która jest właściwością obiektu shoppingBag, aby sprawdzić, czy taki produkt nie znajduje się już u użytkownika w koszyku.
Funkcja gimmeOne() nie oczekuje żadnych argumentów, polega jedynie na wartości zmiennych curCLoc i curPLoc. Jednak może to prowadzić do ciekawej sytuacji - warto zadać pytanie, czy produkt w koszyku nadal jest obiektem product? Jeśli ktoś odpowiedział tak, ma rację. Jednak każdy produkt w koszyku użytkownika musi być innym produktem, o nieco bardziej złożonej strukturze. Nadal ma on nazwę, opis, PLU i cenę, ale poza tym musimy jakoś określić wielkość danego zakupu i jego kategorię.
Każdy element things powinien wobec tego mieć dodatkowe, dynamicznie dodawane właściwości. W wierszach 125-129 zobaczysz, jak funkcja gimmeOne() dodaje takie specjalizowane obiekty product do tablicy things i jak dopisuje im nowe właściwości:
shoppingBag.things[shoppingBag.things.length] =
categorySet[curCLoc].prodLine[curPLoc];
shoppingBag.things[shoppingBag.things.length - 1].itemQty = 1;
shoppingBag.things[shoppingBag.things.length - 1].category =
categorySet[curCLoc].name;
shoppingBag.things[shoppingBag.things.length] oznacza odwołanie się do obiektu product określonego przez categorySet[curCLoc].prodLine[curPLoc]. W ten sposób do koszyka dodawany jest „zwykły” produkt. Następne dwa wiersze wprowadzają odpowiednio właściwość itemQty (ilość), ustawiając ją początkowo na 1. oraz category, nazwę kategorii, do której należy dodawany produkt.
Na końcu funkcja gimmeOne() musi jeszcze poinformować użytkownika, że pobranie nowego towaru przebiegło poprawnie.
Proces ten powtarza się, póki użytkownik chce jeszcze wkładać coś do koszyka, a w końcu przychodzi czas zapłaty.
|
Techniki języka JavaScript: Istnieje kilka metod dodawania właściwości do obiektów tworzonych przez użytkownika. Najprostsza metoda to wymyślenie właściwości, jej wartości i dodanie ich do obiektu. Każdy element tablicy things jest obiektem product, ale produkty te mają mieć dwie nowe właściwości, itemQty i category. Oto odpowiedni kod: shoppingBag.things[shoppingBag.things.length - 1].itemQty = 1; shoppingBag.things[shoppingBag.things.length - 1].category = categorySet[curCLoc].name; Obiekty te zostały jednak już utworzone, zatem konieczne jest każdorazowe dodawanie właściwości, jedna po drugiej. Jeśli będziemy chcieli wszystkim obiektom tworzonym w przyszłości dodać nowe właściwości, zastosujemy właściwość prototype. Załóżmy, że chcemy dodać do wszystkich produktów cenę sprzedaży: product.prototype.salePrice = 0.00; Wszystkie tworzone obiekty będą teraz miały właściwość salePrice - z wartością domyślną 0.00. |
|
Wyszukiwanie produktów
Zauważmy, że wyszukiwanie produktów to tak naprawdę przeszczep z rozdziału 1. Wyszukiwarka działająca po stronie klienta została zmodyfikowana, aby lepiej odpowiadała potrzebom naszego sklepu. Wszystko jednak odbywa się bardzo podobnie. Ograniczone zostały możliwości obsługi złożonych warunków wyszukiwania do logicznego LUB. Jeśli zatem użytkownik wprowadzi jakiś tekst, odnajdowane są wszystkie produkty zawierające choć jedno słowo ze słów podanych przez użytkownika. Nie mamy logicznej koniunkcji warunków ani wyszukiwania przez adres URL, ale i tak możliwości naszej wyszukiwarki są tutaj wystarczające. Nie włączano w tym miejscu też żadnych nowych możliwości, które nie byłyby prezentowane wcześniej, w rozdziale 1. Użytkownicy mogą podać tekst pusty, wciskając po prostu klawisz ENTER. Realizowane jest wtedy wyszukiwanie puste, które w rezultacie daje wszystkie produkty z bazy danych. Tym razem nie będziemy już tego kodu omawiali tak szczegółowo, jak w rozdziale 1., ale warto przeczytać następnych kilka akapitów, aby zobaczyć, jak łatwo można rozszerzać rozsądnie zorganizowane aplikacje JavaScriptowe. W końcu zresztą użytkownik, tak jak we wszystkich wyszukiwarkach, chce po prostu na podany tekst uzyskać odpowiedź w postaci ciągu łącz. Aby bez problemu włączyć to do naszego sklepu, musimy wyszukiwarkę nieco dostosować:
Wyniki muszą być wyświetlane zgodnie z systemem nawigacji kategoria + produkt, opisanym w etapie 2.
Musimy być w stanie przeszukiwać istniejącą już bazę towarową.
Trzeba zwracać łącza do wszystkich produktów z bazy danych.
Na szczęście wszystkie te zmiany można zrealizować w jednym pliku, search/nav.html. Dzięki temu nie trzeba dokładnie studiować następnych setek wierszy kodu, ale wystarczy się przyjrzeć tylko odpowiednim jego fragmentom.
Odwzorowanie produktów i kategorii
Jeśli wszystko ma działać poprawnie, musimy pewne rzeczy zmienić. Zmiany te dotyczą utworzenia dwóch nowych zmiennych oraz nowej funkcji:
var ref = top.frames[1];
var prodProfiles = new Array();
kategoria/produkt.
function genProfile() {
for (var i = 0; i < ref.categorySet.length; i++) {
for (var j = 0; j < ref.categorySet[i].prodLine.length; j++) {
prodProfiles[prodProfiles.length] = new Array(i, j);
}
}
}
Zmienna ref używana jest jako wskaźnik na top.frames[1]. Ponieważ większość obiektów i zmiennych używanych w tej przeglądarce znajduje się w pliku manager.html, odwoływanie się do obiektu w obiekcie da w efekcie długie zapisy z mnóstwem kropek. Użycie ref przynajmniej trochę skróci zapis. Zmienna prodProfiles początkowo jest pustą tablicą, ale wypełniana jest dzięki wywołaniu funkcji genProfile().
Funkcja genProfile() ma tylko jedno zadanie: przygotować metodę odwoływania się do dowolnego obiektu product z dowolnej kategorii na podstawie indeksów, produktu i kategorii.
Załóżmy na przykład, że zmienna i, numer kategorii, równa jest 1 oraz j, numer produktu, równa jest 2. Jeśli zajrzymy do inventory.js, zauważymy, że w takim przypadku categorySet[i]. prodLine[j] oznacza igloo z kategorii buildings, budynki. Przypomina to przecinanie się współrzędnych na mapie.
Zagnieżdżone pętle for w funkcji genProfile() pozwalają przejrzeć kolejne kategorie category(i) oraz produkty product(j). Możemy powiedzieć, że genProfile() rejestruje w tablicy prodProfiles pary liczb oznaczających produkty i kategorie.
Ktoś mógłby zadać pytanie, czyż nie w taki sposób odwoływaliśmy się do produktów do tej pory, Owszem, tak, ale funkcje wyszukiwarki teraz wiedzą, jakie są możliwe kombinacje. Dzięki temu do produktów łatwo jest się odwoływać (zatem też je wyszukiwać i wyświetlać).
Przeszukiwanie istniejącej bazy danych
Pierwotna wersja przeszukiwała tytuł, opis i adres strony sieciowej. W przypadku naszego sklepu uwzględniamy podobnie zagadnienia. Problem polega na tym, że musimy to wykonać zgodnie z istniejącą bazą danych. Na szczęście możemy wprowadzić kilka zmian w funkcji allowAny() z pliku search/nav.html:
function allowAny(t) {
var findings = new Array();
for (var i = 0; i < prodProfiles.length; i++) {
var compareElement =
ref.categorySet[prodProfiles[i][0]].prodLine[prodProfiles[i][1]].
name + " " +
ref.categorySet[prodProfiles[i][0]].prodLine[prodProfiles[i][1]].
description + " " +
ref.categorySet[prodProfiles[i][0]].prodLine[prodProfiles[i][1]].
price.toString() + " " +
ref.categorySet[prodProfiles[i][0]].prodLine[prodProfiles[i][1]].
plu;
compareElement = compareElement.toUpperCase();
for (var j = 0; j < t.length; j++) {
var compareString = t[j].toUpperCase();
if (compareElement.indexOf(compareString) != -1) {
findings[findings.length] = new Array(prodProfiles[i][0],
prodProfiles[i][1]);
break;
}
}
}
verifyManage(findings);
}
Tak naprawdę niewiele się zmieniło. Jednak powinna nas zainteresować zmiana wyszukiwanego obiektu. Użytkownik może wprowadzić nazwę produktu, jego opis, cenę i PLU. Funkcja allowAny() łączy wszystkie te cztery cechy produktów w całość i taki napis porównywany jest z przekazanymi do wybierania słowami. Jeśli wszystko do siebie pasuje, w tablicy findings pojawia się nowy element, będący tablicą o wartościach prodProfiles[i][0] i prodProfiles[i][1]. Pamiętajmy, że te dwa elementy to liczby całkowite, które będą użyte wkrótce do wyświetlenia wyników.
Obsługa nawigacji między produktami i kategoriami
Załóżmy, że w wyniku wyszukiwania uzyskaliśmy zestaw odpowiedzi. Teraz musimy go umieścić na stronie. Jeśli jednak zdecydowałby się zakodować wyświetlanie produktów w wyszukiwarce, ignorując cały układ kategorii i produktów, stałby się szybką ofiarą zapalenia nadgarstka. Kiedy na stronie wynikowej pokazywane będą łącza do jakichkolwiek produktów, powinniśmy wyświetlać produkty jak zwykle, aby móc następnie standardowo nawigować między nimi, stosując opcje Poprzedni i Następny. Oto odpowiednie zmiany w funkcji formatResults():
function formatResults(results, reference, offset) {
docObj.open();
docObj.writeln('<HTML>\n<HEAD>\n<TITLE>Wyniki wyszukiwania</TITLE>\n</HEAD>' +
'<BODY BGCOLOR=WHITE TEXT=BLACK>' +
'<TABLE WIDTH=780 BORDER=0 ALIGN=CENTER CELLPADDING=3><TR><TD>' +
'<HR NOSHADE WIDTH=100%></TD></TR><TR><TD VALIGN=TOP><B>' +
'Zapytanie: <I>' +
parent.frames[0].document.forms[0].query.value + '</I><BR>\n' +
'Wyniki wyszukiwania: <I>' + (reference + 1) + ' - ' +
(reference + offset > results.length ? results.length :
reference + offset) +
' z ' + results.length + '</I><BR><BR>' + '<B>' +
'\n\n<!- Początek wyników //-->\n\n\t<DL>');
var currentRecord = (results.length < reference + offset ?
results.length : reference + offset);
for (var i = reference; i < currentRecord; i++) {
docObj.writeln('\n\n\t<DT>' + '<FONT SIZE=4>' +
'<A HREF="javascript: top.frames[1].reCall(' + results[i][0]+
', ' + results[i][1] + ')">' +
ref.categorySet[results[i][0]].prodLine[results[i][1]].name +
'</A></FONT>\t<DD>' +
ref.categorySet[results[i][0]].prodLine[results[i][1]].description +
'\t<DD>' + 'Cena: <I>$' +
ref.numberFormat(ref.categorySet[results[i][0]].
prodLine[results[i][1]].price) +
'</I> ' + 'PLU Number: <I>' +
ref.categorySet[results[i][0]].prodLine[results[i][1]].plu +
'</I><P>');
}
docObj.writeln('\n\t</DL>\n\n<!- End result set //-->\n\n');
prevNextResults(results.length, reference, offset);
docObj.writeln('<HR NOSHADE WIDTH=100%>' +
'</TD>\n</TR>\n</TABLE>\n</BODY>\n</HTML>');
docObj.close();
document.forms[0].query.select();
}
Każdy wynik zawiera nazwę produktu, jego opis, cenę i numer PLU. Funkcja przegląda elementy results i pobiera odpowiednie informacje z prodLine na podstawie liczb results[i][0] i results[i][1]. Innymi słowy, wyniki wyglądają tak:
results = new Array(
new Array(0, 1), // Pamiętaj, że element 0 oznacza
new Array(2, 2), // kategorię, element 1 oznacza numer
new Array(4, 1) // produktu
);
Zatem wynikiem jest suszarka (kategoria 0, produkt 1), torebka (kategoria 2, produkt 2) oraz frytki (kategoria 4, produkt 1). Używając takich par liczb, łatwo jest ograniczyć ilość danych i jednocześnie zarejestrować potrzebne nam fakty.
|
Techniki języka JavaScript: Szczęśliwi są ci programiści, którzy mogą użyć danych zaklętych w tablice i obiekty JavaScriptu, a jeszcze szczęśliwsi ci, którzy mogą sięgnąć do informacji innych aplikacji bez konieczności powtórnego kodowania wszystkiego. Tak właśnie dzieje się w przypadku wyszukiwarki produktów w naszej aplikacji. Z uwagą na jej dość prostą strukturę, nie trzeba przebudowywać bazy danych w celu jej przeszukiwania. Wystarczy kilka zmienionych wierszy kodu w wyszukiwarce i wszystko działa wspaniale. Jeśli ktoś chce sięgać do swoich danych w bazie z różnych aplikacji, musi pamiętać, że tylko zachowanie iście spartańskiej prostoty pozwoli tych danych używać bez potrzeby dodatkowego kodowania. W celu wyświetlania produktów na ekranie, potrzebujemy pary liczb. Pętla for w formatResults() pokazuje nazwy, opisy, ceny i numery PLU, używając właśnie tych liczb: ref.categorySet[results[i][0]].prodLine[results[i][1]].name ref.categorySet[results[i][0]].prodLine[results[i][1]].description ref.numberFormat(ref.categorySet[results[i][0]]. prodLine[results[i][1]].price ref.categorySet[results[i][0]].prodLine[results[i][1]].plu Każdy wynik wyświetlany będzie jako tekst podobny do tego poniżej: Hairdrye Kolorowe, żółte wzornictwo, trwały kabel. Dobry zakup. Cena: $1.15 Numer PLU: HAI1 |
|
Kod w łączach
Wyniki zostały wyświetlone, ale jak teraz to zakodować, aby w łączu użyty został system nawigacyjny, o którym tyle mówiono?
W funkcji formatResults() znajdziemy następujące rozwiązanie:
'<A HREF="javascript: top.frames[1].reCall(' + results[i][0]+
', ' + results[i][1] + ')">'
W każdym łączu używany jest protokół javascript: wywołujący funkcję reCall(), będącą tą samą funkcją, która jest używana z do oglądania produktów w przypadku przeglądania wszystkich kategorii. Jak zapewne pamiętamy, funkcja reCall() oczekuje dwóch argumentów: numeru kategorii i numeru produktu. W taki sposób używaliśmy tego w naszej wyszukiwarce. Pozostaje tylko jedno: włączyć każdy z elementów z par liczb do wywołania, i gotowe. Zatem suszarka (hairdryer) ma następujące łącze:
'<A HREF="top.frames[1].reCall(0, 1)">Hairdryer</A>
Spójrz teraz, co się dzieje z liczbami 0 i 1, kiedy pojawiają się w wywołaniu reCall():
function reCall(cReset, pReset) {
browseControl = true;
curCLoc = cReset;
curPLoc = pReset;
display(0, 0);
}
Zmienna curCLoc otrzymuje wartość kategorii z cReset, podobnie curPLoc numer produktu z pReset. Wyszukiwarka świetnie koegzystuje z całą resztą aplikacji i naprawdę wymaga niewiele modyfikacji.
Etap 5. Zmiana zamówienia, płacenie
Kiedy użytkownik nie ma już ani grosza lub niczego więcej nie chce, czas skierować się ku drzwiom. Kliknięcie łącza Przegląd/korekta koszyka powoduje wyświetlenie ekranu podobnego, jak na rysunku 8.7. Koszyk użytkownika nie może ograniczyć się tylko do wyświetlenia swojej zawartości, ale musi też spełniać następujące wymagania:
Wyświetlać każdy produkt z koszyka, jego kategorię, numer PLU i cenę jednostkową.
Zapewnić interaktywny formularz, w którym można będzie zmienić ilości produktów, usuwać produkty i przeliczać wartości.
Wyświetlać bieżące sumy dla określonych wielkości zakupów, sumy pośrednie i wszelkie odpowiednie podatki.
Nie powinno zatem zaskakiwać, że jest tu także kilka funkcji gotowych, aby zaraz zacząć pełnić swoje zadania w Shopping Bag. Oto one:
showBag()
Wyświetla zawartość koszyka.
genSelect()
Generuje dynamiczną listę wyboru umożliwiającą zmianę ilości produktów.
runningTab()
Steruje obliczeniami i wyświetla ceny i wartości.
numberFormat()
Zapewnia dokładność obliczeń i jednolity sposób wyświetlania, w formacie 0.00.
round()
Zapewnia potrzebną dokładność obliczeń.
changeBag()
Usuwa wybrane przez użytkownika produkty i zmienia ich zamawiane ilości.
Funkcja showBag() wywoływana jest, gdy tylko użytkownik wybierze odpowiednie łącze. Znajdziemy ją w wierszach 135-198:
function showBag() {
if (shoppingBag.things.length == 0) {
alert("Twój koszyk jest obecnie pusty. Włóż coś do niego.");
return;
}
gimmeControl = false;
var header = '<HTML><HEAD><TITLE>Twój koszyk</TITLE>' +
'</HEAD><BODY BGCOLOR=FFFFFF ' +
onLoad="parent.frames[1].runningTab(document.forms[0]);">';
var intro = '<H2>Twój koszyk!!!</H2>' +
'<FORM onReset="' +
'setTimeout(\'parent.frames[1].runningTab(document.forms[0])\', ' +
'25);">';
var tableTop = '<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=5>' +
'<TR><TH><B>Index' +
'<TH><B>Produkt<TH><B>Kategoria' +
'<TH><B>PLU<TH><B>Cena jednostkowa' +
'<TH><B>Ilość<TH><B>Kwota' +
'<TH><B>Usuń' +
'</TR>';
var itemStr = '';
for (var i = 0; i < shoppingBag.things.length; i++) {
itemStr += '<TR>' +
'<TD ALIGN=CENTER>' + (i + 1) + '</TD>' +
'<TD>' + shoppingBag.things[i].name + '</TD>' +
'<TD>' + shoppingBag.things[i].category + '</TD>' +
'<TD>' + shoppingBag.things[i].plu + '</TD>' +
'<TD ALIGN=RIGHT>$' +
parent.frames[1].numberFormat(shoppingBag.things[i].price) +
'</TD>' +
'<TD ALIGN=CENTER>' +
parent.frames[1].genSelect(shoppingBag.things[i].price,
shoppingBag.things[i].itemQty, i) + '</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 VALUE="' +
parent.frames[1].numberFormat(shoppingBag.things[i].price *
shoppingBag.things[i].itemQty) +
'" onFocus="this.blur();"></TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=CHECKBOX></TD>' +
'</TR>';
}
var tableBottom = '<TR>' +
'<TD ALIGN=RIGHT COLSPAN=6>SubTotal:</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="subtotal" ' +
'onFocus="this.blur();"></TD></TR>' +
'<TR>' + '<TD ALIGN=RIGHT COLSPAN=6> + 6% Tax:</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="tax" ' +
'onFocus="this.blur();"></TD></TR><TR><TD ALIGN=RIGHT COLSPAN=6>' +
'2% Shipping:</TD><TD ALIGN=CENTER><INPUT TYPE=TEXT ' +
'SIZE=10 NAME="ship" onFocus="this.blur();"></TD></TR>' +
'<TR>' +
'<TD ALIGN=RIGHT COLSPAN=3><INPUT TYPE=BUTTON VALUE="Do kasy" ' +
'onClick="parent.frames[1].checkOut(this.form);"></TD>' +
'<TD ALIGN=RIGHT><INPUT TYPE=RESET VALUE="Wyzeruj ilości"></TD>' +
'<TD ALIGN=RIGHT><INPUT TYPE=BUTTON VALUE="Zmiana koszyka" ' +
'onClick="parent.frames[1].changeBag(this.form, true);"></TD>' +
'<TD ALIGN=RIGHT>Suma:</TD><TD ALIGN=CENTER>' +
'<INPUT TYPE=TEXT NAME="total" SIZE=10 onFocus="this.blur();">' +
'</TD></TR>';
var footer = '</TABLE></FORM></BODY></HTML>';
infoStr = header + intro + tableTop + itemStr + tableBottom + footer;
parent.frames[0].location.replace(
'javascript: parent.frames[1].infoStr');
}
Jak sami się przekonamy, funkcja showBag() nie wykonuje właściwie nic poza wygenerowaniem tablicy i formularza z rysunku 8.7. Najpierw jednak funkcja musi sprawdzić, czy w koszyku w ogóle cokolwiek się znajduje:
if (shoppingBag.things.length == 0) {
alert("Twój koszyk jest obecnie pusty. Włóż coś do niego.");
return;
}
Jeśli things.length równe jest 0, oznacza to, że użytkownik niczego do koszyka nie włożył i nie ma już nic do roboty. Jeśli koszyk zawiera cokolwiek, możemy iść dalej. W wierszach 140-154 ustawiane są zmienne header, intro i tableTop, zawierające górną część tabeli z nagłówkami i kolumnami. Jak widać, showBag() wywołuje kilka innych funkcji:
gimmeControl = false;
var header = '<HTML><HEAD><TITLE>Twój koszyk</TITLE>' +
'</HEAD><BODY BGCOLOR=FFFFFF ' +
onLoad="parent.frames[1].runningTab(document.forms[0]);">';
var intro = '<H2>Twój koszyk!!!</H2>' +
'<FORM onReset="' +
'setTimeout(\'parent.frames[1].runningTab(document.forms[0])\', ' +
'25);">';
var tableTop = '<TABLE BORDER=1 CELLSPACING=0 CELLPADDING=5>' +
'<TR><TH><B>Index' +
'<TH><B>Produkt<TH><B>Kategoria' +
'<TH><B>PLU<TH><B>Cena jednostkowa' +
'<TH><B>Ilość<TH><B>Kwota' +
'<TH><B>Usuń' +
'</TR>';
Zauważmy, że generowany jest tylko zwykły kod statyczny, jedynie obsługa zdarzenia onLoad polega na wywołaniu parent.frames[1].runningTab() - za chwilę sprawdzimy, jak to działa. Kiedy już ustalona zostanie zawartość nagłówka tabeli, czas przejrzeć kolejno produkty włożone przez użytkownika do koszyka. Jak można się domyślić, funkcja showBag() realizuje pętlę z things. length kroków i tworzy w ten sposób w każdym kroku jeden wiersz danych. Oto kod z wierszy 155-174:
var itemStr = '';
for (var i = 0; i < shoppingBag.things.length; i++) {
itemStr += '<TR>' +
'<TD ALIGN=CENTER>' + (i + 1) + '</TD>' +
'<TD>' + shoppingBag.things[i].name + '</TD>' +
'<TD>' + shoppingBag.things[i].category + '</TD>' +
'<TD>' + shoppingBag.things[i].plu + '</TD>' +
'<TD ALIGN=RIGHT>$' +
parent.frames[1].numberFormat(shoppingBag.things[i].price) +
'</TD>' +
'<TD ALIGN=CENTER>' +
parent.frames[1].genSelect(shoppingBag.things[i].price,
shoppingBag.things[i].itemQty, i) + '</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 VALUE="' +
parent.frames[1].numberFormat(shoppingBag.things[i].price *
shoppingBag.things[i].itemQty) +
'" onFocus="this.blur();"></TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=CHECKBOX></TD>' +
'</TR>';
}
Tworzenie list wyboru
Aby dopasować się do istniejących już nagłówków tabeli, pętla for tworzy kolumny z indeksem towarów (tak, abwy można było produkty kolejno zliczać), nazwą, kategorią, PLU, ceną jednostkową, listą wyboru z ilością, łączną wartością oraz polem opcji, pozwalającym usunąć dany produkt. Każda z tych wartości ma swój własny znacznik TD. Stworzenie listy wyboru ilości jest nieco bardziej złożone niż zwykle i warto się temu procesowi dokładniej przyjrzeć. Lista tworzona jest przez funkcję genSelect(). W tej książce znajdują się też inne wersje tejże funkcji, które zapewne są już znane czytelnikowi. Oto wersja używana obecnie - z wierszy 200-210:
function genSelect(priceAgr, qty, idx) {
var selStr = '<SELECT onChange="this.form.elements[' + (idx * 3 + 1) +
'].value = this.options[this.selectedIndex].value; ' +
'parent.frames[1].runningTab(this.form);">';
for (var i = 1; i <= 10; i++) {
selStr += '<OPTION VALUE="' + numberFormat(i * priceAgr) + '"' +
(i == qty ? ' SELECTED' : '') + '>' + i;
}
selStr += '</SELECT>';
return selStr;
}
Funkcja ta oczekuje trzech parametrów: ceny towaru, dotychczasowej ilości i liczby (będącej wartością zmiennej i pętli for), pozwalającej sięgnąć do pola tekstowego, które będzie pokazane zaraz listą wyboru. Wstępnie można ustawić dopuszczalną wielkość zamówienia na 10, jak i dowolnie zwiększyć tę wielkość. Aby utworzyć listę, genSelect() przechodzi przez kolejne liczby od 1 do 10 i tworzy znaczniki OPTION zgodnie z podaną niżej składnią, przypisując wynik przyrostowo do zmiennej selStr:
selStr += '<OPTION VALUE="' + numberFormat(i * priceAgr) + '"' +
(i == qty ? ' SELECTED' : '') + '>' + i;
Poszczególne znaczniki OPTION są proste. Wartością atrybutów VALUE jest cena jednostkowa przemnożona przez i, czyli liczbę związaną z daną opcją. Na przykład produkt w cenie 1 dolara da następujące znaczniki OPTION:
<OPTION VALUE="1.00" SELECTED>1
<OPTION VALUE="2.00">2
<OPTION VALUE="3.00">3
<OPTION VALUE="4.00">4
<OPTION VALUE="5.00">5
<OPTION VALUE="6.00">6
<OPTION VALUE="7.00">7
<OPTION VALUE="8.00">8
<OPTION VALUE="9.00">9
<OPTION VALUE="10.00">10
Jeśli i równe jest obecnej ilości danego produktu (qty), odpowiedni znacznik otrzymuje atrybut SELECTED. Jako że domyślna ilość wszystkich produktów wynosi 1, początkowo zawsze będzie wybrana opcja z tekstem „1”. Jest to bardzo użyteczne, kiedy ilości te będą się zmieniały, czym za chwilę się zajmiemy.
Pominięto przedtem wartość początkową selStr, dlatego warto teraz do tego wrócić:
var selStr = '<SELECT onChange="this.form.elements[' + (idx * 3 + 1) +
'].value = this.options[this.selectedIndex].value; ' +
'parent.frames[1].runningTab(this.form);">';
Każda lista wyboru związana jest z procedurą obsługi zdarzenia onChange, która zmienia wartość elements[(idx*3)+1] na wartość opcji właśnie wybranej. Pamiętajmy, że każda wartość OPTION jest iloczynem ceny jednostkowej produktu przez liczbę 1 do 10. Jeśli w przypadku przykładu z towarem za jednego dolara użytkownik wybierze z listy wartość 4, wartość elements[(idx*3)+1] zmieni się na 4.00. Jest to trochę skomplikowane. Właściwie z którym elementem formularza mamy do czynienia? Aby odpowiedzieć na to pytanie, zajrzyjmy do kodu z wierszy 166-167:
parent.frames[1].genSelect(shoppingBag.things[i].price,
shoppingBag.things[i].itemQty, i)
Teraz spójrzmy na argumenty, jakich w wierszu 200 oczekuje genSelect():
function genSelect(priceAgr, qty, idx) {
Jak widać, wartością idx zawsze jest bieżąca wartość i, inicjalizowana i zwiększana o 1 w wierszu 16. Jeśli użytkownik ma w koszyku 10 produktów, idx będzie miał wartości od 1 do 10, zatem znaczniki wyboru utworzone w showBage() dadzą w wyniku coś takiego:
<!-- Dla pierwszego produktu //-->
<SELECT onChange='this.form.elements[1].value =
this.options[this.selectedIndex].value;
parent.frames[1].runningTab(this.form);'>
<!-- Dla drugiego produktu //-->
<SELECT onChange='this.form.elements[4].value =
this.options[this.selectedIndex].value;
parent.frames[1].runningTab(this.form);'>
<!-- Dla trzeciego produktu //-->
<SELECT onChange='this.form.elements[10].value =
this.options[this.selectedIndex].value;
parent.frames[1].runningTab(this.form);'>
...i tak dalej. Zastanówmy się: form.elements[1] to tekst pola tuż za pierwszą listą wyboru, a przynajmniej tak będzie, bo w chwili tworzenia obsługi zdarzenia onChange pole to jeszcze nie istniało. form.elements[4] odnosi się do pola tekstowego zaraz za listą wyboru w następnym wierszu. Odwołanie się do pola tekstowego to kwestia wyliczenia, jaki indeks zostanie mu przypisany już po utworzeniu formularza. I tak właśnie doszliśmy do wyrażenia (idx*3)+1.
Każdy wybrany produkt wyświetlany jest w jednym wierszu tabeli, każdy wiersz zawiera trzy elementy formularza w jednakiej kolejności:
listę wyboru ilości,
pole tekstowe wyświetlające łączną wartość produktu,
pole opcji pozwalające produkt usunąć.
Oznacza to, że pierwsze pole tekstowe to elements[1], a następne to elements[4]. Pole tekstowe jest drugim elementem każdej trzyelementowej grupy. Funkcja genSelect() tworzy odpowiedni kod, mnożąc indeks za każdym razem przez 3 i dodając 1.
Zapisywanie rachunku
A co z resztą kodu obsługi zdarzenia onChange? Nie tylko pokazywane jest podsumowanie danego produktu, ale też wywoływana jest funkcja runningTab(), przeliczająca ogólną sumę zakupu. Oto funkcja runningTab() z wierszy 212-227:
function runningTab(formObj) {
var subTotal = 0;
for (var i = 0; i < shoppingBag.things.length; i++) {
subTotal += parseFloat(formObj.elements[(i * 3) + 1].value);
}
formObj.subtotal.value = numberFormat(subTotal);
formObj.tax.value = numberFormat(subTotal * shoppingBag.taxRate);
formObj.ship.value = numberFormat(subTotal * shoppingBag.shipRate);
formObj.total.value = numberFormat(subTotal +
round(subTotal * shoppingBag.taxRate) +
round(subTotal * shoppingBag.shipRate));
shoppingBag.subTotal = formObj.subtotal.value;
shoppingBag.taxTotal = formObj.tax.value;
shoppingBag.shipTotal = formObj.ship.value;
shoppingBag.bagTotal = formObj.total.value;
}
Funkcja ta jest całkiem prosta, gdyż realizuje trzy podstawowe operacje:
wylicza i wyświetla sumę pośrednią, która jest sumą wszystkich sum produktów (wiersze 213-217),
wylicza i wyświetla podatek od sprzedaży, koszty wysyłki i sumę całkowitą (wiersze 218-222),
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'; }
}
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: 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 = '<TR>' +
'<TD ALIGN=RIGHT COLSPAN=6>SubTotal:</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="subtotal" ' +
'onFocus="this.blur();"></TD></TR>' +
'<TR>' + '<TD ALIGN=RIGHT COLSPAN=6> + 6% Tax:</TD>' +
'<TD ALIGN=CENTER><INPUT TYPE=TEXT SIZE=10 NAME="tax" ' +
'onFocus="this.blur();"></TD></TR><TR><TD ALIGN=RIGHT COLSPAN=6>' +
'2% Shipping:</TD><TD ALIGN=CENTER><INPUT TYPE=TEXT ' +
'SIZE=10 NAME="ship" onFocus="this.blur();"></TD></TR>' +
'<TR>' +
'<TD ALIGN=RIGHT COLSPAN=3><INPUT TYPE=BUTTON VALUE="Do kasy" ' +
'onClick="parent.frames[1].checkOut(this.form);"></TD>' +
'<TD ALIGN=RIGHT><INPUT TYPE=RESET VALUE="Wyzeruj ilości"></TD>' +
'<TD ALIGN=RIGHT><INPUT TYPE=BUTTON VALUE="Zmiana koszyka" ' +
'onClick="parent.frames[1].changeBag(this.form, true);"></TD>' +
'<TD ALIGN=RIGHT>Suma:</TD><TD ALIGN=CENTER>' +
'<INPUT TYPE=TEXT NAME="total" SIZE=10 onFocus="this.blur();">' +
'</TD></TR>';
var footer = '</TABLE></FORM></BODY></HTML>';
Oglądając ten kod, nietrudno zauważyć, że pola wyświetlające sumy początkowo są puste. Wywołanie runningTab() w ramach obsługi zdarzenia onLoad wstawia do tych pól odpowiednie wartości. Zauważmy też, że w każdym z pól znajduje się taki kod:
onFocus='this.blur();'
Jako że nie należy pozwalać użytkownikowi na modyfikowanie zawartości tych pól, kliknięcie myszą w to pole powoduje i tak utratę przez nie kursora, dzięki czemu nasi klienci nie mogą sobie dowolnie modyfikować wyliczonych danych.
Teraz przyjrzyjmy się trzem naszym przyciskom.
Przycisk Do kasy
Kiedy użytkownik ma już dość kupowania, musi przekazać dane o sposobie płatności i wysłać zamówienie. Kliknięcie omawianego klawisza powoduje wywołanie funkcji checkOut(). Funkcja ta wykonuje dwie rzeczy:
generuje formularz zamówienia z danymi o płatności,
generuje dodatkowe pola ukryte (HIDDEN), które odpowiadają wszystkim wybranym produktom.
Funkcja ta jest długa, więc podzielimy ją na dwie części. Oto wiersze 263-312:
function checkOut(formObj) {
gimmeControl = false;
if(!confirm("Czy masz już wszystko, czego potrzebujesz, " +
"w potrzebnych Ci ilościach? Pamiętaj, że do usunięcia czegoś " +
"lub zmiany ilości musisz wybrać przycisk zmiany. Jeśli jesteś " +
"gotów, wciśnij OK.")) {
return;
}
if(shoppingBag.things.length == 0) {
showStore();
return;
}
var header = '<HTML><TITLE>Shopping Bag - płatności</TITLE>' +
'<BODY BGCOLOR=FFFFFF>';
var intro = '<H2>Shopping Bag - płatności</H2><FORM METHOD=POST ' +
'ACTION="http://www.serve.com/hotsyte/cgi-bin/bag.cgi" ' +
'onSubmit="return parent.frames[1].cheapCheck(this);">';
var shipInfo = '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=5>' +
'<TR><TD><B>Informacje o wysyłce</TD></TR>'+
'<TR><TD>Imię</TD>' + '<TD><INPUT TYPE=TEXT NAME="fname"></TD>' +
'</TR><TR><TD>Nazwisko</TD>' +
'<TD><INPUT TYPE=TEXT NAME="lname"></TD></TR><TR><TD>Firma</TD>' +
'<TD><INPUT TYPE=TEXT NAME="cname"></TD></TR><TR>' +
'<TD>adres - ulica I</TD><TD><INPUT TYPE=TEXT NAME="saddress1">' +
'</TD></TR><TR><TD>adres - ulica II</TD>' +
'<TD><INPUT TYPE=TEXT NAME="saddress2"></TD></TR><TR>' +
'<TD>Miasto</TD>' + '<TD><INPUT TYPE=TEXT NAME="city"></TD></TR>' +
'<TR><TD>Województwo/region</TD>' +
'<TD><INPUT TYPE=TEXT NAME="stpro"></TD></TR><TR>' +
'<TD>Kraj</TD>' + '<TD><INPUT TYPE=TEXT NAME="country"></TD></TR>' +
'<TR><TD>Kod pocztowy</TD><TD><INPUT TYPE=TEXT NAME="zip"></TD>' +
'</TR><TR><TD><BR><BR></TD></TR></TABLE>';
var payInfo = '<TABLE BORDER=0 CELLSPACING=0 CELLPADDING=5>' +
'<TR><TD><B>Informacje o płatności</TD></TR>'+
'<TR><TD>Typ karty kredytowej </TD>' +
'<TD>Visa <INPUT TYPE=RADIO NAME="ctype" VALUE="visa" CHECKED>' +
' +
'Amex <INPUT TYPE=RADIO NAME="ctype" VALUE="amex"> ' +
' ' +
'Discover <INPUT TYPE=RADIO NAME="ctype" VALUE="disc"> ' +
</TD>' + '</TR>' +
'<TR><TD>Numer karty kredytowej</TD>' +
'<TD><INPUT TYPE=TEXT NAME="cnumb"></TD></TR><TR>' +
'<TD>Data ważności</TD><TD><INPUT TYPE=TEXT NAME="edate"></TD>' +
'</TR><TR><TD><INPUT TYPE=SUBMIT VALUE="Wyślij zamówienie"></TD>' +
'<TD><INPUT TYPE=RESET VALUE="Wyczyść dane"></TD>' + '</TR>' +
'</TABLE>';
To długi fragment, ale jest to tylko statyczny kod HTML. Generowany jest formularz płatności, jaki pokazano na rysunku 8.8. Formularz zawiera pola, w których użytkownik może wpisać podstawowe dane dotyczące płatności. Każde z tych pól ma niepowtarzalną nazwę, dzięki czemu skrypt działający na serwerze może poszczególne informacje prawidłowo zinterpretować.
Ostatnia część funkcji checkOut() przygotowuje pola HIDDEN, w których będą rejestrowane produkty wybrane przez użytkownika. Poniżej przedstawiono wiersze 314-319:
var itemInfo = '';
for (var i = 0; i < shoppingBag.things.length; i++) {
itemInfo += '<INPUT TYPE=HIDDEN NAME="prod' + i +
'" VALUE="' + shoppingBag.things[i].plu + '-' +
shoppingBag.things[i].itemQty + '">';
}
W ten sposób generowane są pola HIDDEN o nazwie prod uzupełnionej wartością zmiennej i. Wartość składniowo zgodna jest z PLU-ilość. Jeśli zatem użytkownik zażyczy sobie dwóch torebek frytek, pole HIDDEN ustawione zostanie na VALUE="FRI1-2". Kiedy pola HIDDEN zostaną już stworzone, funkcja checkOut() zbierze wszystkie wartości sum z tych pól - wiersze 320-327:
var totalInfo = '<INPUT TYPE=HIDDEN NAME="subtotal" VALUE="' +
shoppingBag.subTotal + '">' +
'<INPUT TYPE=HIDDEN NAME="taxtotal" VALUE="' +
shoppingBag.taxTotal + '">' +
'<INPUT TYPE=HIDDEN NAME="shiptotal" VALUE="' +
shoppingBag.shipTotal + '">' +
'<INPUT TYPE=HIDDEN NAME="bagtotal" VALUE="' +
shoppingBag.bagTotal + '">';
Dodajemy teraz jeszcze przyciski Wyślij zamówienie i Wyczyść dane (pozwalający wyzerować cały formularz zamówienia). Zanim przejdziemy dalej, zwróćmy uwagę, że obsługa zdarzenia onSubmit tego - wygenerowanego w locie - formularza wywołuje funkcję cheapCheck(). Funkcja ta nie robi nic poza sprawdzeniem, czy w przesyłanym formularzu nie pozostało puste żadne z pól, których wypełnienie jest obowiązkowe. Oto wspomniana funkcja z wierszy 337-352:
function cheapCheck(formObj) {
for (var i = 0; i < formObj.length; i++) {
if (formObj[i].type == "text" && formObj.elements[i].value == "") {
alert ("Musiszy wypełnić wszystkie pola.");
return false;
}
}
if(!confirm("Jeśli wszystko już poprawisz, wybierz OK w celu " +
"wysłania zamówienia lub wybierz Anuluj w celu zrobienia zmian.")) {
return false;
}
alert("Dziękujemy. Już niedługo będziemy mogli skorzystać z Twoich pieniędzy.");
shoppingBag = new Bag();
showStore();
return true;
}
Jeśli którekolwiek z wymaganych pól jest puste, funkcja cheapCheck() ostrzega o tym użytkownika i zwraca wartość false, co powoduje zablokowanie wysyłania formularza. Zapewne zechcemy zaimplementować funkcję przeprowadzającą dokładniejszą kontrolę zawartości formularza, ale tak przynajmniej mamy jakiś punkt wyjściowy. Zwróćmy też uwagę, że jeśli użytkownik poprawnie wypełnił formularz, wartością zmiennej shoppingBag staje się nowy obiekt Bag(), a użytkownik jest znowu kierowany do strony z wszystkimi kategoriami oferowanych towarów, co realizuje funkcja showStore().
Koniec wyświetlania
Teraz, kiedy zmienne header, intro, tableTop, itemStr, totalInfo, tableBottom i footer mają odpowiednie wartości umożliwiające wyświetlenie strony, funkcja zestawia informacje na ekranie - wiersze 195-197:
infoStr = header + intro + tableTop + itemStr + tableBottom + footer;
parent.frames[0].location.replace(
'javascript: parent.frames[1].infoStr');
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.
Skopiuj plik bag.cgi do katalogu, z którego uruchamiasz skrypty CGI (na przykład cgi-bin).
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:
utwórz pustą tablicę tempBagArray,
przeglądaj kolejne elementy tablicy things,
jeśli nie zaznaczono pola opcji, dodaj do temBagArray następny element,
ustaw ilość dodanego produktu na ilość ustawioną przez użytkownika w odpowiedniej
liście wyboru,
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() {
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>
<OPTION VALUE="Appliances">Artykuły AGD
<OPTION VALUE="Building">Budynki
<OPTION VALUE="Cloting">Ubrania
<OPTION VALUE="Electronics">Elektronika
<OPTION VALUE="Food">Jedzenie
<OPTION VALUE="Hardware">Urządzenia
<OPTION VALUE="Music">Muzyka
</SELECT>
Choć tutaj kategorie wpisane są „na sztywno”, to można też zastosować logikę podobną, jak w genSelect(), aby uzyskać bardziej dynamiczne działanie. Kiedy wyszukiwane są produkty, możemy przeszukiwać je jedynie z kategorii wybranych przez użytkownika, dzięki czemu ograniczamy zakres przeszukiwania.
Następna możliwość rozbudowy to umożliwienie wyszukiwania według zakresu cen. Jeśli kupujący szuka produktów poniżej 50 dolarów, droższych od 100 dolarów lub w cenie między 50 a 100, może użyć operatorów >, <, >= i <=. W tym celu będziemy musieli dokonać odpowiednich zmian w funkcjach validate() i formatResults().
Obsługa ciasteczek
Załóżmy, że mamy sklep, w którym niektóre towary często się zmieniają. Niektórzy klienci mogą w związku z tym często nas odwiedzać - dobrze byłoby więc, jeśli nie musieliby wypełniać wszystkich danych o sobie. Dlaczego nie dodać zatem funkcji korzystających z ciasteczek z rozdziału 7.? W ten sposób dane o użytkowniku byłyby trzymane w przeglądarce u klienta i można byłoby do nich sięgać, kiedy tylko użytkownik chce coś zamówić. Jest to nieco wymagające zadanie dla programisty, ale kiedy skończymy, będziemy uważali się za herosów JavaScriptu. Choć funkcje GetCookie() i SetCookie() umożliwiają zapisywanie i odczytywanie ciasteczek, trzeba stworzyć funkcję zbierającą dane z poszczególnych pól (zapewne w jeden, długi napis) i drugą, która będzie dane odczytywała, wstawiając je do pól formularza.
Cechy aplikacji:
Prezentowane techniki:
|
9 Szyfry w JavaScripcie |
|
Teraz czas na chwilę odprężenia umysłowego. Ten rozdział nie jest tak śmiertelnie poważny, podstawą tworzonej aplikacji jest czysta zabawa - techniki szyfrowania w JavaScripcie. Aplikacja przekształca wiadomość tekstową w coś, co wygląda jak cyfrowe śmieci, czytelne tylko dla tych, którzy znają klucz do tego sekretu.
Interfejs pokazany na rysunku 9.1 jest dość prosty. Wybrano szyfr Cezara, widać wobec tego opis tego szyfru, listę wyboru umożliwiającą wybranie przesunięcia i pole tekstowe ułatwiają wpisanie tekstu, który będzie szyfrowany i deszyfrowany.
|
|
Rysunek 9.1. Interfejs szyfrów
Tekst „JavaScript jest językiem skryptowym przyszłości, nieprawda?” wpisano do okienka tekstowego. Wybranie z listy wyboru przesunięcia liczby 6 i następnie kliknięcie przycisku Zaszyfruj da tekst zakodowany, tak jak na rysunku 9.2. Oto ten tekst:
pg1gyixovz pkyz p54qoks yqx4vzu24s vx54y5uio tokvxg2jg
|
|
Rysunek 9.2. Użycie szyfru Cezara
Wybór przycisku Odszyfruj powoduje przywrócenie tekstu do pierwotnej postaci. Zwróćmy uwagę, że tekst wynikowy zapisany jest małymi literami. Szyfr Vigenere działa bardzo podobnie. Wybranie tego szyfru z listy wyboru spowoduje pokazanie ekranu z rysunku 9.3.
Kiedy przychodzi do szyfrowania, nie ma już listy z wyborem numeru kluczowego, ale pojawi się okienko tekstowe, w którym należy podać słowo kluczowe lub nawet frazę kluczową. Teraz do kodowania użyjemy wyrażenia „code junky” - wynik pokazano na rysunku 9.4, a otrzymany tekst ma postać:
loye1w4sdv mi1d tn0yliv 5uf03ws5iz dtd1w88ps pwht0u9ny
Oczywiście odcyfrowanie tego tekstu przy użyciu takiego samego kodu spowoduje przywrócenie tekstu do postaci pierwotnej. Zakładam, że czytelnik nie musi znać się na szyfrach, więc zaczynamy od krótkiego kursu na ten temat. Wyjaśnione zostaną podstawowe pojęcia i szczegółowo omówimy dwa szyfry używane w naszej aplikacji.
|
Rysunek 9.3. Interfejs szyfru Vigenere
|
Rysunek 9.4. Szyfr Vigenere w działaniu
Jak działają szyfry
No dobrze, czym zatem jest szyfr? Szyfr to algorytm lub zestaw algorytmów, przekształcających wiadomość tekstową na coś, co wydaje się być bezsensownym zlepkiem symboli. Wiadomość oryginalną mogą odczytać jedynie osoby wtajemniczone. Poniższe definicje pomogą ogólnie zrozumieć szyfrowanie i związane z nim techniki programowe.
Tekst otwarty jest tym, co przekazuje się do odbiorców, czyli wiadomością oryginalną.
Tekst zaszyfrowany to oryginalna wiadomość poddana działaniu algorytmu szyfrującego. Tekst ten znowu staje się tekstem otwartym, kiedy zostanie odszyfrowany.
W wielu szyfrach używane są klucze - jeden lub więcej. Klucz to tekst lub zestaw bitów używanych do szyfrowania i odszyfrowywania danych. Firma RSA Data Security, Inc. (http://www.rsa. com/), przodująca na rynku technologii kryptograficznych, definiuje klucz tak, że służy on do przekształcania tekstu otwartego w tekst zaszyfrowany. Kluczem może być cokolwiek, na przykład słowo „repozytorium”, zdanie „Najlepsze kasztany rosną na placu Pigalle”, liczba dwójkowa 10011011, a nawet całkiem dziwaczny napis, jak „%_-.;,(<<*&^”.
Szyfry, w których nadawca i odbiorca posługują się tym samym kluczem, nazywamy szyframi z kluczem symetrycznym. Jeśli każdy klucz jest inny i jeden podawany jest do publicznej wiadomości, a drugi zna tylko odbiorca, mamy do czynienia z szyfrowaniem z kluczem publicznym. W naszej aplikacji używamy tylko szyfrowania symetrycznego.
Istnieją setki opisanych metod szyfrowania. Niektóre mają już tysiące lat, a wymyślone zostały przez wielkich uczonych starożytności. Inne mają ledwie kilka tygodni i zostały stworzone przez nawiedzonego nastolatka, który doznał olśnienia po ustanowieniu rekordowego wyniku w grze Tomb Raider. Niezależnie od ich pochodzenia - szyfry dzielą się na trzy podstawowe kategorie: ukrycie, przestawienie i podstawienie.
Szyfry stosujące ukrycie zawierają tekst otwarty z tekstem zaszyfrowanym. Odbiorca musi wiedzieć, które litery i cyfry należy odrzucić w celu odczytania wiadomości. Oto mały przykład:
l234u34b09i23ę87776c32z123e090k098o88787l2a33d234ę
Wystarczy pominąć wszystkie cyfry, by odczytać zdanie lubię czekoladę. Spójrzmy na kolejny przykład.
U Ciebie i Elizy kwitną astry jakieś.
Pierwsze litery kolejnych wyrazów powiedzą, że należy uciekać. W obu przypadkach odgadnięcie tekstu nie było specjalnie skomplikowane, ale wielu ludzi doszło do niesłychanej wprost wprawy w wymyślaniu takich ukrytych wiadomości. Przy okazji zauważmy, że w tego typu szyfrach tekst zaszyfrowany nie jest w ogóle potrzebny, co widać było w powyższych przykładach. Pomyślmy o sympatycznym atramencie, który znika, kiedy wysycha. Bardziej drastycznym przykładem może być niejaki Histiasz, który w piątym wieku przed naszą erą zgolił głowę zaufanemu niewolnikowi i wytatuował na niej wiadomość. Kiedy włosy odrosły, Histiasz posłał go z wieścią. Arystagoras zgolił głowę niewolnika i odczytał w ten sposób instrukcje dotyczące wzniecenia powstania.
Szyfry przestawne także zachowują wszystkie znaki pierwotnej wiadomości, a ich zasada działania polega po prostu na przestawieniu kolejności tekstów wiadomości szyfrowanej. Oto przykład:
ei be ic do yż el az ik yt ka la gs ol
Sklejmy te grupy liter w całość i przeczytajmy od końca - no tak, znowu „los Galaktyki zależy od Ciebie”.
Szyfry podstawiane zamieniają każdy znak na inny znak lub symbol. Oto przykład:
17-29-24-15-8-25-28-15-14-19-15-12-4-30-3-1-17-12
Jeśli każdej literze przypiszemy liczbę, oznaczającą jej kolejny numer w alfabecie (polskim), uzyskamy zdanie „myślę tylko liczbami” (bo m jest siedemnastą literą, y dwudziestą dziewiątą, i tak dalej). Cyfry zastępcze mogą być użyte niezależnie od stosowanego alfabetu. Oba szyfry stosowane w naszej aplikacji należą do grupy szyfrów podstawianych.
Kilka słów o łamaniu kodów
Generowany przez tę aplikację zaszyfrowany tekst może w pierwszej chwili wyglądać dość skomplikowanie. Tak naprawdę dobry kryptoanalityk mógłby je złamać w ciągu kilku minut za pomocą tylko kartki i ołówka. Na szczęście można zapewnić znacznie wyższy poziom bezpieczeństwa, jeśli zastosuje się algorytmy takie, jak RSA, IDEA czy potrójny DES. Autor nie może pokazać, jak łamać takie szyfry, ale może pokazać, na co łatwo jest nabrać szyfry podstawiane i przestawne.
Podstawową bronią agenta jest częstość występowania liter. Niektóre litery w każdym języku w typowym tekście potocznym występują częściej, inne rzadziej. W angielskim najczęstsze litery to kolejno E, T, N, R, O, A, I i S, najrzadziej występujące to J, K, Q, X i Z.
Inna metoda to analiza par i trójek znaków. Także dwuznaki i trójznaki mają swoje częstości występowania. Wspomniany przed chwilą podręcznik jako najczęstsze dwuznaki angielskiego podaje EN, ER, RE, NT, TH, ON i IN, jako najrzadsze DF, HU, IA, LT i MP. Z kolei najpopularniejsze trójznaki to ENT, ION, AND, ING, IVE, THO i FOR, najrzadsze - ERI, HIR, IET, DER i DRE.
Litery pojawiające się najczęściej oraz występowanie dwuznaków i trójznaków nie tylko może zasugerować niektóre litery, ale też wskazać prawdopodobnych ich sąsiadów. Zastanówmy się, ilu dwu- i trójznaków używamy na co dzień: na, do, od, się, i tak dalej. Choć jednak szyfry omawiane w naszej aplikacji nie są doskonałe, to możemy się z nimi trochę pobawić, a i tak będą w stanie zniechęcić przypadkowe, wścibskie osoby.
Szyfr Cezara
Szyfr ten, stosowany przez Juliusza Cezara do kontaktowania się z jego generałem, jest pierwszym szyfrem, o którym wiadomo, że był stosowany do zabezpieczania przesyłania komunikatów. Stosowany algorytm jest prostym przesunięciem liter o 1 do 25 miejsc w alfabecie (od b do z), zatem przesunięcie o 3 spowoduje przejście litery a na d. Natomiast litery z końca alfabetu przesuwane są z powrotem na litery początkowe, zatem przy przesunięciu o 3 pozycje litera z zostanie zastąpiona literą c. Liczba określająca przesunięcie jest kluczem do szyfrowania i odszyfrowania wiadomości.
Zwróćmy uwagę, że jeśli zostanie wybrany jakiś klucz, to wszystkie wystąpienia danej litery będą miały już stale takie same odpowiedniki - szyfr Cezara jest monoalfabetyczny.
Szyfr Vigenere
Ten szyfr z kolei zaprezentowany został przez matematyka Błażeja de Vigenere w XVI wieku. Jest to szyfr polialfabetyczny, gdyż używa się w nim więcej niż jednego alfabetu szyfrującego. Innymi słowy literze a nie zawsze musi odpowiadać litera d, jak to miało miejsce w przypadku szyfru Cezara z przesunięciem 3.
Zamiast liczby w tym szyfrze używa się słowa kluczowego. Załóżmy, że chcemy zakodować propozycję spotkania się o północy, „meet at midnight”, i zamierzamy użyć jako słowa kluczowego nazwy szyfru, vinegar. Litery słowa kluczowego należy ustawić kolejno obok liter szyfrowanego tekstu:
vine ga rvinegar
meet at midnight
W porządku. V to dwudziesta druga litera alfabetu, I jest dziewiąta. n, e, g, a i r to odpowiednio litery o numerach 14, 5, 7, 1 i 18. Zatem litera m tekstu szyfrowanego zostanie przesunięta o 22 pozycje, pierwsze e o 9 pozycji, drugie e o 14, i tak dalej. W rezultacie otrzymamy:
hmrx gt ddlammhk
Jeśli się nad tym zastanowić, to właściwie mamy tu do czynienia z generowaniem szyfrów Cezara - od nowa dla każdego następnego znaku.
|
|
|
Jeśli czytelnik chce dowiedzieć się nieco więcej o szyfrach i zna język angielski, może ściągnąć podręcznik armii amerykańskiej w postaci szeregu plików PDF spod adresu http://www.und. nodak.edu/org/crypto/crypto/army.field.manual/separate.chaps/. Ta kopia dokumentów znajduje się w witrynie Crypto Drop Box. Można zajrzeć na stronę główną http://www.und.nodak.edu/org/crypto/crypto/ - znajduje się tu dość materiałów, aby mieć zajęcie na wiele dni. |
|
|
Wymagania programu
W aplikacji tej używamy JavaScriptu 1.2 i DHTML, konieczne jest zatem użycie przeglądarki w wersji 4.x lub nowszej. Wielokrotnie używamy tutaj dopasowywania i podmiany tekstów, więc wersja 1.2 w pełni odpowiada naszym wymaganiom.
Struktura programu
Na szczęście w tej aplikacji używane są tylko dwa pliki. Co więcej, będziemy oglądać kod tylko jednego z nich. Owe dwa pliki to index.html oraz dhtml.js (ten drugi omawialiśmy już w rozdziale 6.). Zanim zajrzymy do kodu, zastanówmy się jeszcze, jak ta aplikacja powinna wyglądać teoretycznie. Aplikacja została skonstruowana w pełni obiektowo. Koszyk na zakupy z rozdziału 8. też był tworzony jako aplikacja obiektowa, ale w bieżącym rozdziale pójdziemy jeszcze dalej.
W tej aplikacji używamy dwóch szyfrów. Każdy szyfr jest w jakiejś mierze podobny do innych, bez względu na to, jakiego jest rodzaju. Pamiętajmy tylko, że istnieją trzy podstawowe grupy szyfrów. W naszej aplikacji mają być dwa szyfry podstawiane, szyfr Cezara i Vigenere. Na rysunku 9.5 pokazano podstawowy układ opisanej hierarchii.
|
Rysunek 9.5. Struktura klas szyfrów
Z rysunku wynika, że obiekty klas CencealmentCipher, TranspositionCipher i SubstitutionCipher dziedziczą po obiekcie Cipher. Wobec tego szyfry Cezara i Vigenere będą obiektami klasy SubstitutionCipher, a zawierać będą wszystkie właściwości i metody tej klasy.
Aby trochę sobie poszerzyć horyzonty, zastanówmy się, jak by można było ten model rozwinąć. Na rysunku 9.6 pokazano, że łatwo do tej hierarchii byłoby dodać inne rodzaje szyfrów bez zmieniania czegokolwiek w strukturze już istniejącej. Pogrubione elementy opisują część używaną w aplikacji.
Jak widać, do stworzonej tak struktury można dodawać dowolnie wiele innych rodzajów szyfrów i konkretnych szyfrów bez konieczności modyfikowania istniejącego kodu. Można też tworzyć kolejne klasy potomne, znajdujące się w strukturze jeszcze głębiej. Pamiętajmy o tym, kiedy będziemy na kolejnych stronach omawiać odpowiedni kod. Zobaczymy, jak łatwo jest do tej aplikacji dodawać kolejne szyfry bez konieczności zmieniania czegokolwiek.
Spójrzmy teraz na plik index.html, pokazany w przykładzie 9.1.
Przykład 9.1. index.html
1 <HTML>
2 <HEAD>
3 <TITLE>Szyfr</TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6 BODY { margin-left: 50 px; font-family: arial; }
7 I { font-weight: bold; }
8 //-->
9 </STYLE>
10 <SCRIPT LANGUAGE="JavaScript1.2" SRC="dhtml.js"></SCRIPT>
|
Rysunek 9.6. Rozszerzanie struktury klas szyfrów
Przykład 9.1. index.html (ciąg dalszy)
11 <SCRIPT LANGUAGE="JavaScript1.2">
12 <!--
13
14 var caesar = '<FONT SIZE=2>Stworzony przez Juliusza Cezara szyfr polega ' +
15 'na przesuwaniu liter. Zwykły tekst jest szyfrowany przez przesunięcie ' +
16 'każdego znaku alfabetu o ustaloną liczbę znaków.' +
17 '<BR><BR>Na przykład przesuwanie o 1 zmienia <I>a</I> na <I>b</I>, ' +
18 '<I>b</I> na <I>c</I> i tak dalej.' +
19 'Znaki z końca alfabetu zamieniane są z powrotem na znaki z początku, ' +
20 'zatem na przykład <I>z</I> zamieniane jest na <I>a</I>. W aplikacji ' +
21 'do alfabetu dodawane są także cyfry, wobec czego <I>9</I> zamieniane ' +
22 'jest na <I>a</I>. Proces odwrotnego przesunięcia umożliwia odcyfrowanie ' +
23 'danych.' +
24 '<BR><FORM>Przesunięcie: ' +
25 genSelect('Shift', 35, 0, 0) +
26 '</FORM><BR>Uwaga: mówi się, że Cezar preferował użycie przesunięcia 3.'
27
28 var vigenere = '<FONT SIZE=2>Stworzony przez znanego matematyka Błażeja ' +
29 'VigenÚre, szyfr VigenÚre uważać można za "dynamiczną" wersję szyfru ' +
30 'Cezara. Zamiast przesuwać wszystkie znaki o ustaloną liczbę znaków ' +
31 'w alfabecie, przesunięcie jest zmieniane w zależności od liter w słowie ' +
32 'wybranym jako klucz. Jeśli słowem tym będzie na przykład <I>dog</I>, to ' +
33 'wobec tego, że litery <I>d</I>, <I>o</I> i <I>g</I> są odpowiednio ' +
34 'literami o numerach 4, 15 i 7, każdy znak będzie przesuwany odpowiednio ' +
35 'o 4, 15 i 7 pozycji. W aplikacji uwzględniono też cyfry, zatem i słowo ' +
36 'kluczowe zawierać może litery i cyfry.' +
37 '<BR><BR><FORM>Słowo kluczowe: <INPUT TYPE=TEXT NAME="KeyWord" SIZE=25>' +
Przykład 9.1. index.html (ciąg dalszy)
38 '</FORM><BR>. Uwaga: szyfr ten ma wiele odmian, z których jedną wymyślił ' +
39 'Lewis Carroll, autor "Alicji w Krainie Czarów".';
40
41 var curCipher = "caesar";
42
43 function Cipher() {
44 this.purify = purify;
45 this.chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
46 }
47
48 function purify(rawText) {
49 if (!rawText) { return false; }
50 var cleanText = rawText.toLowerCase();
51 cleanText = cleanText.replace(/\s+/g,' ');
52 cleanText = cleanText.replace(/[^a-z0-9\s]/g,'');
53 if(cleanText.length == 0 || cleanText.match(/^\s+$/) != null) {
54 return false;
55 }
56 return cleanText
57 }
58
59 function SubstitutionCipher(name, description, algorithm) {
60 this.name = name;
61 this.description = description;
62 this.substitute = substitute;
63 this.algorithm = algorithm;
64 }
65 SubstitutionCipher.prototype = new Cipher;
66
67 function substitute(baseChar, shiftIdx, action) {
68 if (baseChar == ' ') { return baseChar; }
69 if(action) {
70 var shiftSum = shiftIdx + this.chars.indexOf(baseChar);
71 return (this.chars.charAt((shiftSum < this.chars.length) ?
72 shiftSum : (shiftSum % this.chars.length)));
73 }
74 else {
75 var shiftDiff = this.chars.indexOf(baseChar) - shiftIdx;
76 return (this.chars.charAt((shiftDiff < 0) ?
77 shiftDiff + this.chars.length : shiftDiff));
78 }
79 }
80
81 function caesarAlgorithm (data, action) {
82 data = this.purify(data);
83 if(!data) {
84 alert('Nieprawidłowy tekst dla: ' + (action ? 'cipher.' : 'decipher.'));
85 return false;
86 }
87 var shiftIdx =
88 (NN ? refSlide("caesar").document.forms[0].Shift.selectedIndex : document.forms[1].Shift.selectedIndex);
89 var cipherData = '';
90 for (var i = 0; i < data.length; i++) {
91 cipherData += this.substitute(data.charAt(i), shiftIdx, action);
92 }
93 return cipherData;
94 }
95
96 function vigenereAlgorithm (data, action) {
97 data = this.purify(data);
Przykład 9.1. index.html (ciąg dalszy)
98 if(!data) {
99 alert('Nieprawidłowy tekst dla: ' + (action ? 'cipher.' : 'decipher.'));
100 return false;
101 }
102 var keyword = this.purify((NN ?
103 refSlide("vigenere").document.forms[0].KeyWord.value :
104 document.forms[2].KeyWord.value));
105 if(!keyword || keyword.match(/\^s+$/) != null) {
106 alert('Nieprawidłowe słowo kluczowe dla: ' +
107 (action ? 'ciphering.' : 'deciphering.'));
108 return false;
109 }
110 keyword = keyword.replace(/\s+/g, '');
111 var keywordIdx = 0;
112 var cipherData = '';
113 for (var i = 0; i < data.length; i++) {
114 shiftIdx = this.chars.indexOf(keyword.charAt(keywordIdx));
115 cipherData += this.substitute(data.charAt(i), shiftIdx, action);
116 keywordIdx = (keywordIdx == keyword.length - 1 ? 0 : keywordIdx + 1);
117 }
118 return cipherData;
119 }
120
121 var cipherArray = [
122 new SubstitutionCipher("caesar", caesar, caesarAlgorithm),
123 new SubstitutionCipher("vigenere", vigenere, vigenereAlgorithm)
124 ];
125
126 function showCipher(name) {
127 hideSlide(curCipher);
128 showSlide(name);
129 curCipher = name;
130 }
131
132 function routeCipher(cipherIdx, data, action) {
133 var response = cipherArray[cipherIdx].algorithm(data, action);
134 if(response) {
135 document.forms[0].Data.value = response;
136 }
137 }
138
139 //-->
140 </SCRIPT>
141 </HEAD>
142 <BODY BGCOLOR=#FFFFFF>
143
144 <DIV>
145 <TABLE BORDER=0>
146 <TR>
147 <TD ALIGN=CENTER COLSPAN=3>
148 <IMG SRC="images/cipher.jpg">
149 </TD>
150 </TR>
151 <TR>
152 <TD VALIGN=TOP WIDTH=350>
153 <FORM>
154 <SELECT NAME="Ciphers"
155 onChange="showCipher(this.options[this.selectedIndex].value);">
156 <OPTION VALUE="caesar">Szyfr Cezara
157 <OPTION VALUE="vigenere">Szyfr VigenÚre
158 </SELECT>
159 </TD>
160 <TD ALIGN=CENTER>
Przykład 9.1. index.html (dokończenie)
161 <TEXTAREA NAME="Data" ROWS="15" COLS="40" WRAP="PHYSICAL"></TEXTAREA>
162 <BR><BR>
163 <INPUT TYPE=BUTTON VALUE="Zaszyfruj"
164 onClick="routeCipher(this.form.Ciphers.selectedIndex,
165 this.form.Data.value, true);">
166 <INPUT TYPE=BUTTON VALUE="Odszyfruj"
167 onClick="routeCipher(this.form.Ciphers.selectedIndex,
168 this.form.Data.value, false);">
169 <INPUT TYPE=BUTTON VALUE=" Wyzeruj "
170 onClick="this.form.Data.value='';">
171 </FORM>
172 </TD>
173 </TR>
174 </TABLE>
175 </DIV>
176
177 <SCRIPT LANGUAGE="JavaScript1.2">
178 <!--
179 document.forms[0].Ciphers.selectedIndex = 0;
180 genLayer("caesar", 50, 125, 350, 200, showName, caesar);
181 genLayer("vigenere", 50, 125, 350, 200, hideName, vigenere);
182 //-->
183 </SCRIPT>
184 </BODY>
185 </HTML>
Najpierw interpretowany jest kod pliku źródłowego JavaScriptu dhtml.js. Kod z tego pliku używa DHTML do ustawienia warstw i wygenerowania w biegu list wyboru. Niedługo zajmiemy się tym tematem. Dalej interesujący kod znajdziemy w wierszach 14-39. Zmienne caesar i vigenere jako wartości otrzymują tekst HTML. Każda z nich, jak można zgadnąć, definiuje warstwę interfejsu szyfru. Wszystko jest statyczne poza jednym wywołaniem funkcji genSelect() w wartości zmiennej caesar:
genSelect('Shift', 35, 0, 0)
Tworzona jest lista Shift, która zawiera liczby od 0 do 35, przy czym początkowo zaznaczona jest opcja 0. Wartościami atrybutów VALUE i TEXT stają się kolejne liczby. Jest to kod żywcem wzięty z rozdziału 5. Jeśli czytelnik zrobił sobie bibliotekę JavaScriptu, jak to sugerowano w rozdziale 6., zapewne ma tam ów kod. Funkcja ta zdefiniowana jest na końcu pliku dhtml.js.
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. 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: 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:
function purify(rawText) {
if (!rawText) { return false; }
var cleanText = rawText.toLowerCase();
cleanText = cleanText.replace(/\s+/g,' ');
cleanText = cleanText.replace(/[^a-z0-9\s]/g,'');
if(cleanText.length == 0 || cleanText.match(/^\s+$/) != null) {
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: 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. |
|
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:
Każdy z nich ma nazwę i opis.
Każdy używa ogólnej metody służącej do podstawiania znaków - zarówno w ramach szyfrowania, jak i deszyfrowania.
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.
Każdy obiekt SubstitutionCipher jest jednocześnie obiektem Cipher.
Nadanie nazwy i opisu jest proste. Wystarczy po prostu przekazać do konstruktora dwa napisy. Jako opisów użyjemy zmiennych caesar i vigenere, ustawionych wcześniej w kodzie HTML. Tak oto poradziliśmy sobie z założeniem pierwszym. Co teraz ze zdefiniowaniem ogólnej metody podstawiania? Ma ona umożliwiać podstawianie jednych znaków za inne. No właśnie - każde wywołanie metody zwróci jeden znak, który będzie zastępował inny znak.
Podstawowa metoda podstawiania
Każdy obiekt SubstitutionCipher używa tej samej metody do zamiany pojedynczego znaku chars na inny. Pokazana poniżej funkcja substitute() zdefiniowana została jako metoda dla każdego wystąpienia obiektu SubstitutionCipher:
function substitute(baseChar, shiftIdx, action) {
if (baseChar == ' ') { return baseChar; }
if(action) {
var shiftSum = shiftIdx + this.chars.indexOf(baseChar);
return (this.chars.charAt((shiftSum < this.chars.length) ?
shiftSum : (shiftSum % this.chars.length)));
}
else {
var shiftDiff = this.chars.indexOf(baseChar) - shiftIdx;
return (this.chars.charAt((shiftDiff < 0) ?
shiftDiff + this.chars.length : shiftDiff));
}
}
Metoda ta oczekuje trzech argumentów. baseChar to znak, który będzie zastępowany, shiftIdx to liczba całkowita mówiąca, o ile należy przesunąć znaki, action to wartość logiczna informująca, czy przekazany znak baseChar należy traktować jako część tekstu otwartego, czy tekstu zaszyfrowanego. Aby spacje nie były na nic zamieniane, w pierwszym wierszu przekazany znak jest zwracany bez zmian, jeśli jest on spacją. W przeciwnym wypadku metoda użyje parametru action, aby zdecydować, jak traktować przesunięcie. Jeśli action ma wartość true, realizowane jest szyfrowanie; w przeciwnym wypadku zachodzi deszyfrowanie.
Pamiętajmy, że chars zawiera napis wszystkich znaków podlegających przetwarzaniu. Algorytm szyfrujący po prostu określa indeks znaku baseChar w chars, a następnie wybiera znak o indeksie powiększonym o zadane przesunięcie, czyli indeks baseChar plus shiftIdx.
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. chars.charAt(9) to litera j. To wyjaśnia, dlaczego w końcu zwracamy taką dziwną wartość:
return (this.chars.charAt((shiftSum < this.chars.length) ?
shiftSum : (shiftSum % this.chars.length)));
W tym wypadku funkcja substitute() zwróci chars.charAt(shiftSum) lub chars.charAt (shiftSum % this.chars.length), zależnie od wartości shiftSum i długości zmiennej chars. A co ze słowem kluczowym this? Być może ktoś się zastanawia, co ono tutaj robi. Pamiętajmy, że substitute() nie jest zwykłą funkcją, ale metodą obiektu SubstitutionCipher. Użycie this w metodzie odnosić się będzie do zmiennej obiektowej. Jako że SubstitutionCipher dziedziczy wszystkie właściwości Cipher, nowa zmienna zawierać też będzie właściwość chars.
Podobna procedura obowiązuje w przypadku algorytmu deszyfrującego. Jedyna zmiana polega na tym, że aby uzyskać w chars odpowiedni znak, odejmujemy shiftIdx. W tym wypadku shiftDiff uzyskuje wartość różnicy indeksu znaku baseChar i wartości shiftIdx:
var shiftDiff = this.chars.indexOf(baseChar) - shiftIdx;
Znów rzecz jest dość prosta. Jeśli jednak shiftDiff jest mniejsza od 0, mamy taki sam problem, jak wtedy, gdy shiftSum była większa od chars.lenfth-1. Rozwiązaniem jest dodanie do shiftDiff wartości chars.length. Tak... dodanie. shiftDiff jest ujemna, zatem dodanie długości tablicy chars da w wyniku liczbę mniejszą od tej długości, której będziemy mogli użyć jako potrzebnego nam indeksu. W poniższym fragmencie kodu podejmowana jest decyzja, czy funkcja substitute() jako indeksu użyć ma wartości shiftDiff, czy shiftDiff + chars. ength:
return (this.chars.charAt((shiftDiff < 0) ?
shiftDiff + this.chars.length : shiftDiff));
Różne podstawienia do różnych szyfrów
Sprawdziliśmy właśnie, co mają wspólnego wszystkie obiekty SubstitutionCipher - metodę substitute(). Teraz przyjrzyjmy się, co je różni. Konstruktor oczekuje argumentu o nazwie algorithm. Argument ten nie jest napisem, wartością logiczną, liczbą czy nawet obiektem. Jest on odwołaniem do funkcji, która będzie użyta do wywołania metody substitute().
W przypadku szyfru Cezara przekazujemy odwołanie do funkcji caesarAlgorithm(), tymczasem w przypadku szyfru Vigenere - odwołanie do funkcji vigenereAlgorithm(). Przyjrzyjmy się teraz obu sytuacjom.
Algorytm szyfru Cezara
Spośród dwóch omawianych szyfrów algorytm szyfru Cezara jest prostszy. Odpowiedni kod zawierają wiersze 81-94:
function caesarAlgorithm (data, action) {
data = this.purify(data);
if(!data) {
alert('Nieprawidłowy tekst dla: ' + (action ? 'cipher.' : 'decipher.'));
return false;
}
var shiftIdx =
(NN ? refSlide("caesar").document.forms[0].Shift.selectedIndex : document.forms[1].Shift.selectedIndex);
var cipherData = '';
for (var i = 0; i < data.length; i++) {
cipherData += this.substitute(data.charAt(i), shiftIdx, action);
}
return cipherData;
}
Pierwszych kilka wierszy formatuje dane, a następnie sprawdza się, czy zostały jeszcze jakieś znaki do przetwarzania. Dane z parametru data formatowane są przez wywołanie funkcji purify(). O ile tylko purify() nie zwróci wartości false, przetwarzanie trwa dalej. Szczegóły dotyczące samej funkcji purify() i zwracanych przez nią wartości opisano wcześniej.
Następnie należy określić, o ile znaków dane mają być przesuwane. Tym razem jest to proste - otrzymujemy wartość z listy wyboru z warstwy caesar. Nie wspomniano jeszcze o tym, ale możemy przeskoczyć do przodu, do wierszy 180-181, i zobaczyć, jak warstwy są tworzone. Jeśli jednak chodzi o odwoływania się do elementów formularza z warstw, DOM Netscape Navigatora i DOM Internet Explorera się różnią. Lista wyboru nazywa się Shift.
W Netscape Navigatorze odwołanie wygląda tak:
document.layers['caesar'].document.forms[0].Shift.selectedIndex
W Internet Explorerze wygląda ono następująco:
document.forms[1].Shift.selectedIndex
Zmienna shiftIdx rozwiązuje tę różnicę przy pomocy zmiennej globalnej NN, która pozwala określić typ używanej przeglądarki. Wywołanie refSlide() w wierszu 88 jest wygodnym sposobem odwołania się do document.layers["caesar"]. Teraz, kiedy wartość shiftIdx została określona, funkcja caesarAlgorithm() iteracyjnie sprawdza dane data.length razy, każdorazowo wywołując funkcję substitute() i łącząc uzyskaną z niej wartość z początkowo pustą zmienną cipherData. Każdorazowo przekazywany jest też parametr action, który pozwala zdecydować, czy chodzi o zaszyfrowanie danych, czy ich odszyfrowanie. Kiedy skończy się iteracja, caesarAlgorithm() zwraca cipherData, która to zmienna zawiera teraz przekształcony napis.
Algorytm szyfru Vigenere
Prostszy algorytm szyfrowania już objaśniliśmy, teraz zabierzmy się za vigenereAlgorithm(). Podstawowa różnica polega na tym, że argument shiftIdx przekazywany do substitute() w funkcji caesarAlhorithm() miał wartość stałą. Tym razem shiftIdx może się zmieniać przy każdym wywołaniu substitute() (i zwykle się zmienia). Inna różnica polega na tym, że zamiast liczby użytkownik wybiera słowo kluczowe. Oto wiersze 96-119:
function vigenereAlgorithm (data, action) {
data = this.purify(data);
if(!data) {
alert('Nieprawidłowy tekst dla: ' + (action ? 'cipher.' : 'decipher.'));
return false;
}
var keyword = this.purify((NN ?
refSlide("vigenere").document.forms[0].KeyWord.value :
document.forms[2].KeyWord.value));
if(!keyword || keyword.match(/\^s+$/) != null) {
alert('Nieprawidłowe słowo kluczowe dla: ' +
(action ? 'ciphering.' : 'deciphering.'));
return false;
}
keyword = keyword.replace(/\s+/g, '');
var keywordIdx = 0;
var cipherData = '';
for (var i = 0; i < data.length; i++) {
shiftIdx = this.chars.indexOf(keyword.charAt(keywordIdx));
cipherData += this.substitute(data.charAt(i), shiftIdx, action);
keywordIdx = (keywordIdx == keyword.length - 1 ? 0 : keywordIdx + 1);
}
return cipherData;
}
|
|
|
Jak właśnie widać, sięganie do formularzy i ich elementów w warstwach wymaga stosowania w różnych przeglądarkach różnej składni. Obiektowy model dokumentu (DOM) w Netscape Navigatorze różni się od tegoż modelu w Internet Explorerze. Nie pierwszy raz spotykamy się z tym w naszej książce. Tak naprawdę większa część kodu pliku dhtml.js ma na celu jedynie tworzenie i obsługę warstw w obu tych przeglądarkach. Warto wyświadczyć sobie przysługę i upewnić się, kiedy trzeba dostosować się do obu przeglądarek, a kiedy nie. Póki modele DOM nie zostaną ujednolicone, przydatne będą poniższe zasoby. Obiekty DHTML Microsoftu: http://www.microsoft.com/workshop/author/dhtml/reference/objects.asp Podręcznik Netscape do arkuszy stylów i aplikacji JavaScript działających po stronie klienta: http://developer1.netscape.com:80/docs/manuals/communicator/dynhtml/ jss34.htm oraz http:/developer.netscape.com/docs/manuals/js/client/jsref/index.htm |
|
|
Pierwszych pięć wierszy ma taką samą postać, jak w funkcji caesarAlgorithm(). Przeprowadzane jest takie samo formatowanie oraz identyczna kontrola danych. Następnych kilka wierszy postępuje podobnie ze słowem kluczowym. Słowo to pochodzi z pola formularza z warstwy vigenere. Pamiętajmy o zachowaniu zgodności z oboma modelami DOM.
W Netscape Navigatorze odwołanie wygląda tak:
document.layers['vigenere'].document.forms[0].KeyWord.value
W Internet Explorerze to samo odwołanie ma taką oto postać:
document.forms[2].KeyWord.value
Wartość zmiennej keyword określana jest następująco:
var keyword = this.purify((NN ?
refSlide("vigenere").document.forms[0].KeyWord.value :
document.forms[2].KeyWord.value));
Zwróćmy uwagę na ponowne użycie metody purify(). Jest ona wprawdzie przystosowana głównie do przetwarzania tekstu otwartego i tekstu zaszyfrowanego, ale wymagania wobec słowa kluczowego są bardzo podobne. Jako że metoda substitute() umożliwia podstawianie jedynie znaków z napisu chars, słowo kluczowe też musi składać się tylko z takich znaków. Dopuszczalne słowa kluczowe to ludzie, maszyny, init2wnit czy 1lub2lub3. Jednak użycie innych znaków, spoza chars, też jest możliwe. Pamiętajmy, że funkcja purify() usuwa wszystkie znaki nienależące do zakresu a-z ani 0-9, a także zastępuje wszystkie grupy spacji i znaki nowego wiersza pojedynczymi spacjami. O ile użytkownik może jako słowo kluczowe podać 1@@#derft, to purify() i tak przekształci to słowo na 1derft, które już zawiera tylko znaki dopuszczalne. Teraz weźmy pod uwagę słowo kluczowe z białymi znakami - usunięte one zostaną w wierszu 110:
keyword = keyword.replace(/\s+/g, '');
Zasada jest następująca: jeśli w słowie kluczowym znajdzie się choć jeden znak należący do chars, można takiego słowa użyć w funkcji vigenereAlgorithm().
Jak działa shiftIdx
Tekst otwarty lub zaszyfrowany oraz słowo kluczowe zostały już sformatowane. Pozostaje teraz tylko zastąpić wszystkie znaki ich odpowiednikami. Z definicji szyfru Vigenere wynika, że każdy znak tekstu jest szyfrowany lub deszyfrowany zgodnie ze wskaźnikiem kolejnego znaku w słowie kluczowym - i tak dochodzimy do wierszy 111-118:
var keywordIdx = 0;
var cipherData = '';
for (var i = 0; i < data.length; i++) {
shiftIdx = this.chars.indexOf(keyword.charAt(keywordIdx));
cipherData += this.substitute(data.charAt(i), shiftIdx, action);
keywordIdx = (keywordIdx == keyword.length - 1 ? 0 : keywordIdx + 1);
}
return cipherData;
Używając zmiennej keywordIdx zaczynającej swoje działanie od 0, możemy uzyskać indeksy poszczególnych znaków słowa kluczowego następująco:
keyword.charAt(keywordIdx)
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: 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.
|
Techniki języka JavaScript: W JavaScripcie 1.2 możemy zastąpić kod typu var myArray = new Array(1,2,3); jego skróconą wersją: var myArray = [1,2,3]; Można tworzyć też obiekty w biegu, na przykład zamiast function myObj() { this.nazwa="Nowy obiekt"; this.opis = "Obiekt starej daty"; } można napisać: var myObj = {nazwa: "Nowy obiekt", opis: "Obiekt nowej daty"}; Zwróćmy uwagę, że pary nazwa-wartość tak właściwości, jak i metod, oddzielane są od siebie przecinkiem. Składnię taką obsługują zarówno Internet Explorer, jak i Netscape Navigator w wersjach 4.x. Wybór składni zależy od indywidualnych preferencji. |
|
Warto zauważyć, że każde wywołanie konstruktora SubstitutionCipher() przekazuje mu oczekiwane przez niego napisy, nazwę i opis, a także wskaźnik do funkcji, która będzie przypisana właściwości algorithm poszczególnych obiektów. Tak właśnie tworzone są nasze obiekty. Przyjrzyjmy się teraz interfejsowi. Wszystko zachodzi między znacznikami BODY:
<DIV>
<TABLE BORDER=0>
<TR>
<TD ALIGN=CENTER COLSPAN=3>
<IMG SRC="images/cipher.jpg">
</TD>
</TR>
<TR>
<TD VALIGN=TOP WIDTH=350>
<FORM>
<SELECT NAME="Ciphers"
onChange="showCipher(this.options[this.selectedIndex].value);">
<OPTION VALUE="caesar">Szyfr Cezara
<OPTION VALUE="vigenere">Szyfr Vigenére
</SELECT>
</TD>
<TD ALIGN=CENTER>
<TEXTAREA NAME="Data" ROWS="15" COLS="40" WRAP="PHYSICAL"></TEXTAREA>
<BR><BR>
<INPUT TYPE=BUTTON VALUE="Zaszyfruj"
onClick="routeCipher(this.form.Ciphers.selectedIndex,
this.form.Data.value, true);">
<INPUT TYPE=BUTTON VALUE="Odszyfruj"
onClick="routeCipher(this.form.Ciphers.selectedIndex,
this.form.Data.value, false);">
<INPUT TYPE=BUTTON VALUE=" Wyzeruj "
onClick="this.form.Data.value='';">
</FORM>
</TD>
</TR>
</TABLE>
</DIV>
Kod ten tworzy dwuwierszową tabelę. Wiersz górny zawiera grafikę, przy czym COLSPAN znacznika TD ustawiono na 2. Wiersz dolny zawiera dwie komórki z danymi. Lewa zawiera listę wyboru i ma postać:
<SELECT NAME="Ciphers"
onChange="showCipher(this.options[this.selectedIndex].value);">
<OPTION VALUE="caesar">Szyfr Cezara
<OPTION VALUE="vigenere">Szyfr Vigenére
</SELECT>
Lista pozwala określić interfejs, którego szyfr ma być wyświetlany. Jako że w naszej aplikacji mamy tylko dwa szyfry, musi to być jeden z nich. Procedura obsługi zdarzenia onChange wywołuje funkcję showCipher(), przekazując jej wartość wybranej opcji. Sama funkcja jest całkiem krótka, a znajdziesz ją w wierszach 126-130:
function showCipher(name) {
hideSlide(curCipher);
showSlide(name);
curCipher = name;
}
Kod ten może wydać się znany. Coś podobnego widzieliśmy już w rozdziałach 3. i 6. Funkcje hideSlide() i showSlide() znajdziemy w pliku dhtml.js, a dokładny opis mieści się w rozdziale 3.
Zwróćmy uwagę, że komórka danych ma szerokość 350 pikseli. W przeciwieństwie do komórki z listą wyboru, ta wydaje się pustawa. Na szczęście mamy dwie warstwy, które będą ją wypełniały. Tworzące je wywołania znajdziemy w wierszach 180 i 181. Funkcja genLayer(), tworząca warstwy szyfrów, także znajduje się pliku dhtml.js. Też jest to funkcja omawiana już wcześniej i w tym rozdziale nie będziemy się nią zajmowali:
genLayer("caesar", 50, 125, 350, 200, showName, caesar);
genLayer("vigenere", 50, 125, 350, 200, hideName, vigenere);
W ten sposób powstaje tekst pokazywany wraz z każdym z szyfrów, w przypadku szyfru Cezara dodatkowo tworzona jest lista wyboru, a w przypadku szyfru Vigenere - pole tekstowe. Jak już wspomniano, między tymi szyframi możemy wybierać za pomocą listy wyboru na górze strony - ta właśnie lista wyświetli odpowiednią warstwę.
Jeśli chodzi o pozostałą komórkę w dolnym wierszu tabeli, zawiera ona wielowierszowe pole tekstowe oraz trzy przyciski. Oto odpowiedni kod z wierszy 161-170:
<TEXTAREA NAME="Data" ROWS="15" COLS="40" WRAP="PHYSICAL"></TEXTAREA>
<BR><BR>
<INPUT TYPE=BUTTON VALUE="Zaszyfruj"
onClick="routeCipher(this.form.Ciphers.selectedIndex,
this.form.Data.value, true);">
<INPUT TYPE=BUTTON VALUE="Odszyfruj"
onClick="routeCipher(this.form.Ciphers.selectedIndex,
this.form.Data.value, false);">
<INPUT TYPE=BUTTON VALUE=" Wyzeruj "
onClick="this.form.Data.value='';">
Pole tekstowe zawiera tekst otwarty lub zaszyfrowany. Przycisk Zaszyfruj powoduje zaszyfrowanie tekstu z pola, a przycisk Odszyfruj wywołuje odszyfrowanie tego tekstu. Oba te przyciski wywołują tę samą funkcję, routeCipher().Przekazują zawartość pola tekstowego, a wywołania różnią się tylko ostatnim parametrem - o wartości odpowiednio true lub false.
Dobór odpowiedniego szyfru
Wybór odpowiedniego szyfru jest łatwy. Jest to zawsze szyfr odpowiadający indeksowi listy wyboru z górnej części formularza i indeksowi cipherArray. Wynika to zresztą z treści funkcji routeCipher():
function routeCipher(cipherIdx, data, action) {
var response = cipherArray[cipherIdx].algorithm(data, action);
if(response) {
document.forms[0].Data.value = response;
}
}
Funkcja ta oczekuje trzech argumentów. Dwa z nich już omówiliśmy: data to przekazywany tekst, action to owa wartość true lub false. Pierwszy argument - cipherIdx - pochodzi natomiast z document.forms[0].Ciphers.selectedIndex. Musi być to 0 lub 1. Tak czy inaczej wywołana zostanie odpowiednia metoda algorithm() z określonego obiektu SubstitutionCipher z tablicy cipherArray. Jeśli algorithm() zwróci wartość inną niż false, to będzie to tekst wynikowy - zaszyfrowany lub odszyfrowany.
Na koniec
Zapewne łatwo się było domyślić, że kod z wiersza 179:
document.forms[0].Ciphers.selectedIndex = 0;
Po prostu ustawia wybraną opcję górnej listy wyboru na opcję pierwszą. W ten sposób wybrana opcja odpowiada pokazanej warstwie szyfru, nawet jeśli użytkownik przeładuje stronę.
Kierunki rozwoju
O ile ta aplikacja w swojej obecnej postaci nieźle nadaje się do zabawy, następnym krokiem będzie użycie jej do wysyłania poczty elektronicznej. W tym celu wystarczy wykonać trzy proste kroki. Najpierw należy skopiować poniższą funkcję i wkleić ją między znaczniki SCRIPT:
function sendText(data) {
paraWidth = 70;
var iterate = parseInt(data.length / paraWidth);
var border = '\n-------\n';
var breakData = '';
for (var i = 1; i <= iterate; i++) {
breakData += data.substring((i - 1) * paraWidth,
i * paraWidth) + '\r';
}
breakData += data.substring((i - 1) * paraWidth, data.length);
document.CipherMail.Message.value = border + breakData + border;
document.CipherMail.action =
"mailto:someone@somewhere.com\?subject=The Top Secret Message";
return true;
}
W ten sposób tuż przed wysłaniem wiadomości formatuje się jej treść. Polega to na wstawieniu znaków końca wiersza między każdymi znakami o numerach paraWidth. W ten sposób zapewniamy, że wiadomość ta u odbiorcy nie będzie jednym zbyt długim wierszem. Następnie należy dodać drugie pole do formularza, po końcowym znaczniku FORM w dokumencie wstawić następujący kod:
<FORM NAME="CipherMail" ACTION="" METHOD="POST"
ENCTYPE="text/plain" onSubmit="return
sendText(document.forms[0].Data.value);">
<INPUT TYPE=HIDDEN NAME="Message">
<INPUT TYPE=SUBMIT VALUE=" Send ">
</FORM>
Formularz ten, nazwany CipherMail, zawiera jedno pole ukryte (HIDDEN). Ostatnią czynnością jest zmiana sposobu odwołania się do formularza w obu funkcjach, zawierających algorytm szyfru.
Zmieńmy wiersze 87-89:
var shiftIdx = (NN ?
refSlide("caesar").document.forms[0].Shift.selectedIndex :
document.forms[1].Shift.selectedIndex);
na następujące:
var shiftIdx = (NN ?
refSlide("caesar").document.forms[0].Shift.selectedIndex :
document.forms[2].Shift.selectedIndex);
Następnie sprowadź wiersze 102-104:
var keyword = this.purify((NN ?
refSlide("vigenere").document.forms[0].KeyWord.value :
document.forms[2].KeyWord.value));
do poniższej postaci:
var keyword = this.purify((NN ?
refSlide("vigenere").document.forms[0].KeyWord.value :
document.forms[3].KeyWord.value));
Musimy wprowadzić wszystkie te zmiany, gdyż w poprzednim kroku dodaliśmy do hierarchii kolejny formularz. Funkcja sendText() ustala wartość ukrytego pola na wartość tekstu z wielowierszowego pola tekstowego. Następnie sendText() wysyła ten formularz, którego atrybut ACTION ustawiono na mailto:twój-e-mail@twój-serwer-e-mail.com. Na rysunku 9.7 pokazano wygląd przychodzącej wiadomości. Jest to widok mojego konta Hotmail.
P.S. Wszystko to zadziała tylko wtedy, gdy użytkownik ma prawidłowo skonfigurowanego klienta poczty elektronicznej Netscape Navigatora lub Internet Explorera, ale zwykle nie ma z tym problemów.
P.S.2 W pliku \ch09\cipher2.html znajdziemy już gotowe wszystkie poprawki.
Cechy aplikacji:
Prezentowane techniki:
|
10
Elektroniczne życzenia: |
|
Ta aplikacja służy jedynie do zabawy. Użytkownicy będą skłonni spędzić na naszej stronie długie godziny, jeśli umożliwimy im wysyłanie z niej życzeń znajomym i ukochanym; życzeń wypełnionych zabawnymi rysunkami i z najdziwniejszymi tłami. Na rysunku 10.1 pokazano interfejs początkowy.
|
|
Rysunek 10.1. Domyślny ekran elektronicznych życzeń
Po lewej stronie znajduje się formularz, który użytkownicy wypełniają, podając między innymi adres odbiorcy, treść wiadomości i rodzaj życzeń. Klikając przycisk Tło ->, użytkownik może wybrać tło, a klikając przycisk Ikony ->, może wybrać obrazki.
Po prawej stronie pojawia się wynik pracy. Tutaj użytkownik widzi dostępne tła i dostępne ikony. Użytkownik może ikonę wybierać i przeciągać ją nad obszar tła. Wszystkie ikony są włączane do życzeń, co pokazano na rysunku 10.2.
|
|
Rysunek 10.2. Czy poznajesz kogoś na tej fotografii grupowej?
Kiedy życzenia są już gotowe, użytkownik może obejrzeć swoją pracę na bieżącym etapie klikając przycisk Test. Otwiera się wówczas osobne okno pokazujące, jak w danej chwili wygląda wynik jego pracy - a więc rezultat, jaki zobaczy odbiorca to rysunek 10.3.
Kiedy użytkownik uzna wynik swej pracy za zadawalający, wybiera przycisk Wyślij. Wtedy formularz jest wysyłany do czekającego już na dane skryptu na serwerze, tworzącego wiadomość poczty elektronicznej, i zwraca ostateczną stronę z potwierdzeniem i przyciskiem Wyślij (rysunek 10.4). Kiedy przycisk ten zostanie kliknięty, skrypt wysyła wiadomość do odbiorcy, podając adres URL kartki z życzeniami.
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ń.
|
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.
|
Rysunek 10.7. Logika elektronicznych pozdrowień: jak użytkownik otrzymuje wiadomość
Przykład 10.1. index.html
1 <HTML>
2 <HEAD>
3 <TITLE>Cyber Greetings</TITLE>
4 <SCRIPT LANGUAGE="JavaScript1.2">
5 <!--
6
7 var greetings = [
8 'Choose One', 'Family Reunion!',
9 'Get Well Soon','Thinking Of You',
10 'Big Party!', 'Psst... You\'re Invited.',
11 'Happy Birthday!', 'Congratulations!',
12 'We\'re Gonna Miss U', 'Just A Reminder',
13 'Don\'t Forget'
14 ];
15
16 var baseURL = ".";
17
18 //-->
19 </SCRIPT>
20 </HEAD>
21 <FRAMESET COLS="450,*" FRAMEBORDER="2" BORDER="0">
22 <FRAME SRC="front.html" NAME="Front" NORESIZE>
23 <FRAME SRC="back.html" NAME="Back" NORESIZE SCROLLING="NO">
24 </FRAMESET>
25 </HTML>
Plik index.html zawiera tablicę greetings - wiersze 7-14. To właśnie stąd użytkownicy mogą wybierać rodzaj życzeń. baseURL zawiera katalog bazowy aplikacji na serwerze sieciowym. W tym katalogu mamy wszystko: cztery pliki, obrazki oraz katalog na życzenia użytkowników. baseURL zawarty jest nawet w samych życzeniach. Kiedy zmieniamy tę wartość, zmieniamy ją dla całej aplikacji - tak po stronie klienta, jak i serwera.
Po co więc w ogóle deklarować zmienną i tablicę już w tym pliku? Oba pliki zawarte w ramkach potrzebują odpowiednich danych do tworzenia swoich stron podczas ładowania się. Jeśli zmienna greetings zostałaby zdefiniowana w jednym z tych dwóch plików, mogłaby nie zostać załadowana wraz z kodem JavaScript, który z niej korzysta - to samo dotyczy zmiennej baseURL. Dzięki takiej konstrukcji, jaką zrealizowaliśmy, unikamy błędów związanych z różnymi sposobami ładowania aplikacji.
Pozostałe dwa dokumenty
Idea strony przedniej (front) i tylnej (back) wywodzi się bezpośrednio z tradycyjnej pocztówki. Z przodu jest adres i sam tekst (wydaje mi się, że to jest przód), a z tyłu znajduje się jakiś obrazek z plażowiczami. W naszym wypadku back.html zawiera pokazywany obrazek z wybranymi ikonami. Plik ten jest odpowiedzialny za znaczną część wstępnego procesu podczas ładowania dokumentu. font.html ułatwia dalsze działanie po załadowaniu dokumentu, na przykład wpisanie wiadomości oraz wybieranie rodzaju życzeń i ich wysyłanie. Wobec tego rozsądnie będzie najpierw omówić back.html. Tak się szczęśliwie składa, że większość jego kodu już znamy z poprzednich rozdziałów - obejrzymy przykład 10.2.
Przykład 10.2. back.html
1 <HTML>
2 <HEAD>
3 <TITLE>Drag-n-Drop E-mail</TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6
7 .Greeting
8 {
9 font-family: Arial;
10 font-size: 48px;
11 font-weight: bold;
12 }
13
14 //-->
15 </STYLE>
16 <SCRIPT LANGUAGE="JavaScript1.2">
17 <!--
18
19 var NN = (document.layers ? true : false);
20 var hideName = (NN ? 'hide' : 'hidden');
21 var showName = (NN ? 'show' : 'visible');
22 var zIdx = -1;
23
24 var iconNum = 4;
25 var startWdh = 25;
26 var imgIdx = 0;
27 var activate = false;
28 var activeLayer = null;
29
30 var backImgs = [];
31 var icons = [
32 'bear', 'cowprod', 'dragon', 'judo',
33 'robot', 'seniorexec', 'dude', 'juicemoose',
34 'logo1', 'logo2', 'logo3','tree',
Przykład 10.2. back.html (ciąg dalszy)
35 'sun', 'gator', 'tornado', 'cactus'
36 ];
37
38 function genLayout() {
39
40 for (var i = 0; i <= 7; i++) {
41 backImgs[i] = new Image();
42 backImgs[i].src = parent.Front.baseURL +
43 '/images/background' + i + '.jpg';
44 }
45
46 genLayer("Back", 10, 250, backImgs[1].width, backImgs[1].height,
47 showName, '<IMG NAME="background" SRC="' + parent.Front.baseURL +
48 '/images/background0.jpg">');
49
50 for (var j = 0; j < parent.greetings.length; j++) {
51 genLayer("greeting" + j, 50, 275, 500, 100, hideName,
52 '<SPAN CLASS="Greeting">' + parent.greetings[j] + '</SPAN>');
53 }
54
55 for (var i = 0; i < icons.length; i++) {
56 if (i % iconNum == 0) { startWdh = 25; }
57 else { startWdh += 110; }
58 genLayer(icons[i], startWdh, 15, 100, 100, (i < iconNum ? showName :
59 hideName), '<A HREF="javascript: changeAction(\'' + icons[i] +
60 '\',' + (i + 1) + ');">' + '<IMG SRC="' + parent.Front.baseURL +
61 '/images/' + icons[i] + '.gif" BORDER="0"></A>');
62 }
63 startWdh = 25;
64 }
65
66 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
67 if (NN) {
68 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
69 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
70 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + (++zIdx) + '>' +
71 copy + '</LAYER>');
72 }
73 else {
74 document.writeln('<DIV ID="' + sName +
75 '" STYLE="position:absolute; overflow:none; left:' +
76 sLeft + 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' +
77 sHgt + 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) +
78 '">' + copy + '</DIV>'
79 );
80 }
81 }
82
83 function hideSlide(name) {
84 refSlide(name).visibility = hideName;
85 }
86
87 function showSlide(name) {
88 refSlide(name).visibility = showName;
89 }
90
91 function refSlide(name) {
92 if (NN) { return document.layers[name]; }
93 else { return eval('document.all.' + name + '.style'); }
94 }
95
96 function motionListener() {
97 if (NN) {
98 window.captureEvents(Event.MOUSEMOVE);
99 window.onmousemove = grabXY;
Przykład 10.2. back.html (dokończenie)
100 }
101 else {
102 document.onmousemove = grabXY;
103 }
104 }
105
106 function grabXY(ev) {
107 if (activate) {
108 if(NN) {
109 var itemWdh = refSlide(activeLayer).document.images[0].width;
110 var itemHgt = refSlide(activeLayer).document.images[0].height;
111 refSlide(activeLayer).left = ev.pageX - parseInt(itemWdh / 2);
112 refSlide(activeLayer).top = ev.pageY - parseInt(itemHgt / 2);
113 }
114 else {
115 var itemWdh = document.images[imgIdx].width;
116 var itemHgt = document.images[imgIdx].height;
117 refSlide(activeLayer).left = event.x - parseInt(itemWdh / 2);
118 refSlide(activeLayer).top = event.y - parseInt(itemHgt / 2);
119 }
120 }
121 }
122
123 function changeAction(name, MSIERef) {
124 activate = !activate;
125 activeLayer = name;
126 imgIdx = MSIERef;
127 }
128
129 //-->
130 </SCRIPT>
131 </HEAD>
132 <BODY onLoad="motionListener();">
133
134 <SCRIPT LANGUAGE="JavaScript1.2">
135 <!--
136
137 genLayout();
138
139 //-->
140 </SCRIPT>
141
142 </BODY>
143 </HTML>
Zanim nadawca będzie mógł stworzyć pozdrowienia, kilka funkcji musi wygenerować mnóstwo warstw i określić położenie wskaźnika myszy względem dokumentu. Podobne funkcje omawialiśmy już w rozdziałach 3. i 8., zresztą część funkcji pochodzi bezpośrednio z tych rozdziałów. Będziemy odnosić się do nich w miarę omawiania rozdziału. Teraz przyjrzyjmy się najważniejszym zmiennym zadeklarowanym w wierszach 19-36:
var NN = (document.layers ? true : false);
var hideName = (NN ? 'hide' : 'hidden');
var showName = (NN ? 'show' : 'visible');
var zIdx = -1;
var iconNum = 4;
var startWdh = 25;
var imgIdx = 0;
var activate = false;
var activeLayer = null;
var backImgs = [];
var icons = [
'bear', 'cowprod', 'dragon', 'judo',
'robot', 'seniorexec', 'dude', 'juicemoose',
'logo1', 'logo2', 'logo3','tree',
'sun', 'gator', 'tornado', 'cactus'
];
Pierwsze cztery zmienne używane już były w poprzednich skryptach. NN pomaga określić typ używanej przeglądarki, showName i hideName to napisy pokazujące i ukrywające warstwy w sposób zależny od przeglądarki, a zIdx jest liczbą całkowitą używaną do określania współrzędnej z (czyli wysokości) poszczególnych warstw. Zmienna iconNum to liczba całkowita określająca liczbę ikon wyświetlanych na obrazku jednocześnie. Zaczniemy od czterech. startWdh wstępnie pozycjonuje ikony, a wkrótce ją ujrzymy w funkcji genLayout().
Zmienna imgIdx śledzi obrazki. activate to wartość logiczna, decydująca, czy warstwa ma być przenoszona myszką. activeLayer określa, nad którą warstwą użytkownik klika właśnie myszką. Jeśli to nie wystarczy, mamy jeszcze dwie zmienne tablicowe. backImgs początkowo jest pustą tablicą. Wkrótce wstawimy do niej obiekty Image, przy czym każdy z nich będzie zawierał jeden obrazek tła. Obrazki te mają nazwy background0.jpg, background1.jpg, background2.jpg i tak dalej.
Z kolei icons to tablica napisów identyfikujących poprzez nazwy poszczególne ikony. Oznacza to, że każda ikona będzie tworzona na warstwie o nazwie opisanej elementem tablicy icons. Używany obrazek ikony ma też taką samą nazwę. Na przykład warstwa o nazwie bear będzie zawierała obrazek bear.gif. Warto zauważyć, że: wszystkie obrazki ikon są obrazkami GIF z przezroczystym tłem - biały jest kolorem przezroczystym. Jako że zwykle tłem obrazków jest właśnie biel, możemy umieszczać ikony jedna nad drugą i widzieć „aż do dna”, do tła naszej kartki.
Co już wiemy
Jeśli ktoś śledził uważnie poprzednie rozdziały tej książki, ucieszy się zapewne wiadomością, że jego dotychczasowa ciężka praca zostanie nagrodzona. Nowe funkcje tej aplikacji były już wielokrotnie używane, więc nie musimy teraz znów się dokładnie zastanawiać nad ich działaniem. Nieraz już tak się zdarzało we wcześniejszych rozdziałach, ale tym razem mamy wyjątkowy powód do zadowolenia.
W tabeli 10.1 zestawiono w celu przypomnienia funkcje, z którymi mieliśmy już do czynienia.
Tabela 10.1. Funkcja obsługująca warstwy
Funkcja |
Zastosowanie |
Rozdział(y) |
genLayer() |
tworzenie warstw w Netscape Navigatorze i Internet Explorerze |
3, 4, 6, 9, 11 |
hideSlide() |
ukrywanie warstw o zadanej nazwie |
3, 4, 6, 9, 11 |
ShowSlide |
pokazywanie warstw o zadanej nazwie |
3, 4, 6, 9, 11 |
refSlide() |
odwołanie się do warstwy o zadanej nazwie |
3, 4, 6, 9, 11 |
motionListener() |
śledzenie ruchów myszy |
11 |
grabXY() |
określenie współrzędnych x i y elementu |
11 |
|
|
|
Pierwsze cztery funkcje już dokładnie znamy z wcześniejszych rozdziałów. Jeśli ich jeszcze nie rozumiesz, zajrzyj do rozdziału 3. Jednak motionListener() jest nieco zmodyfikowana i warto ją omówić. grabXY() zostanie przedstawiona w rozdziale 11.; ona też została znacząco zmodyfikowana. Oto reszta funkcji, których używamy w tej aplikacji.
Proszę zająć miejsca!
Kiedy aplikacja się ładuje, back.html ciężko pracuje nad wstępnym załadowaniem wszystkich obrazków, stworzeniem i umieszczeniem na swoich miejscach warstw, po czym warstwy te w miarę potrzeb są pokazywane lub ukrywane. Funkcja genLayout() wszystkie te działania koordynuje - znajdziemy ją w wierszach 38-64:
function genLayout() {
for (var i = 0; i <= 7; i++) {
backImgs[i] = new Image();
backImgs[i].src = parent.Front.baseURL +
'/images/background' + i + '.jpg';
}
genLayer("Back", 10, 250, backImgs[1].width, backImgs[1].height,
showName, '<IMG NAME="background" SRC="' + parent.Front.baseURL +
'/images/background0.jpg">');
for (var j = 0; j < parent.greetings.length; j++) {
genLayer("greeting" + j, 50, 275, 500, 100, hideName,
'<SPAN CLASS="Greeting">' + parent.greetings[j] + '</SPAN>');
}
for (var i = 0; i < icons.length; i++) {
if (i % iconNum == 0) { startWdh = 25; }
else { startWdh += 110; }
genLayer(icons[i], startWdh, 15, 100, 100, (i < iconNum ? showName :
hideName), '<A HREF="javascript: changeAction(\'' + icons[i] +
'\',' + (i + 1) + ');">' + '<IMG SRC="' + parent.Front.baseURL +
'/images/' + icons[i] + '.gif" BORDER="0"></A>');
}
startWdh = 25;
}
Funkcja genLayout()najpierw ładuje obrazki tła. Użytkownik zapewne zechce te obrazki obejrzeć, zanim coś wybierze, więc wstępne ich załadowanie jest dobrym pomysłem. Używając backImgs, funkcja tworzy obiekt Image dla każdego elementu i przypisuje mu plik źródłowy, używając top. baseURL (zadeklarowaliśmy taką zmienną wcześniej, w pliku index.html), napisu background, wartości i i napisu .jpg:
for (var i = 0; i <= 7; i++) {
backImgs[i] = new Image();
backImgs[i].src = top.baseURL +
'/images/background' + i + '.jpg';
}
Po załadowaniu wszystkich obrazków przystępujemy do załadowania domyślnego tła. Można wybrać dowolne z nich, ale dla ułatwienia warto zdecydować się na background0.jpg, po czym wstawić je do warstwy Back. Szerokość i wysokość tej warstwy są ustawiane na odpowiednie wymiary obrazka. Staje się to ważne później, przy pozycjonowaniu ikon:
genLayer("Back", 10, 250, backImgs[1].width, backImgs[1].height,
showName, '<IMG NAME="background" SRC="' + parent.Front.baseURL +
'/images/background0.jpg">');
Teraz warstwa tła i domyślny obrazek tła są już na swoich miejscach. Następnie, trzeba, umożliwić wybranie rodzaju życzeń, chodzi po prostu o duże napisy opisujące wybrany rodzaj życzeń. Wszystkie możliwe rodzaje znajdują się w tablicy greetings, zadeklarowanej w pliku index. html. Oto wiersze 50-53:
for (var j = 0; j < parent.greetings.length; j++) {
genLayer("greeting" + j, 50, 275, 500, 100, hideName,
'<SPAN CLASS="Greeting">' + parent.greetings[j] + '</SPAN>');
}
Znaczy to, że będzie parent.greetings.length pozdrowień, a wszystkie będą miały takie samo położenie: 50 i 275. Użytkownik nie może ich przemieszczać, ale wszystkie one umieszczone są w lewym górnym rogu obszaru wyświetlania. Każde pozdrowienie ma swoją własną warstwę, która zawiera szereg znaczników SPAN, używających definicji klasy arkusza stylów o nazwie Greeting, co widać na górze dokumentu.
Kiedy na miejscu są już tło i pozdrowienie, pozostaje tylko porozmieszczać ikony - wiersze 55-62:
for (var i = 0; i < icons.length; i++) {
if (i % iconNum == 0) { startWdh = 25; }
else { startWdh += 110; }
genLayer(icons[i], startWdh, 15, 100, 100, (i < iconNum ? showName :
hideName), '<A HREF="javascript: changeAction(\'' + icons[i] +
'\',' + (i + 1) + ');">' + '<IMG SRC="' + parent.Front.baseURL +
'/images/' + icons[i] + '.gif" BORDER="0"></A>');
}
Każdy element tablicy icons będzie reprezentował warstwę ikony. Zmienna iconNum decyduje o tym, że jednorazowo pokazywane będą cztery ikony. Każdy obrazek będzie miał 100 pikseli szerokości, wysokość będzie się zmieniała. Zmienna startWdh zaczyna wartość od 25. Wartość określać będzie położenie lewego piksela tworzonych warstw. Wybrałem z góry odstępy szerokości 10 pikseli między poszczególnymi ikonami. Zatem poczynając od 25 pikseli w prawo od lewego marginesu - nowe ikony są układane co 110 pikseli (100 to szerokość ikony, 10 -odstęp między nimi). Kiedy zostanie utworzone iconNum ikon, znów proces zaczyna się od tego samego odstępu 25 pikseli. Umożliwiają to dwie techniki programistyczne: jedna to instrukcja if-else, wywoływana przed utworzeniem w genLayer() kolejnej warstwy, druga to użycie operatora modulo (%). Przyjrzyjmy się temu dokładniej:
if (i % iconNum == 0) { startWdh = 25; }
else { startWdh += 110; }
Kiedy wykonywana jest pętla for, wartość i stale wzrasta - za każdym razem, kiedy zmienna ta jest mnożona przez iconNum (u nas 4), należy zacząć nową grupę ikon z pierwszą ikoną umieszczoną 25 pikseli od lewego marginesu. startWdh otrzymuje wartość 25. Zatem następną grupę ikon zaczyna się, kiedy i ma wartości kolejno 4, 8, 12, 16 i 20. Jeśli i jest dowolną inną wartością, oznacza to, że następna ikona ma być przesunięta względem poprzedniej o 110 pikseli - dlatego właśnie startWdh zwiększamy o 110. Operator modulo zwraca liczbę całkowitą będącą resztą z dzielenia. Jeśli resztą jest 0, i jest wielokrotnością iconNum.
Określenie położenia lewego brzegu warstwy jest trudnym zadaniem. Teraz genLayout() może skończyć realizację swoich zadań, tworząc warstwę dla każdej ikony dzięki odpowiedniemu wywołaniu genLayer():
genLayer(icons[i], startWdh, 15, 100, 100, (i < iconNum ? showName :
hideName), '<A HREF="javascript: changeAction(\'' + icons[i] +
'\',' + (i + 1) + ');">' + '<IMG SRC="' + parent.Front.baseURL +
'/images/' + icons[i] + '.gif" BORDER="0"></A>');
Warstwa każdej ikony zawiera pojedynczy znacznik IMG otoczony znacznikami zakładki (anchor, A). Zwróćmy uwagę, że drugi i trzeci argument, przekazywany do genLayer(), są wartościami lewego i górnego brzegu warstwy. startWdh zawsze oznacza brzeg lewy, brzeg górny stale jest ustawiony na 15 pikseli. Szósty parametr określa, czy ikona ma być widoczna, czy ukryta. Domyślnie pokazywany jest tylko pierwszy zestaw utworzonych ikon. W naszym wypadku są to pierwsze cztery warstwy. Wobec tego operator warunkowy szóstego parametru jest taki, że jeśli i jest mniejsze od iconNum (czyli u nas ma wartość 0, 1, 2 lub 3), warstwa ma być widoczna, natomiast wszystkie pozostałe warstwy mają być ukryte. Jeśli ikona ma być widoczna, przekazuje się zmienną showName; w przeciwnym wypadku przekazuje się hideName.
W tym miejscu zostało nam jeszcze omówienie natury znacznika zakładki. Zastanówmy się: kiedy tylko użytkownik przesuwa wskaźnik myszki nad ikoną i klika tam po raz pierwszy, chce oczywiście „wziąć” ikonę i ją gdzieś przeciągnąć. Aby było to możliwe, w protokole javascript: w atrybucie HREF wywołujemy funkcję changeAction(), którą zresztą zaraz omówimy. Wszystkie atrybuty HREF powodują to samo wywołanie, ale łącze każdej ikony musi przekazać funkcji changeAction() dane o sobie.
Najpierw changeAction() musi znać nazwę ikony, która ma być obsługiwana. To łatwe - przekazujemy icons[i], gdzie zapisano odpowiedni tekst. Następnie trzeba przekazać liczbę całkowitą odpowiadającą ikonie w obiektowym modelu dokumentu Internet Explorera. Oznacza to, że aby można było zastosować technikę „przeciągnij i upuść”, Internet Explorer będzie musiał wiedzieć, o jaki obrazek chodzi. Pamiętajmy, że pierwszy obrazek na stronie był tłem -document. images[0]. Pierwsza ikona to document.images[1], chodzi o wszystkie pozostałe ikony to document.images[i+1], dlatego właśnie przekazujemy wartość (i+1). Stanie się to jaśniejsze, kiedy zajrzymy do changeAction() i grabXY().
Sporo tego wyjaśniania, jak na 27 wierszy tej funkcji. Ustawiamy startWdh na 25 i idziemy dalej.
Śledzenie położenia myszy
Funkcja motionListener() umożliwia JavaScriptowi przechwytywać ruchy myszki w ramach obsługi zdarzenia onmousemove. Łatwo taką obsługę ustawić, a jedyna różnica między przeglądarkami polega na tym, że w Netscape Navigatorze należy wywołać metodę captureEvents() obsługującą okno, tymczasem w Internet Explorerze obsługiwany jest dokument. Oto wiersze 96-104:
function motionListener() {
if (NN) {
window.captureEvents(Event.MOUSEMOVE);
window.onmousemove = grabXY;
}
else {
document.onmousemove = grabXY;
}
}
Kiedy tylko użytkownik przesunie myszkę, wywoływana jest funkcja grabXY(). Pamiętajmy, że w tym wypadku w wywołaniu nawiasy są zbędne. onmousemove po prostu używa funkcji grabXY(), ale nie wywołuje jej standardowo. Zdarzenie onLoad wywołuje funkcję motionListener() - wiersz 132. Funkcja ta wywoływana jest tylko raz, więc mysz jest śledzona przez cały czas działania aplikacji.
Wywoływanie wszystkich ikon
Kiedy użytkownik klika ikonę, wywołanie changeAction() tę ikonę ożywia, umożliwiając jej przenoszenie. Oto szczegóły z wierszy 123-127:
function changeAction(name, MSIERef) {
activate = !activate;
activeLayer = name;
imgIdx = MSIERef;
}
Pamiętamy zmienne activate i activeLayer? Zostały zadeklarowane dawno temu na początku dokumentu. activate początkowo miała wartość false, co oznacza nieprzenoszenie warstw przy ruchu myszki - okaże się to już wkrótce, podczas omawiania funkcji grabXY(). Za pierwszym razem, przy wywołaniu changeAction(), activate uzyskuje wartość true, uruchamiając przez to grabXY(). Warstwa będzie teraz przenoszona tam, gdzie przeniesiony zostanie wskaźnik myszy. Jedyny sposób przerwania tego to powtórne kliknięcie myszką, kiedy to zmienna activate znów zmienia swój stan na przeciwny, czyli tym razem false. W ten sposób przeciąganie się skończyło.
Warto przypomnieć, że changeAction() miała otrzymywać dwa parametry. Jeden z nich to nazwa warstwy, na której wszystko ma się odbywać, przypisywana zmiennej name. Drugi parametr to indeks obrazka, pozwalający się odwołać do tablicy document.images w Internet Explorerze. Wartość ta przypisywana jest do właściwości MSIERef.activeLayer, co jest z kolei używane do ustawienia name, natomiast imgIdx uzyskuje wartość MSIERef. Tak właśnie musimy ikony przeciągać niezależnie od użytej przeglądarki.
Przenoszenie ikon
Funkcja motionListener() skonstruowana jest tak, że przy każdym ruchu wskaźnika myszy wywoływana jest grabXY() - oto wiersze 106-121:
function grabXY(ev) {
if (activate) {
if(NN) {
var itemWdh = refSlide(activeLayer).document.images[0].width;
var itemHgt = refSlide(activeLayer).document.images[0].height;
refSlide(activeLayer).left = ev.pageX - parseInt(itemWdh / 2);
refSlide(activeLayer).top = ev.pageY - parseInt(itemHgt / 2);
}
else {
var itemWdh = document.images[imgIdx].width;
var itemHgt = document.images[imgIdx].height;
refSlide(activeLayer).left = event.x - parseInt(itemWdh / 2);
refSlide(activeLayer).top = event.y - parseInt(itemHgt / 2);
}
}
}
Wspomniano wyżej o wywoływaniu funkcji grabXY(), ale jej kod jest wykonywany jedynie wtedy, gdy zmienna activate ma wartość true. Przy pierwszym kliknięciu łącza ikony taka właśnie wartość jest ustawiana i do głosu dochodzi zagnieżdżona instrukcja if-else. Jeśli użytkownik używa przeglądarki Netscape Navigator, wykonywany jest blok if, w przeciwnym wypadku mamy blok else. W obu wypadkach realizowana jest ta sama funkcja, ale za każdym razem w innej przeglądarce.
W obu blokach kodu deklaruje się zmienne lokalne itemWdh i itemHgt. Będą one określały położenie lewego i górnego rogu klikniętej ikony. Dlaczego właściwie nie ustawić ich wartości na współrzędne bieżącego położenia myszki? W końcu przecież w ten sposób mamy śledzić położenie kursora.
Tak, można byłoby tak zrobić, ale jest tu pewna pułapka. Jeśli tak postąpimy, będzie to znaczyło, że kursor myszki znajdzie się w lewym górnym rogu ikony podczas tego przeciągania. Wygląda to dość dziwnie, a co gorsza, użytkownik może poruszać myszką na tyle szybko, aby „uciec przeciąganiu” i kliknąć, kiedy myszka nie jest nad ikoną. Użytkownik może kliknąć kilka razy, aby ikonę zwolnić.
Rozwiązaniem jest takie umieszczenie ikony, aby kursor myszki był zawsze pośrodku tej ikony. Niezależnie od stosowanej przeglądarki - itemWdh i itemHgt reprezentują odpowiednio szerokość i wysokość obrazka ikony klikniętej przez użytkownika. Wartości te musimy jednak pobierać różnie, w zależności od stosowanej przeglądarki. W Netscape Navigatorze wygląda to tak:
var itemWdh = refSlide(activeLayer).document.images[0].width;
var itemHgt = refSlide(activeLayer).document.images[0].height;
Aby dostać się do obrazka, musimy odwołać się do odpowiedniej warstwy, następnie dokumentu, w końcu do images[0] (na warstwie jest tylko jeden obrazek). Inaczej jest w Internet Explorerze:
var itemWdh = document.images[imgIdx].width;
var itemHgt = document.images[imgIdx].height;
W drugim przypadku nie ma tablicy warstw. Do obrazków możemy sięgać bezpośrednio przez tablicę images, jednak musimy wiedzieć, o który obrazek nam chodzi - stąd użycie imgIdx. Pamiętajmy, że ustawiamy tę zmienną przy każdym wywołaniu changeAction(). W razie jakiś wątpliwości przejrzyjmy jeszcze raz tę funkcję.
Mamy teraz szerokość i wysokość potrzebnego obrazka, więc wystarczy jedynie proste wyliczenie matematyczne umożliwiające ustawienie kursora myszy nad środkiem obrazka.
Teraz pora na przykład. Załóżmy, że użytkownik kliknął ikonę, którą chce przeciągać. W momencie kliknięcia kursor myszy był 100 pikseli w prawo od lewego brzegu dokumentu i 100 pikseli poniżej od górnej krawędzi dokumentu (nie ekranu). Załóżmy, że ikona jest szeroka na 100 pikseli i na 150 pikseli wysoka. Jeśli ustawimy właściwości left i top ikony na: 100, 100, lewy górny róg ikony zostanie umieszczony dokładnie pod kursorem myszki. Dobrze, ale kursor miał być pośrodku ikony.
Aby uzyskać taki efekt, od położenia lewego brzegu należy odjąć połowę szerokości i od górnego brzegu odjąć połowę wysokości. Wiemy, że itemWdh równe jest 100, itemHgt równe jest 150. Oto sposób wyliczania nowych pozycji:
Ikona.left = kursor myszki w poziomie (x) - (100/2) = 100-(50) = 50
Ikona.top = kursor myszki w pionie (y) - (150/2) = 100-(75) = 25
Tak więc left i top uzyskują ostatecznie wartości 50 i 25, a nie 100 i 100. W ten sposób umieszczamy strzałkę kursora myszy pośrodku ikony. Aby się upewnić, że z dzielenia przez dwa uzyskamy liczbę całkowitą, użyjemy funkcji parseInt(), zwracającą część całkowitą swojego argumentu. Spójrzmy jeszcze raz na kod funkcji grabXY() - oto implementacja w Netscape Navigatorze:
refSlide(activeLayer).left = ev.pageX - parseInt(itemWdh / 2);
refSlide(activeLayer).top = ev.pageY - parseInt(itemHgt / 2);
W modelu zdarzeń w Netscape Navigatorze używa się obiektów zdarzeń tworzonych w biegu, czego odzwierciedleniem jest tutaj zmienna lokalna ev. Właściwości pageX i pageY tego obiektu zdarzenia zawierają wartości współrzędnych x i y aktywnej warstwy. Z kolei w Internet Explorerze istnieją globalne obiekty zdarzeń, z których można sięgać do współrzędnych:
refSlide(activeLayer).left = event.x - parseInt(itemWdh / 2);
refSlide(activeLayer).top = event.y - parseInt(itemHgt / 2);
Odpowiednie wartości znajdują się we właściwościach x i y. Teraz można już ikonę przeciągać i kłaść na kartce z życzeniami.
Kiedy dokumenty już się załadują
Tak naprawdę zaczynamy działać w pliku front.html, którego kod pokazano w przykładzie 10.3. Pierwszych kilkanaście wierszy zawiera właściwości arkusza stylów, następnych kilkaset to zmienne i funkcje JavaScriptu odpowiedzialne za przechwytywanie informacji umożliwiających tworzenie, testowanie i w końcu wysyłanie życzeń.
Przykład 10.3. front.html
1 <HTML>
2 <HEAD>
3 <TITLE></TITLE>
4 <STYLE TYPE="text/css">
5 <!--
6
7 TD
8 {
9 font-family: Arial;
10 }
11
12 .Front
13 {
14 position: absolute;
15 left: 25;
16 top: 25;
17 width: 325;
18 border: 1px solid;
19 background: #ffffee;
20 }
21
22 //-->
23 </STYLE>
24 <SCRIPT LANGUAGE="JavaScript1.2">
25 <!--
26
27 var curGreet = iconIdx = 0;
28 var backgroundIdx = 0;
29 var baseURL = ".";
30 var bRef = parent.Back;
31
32 function showGreeting(selIdx) {
33 if (selIdx > 0) {
34 bRef.hideSlide("greeting" + curGreet);
35 bRef.showSlide("greeting" + selIdx);
36 curGreet = selIdx;
37 }
38 }
39
40 function nextBackground() {
41 backgroundIdx = (backgroundIdx == bRef.backImgs.length - 1 ?
42 backgroundIdx = 0 : backgroundIdx + 1);
43 if(document.all) {
44 bRef.document.background.src = bRef.backImgs[backgroundIdx].src;
45 }
46 else {
47 bRef.document.layers["Back"].document.images[0].src =
48 bRef.backImgs[backgroundIdx].src;
49 }
50 }
51
52 function nextIcons() {
53 for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
54 bRef.iconNum; i++) {
Przykład 10.3. front.html (ciąg dalszy)
55 if (i < bRef.icons.length && !onCard(i)) {
56 bRef.hideSlide(bRef.icons[i]);
57 }
58 }
59 iconIdx = (iconIdx >= (bRef.icons.length / bRef.iconNum) - 1 ? 0 :
60 iconIdx + 1);
61 for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
62 bRef.iconNum; i++) {
63 if (i < bRef.icons.length) {
64 bRef.showSlide(bRef.icons[i]);
65 }
66 else { break; }
67 }
68 }
69
70 function resetForm() {
71 if (document.all) {
72 bRef.hideSlide("greeting" +
73 document.EntryForm.Greetings.selectedIndex);
74 document.EntryForm.reset();
75 }
76 else {
77 bRef.hideSlide("greeting" +
78 document.layers["SetupForm"].document.EntryForm.Greetings.selectedIndex);
79 document.layers["SetupForm"].document.EntryForm.reset();
80 }
81 }
82
83 function onCard(iconRef) {
84 var ref = bRef.refSlide(bRef.icons[iconRef]);
85 var ref2 = bRef.refSlide("Back");
86 if(document.all) {
87 if((parseInt(ref.left) >= parseInt(ref2.left)) &&
88 (parseInt(ref.top) >= parseInt(ref2.top)) &&
89 (parseInt(ref.left) + parseInt(ref.width) <= parseInt(ref2.left) +
90 parseInt(ref2.width)) &&
91 (parseInt(ref.top) + parseInt(ref.height) <= parseInt(ref2.top) +
92 parseInt(ref2.height))) {
93 return true;
94 }
95 }
96 else {
97 if((ref.left >= ref2.left) &&
98 (ref.top >= ref2.top) &&
99 (ref.left + ref.document.images[0].width <= ref2.left +
100 ref2.document.images[0].width) &&
101 (ref.top + ref.document.images[0].height <= ref2.top +
102 ref2.document.images[0].height)) {
103 return true;
104 }
105 }
106 ref.left = ((iconRef % bRef.iconNum) * 110) + bRef.startWdh;
107 ref.top = 15;
108 return false;
109 }
110
111 function shipGreeting(fObj) {
112 if (fObj.Recipient.value == "") {
113 alert('Musisz podać adres e-mail!');
114 return false;
115 }
116 else if (fObj.Message.value == "") {
117 alert("Musisz wpisać wiadomość.");
118 return false;
119 }
Przykład 10.3. front.html (ciąg dalszy)
120 else if (fObj.Greetings.selectedIndex == 0) {
121 alert('Musisz wybrać rodzaj życzeń.');
122 return false;
123 }
124
125 fObj.EntireMessage.value = genGreeting(fObj);
126 fObj.UniqueID.value = Math.round(Math.random() * 1000000);
127 fObj.BaseURL.value = baseURL;
128 return true;
129 }
130
131 function testGreeting(fObj) {
132 var msgStr = '<HTML><TITLE>Cyber Greeting Test Page</TITLE>' +
133 genGreeting(fObj) + '<TABLE ALIGN="CENTER"><TR><TD><FORM>' +
134 '<INPUT TYPE=BUTTON VALUE=" OK " onClick="self.close();">' +
135 '</FORM></TD></TR></TABLE></HTML>';
136 newWin = open('', '', 'width=' + (
137 bRef.backImgs[backgroundIdx].width + 50) +
138 ',height=600,scrollbars=yes');
139 with(newWin.document) {
140 open();
141 writeln(msgStr);
142 close();
143 }
144 newWin.focus();
145 }
146
147 function genGreeting(fObj) {
148 var greetingIdx = fObj.Greetings.selectedIndex;
149 var msg = fObj.Message.value;
150
151 msg = msg.replace(/\r+/g, "");
152 msg = msg.replace(/\n+/g, "<BR><BR>");
153
154 var msgStr = '<TABLE BORDER=0><TR><TD COLSPAN=2><FONT FACE=Arial>' +
155 '<H2>Twoje elektroniczne życzenia</H2>Do: ' + fObj.Recipient.value +
156 '<BR><BR></TD></TR>' + '<TR><TD VALIGN=TOP><IMG SRC="' +
157 baseURL + '/images/background' + backgroundIdx + '.jpg">' +
158 '<DIV STYLE="position:relative;left:40;top:-255;' +
159 'font-family:Arial;font-size:48px;font-weight:bold;">' +
160 parent.greetings[greetingIdx] + '</DIV>';
161
162 var iconStr = '';
163 for (var i = 0; i < bRef.icons.length; i++) {
164 if(onCard(i)) {
165 iconStr += '<DIV STYLE="position:absolute;left:' +
166 bRef.refSlide(bRef.icons[i]).left + ';top:' +
167 (parseInt(bRef.refSlide(bRef.icons[i]).top) -
168 (document.all ? 140 : 150)) + ';"><IMG SRC="' +
169 baseURL + '/images/' + bRef.icons[i] + '.gif"></DIV>';
170 }
171 }
172
173 msgStr += iconStr + '</TD></TR><TR><TD WIDTH=' +
174 bRef.backImgs[backgroundIdx].width + '><FONT FACE=Arial>' +
175 msg + '</TD></TR></TABLE>';
176 return msgStr;
177 }
178
179 //-->
180 </SCRIPT>
181
182 </HEAD>
183 <BODY onLoad="resetForm();">
Przykład 10.3. front.html (ciąg dalszy)
184 <DIV ID="SetupForm" CLASS="Front">
185 <FORM NAME="EntryForm"
186 ACTION=http://www.your_domain.com/cgi-bin/greetings/greet.pl"
187 METHOD="POST" TARGET="_top" OnSubmit="return shipGreeting(this);">
188 <INPUT TYPE=HIDDEN NAME="EntireMessage">
189 <INPUT TYPE=HIDDEN NAME="UniqueID">
190 <INPUT TYPE=HIDDEN NAME="BaseURL">
191 <TABLE CELLSPACING="0" CELLPADDING="5" WIDTH="375">
192 <TR>
193 <TD COLSPAN="3"><CENTER><H2>Cyber-życzenia</H2></CENTER></TD>
194 </TR>
195 <TR>
196 <TD HEIGHT="40" VALIGN="TOP">
197 Do:
198 </TD>
199 <TD COLSPAN="2" VALIGN="TOP">
200 <INPUT TYPE=TEXT NAME="Recipient" SIZE="25">
201 </TD>
202 </TR>
203 <TR>
204 <TD HEIGHT="80" VALIGN="TOP">Wiadomość: </TD>
205 <TD COLSPAN="2" VALIGN="TOP">
206 <TEXTAREA ROWS="7" COLS="25" NAME="Message" WRAP="PHYSICAL">
207 </TEXTAREA>
208 </TD>
209 </TR>
210 <TR>
211 <TD>Obrazki:</TD>
212 <TD HEIGHT="40" COLSPAN="2">
213 <INPUT TYPE=BUTTON VALUE=" Ikony - > " onClick="nextIcons();">
214
215 <INPUT TYPE=BUTTON VALUE=" Tła - > "
216 onClick="nextBackground();">
217 </TD>
218 </TR>
219 <TR>
220 <TD>Życzenia:</TD>
221 <TD HEIGHT="40" COLSPAN="2">
222 <SCRIPT LANGUAGE="JavaScript1.2">
223 <!--
224
225 var sel = '<SELECT NAME="Greetings"
226 onChange="showGreeting(this.selectedIndex);">';
227 for (var i = 0; i < parent.greetings.length; i++) {
228 sel += '<OPTION>' + parent.greetings[i];
229 }
230 sel += '</SELECT>';
231 document.writeln(sel);
232
233 //-->
234 </SCRIPT>
235 </TD>
236 </TR>
237 <TR>
238 <TD VALIGN=TOP>Wysyłanie: </TD>
239 <TD HEIGHT="40" ALIGN="CENTER">
240 <INPUT TYPE=BUTTON VALUE=" Test "
241 onClick="testGreeting(this.form);">
242
243 <INPUT TYPE=BUTTON VALUE=" Wyczyść " onClick="resetForm();">
244
245 <INPUT TYPE=SUBMIT VALUE=" Wyślij ">
246 </FORM>
247 </TD>
248 </TR>
Przykład 10.3. front.html (dokończenie)
249 </TABLE>
250 </FORM>
251 </DIV>
252 </BODY>
253 </HTML>
Poznaj zmienne
Choć plik font.html nie zawiera tylu zmiennych, co back.html, to kilka znajdziemy w wierszach 28-30:
var curGreet = iconIdx = 0;
var backgroundIdx = 0;
var bRef = parent.Back;
Zmienna curGreet zawiera indeks listy wyboru rodzaju życzeń. Początkowo ma wartość 0. iconIdx jest zmienną używaną do śledzenia ikon według indeksu i początkowo też ma wartość 0. Ostatnia zmienna to bRef, która jest po prostu odnośnikiem do obiektu skryptu i okna w ramce o nazwie Back. Ułatwi to nam życie.
Techniki języka JavaScript: 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. |
|
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;
}
}
Techniki języka JavaScript: 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) <= parseInt(ref2.left) + parseInt(ref2.width)) && (parseInt(ref.top) + parseInt(ref.height) <= parseInt(ref2.top) + parseInt(ref2.height))) { return true; } } else { if((ref.left >= ref2.left) && (ref.top >= ref2.top) && (ref.left + ref.document.images[0].width <= ref2.left + ref2.document.images[0].width) && (ref.top + ref.document.images[0].height <= ref2.top + ref2.document.images[0].height)) { return true; } } ref.left = ((iconRef % bRef.iconNum) * 110) + bRef.startWdh; ref.top = 15; return false; } Nie jest to najdłuższa funkcja, jaką zdarzyło mi się napisać, ale zastanówmy się, co by się stało, gdyby nie używać w niej zmiennych bRef, ref i ref2 - byłaby dłuższa i trudniejsza do zrozumienia: function onCard(iconRef) { if(document.all) { if((parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).left) >= parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).left)) && (parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).top) >= parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).top)) && (parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).left) + parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).width) <= parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).left) + parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).width)) && - dokończenie na następnej stronie- |
parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).top) + parseInt(parent.Back.refSlide(parent.Back.icons[iconRef]).height))) { return true; } } else { if((parent.Back.refSlide(parent.Back.icons[iconRef]).left >= parent.Back.refSlide(parent.Back.icons[iconRef]).left) && (parent.Back.refSlide(parent.Back.icons[iconRef]).top >= parent.Back.refSlide(parent.Back.icons[iconRef]).top) && (parent.Back.refSlide(parent.Back.icons[iconRef]).left + parent.Back.refSlide(parent.Back.icons[iconRef]).document. images[0].width <= parent.Back.refSlide(parent.Back.icons[iconRef]).left + parent.Back.refSlide(parent.Back.icons[iconRef]).document. images[0].width) && (parent.Back.refSlide(parent.Back.icons[iconRef]).top + parent.Back.refSlide(parent.Back.icons[iconRef]).document. images[0].height <= parent.Back.refSlide(parent.Back.icons[iconRef]).top + parent.Back.refSlide(parent.Back.icons[iconRef]).document. images[0].height)) { return true; } } parent.Back.refSlide(parent.Back.icons[iconRef]).left = ((iconRef % parent.Back.iconNum) * 110) + parent.Back.startWdh; parent.Back.refSlide(parent.Back.icons[iconRef]).top = 15; return false; } |
|
Funkcja showGreeting() spodziewa się jednego parametru, selectIndex z listy wyboru Greetings. O ile tylko selIdx nie jest zerem (co oznacza polecenie wyboru typu życzeń), showGreeting() ukrywa aktualnie widoczną warstwę pozdrowień i wyświetla warstwę związaną z typem życzeń wybranych przez użytkownika. Następnie selIdx staje się bieżącą wartością warstwy widzialnej, co przygotowuje nas do następnego wywołania.
Obrazki po kolei
Aby przejrzeć dostępne obrazki tła, użytkownik po prostu klika przycisk Tła ->, póki nie znajdzie takiego, który mu będzie odpowiadał. Kliknięcie tego przycisku wywołuje funkcję nextBackground() z wiersza 40-50:
function nextBackground() {
backgroundIdx = (backgroundIdx == bRef.backImgs.length - 1 ?
backgroundIdx = 0 : backgroundIdx + 1);
if(document.all) {
bRef.document.background.src = bRef.backImgs[backgroundIdx].src;
}
else {
bRef.document.layers["Back"].document.images[0].src =
bRef.backImgs[backgroundIdx].src;
}
}
Obrazki tła wstępnie ładowane są w wierszach 40-44 pliku back.html. Jako że każdy z tych obrazków ma nazwę typu background0.jpg, background1.jpg, background2.jpg i tak dalej, możemy użyć liczby całkowitej backgroundIdx, którą będziemy łączyć z napisem, i w ten sposób uzyskamy kolejne obrazki. Kiedy dokument jest ładowany, zmienna backgroundIdx jest ustawiana na 0. Przy każdym kliknięciu przycisku Tła -> wartość ta jest zwiększana o 1, aż dojdziemy do ostatniego obrazka. Kiedy backgroundIdx osiąga wartość top.Back.backImgs.length-1, ponownie jest zerowana, dzięki czemu możemy zaczynać od początku.
Teraz przyszedł czas na wykorzystanie tej świeżo uzyskanej wartości do zmiany właściwości src odpowiedniego obiektu Image. Jako że obrazek tła był umieszczony w warstwie w celu dokładniejszego pozycjonowania, musimy różnie odnosić się do modelu DOM Netscape Navigatora i Internet Explorera.
W przypadku Internet Explorera obrazek jest uważany za właściwość obiektu dokumentu:
top.Back.document.background.src
Z kolei w przypadku Netscape Navigatora odwołujemy się do obiektu dokumentu w warstwie. Ponieważ warstwa nazywa się Back, dostanie się do odpowiedniego obiektu Image wygląda tak:
top.Back.document.layers["Back"].document.images[0].src
Kiedy odpowiednia składnia zostanie już określona, możemy ustawić ścieżkę we właściwości src obrazka backImgs, stosując backgroundIdx. Warto dodać, nie po raz pierwszy używamy takiej iteracji. Podobne przykłady znajdziemy w rozdziałach 3. i 8. Teraz użytkownik może już cyklicznie przeglądać obrazki tła - potrzebujemy podobnego rozwiązania dla ikon. Tutaj właśnie użyjemy funkcji nextIcons() z wierszy 52-68:
function nextIcons() {
for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
bRef.iconNum; i++) {
if (i < bRef.icons.length && !onCard(i)) {
bRef.hideSlide(bRef.icons[i]);
}
}
iconIdx = (iconIdx >= (bRef.icons.length / bRef.iconNum) - 1 ? 0 :
iconIdx + 1);
for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
bRef.iconNum; i++) {
if (i < bRef.icons.length) {
bRef.showSlide(bRef.icons[i]);
}
else { break; }
}
}
Użytkownik przegląda ikony tak jak wcześniej tła, ale tym razem chodzi o coś więcej, niż tylko zmienianie właściwości src pojedynczego obrazka. Zamiast tego każda ikona jest osobnym obrazkiem na osobnej warstwie. Wobec tego kliknięcie przycisku Ikony -> powoduje nieco bardziej złożoną akcję. Nie tylko musimy ukryć wszystkie warstwy obecnie widoczne, ale też zdecydować, które warstwy pokazać, przy czym musimy to wszystko robić też grupami.
Nie powinno być tak, żeby użytkownik musiał klikać 20 razy w celu zobaczenia 20 kolejnych ikon. Może to być nużącej, a przy tym będziemy marnować dostępną przestrzeń okna przeglądarki. Jak na obrazkach na początku tego rozdziału widać, zdecydowano się wyświetlać ikony w czteroelementowych grupach. Jakąkolwiek liczbę wybierzemy, będzie ona zapisywana w zmiennej iconNum ustawianej w wierszu 24 pliku back.html. Tym razem jesteśmy w pliku font.html, więc odwołanie ma postać top.Back.iconNum. Chodzi o wyświetlenie iconNum ikon przy każdym kliknięciu przycisku Ikony ->. Jeśli mamy 20 ikon, użytkownik będzie oczekiwał pięciu grup ikon. Oczywiście chcemy też ułatwić dodawanie i odejmowanie ikon. Jeśli usuwamy jedną ikonę, użytkownik zobaczy cztery grupy czteroikonowe i jedną grupę trójelementową. Nie musimy dokonywać natomiast żadnych zmian w funkcji nextIcons().
Rzecz jest całkiem łatwa. Zaczynamy od pierwszej czwórki, potem ją ukrywamy i wyświetlamy następną, aż nam zbraknie ikon. Wtedy zaczynamy znów od początku. Można wyjaśnić to jeszcze prościej: ukrywamy cztery poprzednie ikony, pokazujemy cztery następne. Przyjrzyjmy się teraz sformułowaniu tego zdania w JavaScripcie. Do identyfikacji poszczególnych grup używamy zmiennej iconIdx, początkowo ustawionej na 0. Pierwsza grupa związana jest właśnie z wartością 0, druga z wartością 1, i tak dalej.
Kiedy użytkownik klika Ikony ->, musimy ukryć wszystkie ikony z grupy związanej z bieżącą wartością iconIdx:
for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
bRef.iconNum; i++) {
if (i < bRef.icons.length && !onCard(i)) {
bRef.hideSlide(bRef.icons[i]);
}
}
Zmienna i ustawiana jest na iconNum * iconIdx. i, będzie zwiększana o 1, póki nie przekroczy wartości (iconNum * iconIdx) + iconNum. Jeśli ktoś ma wątpliwości, niech zastanowi się, co dzieje się po zakończeniu ładowania dokumentu. iconNum równa jest 4, iconIdx równa jest 0. Znaczy to, że przy pierwszym wywołaniu funkcji i - przybierze wartości 0, 1, 2 i 3. Następnym razem iconIdx równa będzie 1, więc i przybierze wartości 4, 5, 6 i 7. I tak dalej.
Zmienna i jest liczbą całkowitą, która będzie używana do sięgania do elementu z tablicy icons. Dlaczego? Każda ikona ma przecież własną warstwę. Kod zawarty w pliku back.html nazywa wszystkie warstwy zgodnie z elementami tablicy icons. Na przykład icons[0] odnosi się do warstwy bear.
Pozostaje teraz ukryć warstwy 0, 1, 2 i 3 - chyba że użytkownik przeciągnął którąś z nich na kartkę z życzeniami. Zajmuje się tym funkcja onCard(), którą wkrótce omówimy. Załóżmy na razie, że nie zostały przeniesione jeszcze żadne ikony, bo uprości nam to dalszą analizę funkcji. Wywołujemy po prostu funkcję hideSlide z pliku back.html i przekazujemy jej nazwę odpowiedniej warstwy, którą identyfikujemy przez i:
bRef.hideSlide(bRef.icons[i]);
Ikon poprzednich już nie ma, teraz musimy pokazać następną grupę. Zanim jednak to zrobimy, upewnimy się, że nie jesteśmy już przy ostatniej grupie. Jeśli tak, ustawiamy iconIdx ponownie na 0. W przeciwnym wypadku powiększamy iconIdx o 1. Oto wiersze 59-60:
iconIdx = (iconIdx >= (bRef.icons.length / bRef.iconNum) - 1 ? 0 :
iconIdx + 1);
Jeszcze jedna iteracja i widoczna będzie następna grupa. Wiersze 61-67 zawierają pętlę for, która zajmuje się pokazaniem ikon:
for (var i = bRef.iconNum * iconIdx; i < (bRef.iconNum * iconIdx) +
bRef.iconNum; i++) {
if (i < bRef.icons.length) {
bRef.showSlide(bRef.icons[i]);
}
else { break; }
}
Zamierzamy zrobić iconNum iteracji i pokazać następną grupę ikon. Poprzednio powiększyliśmy lub wyzerowaliśmy w wierszach 59-60 zmienną iconIdx, więc teraz wystarczy tylko wykonać prawie to samo w pętli for, co poprzednio robiliśmy, ukrywając grupę poprzednią. Tym razem jednak użyjemy funkcji showSlide(). Jest tu jednak pewna pułapka. Pamiętajmy, że chcemy zrobić iconNum iteracji, ale co się stanie, jeśli jest to ostatnia grupa i nie ma już w niej iconNum ikon? Jeśli mamy 20 ikon i chcemy je pokazywać czwórkami, będzie pięć takich czwórek. Jeśli jednak mamy 19 ikon i też chcemy je wyświetlać czwórkami, nadal jest pięć grup, ale ostatnia z nich zawiera tylko cztery ikony. Dlatego właśnie potrzebujemy dodatkowej instrukcji if-else, która będzie sprawdzać, czy nie mamy do dyspozycji mniej ikon, niż wynikałoby to z indeksów. Jeśli tak, nextIcons() uwidacznia ikony. Jeśli nie, nie ma już w tej grupie ikon i pętla jest przerywana instrukcją break.
Utrzymanie ikon na miejscu
Jak można się było dowiedzieć, iteracja przez ikony obejmuje ukrywanie starych ikon i pokazywanie nowych. Działa to nieźle, póki użytkownik nie przeciągnie jakichś ikon na kartkę z życzeniami. Takie ikony chcemy zostawić tam, gdzie się znajdują. Funkcja onCard() ciężko pracuje nad określeniem, czy kolejne ikony mają być zostawione tam, gdzie są, czy mają zostać ukryte. Oto wiersze 83-109 - funkcja onCard() niczego nie ukrywa ani nie pokazuje. Po prostu zwraca true lub false i na podstawie tego inne funkcje podejmują odpowiednie działania:
function onCard(iconRef) {
var ref = bRef.refSlide(bRef.icons[iconRef]);
var ref2 = bRef.refSlide("Back");
if(document.all) {
if((parseInt(ref.left) >= parseInt(ref2.left)) &&
(parseInt(ref.top) >= parseInt(ref2.top)) &&
(parseInt(ref.left) + parseInt(ref.width) <= parseInt(ref2.left) +
parseInt(ref2.width)) &&
(parseInt(ref.top) + parseInt(ref.height) <= parseInt(ref2.top) +
parseInt(ref2.height))) {
return true;
}
}
else {
if((ref.left >= ref2.left) &&
(ref.top >= ref2.top) &&
(ref.left + ref.document.images[0].width <= ref2.left +
ref2.document.images[0].width) &&
(ref.top + ref.document.images[0].height <= ref2.top +
ref2.document.images[0].height)) {
return true;
}
}
ref.left = ((iconRef % bRef.iconNum) * 110) + bRef.startWdh;
ref.top = 15;
return false;
}
Zanim dokładniej przyjrzymy się funkcji onCard(), musimy wiedzieć, które ikony mają zostać uznane za należące do kartki. W przypadku najprostszym wszystkie brzegi ikony (choćby niewidoczne) muszą być wewnątrz wszystkich ograniczeń tła lub na tych granicach. Na rysunku 10.8 pokazano, co zostaje, a co jest usuwane.
Przy założeniu, że biały obszar to tło wyświetlania, mały ludzik po prawej natychmiast zostanie usunięty, bo jest całkiem na zewnątrz. Niewiele brakuje kaktusowi, ale dwa jego brzegi jednak wychodzą za tło, więc kaktus też musi się z nami pożegnać. Na widoku zostanie jedynie karateka - i tak to działa.
|
Rysunek 10.8. Poza granicami: zostanie tylko karateka
Wszystko odbywa się względem położenia wyznaczonego pikselami. Jako że tło zawarte jest w jednej warstwie, możemy użyć DHTML do określenia lewego i górnego brzegu względem brzegów dokumentu. Ponieważ warstwa zawiera tylko jeden obrazek, możemy użyć właściwości width i height obiektu Image, aby dokładnie określić szerokość i wysokość warstwy. Ta sama zasada obowiązuje też w przypadku ikon. Użyjemy właściwości left i top warstwy i właściwości width i height obiektu Image. Funkcja ta ma kilka zagnieżdżonych instrukcji if, ale instrukcja if-else najbardziej zewnętrzna wykonuje w obu blokach tę samą akcję - raz w przeglądarce Internet Explorer, raz w Netscape Navigatorze. Oto pierwsza część funkcji onCard() działająca w przeglądarce Internet Explorer:
if(document.all) {
if((parseInt(ref.left) >= parseInt(ref2.left)) &&
(parseInt(ref.top) >= parseInt(ref2.top)) &&
(parseInt(ref.left) + parseInt(ref.width) <= parseInt(ref2.left) +
parseInt(ref2.width)) &&
(parseInt(ref.top) + parseInt(ref.height) <= parseInt(ref2.top) +
parseInt(ref2.height))) {
return true;
}
}
Obrazek tła ma cztery krawędzie, tak samo wszystkie ikony. Wobec tego musimy przeprowadzić cztery porównania, aby upewnić się, że żaden brzeg ikony nie przekracza brzegu tła. Oto zapisane słownie instrukcje if z wierszy 87-94:
JEŚLI lewy brzeg ikony dotyka lub przekracza w prawo lewy brzeg tła
ORAZ górny brzeg ikony dotyka lub jest poniżej górnego brzegu tła
ORAZ prawy brzeg ikony dotyka lub przekracza w lewo prawy brzeg tła
ORAZ dolny brzeg ikony dotyka lub jest nad dolnym brzegiem tła, TO
ZWRÓĆ true.
Określenie lewego i górnego brzegu poszczególnych warstw jest nieskomplikowane - po prostu używamy właściwości left i top poszczególnych warstw. Określenie prawego i dolnego brzegu też nie jest zbyt trudne, gdyż po prostu dodajemy odpowiednio do lewego brzegu szerokość warstwy, a do górnego brzegu - wysokość warstwy.
Można zauważyć dwie rzeczy. Po pierwsze zmienne ref i ref2 zostały ustawione na warstwy ikony i tła. Zrobiono tak tylko po to, aby ułatwić czytanie tego kodu. Po drugie wszędzie występuje funkcja parseInt().Internet Explorer zwraca wartości właściwości left i top jako 250px zamiast zwykłego 250 i właśnie parseInt() przekształca taki napis na liczbę, dzięki czemu możemy wykonywać swoje obliczenia.
Najbardziej zewnętrzna część else realizuje te same zadania w przeglądarce Netscape Navigatorze. Nie musimy już używać funkcji parseInt(), gdyż Netscape Navigator zwraca liczby:
else {
if((ref.left >= ref2.left) &&
(ref.top >= ref2.top) &&
(ref.left + ref.document.images[0].width <= ref2.left +
ref2.document.images[0].width) &&
(ref.top + ref.document.images[0].height <= ref2.top +
ref2.document.images[0].height)) {
return true;
}
}
Jeśli zatem badana ikona przejdzie wszystkie cztery testy, obie przeglądarki zwrócą true; w przeciwnym wypadku dzieje się rzecz następująca:
ref.left = ((iconRef % bRef.iconNum) * 110) + bRef.startWdh;
ref.top = 15;
return false;
Funkcja onCard() zauważa, że ikony nie mieszczą się na tle i przywraca im pierwotne położenie. Wszystkie ikony są położone względem górnego brzegu tak samo - 15 pikseli poniżej. Jednak położenie lewego brzegu zależy od numeru ikony w grupie. Nie ma żadnego problemu. Szybkie przeliczenie z użyciem zmiennych iconRef i iconNum pozwala określić pierwotne położenie ikony. Okazuje się, że każdy obrazek ma 100 pikseli szerokości, poza tym między obrazkami jest 10 pikseli odległości w poziomie, zatem pozycjonowanie jest całkiem łatwe. Ostatecznie funkcja zwraca wartość false.
Sprawdzanie, co otrzymaliśmy
Dobrze byłoby, gdyby nadawca mógł podejrzeć, co ma dostać jego odbiorca. Wybór przycisku Test otworzy w tym celu osobne okienko - oto wiersze 131-145:
function testGreeting(fObj) {
var msgStr = '<HTML><TITLE>Cyber Greeting Test Page</TITLE>' +
genGreeting(fObj) + '<TABLE ALIGN="CENTER"><TR><TD><FORM>' +
'<INPUT TYPE=BUTTON VALUE=" OK " onClick="self.close();">' +
'</FORM></TD></TR></TABLE></HTML>';
newWin = open('', '', 'width=' + (
bRef.backImgs[backgroundIdx].width + 50) +
',height=600,scrollbars=yes');
with(newWin.document) {
open();
writeln(msgStr);
close();
}
newWin.focus();
}
Funkcja testGreeting() ma tylko dwa zadania: otworzyć okno na tyle szerokie, aby umożliwić wyświetlenie wiadomości i wpisać do niego treści w postaci dokumentu. Treść tego okna jest przechowywana w zmiennej lokalnej msgStr. Jest tu nieco statycznego HTML, reszta to treści generowane dynamicznie przez funkcję genGreeting(). msgStr zawiera też na końcu formularz z przyciskiem pozwalającym zamknąć nasze nowe okno. Kiedy funkcja msgStr zostanie załadowana całym dobrodziejstwem inwentarza, testGreeting() otwiera okno o 50 pikseli szersze od szerokości obrazka tła i o stałej wysokości 600 pikseli. Funkcja zapisuje treść w strumieniu dokumentu, nowe okno staje się aktywne - i to wszystko.
Ostateczne tworzenie kartki
Funkcja testGreeting() udostępnia okno pozwalające podglądnąć okno pozdrowień, ale to funkcja genGreeting() wykonuje całą pracę.
Oto jej wiersze: 147-177:
function genGreeting(fObj) {
var greetingIdx = fObj.Greetings.selectedIndex;
var msg = fObj.Message.value;
msg = msg.replace(/\r+/g, "");
msg = msg.replace(/\n+/g, "<BR><BR>");
var msgStr = '<TABLE BORDER=0><TR><TD COLSPAN=2><FONT FACE=Arial>' +
'<H2>Twoje elektroniczne życzenia</H2>Do: ' + fObj.Recipient.value +
'<BR><BR></TD></TR>' + '<TR><TD VALIGN=TOP><IMG SRC="' +
baseURL + '/images/background' + backgroundIdx + '.jpg">' +
'<DIV STYLE="position:relative;left:40;top:-255;' +
'font-family:Arial;font-size:48px;font-weight:bold;">' +
parent.greetings[greetingIdx] + '</DIV>';
var iconStr = '';
for (var i = 0; i < bRef.icons.length; i++) {
if(onCard(i)) {
iconStr += '<DIV STYLE="position:absolute;left:' +
bRef.refSlide(bRef.icons[i]).left + ';top:' +
(parseInt(bRef.refSlide(bRef.icons[i]).top) -
(document.all ? 140 : 150)) + ';"><IMG SRC="' +
baseURL + '/images/' + bRef.icons[i] + '.gif"></DIV>';
}
}
msgStr += iconStr + '</TD></TR><TR><TD WIDTH=' +
bRef.backImgs[backgroundIdx].width + '><FONT FACE=Arial>' +
msg + '</TD></TR></TABLE>';
return msgStr;
}
Funkcja ta jest nieco trudna w analizie, ale możemy sobie zadanie uprościć, jeśli zastanowimy się, co ma ona właściwie wykonać. Otóż genGreeting() ma tylko zwrócić kod HTML zawierający:
tekst wyświetlający adres poczty elektronicznej odbiorcy,
obrazek tła,
odpowiednio ułożony tekst życzeń,
odpowiednio rozmieszczone ikony,
treść wiadomości.
Nie jest to szczególnie dużo, ale zanim zajmiemy się generowaniem tej treści, musimy zrobić trochę porządku. Oto wiersze 148-152:
var greetingIdx = fObj.Greetings.selectedIndex;
var msg = fObj.Message.value;
msg = msg.replace(/\r+/g, "");
msg = msg.replace(/\n+/g, "<BR><BR>");
Deklarujemy dwie zmienne lokalne, greetingIdx i msg. Pierwsza z nich to selectedIndex listy wyboru Greetings. msg to wiadomość wpisana przez nadawcę. Jako że życzenia będą wyświetlane jako HTML, znaki końca wiersza nie będą właściwie interpretowane, zatem musimy je zastąpić znacznikami <BR>. Teraz możemy zabrać się za tworzenie pozdrowień. Zacznijmy od góry - oto wiersze 154-160:
var msgStr = '<TABLE BORDER=0><TR><TD COLSPAN=2><FONT FACE=Arial>' +
'<H2>Twoje elektroniczne życzenia</H2>Do: ' + fObj.Recipient.value +
'<BR><BR></TD></TR>' + '<TR><TD VALIGN=TOP><IMG SRC="' +
baseURL + '/images/background' + backgroundIdx + '.jpg">' +
'<DIV STYLE="position:relative;left:40;top:-255;' +
'font-family:Arial;font-size:48px;font-weight:bold;">' +
parent.greetings[greetingIdx] + '</DIV>';
Wszystko jest w tablicy ułatwiającej nam ułożenie wszystkiego. Zmienna lokalna msgStr zawiera początek tablicy. Pierwszy wiersz zawiera nagłówek i pierwszy z czterech wymaganych elementów - adres poczty elektronicznej odbiorcy. Adres ten jest wartością pola Recipient formularza EntryForm. Dalej znajduje się obrazek tła. Używając zmiennych baseURL i backgroundIdx nietrudno jest utworzyć napis zawierający ścieżkę do odpowiedniego obrazka tła. Pamiętajmy, że jeśli lista wyboru Greetings ma wartość selectedIndex równą 4, to odpowiednim obrazkiem będzie background4.jpg.
Zwróćmy uwagę, że na tej stronie nie mamy kodu DHTML pozycjonującego obrazek tła na stronie. Nie jest to konieczne, gdyż nagłówek i życzenia będą zapewne osobno. Wiemy, że obrazek będzie gdzieś w pobliżu górnego brzegu strony, wyrównany do lewej. W ostatnich kilku wierszach tego kodu wybrane przez nadawcę życzenia są umieszczane względem obrazka tła, tuż przed nim. Lewy brzeg jest przesunięty o 40 pikseli, górny brzeg ustawiamy na -255. Ustawienia te zostały tak dobrane, aby uwzględnić położenie obrazka tła w back.html i bieżące jego położenie w życzeniach. Kiedy dodamy swoje tła, zapewne będziemy musieli te ustawienia zmodyfikować doświadczalnie tak, aby wszystko do siebie dopasować. Potem nie trzeba będzie się martwić o to, dopóki znowu nie zmienimy czegoś tak istotnego, jak rozmiar obrazka tła.
Za sobą mamy już trzy obowiązkowe elementy, zostały nam jeszcze dwa. Pierwszy to ikony przeciągnięte przez użytkownika - obsługują je wiersze 162-171:
var iconStr = '';
for (var i = 0; i < bRef.icons.length; i++) {
if(onCard(i)) {
iconStr += '<DIV STYLE="position:absolute;left:' +
bRef.refSlide(bRef.icons[i]).left + ';top:' +
(parseInt(bRef.refSlide(bRef.icons[i]).top) -
(document.all ? 140 : 150)) + ';"><IMG SRC="' +
baseURL + '/images/' + bRef.icons[i] + '.gif"></DIV>';
}
}
Zmienna lokalna iconStr początkowo zawiera pusty ciąg, później znajdą się w niej adresy URL i położenia wszystkich ikon. Procedura jest dość prosta: przeglądamy wszystkie ikony i dla każdej, która znajduje się w obszarze wyświetlania, tworzony jest kod HTML, pozwalający utworzyć kopię tej ikony (czyli obrazka) na innej stronie w takim samym położeniu względnym. Pamiętajmy, że funkcja onCard() decyduje o położeniu ikony, a przez to o możliwości umieszczenia tej ikony na kartce.
Kod generowany dla poszczególnych znaczników IMG otoczony jest znacznikami DIV. Te z kolei mają atrybut STYLE, oznacza się dla nich pozycję względem brzegów lewego i górnego: minus 140 lub 150 pikseli - w zależności od tego, czy mamy do czynienia z Internet Explorerem, czy Netscape Navigatorem. Pojawiają się teraz dwa pytania:
Dlaczego możemy użyć wyrównania ikony do lewej strony, skoro jeszcze trzeba przesunąć jej górny brzeg o ponad 100 pikseli?
Dlaczego ilości pikseli są różne dla obu przeglądarek?
To na pewno dobre pytania.
Najpierw zastanówmy się, gdzie na ekranie znajduje się obrazek tła w przypadku tworzenia życzeń. Jest mniej więcej w połowie wysokości strony - zależy to nieco od stosowanej przez nas rozdzielczości. Tymczasem w testowej kartce z życzeniami obrazek tła znajduje się blisko górnego brzegu okna, zaraz pod nagłówkiem i adresem odbiorcy. Właśnie te dodatkowe piksele mają to wyrównać. Gdyby obrazek tła był w obu przypadkach w tym samym miejscu, to dodatkowe pozycjonowanie okazałoby się zbędne. Jeśli chodzi o różnicę między przeglądarkami, to po prostu nieco inaczej rozmieszczane są warstwy - właśnie o tych 10 pikseli.
Po stworzeniu warstwy każdej wstawianej ikony łączymy iconStr z msgStr, dodajemy jeszcze trochę zamykających znaczników HTML i msg, i mamy już gotowe życzenia w tablicy:
msgStr += iconStr + '</TD></TR><TR><TD WIDTH=' +
bRef.backImgs[backgroundIdx].width + '><FONT FACE=Arial>' +
msg + '</TD></TR></TABLE>';
return msgStr;
Wartość msg jest wstawiana do komórki danych o takiej samej szerokości, jak obrazek tła, dzięki czemu uzyskujemy lepszy efekt wizualny. Następnie funkcja zwraca gotowy napis.
Wysyłanie życzeń
Użytkownik już przygotował eleganckie życzenia, kilkakrotnie je przetestował i jest w końcu zadowolony. Wybranie przycisku Wyślij to ostatnia jego czynność - wywołana zostanie tym samym funkcja shipGreeting() zapisana w wierszach 111-129:
function shipGreeting(fObj) {
if (fObj.Recipient.value == "") {
alert('Musisz podać adres e-mail!');
return false;
}
else if (fObj.Message.value == "") {
alert("Musisz wpisać wiadomość.");
return false;
}
else if (fObj.Greetings.selectedIndex == 0) {
alert('Musiszy wybrać rodzaj życzeń.');
return false;
}
fObj.EntireMessage.value = genGreeting(fObj);
fObj.UniqueID.value = Math.round(Math.random() * 1000000);
fObj.BaseURL.value = baseURL;
return true;
}
Funkcja ta jest dość krótka, a wywołuje ją procedura obsługi zdarzenia onSubmit z wiersza 187. shipGreeting() nie tylko przygotowuje formularz do wysyłki do serwera, ale też sprawdza krótko poprawność danych. O ile tylko użytkownik zastosował się do pewnych prostych zaleceń, wszystko idzie bezproblemowo. Jedyne, czego od nadawcy wymagamy, to podanie jakiegoś adresu, pewnej treści oraz wybranie typu życzeń z listy. Na razie takie wymagania wystarczą.
Jeśli wprowadzone informacje przejdą naszą skróconą odprawę, shipGreeting() zmienia wartość trzech ukrytych pól z wierszy 188-190. Początkowo pola te były puste, teraz EntireMessage i BaseURL otrzymują wartości potrzebne skryptowi działającemu na serwerze. Aby skrypt ten nie musiał tworzyć znów kodu HTML życzeń, funkcja genGreeting()zwraca wartość do pola EntireMessage.value.
Kolejna informacja, potrzebna skryptowi z serwera, to adres bazowy URL, względem którego wskazywane będą wszystkie życzenia, obrazki tła i ikony, zatem BaseURL.value otrzymuje wartość top.baseURL. Po tym formularz jest przesyłany na adres wskazany w atrybucie ACTION, do skryptu greet.pl - zajrzyjmy do wiersza 186.
|
Techniki języka JavaScript: Skąd wiadomo, kiedy jakaś funkcja zaczyna robić się zbyt długa? Kiedy należy się zatrzymać i powiedzieć sobie „dobra, resztę wprowadzę do następnej funkcji”? Trudno podać spójny zestaw takich reguł, ale zawsze można uzyskać jak najwięcej z minimalnej ilości kodu. Przyjrzyjmy się funkcji genGreeting(). Pozwala wygenerować kod zarówno do podglądu życzeń, jak i kod do gotowej kartki z życzeniami. A co z onCard()? Jest to całość umożliwiająca określenie pozycji ikon wybranych przez użytkownika, odgrywa też pewną rolę przy tworzeniu kodu podglądu i kodu gotowej kartki. W większości wypadków lepiej jest pisać krótkie funkcje. Nie zawsze jest to możliwe i wygodne, ale jeśli tylko się da, to należy się tego trzymać. |
|
Uwaga
Teraz jeszcze jedna uwaga, zanim przejdziemy do strony serwerowej. Funkcja resetForm() z wierszy 70-81 czyści formularz przy każdym jego załadowaniu - tak pierwszym, jak i powtórnym. Wywoływana jest w ramach obsługi zdarzenia onLoad w znaczniku BODY. Pamiętając o tym, zobaczmy, co się dzieje, kiedy aplikacja zgłosi się do serwera sieciowego.
Po stronie serwera
Tak jak w przypadku wirtualnego koszyka z rozdziału 8., również ta aplikacja wymaga pewnych mechanizmów działających po stronie serwera. Życzenia stworzone przez użytkownika są generowane przez skrypt JavaScript działający po stronie klienta. Informacje są następnie przesyłane do serwera sieciowego, gdzie używane jest takie środowisko, jak Active Server Pages, działający po stronie serwera JavaScript czy Cold Fusion.
Owe skrypty serwerowe odczytują dane przesłane przez użytkownika i tworzą plik zawierający życzenia. Niepowtarzalna nazwa tego pliku jest taka sama, jaką wysyła się odbiorcy życzeń. Plik jest gotów do odczytania i czeka, aż odbiorca kliknie łącze w wiadomości poczty elektronicznej.
Następnie skrypt pokaże nadawcy potwierdzenie, że wszystko przebiegło prawidłowo, a co ważniejsze, przygotuje formularz HTML umożliwiający wysłanie wiadomości odbiorcy.
Czytelnika zapewnie interesuje JavaScript, ale tym razem skrypt przygotowano w języku Perl. Przy okazji warto wspomnieć, że język ten jest względnie prosty i ma duże możliwości, przez co szybko staje się podstawowym językiem skryptowym w Windows NT. W dodatku C można znaleźć więcej informacji o przygotowywaniu środowiska tego języka do uruchamiania skryptów, a także wyjaśnienie, jak działają dwa skrypty Perl przygotowane dla aplikacji pokazywanych w tej książce.
Kierunki rozwoju
Teraz czas na omówienie kilku metod dalszej rozbudowy aplikacji.
Dodanie łącza „wstecz”
Dlaczego nie dodać łącza wstecznego do naszej strony z życzeniami? Wystarczy do funkcji genGreeting() z pliku front.html dopisać następujący kod:
+ ' <A HREF="' + top.baseURL + '/index.html">Do strony życzeń</A>';
Zakładamy przy tym, że pliki index.html, front.html i back.html znajdują się w katalogu wskazywanym przez baseURL. Jeśli nie, należy zamienić top.baseURL w powyższej instrukcji na jawnie podany adres URL, jakiego chcemy użyć.
Dodanie obrazków tematycznych
W tej aplikacji aż się prosi o tematyczne zestawy obrazków, choćby Boże Narodzenie, Walentynki, Wielkanoc, wakacje, i tym podobne. Nie musimy zresztą ograniczać się tylko do świąt. Możemy też użyć teł związanych z porami roku i stosownych ikon, jak również tworzyć zestawy urodzinowe, a także odnoszące się do innych tematów.
Banery reklamowe
Jeśli zamierza udostępniać swoją aplikację za darmo, czemu by sobie tego trochę nie wynagrodzić? W naszej funkcji shipGreeting() można wstawić kod umożliwiający wybranie banera reklamowego i wstawienie kodu znacznika IMG na dole życzeń. Jeśli użyjemy wspomnianych przed chwilą zestawów tematycznych, możemy dobierać reklamy pasujące do danych zestawów.
Życzenia bardziej interaktywne
W tej aplikacji używamy zdarzeń JavaScriptu do stworzenia życzeń zgodnie z żądaniami użytkownika. No tak, ale nie ma w nich w ogóle życia. Można by wysyłać życzenia z przewijaniem obrazków czy elementami reagującymi na klikanie. Jeśli mamy jakieś ciekawe aplety języka Java, możemy je dołączyć do życzeń. Ludzie uwielbiają bezużyteczne gadżety, którymi można się pobawić - właśnie dzięki temu istnieją tysiące stron sieciowych.
Cechy aplikacji:
Prezentowane techniki:
|
11 Pomoc kontekstowa |
|
Bez względu na to, jak prosta jest aplikacja i jak dobrze jest naszym zdaniem udokumentowana, na pewno znajdzie się ktoś, kto będzie miał pytanie bez odpowiedzi. Na niektóre pytania użytkowników bardzo łatwo jest odpowiedzieć, a niektóre mogą doprawdy zdumieć - mimo że to my jesteśmy autorami aplikacji! W zależności od jakości dokumentacja może szybko przywrócić użytkownika na właściwą drogę, ale może też zwieść go jeszcze dalej. Pliki pomocy same w sobie też muszą być proste w użyciu - i tym się zajmiemy w tym rozdziale.
Aplikacja ma być nie tylko prosta w użyciu dla klienta, ale także prosta dla nas w przygotowaniu i utrzymaniu. Jest to kolejna aplikacja, która sama w sobie niczemu specjalnemu nie służy. Tak jak w przypadku aplikacji z rozdziału 7., również tym razem pokazany kod możemy wstawiać do swoich własnych aplikacji.
Nazwałem tę aplikację Listą SELECT w JavaScripcie - wygląd nie jest zbyt efektowny, ale zobaczymy, co można osiągnąć, łącząc z naszym ulubionym językiem programowania listy wyboru. Na rysunku 11.1 pokazano wygląd aplikacji po załadowaniu.
W aplikacji tej pokazano, jak lista wyboru może zmieniać kolor tła, ładować dokumenty oraz rozwijać inne listy - a do tego wszystkiego potrzeba tylko trochę JavaScriptu. W tym wypadku to wszystko służy tylko jako przykład, wybranie łącza Help otwiera okienko z dokumentacją dotyczącą koloru tła. Spójrzmy na rysunek 11.2.
Być może nie ma tu znowu nic szczególnego. Jednak kliknięcie łącza Katalog URL (ładującego stronę katalogów URL), później kliknięcie znowu Help załaduje dokumentację dotyczącą procedury ładowania URL. Spójrzmy na rysunek 11.3 - nieźle. Teraz użytkownicy nie muszą już wyszukiwać interesującego ich pliku pomocy, są przecież duże szanse, że użytkownicy są zainteresowani pomocą dotyczącą tego, co właśnie oglądają. Pliki pomocy są pokazywane w zależności od kontekstu.
Wygląda to tak, jakby aplikacja miała w sobie jakąś mądrość: zawsze „wie”, gdzie jest użytkownik. Oprócz tego część pomocy zawiera hipertekst prowadzący do dalszych objaśnień. Jednak użytkownik nie musi w ogóle klikać łącza, aby załadować następny dokument - samo umieszczenie kursora myszki nad łączem pokaże informacje w podświetlonej warstwie. Obejrzyjmy rysunek 11.4.
Teraz użytkownik nie musi wracać do dokumentu, z którego uruchomił łącze. Zabranie kursora myszki znad łącza z powrotem ukryje pokazaną warstwę. Funkcjonalność taką można zastosować niemalże w dowolnej aplikacji, w której użytkownik może potrzebować pomocy.
|
Rysunek 11.1. Lista SELECT w JavaScripcie
|
Rysunek 11.2. Objaśnienie zasad zmiany koloru tła
|
Rysunek 11.3. To samo łącze, inna treść pomocy
|
Rysunek 11.4. Dodatkowe informacje
Wymagania programu
Będziemy potrzebowali Netscape Navigatora lub Internet Explorera w wersjach co najmniej 4.x, a to z powodu użycia DHTML i nowego modelu zdarzeń. Upewnijmy się, że mamy monitor o rozdzielczości co najmniej 1024x768. Nie jest to obowiązkowe, ale w przeciwnym wypadku okienko pomocy może przykrywać zbyt dużą część okna głównego.
Struktura programu
Aplikacja ta zawarta została w zestawie ramek z dużą liczbą plików. Oto szybki przegląd:
index.html
Zestaw ramek najwyższego poziomu i najważniejsze zmienne.
top.html
Wyświetla nagłówek aplikacji.
nav.html
Wyświetla stronę z łączami.
background.html
Zmienia kolory tła.
multiselect.html
Rozwija listę wyboru na podstawie ustawień dwóch innych list.
urldirectory.html
Ładuje wyszukiwarki według wybranej opcji.
help/background.html
Dokument pomocy związany z plikiem background.html.
help/multiselect.html
Dokument pomocy związany z plikiem multiselect.html.
help/urldirectory.html
Dokument pomocy związany z plikiem urldirectory.html.
help/help.js
Plik źródłowy JavaScriptu.
Raczej małe są szanse, że ktoś będziesz chciał jakoś szczególnie głęboko studiować logikę background.html, multiselect.html czy urldirectory.html. Nie ma tam nic szczególnego i nie o to przecież w tym rozdziale chodzi. Jednak przynajmniej obejrzyj sposób tworzenia list w multiselect.html, a jest dość sprytny. Teraz zajmijmy się właściwym tematem, gdyż zrobimy to w dwóch krokach:
Pomoc kontekstowa: ładowanie prawidłowego dokumentu do okienka pomocy (nav.html).
Pokazywanie i ukrywanie dodatkowych informacji w odpowiedzi na ruchy myszki (help/help.js).
Pomoc kontekstowa
Ta część jest całkiem prosta. Wszystko można znaleźć w pliku nav.html pokazanym w przykładzie 11.1.
Przykład 11.1. nav.html
1 <HTML>
2 <HEAD>
3 <TITLE>nav.html</TITLE>
4 </HEAD>
5 <STYLE TYPE="text/css">
6 <!--
7
8 A
9 {
10 text-decoration: none;
11 }
12
13 BODY
14 {
15 font-family: Arial;
16 text-align: center;
17 }
18
19 //-->
20 </STYLE>
21 <SCRIPT>
22 <!--
23 var helpWin;
24
25 function inContext(currFile) {
26 var start = currFile.lastIndexOf('/') + 1;
27 var stop = currFile.lastIndexOf('.');
28 var helpName = currFile.substring(start, stop);
29 if(helpWin == null || helpWin.closed) {
30 helpWin = open('help/' + helpName + '.html', 'helpFile',
31 'width=' + top.wdh + ',height=' + top.hgt +
32 ',left=100,top=100,scrollbars=no');
33 }
34 else {
35 helpWin.location.href = 'help/' + helpName + '.html';
36 }
37 helpWin.focus();
38 }
39
40 //-->
41 </SCRIPT>
42 <BODY>
43
44 <A HREF="background.html" TARGET="WorkArea">Kolory tła</A>
45
46 <A HREF="urldirectory.html" TARGET="WorkArea">Katalog URL</A>
47
48 <A HREF="multiselect.html" TARGET="WorkArea">Listy wielokrotnego
49 wyboru</A>
50 <A HREF="javascript: inContext(parent.WorkArea.location.href);">Help</A>
51
52 </BODY>
53 </HTML>
Funkcja inContext() zajmuje się jednym: dla każdego dokumentu, dla którego chcemy wyświetlać pomoc, tworzy dokumentację pomocy w pliku o takiej samej nazwie z rozszerzeniem .html. Zatem plik background.html, zmieniający kolory tła, ma w podkatalogu help/ odpowiadający mu plik pomocy background.html. Oto wiersze 25-38:
function inContext(currFile) {
var start = currFile.lastIndexOf('/') + 1;
var stop = currFile.lastIndexOf('.');
var helpName = currFile.substring(start, stop);
if(helpWin == null || helpWin.closed) {
helpWin = open('help/' + helpName + '.html', 'helpFile',
'width=' + top.wdh + ',height=' + top.hgt +
',left=100,top=100,scrollbars=no');
}
else {
helpWin.location.href = 'help/' + helpName + '.html';
}
helpWin.focus();
}
Funkcja jako argumentu oczekuje adresu URL. currFile może być adresem URL bezwzględnym, na przykład http://jakis.serwer.com.pl/gdzies/dokument.html, a może to być też adres względny z zapytaniem, na przykład dokument.cgi?search=all. Niezależnie od tej nazwy potrzebujemy jedynie nazwy samego pliku, bez komputera czy katalogów z przodu ani bez rozszerzenia czy zapytania z tyłu. Innymi słowy - potrzebujemy wszystkiego po ostatnim ukośniku (/), jeśli w ogóle jakiś występuje, aż do ostatniej kropki (choć bez niej - zakładamy, że pliki zawsze mają rozszerzenie, zatem jakaś kropka się pojawi).
Wobec tego zmienna start daje nam indeks ostatniego ukośnika powiększony o 1. Załóżmy, że w adresie nie ma żadnego ukośnika - w związku z tym nie ma sprawy. Funkcja lastIndexOf() zwróci w takim wypadku nam -1, po czym dodajemy 1 i otrzymujemy 0 - tutaj właśnie musimy zacząć. Zmienna stop otrzymuje wartość indeksu ostatniej kropki w adresie. Teraz metoda substring() z wiersza 28 wyłuskuje potrzebny podciąg z URL i przypisuje go zmiennej helpName. Przyjrzyjmy się:
var helpName = currFile.substring(start, stop);
Następnych kilka wierszy otwiera okno, używając helpName - zgodnie z konwencją nazewnictwa - dokumentów pomocy. Pierwszy parametr metody open() w wierszach 30-32 dynamicznie wskazuje odpowiedni plik pomocy:
helpWin = open('help/' + helpName + '.html', 'helpFile',
'width=' + top.wdh + ',height=' + top.hgt +
',left=100,top=100,scrollbars=no');
Zwróćmy uwagę, że szerokość i wysokość okna pomocy określamy w biegu przy pomocy zmiennych top.wdh i top.hgt, ustawiając obie na 300. Te dwie zmienne znajdują się w pliku index.html, zatem aplikacja odwołuje się do nich z innych miejsc, ale możemy tutaj skorzystać z odnośnika top. Dlaczego używam tych zmiennych do określenia rozmiarów okna, okaże się później. Jedyne, czego teraz potrzebujesz, to dobre łącze, które wywoła naszą funkcję- oto wiersz 50:
<A HREF="javascript: inContext(parent.WorkArea.location.href);">Help</A>
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: 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 var NN = (document.layers ? true : false);
2 var hideName = (NN ? 'hide' : 'hidden');
3 var showName = (NN ? 'show' : 'visible');
4 var zIdx = -1;
5 var helpWdh = 200;
6 var helpHgt = 200;
7 var x, y, totalWidth, totalHeight;
8
9 function genLayer(sName, sLeft, sTop, sWdh, sHgt, sVis, copy) {
10 if (NN) {
11 document.writeln('<LAYER NAME="' + sName + '" LEFT=' + sLeft +
12 ' TOP=' + sTop + ' WIDTH=' + sWdh + ' HEIGHT=' + sHgt +
13 ' VISIBILITY="' + sVis + '"' + ' z-Index=' + (++zIdx) + '>' +
14 copy + '</LAYER>');
15 }
16 else {
17 document.writeln('<DIV ID="' + sName +
18 '" STYLE="position:absolute; overflow:none; left:' +
19 sLeft + 'px; top:' + sTop + 'px; width:' + sWdh + 'px; height:' +
20 sHgt + 'px;' + ' visibility:' + sVis + '; z-Index=' + (++zIdx) +
21 '">' + copy + '</DIV>'
22 );
23 }
24 }
25
26 function hideSlide(name) {
27 refSlide(name).visibility = hideName;
28 }
29
30 function showSlide(name) {
31 refSlide(name).visibility = showName;
32 }
33
34 function refSlide(name) {
35 if (NN) { return document.layers[name]; }
36 else { return eval('document.all.' + name + '.style'); }
37 }
38
39 function motionListener() {
40 if (NN) {
41 window.captureEvents(Event.MOUSEMOVE);
42 window.onmousemove = grabXY;
43 }
44 else {
45 document.onmousemove = grabXY;
46 }
47 }
48
49 function grabXY(ev) {
50 if(NN) {
51 x = ev.pageX;
52 y = ev.pageY;
53 }
54 else {
55 x = event.x;
56 y = event.y;
57 }
58 }
59
60 function helpDisplay(name, action) {
61 if(action) {
62 totalWidth = x + helpWdh;
63 totalHeight = y + helpHgt;
64 x = (totalWidth > wdh ? x -
65 (totalWidth - wdh + 75) : x);
Przykład 11.2. help/help.js (dokończenie)
66 y = (totalHeight > hgt ? y -
67 (totalHeight - hgt) : y);
68 refSlide(name).left = x - 10;
69 refSlide(name).top = y + 8;
70 showSlide(name);
71 }
72 else { hideSlide(name); }
73 }
74
75 motionListener();
Zajmiemy się zawartymi tu funkcjami w dwóch krokach. Najpierw omówimy tworzenie warstw, gdzie są pewne dodatkowe informacje. Następnie przyjrzymy się pokazywaniu i ukrywaniu tych warstw.
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.</SPAN>';
var helpTwo = `<SPAN CLASS=”helpSet”>Ta właściwość obiektu ` +
`<TT>window</TT> zawiera hierarchię obiektów ` +
`bieżącej strony sieciowej.</SPAN>';
genLayer(“bgColor”, 0, 0, helpWdh, helpHgt, hideName, helpOne);
genLayer(“document”, 0, 0, helpWdh, helpHgt, hideName, helpTwo);
Zmienna helpOne zawiera napis, który wyświetli pierwsze dodatkowe łącze (bgColor), a helpTwo podobnie zachowuje się w odniesieniu do łącza dokumentu. Nie mamy tu jednak do czynienia tylko ze zwykłym tekstem - oba napisy zawierają parę znaczników SPAN, którym przypisano definicję klasy arkusza stylów .helpSet. Klasa ta jest ujęta w znaczniki STYLE. Przyjrzyjmy się temu nieco - nie jest to szczególnie dopracowana definicja klasy arkusza stylów, ale i tak całkiem nieźle sprawdza się przy definiowaniu warstw:
.helpSet
{
background-color: #CCFFCC;
padding: 5px;
border: 2px;
width: 200px;
font: normal 10pt Arial;
text-align: left;
}
Skrypt zawiera dwa wywołania funkcji genLayer(). Zwróćmy uwagę, że zamiast przekazywać każdej warstwie szerokość i wysokość, przekazujemy zmienne helpWdh i helpHgt. W ten sposób wygodniej będzie nam później dopracować pozycjonowanie warstw. Każda warstwa początkowo jest ukryta przez ustawienie zmiennej hideName.
Warstwy mamy już gotowe, teraz tylko trzeba umożliwić użytkownikowi wygodne ich wyświetlanie na życzenie. Zajmują się tym funkcje motionListener(), grabXY() i helpDisplay(). Pierwszą z nich, motionListener(), znajdziemy w wierszach 39-47:
function motionListener() {
if (NN) {
window.captureEvents(Event.MOUSEMOVE);
window.onmousemove = grabXY;
}
else {
document.onmousemove = grabXY;
}
}
Powinniśmy wyświetlić warstwę, kiedy tylko ktoś dotknie jej łącze na stronie. Aby to zrobić, musimy śledzić lokalizację myszy na ekranie, aby wiedzieć, kiedy znajduje się nad tym łączem. Funkcja motionListener() przypisuje wywołanie funkcji grabXY() do zdarzenia onMouseMove. Zarówno Netscape Navigator, jak i Internet Explorer obsługują to zdarzenie, ale w Netscape Navigatorze dotyczy ono obiektu window, a w Internet Explorerze obiektu document. Netscape Navigator musi też wywołać metodę captureEvents().
Funkcja grabXY() przypisuje zmiennym x i y odpowiednio poziomą i pionową współrzędną kursora myszki przy każdym jego ruchu. Oto wiersze 49-58:
function grabXY(ev) {
if(NN) {
x = ev.pageX;
y = ev.pageY;
}
else {
x = event.x;
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: 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: <A HREF="" onMouseOver="zrobCos();" onClick="return false;"> Zrób coś</A> Cokolwiek znajdzie się w atrybucie HREF, dokument ten nie będzie ładowany. |
|
Szerokość i wysokość poszczególnych warstw to 200 pikseli. Tak naprawdę wysokość warstwy rośnie dynamicznie, stosowanie do ilości danych, tak jak dane tabeli rozpychają komórki. Nadal jednak potrzebujemy jakiegoś odniesienia. Nie trzeba być profesorem matematyki, aby stwierdzić, jeśli łącze jest dalej niż 100 pikseli na prawo od lewego brzegu, część wyświetlanej warstwy nie będzie widoczna (tak naprawdę nawet niecałe 100 pikseli, bo zewnętrzny wymiar okna to 300, ale my dokument wyświetlamy wewnątrz). Aby się tego ustrzec, przed pokazaniem warstwy dokonamy pewnych wyliczeń.
Działa to tak: jeśli suma współrzędnej poziomej wskaźnika myszy oraz szerokości wyświetlanej warstwy jest większa od dostępnej szerokości okna, wyświetlamy warstwę bardziej na lewo - oto wiersz 62:
totalWidth = x + helpWdh;
Zmienna totalWidth ma wartość sumy współrzędnej poziomej oraz szerokości warstwy. Teraz widać, czemu używamy do ustawiania wymiarów zmiennych helpWdh i helpHgt, zamiast liczb, na przykład 200. Teraz przyjrzyjmy się wierszom 64-65:
x = (totalWidth > wdh ? x -
(totalWidth - wdh + 75) : x);
Jeśli wartość totalWidth jest większa niż szerokość okna (pomniejszona o szerokość ramki), współrzędna pozioma musi być odpowiednio poprawiona. Po prostu wyrównujemy ją do lewej strony, odejmując różnicę między totalWidth i szerokością okna pomocy. W ten sposób zapewniamy, że wszystkie warstwy wyświetlane będą poziomo. To samo dotyczy wysokości - wiersze 63 i 66-67. Może to nie zadziałać, jeśli helpHgt ma wartość dość niską, a tworzona warstwa ma dużo tekstu.
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: Tym razem widzieliśmy warstwy DHTML jako znaczniki DIV w Internet Explorerze i LAYER w Netscape Navigatorze. Znaczniki LAYER działają poprawnie, ale nie staną się one standardem. Wszystko w konsorcjum W3C wskazuje, że standaryzowany obiektowy model dokumentu będzie bardzo podobny do tego zrealizowanego w Internet Explorerze. Przyjrzyjmy się poniższemu kodowi: <HEAD> <TITLE>Warstwa DHTML</TITLE> <SCRIPT LANGUAGE="JavaScript1.2"> <!-- var action = true; function display(name) { if (document.all) { var layerObj = eval("document.all." + name + ".style"); var hide = "hidden"; var show = "visible"; } else { var layerObj = eval("document." + name); var hide = "hide"; var show = "show"; } layerObj.visibility = (action ? hide : show); action = !action; }
//--> </SCRIPT> </HEAD> <BODY> <DIV ID="dhtml" STYLE="position:relative;background-color:#FFACEE;width:200;"> Jest to warstwa DHTML. </DIV> <BR> <A HREF="javascript:display('dhtml');">Show/Hide</A> </BODY> </HTML> Jest to plik \ch11\layer.html. Jak widać, nie ma tu żadnego znacznika LAYER, a mimo to Netscape Navigator i Internet Explorer wszystko, co trzeba, zrozumieją (warstwa tu jest ukrywana i pokazywana na kliknięcie). Choć Netscape Navigator obecnie nie pozwoli sięgnąć do większości elementów obiektowego modelu dokumentu, to i tak możliwe jest pozycjonowanie. Jak zatem najlepiej postępować? Czemu nie użyliśmy tutaj metody znanej z poprzednich rozdziałów? Obie metody działają dobrze, ale wolę metodę genLayer() dynamicznie tworzonych znaczników LAYER w Netscape Navigatorze i DIV w Internet Explorerze. Ważne jest tak naprawdę to, że dysponujemy inną możliwą metodę postępowania. 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:
function showContents() {
var helpDocs = ['background', 'multiselect', 'urldirectory'];
var helpLinks = '<UL>';
for (var i = 0; i < helpDocs.length; i++) {
helpLinks += '<LI><A HREF="' + helpDocs[i] + '.html">' +
helpDocs[i] + '</A>';
}
helpLinks = '</UL>';
document.writeln(helpLinks);
}
Przeszukiwanie plików pomocy
Jeśli użytkownik potrzebuje kilku dokumentów pomocy, czemu nie umożliwić mu ich przeszukiwania przy pomocy aplikacji z rozdziału 1.? Zawsze jest to elegancka metoda ustąpienia użytkownikowi odrobiny interaktywności.
Pytanie do specjalisty
Czasami użytkownik nie potrafi znaleźć odpowiedzi na swoje pytanie. Jeśli dysponujemy odpowiednim personelem, zastanówmy się nad dodaniem - opartej na formularzu - wiadomości poczty elektronicznej, aby użytkownik mógł uzyskać odpowiedzi na swoje pytania od wykwalifikowanego pracownika.
Pomoc telefoniczna
Jeśli chcemy naprawdę porządnie obsłużyć klientów, podajmy listę numerów telefonicznych i adresów e-mail, aby użytkownicy mogli skontaktować się z wybranymi osobami. Tak jak w przypadku pytań do specjalisty, jest to rozwiązanie dość zasobożerne. Zanim udostępnimy numery telefonów, upewnijmy się, że ktoś będzie owe telefony odbierać. Ludzie będą dzwonić. Dzwoniły do mnie różne osoby po wizycie na mojej stronie, a mój numer niełatwo było tam znaleźć.
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.).
Niektóre przeglądarki Internet Explorer 3.x dla Maca obsługiwały jednak przewijanie obrazków (przyp. aut.).
Nazwy te pochodzą od oznaczeń operatorów warunkowych (przyp. tłum.).
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.).
W języku polskim, jak już Czytelnicy zapewne zauważyli, rzecz nie jest taka prosta. Po pierwsze, dla liczb 2, 3 i 4 należy użyć formy „wyniki”, nie „wyników”. Po drugie, jeden wynik jest „następny”, nie „następne”. Stosowne poprawki proponujemy wykonać w ramach ćwiczenia (przyp. tłum.).
Nie do końca; niektóre odmiany przeglądarki MSIE 3.02 już to robią.
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.).
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.).
Dlatego też nazwy te musiały pozostać w wersji angielskiej. Więcej na ten temat dalej, przy omawianiu konwencji nazewniczych. (przyp. tłum.).
Jeszcze gorzej będzie, jeśli za ukośnikiem znajdzie się n czy r, np. C:\network czy C:\reg.exp, gdyż \n i \r zostaną zinterpretowane jako odpowiednio nowy wiersz (ASCII 10) i powrót karetki (ASCII 13) (przyp.tłum.).
Wciśnięcia klawiszy zbierane są tylko z samego okna dokumentu, jeśli zatem elementem aktywnym jest przycisk Pokaż klawisze, to nic nie zobaczymy. W takim wypadku należy najpierw kliknąć myszką gdzieś w białe tło (przyp. tłum.).
Tak naprawdę znaczników DIV można używać także w Netscape Navigatorze 4.x, o ile tylko włączy się wartość pozycji w atrybucie STYLE. Jednak póki Netscape nie uzupełni obiektowego modelu dokumentu, użycie znacznika LAYER daje dostęp do większej liczby właściwości obiektu Layer (przyp. aut.).
Przynajmniej niektóre wersje Netscape Navigator 4.x zdają się odpowiadać nie na wszystkie kliknięcia myszą i czasem, aby zacząć lub zakończyć śledzenie wskaźnika, trzeba kliknąć dwa razy. W żadnej dokumentacji nie znalazłem omówienia tego niedociągnięcia (przyp. aut.).
Jest to odległość w „metryce miasto” - o czym wiedzą ci, którzy mieli kiedyś styczność z topologią. W ramach ćwiczenia proponuję poprawić funkcję tak, aby wyliczała prawdziwą odległość między punktami (przyp. tłum.).
Z uwagi na automatyczną zmianę strony nie można użyć przycisku Back/Wstecz w swojej przeglądarce. Po użyciu go zostanie załadowana strona poprzednia, która wyświetli natychmiast komunikat i automatycznie załaduje z powrotem stronę, z której chcieliśmy się wycofać. Jeśli zrobisz coś takiego na swojej witrynie w Sieci, nie licz na pobłażanie gości (przyp. tłum.).
Zbyt wiele [poziomów] rekursji (przyp. tłum.).
Kategorie i poszczególne towary nadal mają nazwy angielskie - wynika to z konwencji stosowanych przez autora, a mianowicie z tego, że identyfikatory towarów (i ich obrazków) oraz identyfikatory kategorii są pokazywane użytkownikowi. Jeszcze raz zatem podkreślam, o czym była już mowa w jednym z wcześniejszych rozdziałów - czym innym jest identyfikator, a czym innym opis prezentowany użytkownikowi (przyp. tłum.).
Kodowany jest tylko alfabet łaciński, przedtem „podejrzane” znaki są przekształcane. Dlatego właśnie po powrocie do tekstu pierwotnego, czyli odszyfrowaniu go, polskie litery będą wymienione na inne znaki. Jeśli chcemy to sprawdzić, ustawmy przesunięcie na 0 i wtedy wybierzmy szyfrowanie (przyp. tłum.).
Z tym samym zastrzeżeniem, co poprzednio - konwersji podlegają tylko znaki alfabetu łacińskiego (przyp. tłum.).
Z podręcznika U.S. Army Field Manual 34-40-2 (przyp. aut.).
Pamiętajmy tylko o jednym: jeśli chcemy takiej technologii użyć na swoich stronach sieciowych, to odbiorcy wraz ze stronami otrzymają także pełny kod JavaScriptu, zawierający metody szyfrowania (przyp. tłum.).
Nazwy oznaczają odpowiednio: szyfr z ukryciem, szyfr przestawny i szyfr z podstawianiem; Cipher oznacza szyfr (przyp. tłum.).
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.).
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.)
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.).
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.).
4
7
Spis treści
100
9
11
Wstęp
17
Wprowadzenie
39
Rozdział 1 - Wyszukiwanie danych po stronie klienta
57
Rozdział 2 - Test sprawdzany na bieżąco
77
Rozdział 3 - Interaktywna prezentacja slajdów
93
Rozdział 4 - Interfejs multiwyszukiwarki
101
Rozdział 5 - ImageMachine
141
Rozdział 6 - Realizacja plików źródłowych JavaScriptu
165
Rozdział 7 - Ustawienia użytkownika oparte na ciasteczkach
209
Rozdział 8 - Shopping Bag - wózek sklepowy stworzony w JavaScripcie
229
Rozdział 9 - Szyfry w JavaScripcie
255
Rozdział 10 - Elektroniczne życzenia: poczta elektroniczna metodą przenieś i upuść
265
Rozdział 11 - Pomoc kontekstowa