Ostrzeżenie - ze względu na złożoność zagadnienia wpis liczy około 20 stron A4 - jeżeli interesuje Cię tylko końcowy efekt to w trosce o rolki myszek zamieszczam już tutaj link do niego.

Kilka osób prosiło o jakiś poważniejszy projekt z użyciem MooTools więc chciałem takowy dziś opisać. Początkowo miał to być czytnik RSS, ale doszedłem do wniosku, że będzie to za dużo rzeczy do opisywania zwłaszcza, że czytnik RSS wymaga użycia PHP języka server-side. Dzięki małej webaplikacji nazwanej przeze mnie roboczo jako del.icio.us reader upieczemy dwie pieczenie na jednym ogniu:

  • napiszemy dość złożony skrypt z użyciem MooTools,
  • poznamy bardzo interesujące API tego popularnego serwisu

W projekcie wykorzystamy jako nośnik danych format JSON, który jest jedną z form (poza XML i HTML) udostępniania danych przez serwis del.icio.us.

Dlaczego właśnie JSON ?

Każdy kto spostrzegł potencjał AJAX-a prędzej czy później musiał przejść przez etap próby pobrania danych z innego serwera (a właściwie rozchodzi się tu o inną domenę). Jakież było rozczarowanie gdy w konsoli pojawił się komunikat o treści podobnej do tego:

AJAX cross-site scripting

Niestety zabezpieczenia przeglądarek nie pozwalają na pobieranie danych z innych stron. Ba ! Nawet "www" przed nazwą domeny potrafi stworzyć problemy. Ale sen o pobieraniu danych z innych stron jeszcze nie prysł, pod warunkiem, że mamy do dyspozycji tak dobre API jakie oferuje del.icio.us - ponieważ będziemy operowali na formacie JSON to możemy skorzystać z dynamicznego dołączania skryptów poprzez klasę Asset (wszak format JSON można traktować jako skrypt JS, tylko taki, który nic nie robi).

Oczywiście gwoli ścisłości dodam, że dane w formacie XML też można pobierać, ale wymaga to napisania prostej strony przekierowującej na przykład w PHP, która pobierze dane z innej strony (może to być na przykład funkcja file_get_contents) i wypisze na stronie umieszczonej w obrębie naszej domeny.

Ale wybrałem format JSON jeszcze z tego powodu, że jest według mnie po prostu wygodniejszy w "obróbce" za pomocą skryptu JS. A pobieranie JSON-a AJAX-em - hmm... według mnie to dziwny pomysł w naszym wypadku - po co się męczyć z PHP jeszcze ;)

Powody użycia formatu JSON jako nośnika danych już znacie. Pora na założenia naszej webaplikacji.

Założenia

Przed napisaniem naszej webaplikacji wypadałoby sobie zaplanować jej funkcjonalność.

Otóż w naszym wypadku użytkownik będzie podawał dwie rzeczy - login użytkownika (nie tylko swojego, może to być dowolny użytkownik del.icio.us) oraz będzie wybierał dane do pobrania wraz z parametrami.

Do wyboru przy pobieraniu będą następujące dane:

  • ostatnie zakładki użytkownika,
  • lista fanów użytkownika,
  • sieci do jakich należy użytkownik,
  • lista tagów użytkownika

W wypadku zakładek mamy do dyspozycji możliwość podania takich parametrów jak :

  • ilość zakładek (do 100),
  • pokazywanie zakładek należących do wybranych tagów

Poza zakładkami jeszcze w wypadku tagów mamy możliwość określenia dodatkowych opcji:

  • minimalna ilość zakładek jakie musi posiadać dany tag by był pokazywany,
  • ilość tagów,
  • sposób sortowania tagów (alfabetycznie lub według ilość zakładek)

Jak widzimy ilość danych jakie możemy pobrać jest spora i posiadają one wiele możliwości skonfigurowania.

Z innych założeń - warto pamiętać o obsłudze błędów (w tym wypadku brak użytkownika o danym nicku), oraz o zadbaniu o parę subtelnych efektów interfejsu aplikacji :)

Myślę, że założenia są gotowe - pora na poznanie struktury danych w formacie JSON i przygotowanie interfejsu.

Struktura pobieranych danych

Zakładki

Przed przygotowaniem interfejsu warto rzucić okiem na budowę plików danych JSON, które będziemy pobierać.

Dane o ostatnio dodanych przez danego użytkownika zakładkach znajdziemy pod adresem:

http://del.icio.us/feeds/json/nazwa_uzytkownika

W moim wypadku będzie to:

http://del.icio.us/feeds/json/Dziudek

Pod powyższym adresem znajdziemy ciąg znaków zaczynający się od:

if(typeof(Delicious) == 'undefined') Delicious = {}; Delicious.posts = (...)

Jest to kod odpowiedzialny za zwrócenie pustego obiektu Delicious w wypadku gdy wybrany użytkownik nie istnieje. Gdy użytkownik istnieje właściwość posts obiektu Delicious jest tablicą obiektów o następującej strukturze:

{
    "u":"http://adres-zakladki.pl",
    "d":"Opis zakładki",
    "n":"Notatka na temat zakładki"
    "t":["tag1", "tag2", ... , "tagN"]
}

Przy czym pola n i t nie zawsze istnieją (gdy chcemy je wykorzystać należy sprawdzić czy w ogóle istnieją).

Zatem sprawdzenie czy dany użytkownik istnieje ogranicza się do sprawdzenia czy tablica Delicious.posts w ogóle istnieje. Mamy jeszcze inną opcję - dodajmy po adresie ciąg ?raw, a dokładniej samą zmienną raw, która powoduje, że zamiast wspomnianego kodu jest od razu zwracana "czysta" tablica znana nam z właściwości Delicious.posts bez dodatkowego kodu JS .

Drugą zmienną jaką możemy określić w adresie jest count. Pisząc:

http://del.icio.us/feeds/json/Dziudek?raw&count=20

Zobaczę samą tablicę obiektów o prezentowanej już strukturze i zawierającą maksymalnie 20 pozycji. Maksymalnie można pobrać 100 zakładek, a domyślna wartość to 15.

I jeszcze jedna cecha - możemy określić także tagi jakie mają zawierać pobierane zakładki. Jeżeli chcemy pobrać wszystkie moje zakładki tak jak wcześniej (jako tablicę i najwyżej 20 pozycji) otagowane jako "javascript" to zapiszemy:

http://del.icio.us/feeds/json/Dziudek/javascript?raw&count=20

Przy czym warto wiedzieć, że możemy też wybrać wszystkie zakładki otagowane jako javascript i webdesign (ale uwaga - chodzi tu o I, a nie LUB, zatem pobrane zostaną tylko zakładki otagowane dwoma tagami, a nie jednym z nich), łącząc tagi znakiem plus (+):

http://del.icio.us/feeds/json/Dziudek/javascript+webdesign?raw&count=20

My nie skorzystamy jednak z opcji raw bo wygodniej nam będzie skorzystać z obiektu Delicious, bo zamiast jednego pola w naszym wypadku będzie miał dwa (o tym niedługo). Gdybyście jednak chcieli pobierać same zakładki to polecam skorzystanie z opcji callback, która określa funkcję jaka pobierze dane (UWAGA: na stronie oficjalnej dokumentacji nie uwzględniono tej opcji, a ja wpadłem na to, że ona istnieje dzięki temu, że wszystkie inne dane przy pobieraniu ją udostępniają ;) ). Dlaczego warto wtedy skorzystać z opcji raw i callback ? Bo nie tworzymy zbędnego obiektu Delicious, który ma tylko jedno pole, co jest moim zdaniem bezsensowne, ale w wypadku pobierania zakładek i tagów nabiera to już sensu i samo w sobie porządkuje dane :)

Fani

Przy pobieraniu "fanów" danego użytkownika nie ma większych komplikacji - po prostu jest zwracana tablica nicków:

["nick1", "nick2", ... , "nickN"]

pod adresem postaci:

http://del.icio.us/feeds/json/fans/nazwa_uzytkownika

Warto skorzystać z opcji callback, która pozwala na podanie nazwy funkcji, która przetworzy tablicę. Na przykład zapis:

http://del.icio.us/feeds/json/fans/Dziudek?callback=test

Spowoduje pobranie tablicy moich "fanów" i podanie ich jako argument funkcji test. Oczywiście warto pamiętać o podaniu w deklaracji funkcji argumentu odpowiedzialnego za tablicę :)

Sieci

Pobieranie informacji o sieciach do jakich należy użytkownik działa tak samo jak w wypadku fanów jedyna różnica to adres:

http://del.icio.us/feeds/json/network/nazwa_uzytkownika

Oraz fakt, że zamiast loginów mamy nazwy sieci. Reszta zagadnień jest identyczna ;)

Tagi

Ostatnia interesująca nas kwestia to pobieranie tagów. To chyba najbardziej rozbudowany obiekt do pobrania (przynajmniej jeśli chodzi o konfigurację).

Z adresu o składni:

http://del.icio.us/feeds/json/tags/nazwa_uzytkownika

Pobieramy standardowo kod JS zaczynający się od:

if(typeof(Delicious) == 'undefined') Delicious = {}; Delicious.tags = (...)

Gdzie właściwość Delicious.tags jest obiektem w którym nazwa pola to nazwa tagu, a wartość pola to ilość zakładek przypisanych do danego tagu:

{
    "tag1":wartosc,
    "tag2":wartosc,
             ...
    "tagN":wartosc
}

Do dyspozycji mamy kilka opcji - oprócz znanych nam już raw i callback oraz count, mamy jeszcze dwie nowe opcje - atleast, która określa ile co najmniej zakładek musi zawierać dany tag by był wyświetlany oraz opcja sort, która może mieć dwie wartości alpha i count.

Zatem aby pobrać co najwyżej 20 tagów z mojego konta (Dziudek), gdzie będą uwzględniane tylko tagi zawierające co najmniej 2 zakładki, a sortowanie będzie przebiegało według ilości zakładek w tagu zapiszemy:

http://del.icio.us/feeds/json/tags/Dziudek?count=20&atleast=2&sort=count

Zatem założenia mamy gotowe i zdobyliśmy niezbędną wiedzę o danych z del.icio.us, pora przygotować interfejs naszej aplikacji.

Interfejs aplikacji

Ponieważ nie chcę by wpis nabrał monstrualnych rozmiarów (i tak będzie długi), sprawę interfejsu opiszę dość pobieżnie i zwrócę tylko uwagę na najbardziej interesujące nas przy pisaniu skryptu elementu. Najlepiej zresztą moją koncepcję zaprezentuje poniższy obrazek:

Projekt interfejsu naszej aplikacji

W "dymkach" mamy wyróżnione najważniejsze dla nas elementy interfejsu, które wykorzystamy podczas pisania skryptu (właściwie to są to najważniejsze punkty odniesienia).

Jak widzicie aplikacja zyskała nazwę "Przeglądarka delicji" dość dobrze oddająca koncepcję aplikacji. Projekt interfejsu nie zawiera właściwie grafiki poza favikonkami

Kod aplikacji

Pora przejść do najważniejszej dla nas w tym wszystkim rzeczy - pisania skryptu :) Na początku dodam, że pozwoliłem sobie skracać kod poprzez stosowanie różnych skróconych form instrukcji warunkowych, po to by skrócić kod aplikacji. Momentami kod może się wydać skomplikowany, ale postaram się wszystko dokładnie wytłumaczyć - aplikacja na końcu liczy około 180 linijek kodu (około 7.5kB).

Nasza mała aplikacja będzie pojedynczą klasą z kilkoma metodami.

Na początek tworzymy sobie klasę deliciousReader i definiujemy zdarzenie domready obiektu window:

var deliciousReader = new Class({   
    initialize: function(){}
});

window.addEvent("domready",function(){});

Chwyt z $this

Z reguły tworząc klasę nie wywołujemy jej metod w innych metodach tej samej klasy. Jednak w naszym wypadku będzie to wręcz niezbędne. Napotkamy przy tym mały problem - otóż umieszczenie w kodzie metody wywołania:

this.nazwaInnejMetody();

Skończy się błędem w konsoli JS. Jak to ominąć ? Ja wszelkie problemy z odwoływaniem się do właściwości obiektu jakim jest klasa w MooTools załatwiam poniższym kodem umieszczonym w metodzie initialize:

$this = this;

W ten sposób tworzymy globalne odwołanie to klasy deliciousReader. I po umieszczeniu tego kodu możemy do woli wywoływać metody w metodach, oczywiście zamiast:

this.nazwaMetody();

piszemy:


$this.nazwaMetody();

Inicjalizacja aplikacji

Aby "uruchomić" naszą aplikację wystarczy zapis:

new deliciousReader();

w zdarzeniu domready obiektu window.

Jednak my będziemy potrzebowali w pewnym momencie dostępu do obiektu naszej aplikacji więc zapisujemy:

window.addEvent("domready",function(){
    dReader = new deliciousReader();
});

Aby od razu ukończyć sekcję obsługi zdarzenia domready możemy dodać jeszcze inicjalizację klasy SmoothScroll (da nam to efekt płynnego przewijania pomiędzy kotwicami dokumentu):

window.addEvent("domready",function(){
    new SmoothScroll();
    dReader = new deliciousReader();
});

I powyższego kodu już modyfikować nie będziemy - pora na metodę initialize naszej aplikacji - na pewno znajdzie się w niej wspomniana już linijka:

$this = this;

Oprócz tego musimy ukryć div "pobraneDane", stworzyć obiekt Delicious zawierający puste pola oraz stworzyć tablicę info zawierającą komunikaty dla użytkownika:

initialize: function(){
        $("pobraneDane").setStyle("display","none");
        Delicious = {posts:null,fans:null,networks:null,tags:null};
        $this = this;
        this.info = [];
},

Teraz musimy zapełnić naszą tablicę komunikatami - należy stworzyć elementy paragrafów i wypełnić je tekstem oraz umieścić pierwszy komunikat w dokumencie po odpowiednim nagłówku:

for(var i=1;i<4;i++) this.info[i] = new Element("p",{"class":"informacja"});   
this.info[1].appendText("Brak załadowanych danych");
this.info[2].setHTML("Trwa ładowanie <img src=\"loader.gif\" alt=\"Ładowanie\" />");
this.info[3].appendText("Uwaga ! Podany użytkownik nie istnieje lub nie ma żadnych danych spełniających podane kryteria.");
this.info[1].injectAfter($ES("h2","div#app")[1]);

Powyższy kod w pętli tworzy trzy paragrafy i zapisuje je w tablicy info na pozycjach od 1 do 3. Następnie do każdego paragrafu dodawany jest tekst lub kod HTML. Na koniec pierwszy komunikat jest umieszczany po drugim nagłówku h2 w elemencie div "app".

Przy inicjalizacji aplikacji musimy wykonać jeszcze dwie operacje - wypełnić pole select odpowiednią ilością znaczników option (chyba, że komuś się chce tworzyć 100 znaczników ręcznie ;) ) i dodać zdarzenie uruchamiające pobieranie danych do przycisku pobierzDane:

for(var i=1;i<=100;i++) new Element("option",((i==15) ? {"value":i,"selected":"selected"} : {"value":i})).appendText(i).injectInside($("iloscZakladek"));
        
$("pobierzDane").addEvent("click", this.pobierzDane);

W pierwszej linijce (z pętlą) pozwoliłem sobie zastosować skróconą wersję warunku if po to by nie powielać zbędnie kodu - prezentowana pętla tworzy znaczniki option o wartościach od 1 do 100, a w wypadku 15 pozycji dodaje także atrybut selected (domyślna wartość).

Druga linijka dodaje do przycisku pobierzDane zdarzenie onclick, które wywoła metodę pobierzDane (niedługo o niej więcej). I to już cała metoda inicjalizująca naszą aplikację, całość wygląda następująco:

initialize: function(){
        $("pobraneDane").setStyle("display","none");
        Delicious = {posts:null,fans:null,networks:null,tags:null};
        $this = this;
        this.info = [];
       
        for(var i=1;i<4;i++) this.info[i] = new Element("p",{"class":"informacja"});   
        this.info[1].appendText("Brak załadowanych danych");
        this.info[2].setHTML("Trwa ładowanie <img src=\"loader.gif\" alt=\"Ładowanie\" />");
        this.info[3].appendText("Uwaga ! Podany użytkownik nie istnieje lub nie ma żadnych danych spełniających podane kryteria.");
        this.info[1].injectAfter($ES("h2","div#app")[1]);
       
        for(var i=1;i<=100;i++) new Element("option",((i==15) ? {"value":i,"selected":"selected"} : {"value":i})).appendText(i).injectInside($("iloscZakladek"));
       
        $("pobierzDane").addEvent("click", this.pobierzDane);
},

Pozostałe metody aplikacji

Jak już widzieliście nasza aplikacja będzie posiadać metodę pobierzDane - pozwolę sobie na stworzenie małego podziału pozostałych metod naszej aplikacji na dwie grupy - będą to metody główne - pobierzDane i pokazDane (nazwy chyba mówią same za siebie) oraz metody pomocnicze (wykonujące dodatkowe operacje) - sprawdzKonfiguracje (sprawdza poprawność wypełnienia formularza), fani i sieci (dwie króciutkie metody związane z wczytaniem danych) oraz metoda czyscDane (przygotowuje aplikację do załadowania nowych danych).

Zaczniemy od metod pomocniczych, bo ich napisanie jest potrzebne do tworzenia metod głównych.

sprawdzKonfiguracje

Metoda sprawdzKonfiguracje będzie sprawdzała czy to co podał użytkownik pozwala na pobranie danych. Na początku tworzymy zmienną daneStatus, która będzie ciągiem znaków uzupełnianym o ewentualne informacje o błędach wypełnienia formularza. Na początku kod metody wygląda następująco:


sprawdzKonfiguracje: function(){
        var daneStatus = "";
},

Dodajemy walidację podanej nazwy użytkownika - czy w ogóle coś wpisano, jeżeli nie to do zmiennej daneStatus dodajemy odpowiedni komunikat:

if($("userLogin").getValue() == "") daneStatus += "- Nie podano nazwy użytkownika.\n";

Sprawdzamy czy użytkownik wybrał choć jeden z checkboksów określających jakie dane zostaną pobrane:

if(!($("zakladki").getValue() == "on" ||
     $("fani").getValue() == "on" ||
     $("sieci").getValue() == "on" ||
     $("tagi").getValue() == "on")) daneStatus += "- Nie wybrałeś żadnych danych do pobrania.\n";

Sprawdzamy czy pola z minimalną ilością zakładek w tagu i z ilością tagów do pobrania zawierają ciąg znaków będący liczbą:

if(isNaN($("minIloscZakladek").getValue())) daneStatus += "- Podania minimalna ilość zakładek nie jest liczbą.\n";
if(isNaN($("iloscTagow").getValue())) daneStatus += "- Podania ilość tagów nie jest liczbą.";

Jak widać po każdej operacji może zostać dodany odpowiedni komunikat. Na koniec metoda zwraca zmienną daneStatus więc jej kod w całości wygląda następująco:

sprawdzKonfiguracje: function(){
        var daneStatus = "";
        if($("userLogin").getValue() == "") daneStatus += "- Nie podano nazwy użytkownika.\n";
        if(!($("zakladki").getValue() == "on" ||
            $("fani").getValue() == "on" ||
            $("sieci").getValue() == "on" ||
            $("tagi").getValue() == "on")) daneStatus += "- Nie wybrałeś żadnych danych do pobrania.\n";   
        if(isNaN($("minIloscZakladek").getValue())) daneStatus += "- Podania minimalna ilość zakładek nie jest liczbą.\n";
        if(isNaN($("iloscTagow").getValue())) daneStatus += "- Podania ilość tagów nie jest liczbą.";
        return daneStatus;
},

Metody fani i sieci

Te dwie metody są bardzo krótkie bo składają się z zaledwie jednej linijki:

fani: function(json){Delicious.fans = json;},

oraz:

sieci: function(json){Delicious.networks = json;},

Są nam one potrzebne do zapisania w obiekcie Delicious danych, które są zwracane jako tablica i oferują jedynie podanie nazwy funkcji do jakiej mają zostać przekazane dane.

czyscDane

Ostatnia z naszych pomocniczych metod - ma za zadanie przygotować aplikację do wczytania nowych danych.

Metoda ta musi usunąć wszystkie elementy list, które zostały poprzednio wczytane, oraz ukryć nagłówki i divy zawierające pobrane dane. Do tego jeszcze trzeba usunąć z sekcji head pobrane poprzedni pliki danych JSON i przywrócić obiekt Delicious do stanu pierwotnego znanego z metody initialize.

Operację czyszczenia danych (poza czyszczeniem obiektu Delicious) wykonamy w pętli each, bo występują tam elementy wspólne:

czyscDane: function(){
        [".zakladki", ".fani", ".sieci", ".tagi"].each(function(el){
            $ES("li", "div"+el).each(function(el){el.remove();});
            for(var i=0;i<2;i++) $ES(el)[i].setStyle("display","none");
            if($("json"+el)) $("json"+el).remove();
        });
}

Tablica:

[".zakladki", ".fani", ".sieci", ".tagi"]

Zawiera nazwy klas niezbędne do wykonania wszystkich operacji - linijka:

$ES("li", "div"+el).each(function(el){el.remove();});

Usuwa wszystkie elementy list, a linijka:

for(var i=0;i<2;i++) $ES(el)[i].setStyle("display","none");



Ukrywa odpowiednie nagłówki i divy. Do tego w linijce:


if($("json"+el)) $("json"+el).remove();

Usuwamy niepotrzebne już znaczniki script - jak widzicie będą one posiadać identyfikatory postaci:

json.typDanych

Na koniec czyścimy obiekt Delicious:

Delicious = {posts:null,fans:null,networks:null,tags:null};

W efekcie metoda czyscDane prezentuje się następująco:

czyscDane: function(){
        [".zakladki", ".fani", ".sieci", ".tagi"].each(function(el){
            $ES("li", "div"+el).each(function(el){el.remove();});
            for(var i=0;i<2;i++) $ES(el)[i].setStyle("display","none");
            if($("json"+el)) $("json"+el).remove();
        });
       
        Delicious = {posts:null,fans:null,networks:null,tags:null};
}

(jest to ostatnia metoda klasy deliciousReader więc po nawiasie klamrowym nie ma już przecinka).

Pora na drugą grupę metod - metody główne.

pobierzDane

Metoda pobierzDane jak sama nazwa wskazuje będzie pobierała dane według zadanych parametrów. Podstawą jej działania jest sprawdzenie czy podane przez użytkownika aplikacji dane są poprawne - w wypadku gdy wszystko jest w porządku omawiana już metoda sprawdzKonfiguracje zwróci pusty ciąg znaków:

pobierzDane: function(){
        if($this.sprawdzKonfiguracje() == ""){
            // jeżeli wszystkie dane są poprawne
        }
        else alert("Wystąpiły następujące błędy:\n" + $this.sprawdzKonfiguracje());
},

Jak widać w wypadku gdy wystąpią błędy pojawi się alert o odpowiedniej treści.

Teraz pora na to co wykona się jeżeli dane będą poprawne. Na pewno poprzednio pobrane dane muszą zostać wyczyszczone, a dodatkowo warto stworzyć zmienną przechowującą wartość pola userLogin, bo jest to zdecydowanie najczęście wykorzystywana w tej części kodu zmienna:

pobierzDane: function(){
        if($this.sprawdzKonfiguracje() == ""){
            $this.czyscDane();
            uLogin = $("userLogin").getValue();
        }
        else alert("Wystąpiły następujące błędy:\n" + $this.sprawdzKonfiguracje());
},

Teraz kolej na samo pobieranie danych - w wypadku każdego typu danych na początku musimy sprawdzić czy w ogóle zaznaczono pobieranie danych tego typu:

if($("zakladki").getValue() == "on"){}

Ponieważ w wypadku zakładek można określić kilka różnych parametrów pobierania danych tworzymy zmienną query przechowującą te parametry w odpowiedniej formie:


var query = "";

Jeżeli określono tagi zakładek to musimy po nazwie użytkownika zacząć od znaku backslash i umieścić po nim tagi zakładek :

query += ($("tagiZakladek").getValue() !== "") ? "/" + $("tagiZakladek").getValue() : "";

Następnie po znaku zapytania (niezależnie czy tagi zakładek istnieją czy nie) musimy podać ilość pobieranych zakładek:

query += "?count=" + $("iloscZakladek").getValue();

na koniec dzięki klasie Asset pobieramy plik JS i jako zmienne w adresie podajemy uLogin (nazwa użytkownika) i zmienną query. Oczywiście określamy tez identyfikator znacznika script:

new Asset.javascript('http://del.icio.us/feeds/json/'+uLogin+query, {id:"json.zakladki"});

W wypadku pobierania danych o fanach i sieciach użytkownikach sprawa jest prosta:

if($("fani").getValue() == "on") new Asset.javascript('http://del.icio.us/feeds/json/fans/'+uLogin+'?callback=dReader.fani', {id:"json.fani"});
if($("sieci").getValue() == "on") new Asset.javascript('http://del.icio.us/feeds/json/network/'+uLogin+'?callback=dReader.sieci', {id:"json.sieci"});

W obu wypadkach jest oczywiście sprawdzane czy użytkownik chce pobrać dane informacje, a potem pobierany jest plik gdzie zmienia się tylko nazwa użytkownika (uLogin) oraz nazwa funkcji - dReader.fani i dReader.sieci - po to właśnie w zdarzeniu domready obiektu window stworzyliśmy zmienną dReader - odwołanie się do klasy, a nie jej instancji wygenerowałoby błąd.

Pozostaje jeszcze pobrać tagi:

if($("tagi").getValue() == "on"){
    var query = "?";
    query += ($("minIloscZakladek").getValue() !== "") ? "atleast="+$("minIloscZakladek").getValue() + "&" : "";
    query += ($("iloscTagow").getValue() !== "") ? "count="+$("iloscTagow").getValue() + "&" : "";
    query += "sort="+$("sortowanieTagow").getValue();
    new Asset.javascript('http://del.icio.us/feeds/json/tags/'+uLogin+query,{id:"json.tagi"});
}

Jak widać sprawa jest trochę bardziej złożona niż w wypadków pobierania zakładek - ciąg query zawsze zaczyna się w tym wypadku od znaku zapytania. Potem wykonujemy sprawdzanie czy określono minimalną ilość zakładek dla tagu. Na końcu ciągu zawsze znajduje się zmienna określająca typ sortowania. Potem już tylko wczytanie pliku JavaScript na zasadzie podobnej jak w wypadku pobierania zakładek.

I w końcu umieszczamy wywołanie metody prezentującej wczytane dane:

$this.pokazDane();

Całość metody wygląda następująco:

pobierzDane: function(){
        if($this.sprawdzKonfiguracje() == ""){
            $this.czyscDane();
            uLogin = $("userLogin").getValue();
           
            if($("zakladki").getValue() == "on"){
                var query = "";
                query += ($("tagiZakladek").getValue() !== "") ? "/" + $("tagiZakladek").getValue() : "";
                query += "?count=" + $("iloscZakladek").getValue();   
                new Asset.javascript('http://del.icio.us/feeds/json/'+uLogin+query, {id:"json.zakladki"});
            }
           
            if($("fani").getValue() == "on") new Asset.javascript('http://del.icio.us/feeds/json/fans/'+uLogin+'?callback=dReader.fani', {id:"json.fani"});
           
            if($("sieci").getValue() == "on") new Asset.javascript('http://del.icio.us/feeds/json/network/'+uLogin+'?callback=dReader.sieci', {id:"json.sieci"});
           
            if($("tagi").getValue() == "on"){
                var query = "?";
                query += ($("minIloscZakladek").getValue() !== "") ? "atleast="+$("minIloscZakladek").getValue() + "&" : "";
                query += ($("iloscTagow").getValue() !== "") ? "count="+$("iloscTagow").getValue() + "&" : "";
                query += "sort="+$("sortowanieTagow").getValue();
                new Asset.javascript('http://del.icio.us/feeds/json/tags/'+uLogin+query, {id:"json.tagi"});
            }
           
            $this.pokazDane();
        }
        else alert("Wystąpiły następujące błędy:\n" + $this.sprawdzKonfiguracje());
},

pokazDane

Dochodzimy już do końca pisania naszej aplikacji - pozostała nam jedna metoda, która jednocześnie zajmuje około połowy kodu aplikacji. Metoda pokazDane jest wywoływana przez metodę pobierzDane i jej zadanie polega na sprawdzeniu czy pobrane dane są puste czy nie oraz stworzeniu efektu podobnego do tego jaki daje zdarzenie onload dla skryptów. Oczywiście po tym wszystkim następuje wygenerowanie kodu prezentowanego jako pobrane dane.

Metodę pokazDane rozpoczynamy od ukrycia poprzednich komunikatów i zaprezentowania tego z informacją o ładowaniu danych:

pokazDane: function(){
        $this.info[1].setStyle("display","none");
        $this.info[3].setStyle("display","none");
        $this.info[2].setStyle("display","block").injectAfter($ES("h2","div#app")[1]);
},

Teraz będziemy musieli się trochę namęczyć z tego powodu, że dla Asset.javascript Internet Explorer nie obsługuje zdarzenia onload. Dlatego musimy stworzyć funkcję, która uzupełni ten brak. Oczywiście możnaby stworzyć wersję dla IE i dla alternatywnych przeglądarek, ale po co dodawać zbędny kod ? Nasz substytut zdarzenia onload dla plików JS będzie oparty o funkcję wykonującą się co 100ms, która będzie sprawdzała czy dane zostały załadowane - stąd właściwości obiektu Delicious mają wartość null - w wypadku załadowania pliku będą tablicami i obiektami (nawet pustymi, ale zawsze będą czymś innym niż null) i w wypadku załadowania danych okresowe wykonywanie funkcji zostanie przerwane.

Zaczynamy od stworzenia podstaw (kod z metody pokazDane):

timer = (function(){
     var load = [false,false,false,false];
}).periodical(100);

Zmienna load o wartości false na każdej pozycji tablicy oznacza, że dane (4 pliki) nie zostały załadowane. Musimy teraz sprawdzić, które elementy mają być załadowane (według checkboksów) i sprawdzać typ odpowiadających im właściwości obiektu Delicious:

timer = (function(){
      var load = [false,false,false,false];
           
      if($("zakladki").getValue() == "on"){
            ($type(Delicious.posts) == "array") ? load[0]= true : load[0]=false
      }else load[0]=true;
           
      if($("fani").getValue() == "on"){
            ($type(Delicious.fans) == "array") ? load[1] = true : load[1] = false;
      }else load[1]=true;
           
      if($("sieci").getValue() == "on"){
            ($type(Delicious.networks) == "array") ? load[2] = true : load[2] = false;
      }else load[2]=true;
           
      if($("tagi").getValue() == "on"){
            ($type(Delicious.tags) == "object") ? load[3] = true : load[3] = false;
      }else load[3]=true;
}).periodical(100);

Jak widzimy sprawdzanie jest odpowiedzią na dwa pytania - przykładowo: czy użytkownik chce załadować dane o zakładkach ? Jeżeli tak to czy obiekt Delicious.post jest tablicą ? Jeżeli tak to ustaw zmienną load na true inaczej ustaw zmiennej load wartość false. Jeżeli użytkownik nie chce ładować jakichś danych to automatycznie mają one wartość true w tablicy load.

Teraz pora na drugie i ostatnie sprawdzanie danych - sprawdzamy czy wszystkie dane są załadowane, a jeżeli tak to przerywamy okresowe wykonywanie funkcji (poniższy kod to dalsza treść funkcji przypisanej do zmiennej timer):

if(load[0] && load[1] && load[2] && load[3]){
        $clear(timer);
}

Zmienna loaded posłuży do sprawdzenia czy chociaż jeden z załadowanych plików ma jakieś dane, a pętla for posłuży sprawdzeniu ilosci elementów obiektu Delicious.tags (możnaby to zrobić bardziej po ludzku dzięki metodzie toSource - wtedy sprawdzalibyśmy czy zwrócony ciąg jest różny od "({})", ale jak zwykle przeszkodą jest IE):

var k = 0;
for(var p in Delicious.tags){k++}
loaded = (
     (($type(Delicious.posts)=="array") ? (Delicious.posts.length > 0) : false) ||
     (($type(Delicious.fans)=="array") ? (Delicious.fans.length > 0) : false) ||
     (($type(Delicious.networks)=="array") ? (Delicious.networks.length > 0) : false) ||
     (($type(Delicious.tags)=="object") ? ((k !== 0) ? true : false) : false)
) ? true : false;  

Jak widać jest to dość złożone wyrażenie, bo zawiera warunki w treści warunku - krótko mówiąc sprawdzane jest czy dany element pobranych danych został pobrany i czy jego długość jest większa od 0. Jeżeli dane informacje zostały pobrane ale mają długość równą 0 lub są pustym obiektem (w wypadku tagów - ilość właściwości obiektu równa 0) to znaczy, że brak informacji spełniających dane kryteria, a gdy wszystkie pobrane dane są puste oznacza to albo złe kryteria, albo brak konta o podanej nazwie.

Dlatego wypiszemy odpowiedni komunikat w wypadku gdy pobrane dane są puste lub wypiszemy pobrane dane gdy pobrane informacje nie są puste:

if(!loaded){
      $this.info[2].setStyle("display","none");
      $this.info[3].setStyle("display","block").injectAfter($ES("h2","div#app")[1]);
}
else{}

W wypadku warunku else rozpoczynamy wypisywanie danych. Jeżeli chodzi o tagi, fanów i sieci schemat jest bardzo podobny i wygląda następująco:

Sprawdzenie czy użytkownik chciał pobierać dany typ danych - jeżeli tak to - stwórz zmienną iteracyjną - pokaż nagłówek i div - w pętli przetwórz dane tworząc elementy listy, link, umieszczając link w elemencie listy i sam element listy w liście zwiększając przy tym zmienną iteracyjną o 1 - dla pierwszej silnej emfazy ustaw tekst będący ilością pobranych elementów, dla drugiej silnej emfazy ustaw jako treść nazwę użytkownika. Poniżej kod dla tych trzech typów danych:

if($("tagi").getValue() == "on"){
     var j = 0;
     for(var i=0;i<2;i++) $ES(".tagi")[i].setStyle("display","block");
     for(var i in Delicious.tags){
        var li = new Element("li");
        var a = new Element("a",{href:"http://del.icio.us/"+uLogin+"/"+i}).appendText(i+"("+Delicious.tags[i]+") ");
        $E("div.tagi ul").adopt(li.adopt(a));
        j++;
     }
     $ES("h3.tagi strong")[0].setHTML(j);
     $ES("h3.tagi strong")[1].setHTML(uLogin);
}
                   
if($("fani").getValue() == "on"){
     var j = 0;
     for(var i=0;i<2;i++) $ES(".fani")[i].setStyle("display","block");
     Delicious.fans.each(function(el){
        var li = new Element("li");
        var a = new Element("a",{href:"http://del.icio.us/"+el}).appendText(el);
        $E("div.fani ul").adopt(li.adopt(a));
         j++;
     });
     $ES("h3.fani strong")[0].setHTML(j);
     $ES("h3.fani strong")[1].setHTML(uLogin);
}
                   
if($("sieci").getValue() == "on"){
     var j = 0;
     for(var i=0;i<2;i++) $ES(".sieci")[i].setStyle("display","block");
     Delicious.networks.each(function(el){
         var li = new Element("li");
         var a = new Element("a",{href:"http://del.icio.us/"+el}).appendText(el);
         $E("div.sieci ul").adopt(li.adopt(a));
         j++;
     });
     $ES("h3.sieci strong")[0].setHTML(j);
     $ES("h3.sieci strong")[1].setHTML(uLogin);
}

Pora na zakładki - sprawa jest bardziej złożona, bo oprócz linka musimy stworzyć paragraf z notatką o linku i pobrać favikonkę jeżeli oczywiście istnieje.

W tym wypadku nie potrzebujemy zmiennej iteracyjnej, za to tak jak wcześniej pokazujemy odpowiedni nagłówek i div. Potem dla silnej emfazy w nagłówku ustawiamy jako tekst nazwę użytkownika. Na pobranych danych wykonujemy następujące operacje:

  • tworzymy element listy,
  • tworzymy obrazek z tłem świadczącym o braku favikony,
  • tworzymy drugi obrazek z domniemaną favikoną strony - jednak nie wszystkie witryny mają własną favikonę lub jest ona pod innym adresem - dlatego dla drugiego obrazka tworzymy zdarzenie onload - jeżeli obrazek istnieje pod podanym adresem i zostanie załadowany to jest podmieniany z pierwszym obrazkiem,
  • tworzymy link,
  • tworzymy paragraf z notatką o linku - właściwość n nie jest obowiązkowa więc sprawdzamy czy jest zdefiniowana (by uniknąć pokazywania tekstu undefined),
  • umieszczamy pierwszy obrazek, link i paragraf w elemencie listy, a sam element listy umieszczamy w liście uporządkowanej:

if($("zakladki").getValue() == "on"){
     for(var i=0;i<2;i++) $ES(".zakladki")[i].setStyle("display","block");
     $E("h3.zakladki strong").setHTML(uLogin);
     Delicious.posts.each(function(item,i){
         var li = new Element("li");
         var img1 = new Element("img",{src:"blank.png", width:16, height:16, alt:"favicon"});
         var img2 = new Element("img",{src: item.u.split('/').splice(0,3).join('/') + '/favicon.ico', alt:"favicon"}).addEvent("load",function(){
              img1.setProperty("src", img2.getProperty("src"));
         });
         var a = new Element("a",{href:item.u}).appendText(item.d);
         var p = new Element("p").appendText(($defined(item.n) ? item.n : ""));
         $E("div.zakladki ol").adopt(li.adopt([img1,a,p]));
     });
}

Cały kod metody pokazDane wygląda następująco:

pokazDane: function(){
        $this.info[1].setStyle("display","none");
        $this.info[3].setStyle("display","none");
        $this.info[2].setStyle("display","block").injectAfter($ES("h2","div#app")[1]);
       
        timer = (function(){
            var load = [false,false,false,false];
           
            if($("zakladki").getValue() == "on"){
                ($type(Delicious.posts) == "array") ? load[0]= true : load[0]=false
            }else load[0]=true;
           
            if($("fani").getValue() == "on"){
                ($type(Delicious.fans) == "array") ? load[1] = true : load[1] = false;
            }else load[1]=true;
           
            if($("sieci").getValue() == "on"){
                ($type(Delicious.networks) == "array") ? load[2] = true : load[2] = false;
            }else load[2]=true;
           
            if($("tagi").getValue() == "on"){
                ($type(Delicious.tags) == "object") ? load[3] = true : load[3] = false;
            }else load[3]=true;
           
            if(load[0] && load[1] && load[2] && load[3]){
                $clear(timer);
                loaded = (
                            (($type(Delicious.posts)=="array") ? (Delicious.posts.length > 0) : false) ||
                            (($type(Delicious.fans)=="array") ? (Delicious.fans.length > 0) : false) ||
                            (($type(Delicious.networks)=="array") ? (Delicious.networks.length > 0) : false) ||
                            (($type(Delicious.tags)=="object") ? (Delicious.tags[0]) : false)
                        ) ? true : false;
                       
                if(!loaded){
                    $this.info[2].setStyle("display","none");
                    $this.info[3].setStyle("display","block").injectAfter($ES("h2","div#app")[1]);
                }
                else{
                    $this.info[2].setStyle("display","none");
                    $("pobraneDane").setStyle("display","block");
                   
                    if($("zakladki").getValue() == "on"){
                        for(var i=0;i<2;i++) $ES(".zakladki")[i].setStyle("display","block");
                        $E("h3.zakladki strong").setHTML(uLogin);
                        Delicious.posts.each(function(item,i){
                            var li = new Element("li");
                            var img1 = new Element("img",{src:"blank.png", width:16, height:16, alt:"favicon"});
                            var img2 = new Element("img",{src: item.u.split('/').splice(0,3).join('/') + '/favicon.ico', alt:"favicon"}).addEvent("load",function(){
                                img1.setProperty("src", img2.getProperty("src"));
                            });
                            var a = new Element("a",{href:item.u}).appendText(item.d);
                            var p = new Element("p").appendText(($defined(item.n) ? item.n : ""));
                            $E("div.zakladki ol").adopt(li.adopt([img1,a,p]));
                        });
                    }
                   
                    if($("tagi").getValue() == "on"){
                        var j = 0;
                        for(var i=0;i<2;i++) $ES(".tagi")[i].setStyle("display","block");
                        for(var i in Delicious.tags){
                            var li = new Element("li");
                            var a = new Element("a",{href:"http://del.icio.us/"+uLogin+"/"+i}).appendText(i+"("+Delicious.tags[i]+") ");
                            $E("div.tagi ul").adopt(li.adopt(a));
                            j++;
                        }
                        $ES("h3.tagi strong")[0].setHTML(j);
                        $ES("h3.tagi strong")[1].setHTML(uLogin);
                    }
                   
                    if($("fani").getValue() == "on"){
                        var j = 0;
                        for(var i=0;i<2;i++) $ES(".fani")[i].setStyle("display","block");
                        Delicious.fans.each(function(el){
                            var li = new Element("li");
                            var a = new Element("a",{href:"http://del.icio.us/"+el}).appendText(el);
                            $E("div.fani ul").adopt(li.adopt(a));
                            j++;
                        });
                        $ES("h3.fani strong")[0].setHTML(j);
                        $ES("h3.fani strong")[1].setHTML(uLogin);
                    }
                   
                    if($("sieci").getValue() == "on"){
                        var j = 0;
                        for(var i=0;i<2;i++) $ES(".sieci")[i].setStyle("display","block");
                        Delicious.networks.each(function(el){
                            var li = new Element("li");
                            var a = new Element("a",{href:"http://del.icio.us/"+el}).appendText(el);
                            $E("div.sieci ul").adopt(li.adopt(a));
                            j++;
                        });
                        $ES("h3.sieci strong")[0].setHTML(j);
                        $ES("h3.sieci strong")[1].setHTML(uLogin);
                    }
                }
            }
        }).periodical(100);
},

I gotowe :)

Kod naszej aplikacji jest gotowy. Po napisaniu tylu linijek kodu możemy podziwiać aplikację w akcji.

Co dalej ?

Napisaliśmy podstawę aplikacji, przydałoby się jeszcze parę dodatków, które możecie wykonać samodzielnie :

  • zapamiętywanie 10 ostatnio wpisanych nazw użytkowników (cookies),
  • zapamiętywanie konfiguracji aplikacji (cookies),
  • zmiana rozmiarów tagów w zależności od ilości załadek (na wzór chmury tagów),
  • wykorzystanie pluginu Tips do pokazywania informacji o polach w konfiguracji,
  • w wypadku braku danych jakiegoś rodzaju, pokazywanie komunikatu o ich braku (obecnie jest puste miejsce).

Jak widzicie sporo jeszcze można poprawić - jeżeli aplikacja się spodoba to w wolnym czasie (kiedyś się znajdzie XD) zaimplementuję wspomniane poprawki :) W tej opisywanej wersji było to niemożliwe bo kod i sam wpis nabrałyby ogromnych rozmiarów. (mam nadzieję, że ktoś w ogóle do końca dotarł ;) ).

Komentarze do wpisu "MooTools w praktyce - del.icio.us reader":

1. reod napisał(a):
20 września 2007, 13:42:05

Ło, piękny tutek.

2. Koval napisał(a):
20 września 2007, 14:06:24

leee nie dziala pod operą mobile… ale i tak gratuleje

3. Amused Monkey napisał(a):
20 września 2007, 14:07:37

Ty jesteś nienormalny :-D

Dzięki!

4. Dziudek napisał(a):
20 września 2007, 14:10:01

@reod – w sumie to mini-książka XD Chociaż chyba w grafice 3D się podobnej długości trafiają :)

@Koval – chyba za dużo byś chciał XD

@Amused Monkey – czekałem kiedy ktoś to powie XD Już w sumie po ukończeniu kursu MooTools się takiego komentarza spodziewałem XD Jak widać dopiero długi wpis okazał się koronnym dowodem XD Ale mam zdrowieć czy nie :>

5. Amused Monkey napisał(a):
20 września 2007, 14:22:56

Nie zdrowiej, ale przenieś się na jakiś inny temat, serio. JS już znamy :P

Może bazy danych, co? ;)

6. Dziudek napisał(a):
20 września 2007, 14:40:04

@Amused Monkey – hmmm… ale ja uwielbiam JS :) A poza tym bazy danych mnie nie pociągają ;) To według mnie nudny i głównie teoretyczny temat, a w JS coś się rusza, reaguje i generalnie jest z tego pożytek :D PHP jest znowu oklepanym tematem, a o CSS już i tak niewiele odkrywczego da się napisać :) O MooTools pisze się niewiele w sumie (przynajmniej w Polsce) i dlatego tak namiętnie o nim piszę :)

7. Piechuła napisał(a):
20 września 2007, 14:54:04

jesteś maniakiem :)

8. Dziudek napisał(a):
20 września 2007, 14:56:28

@Piechuła – w sumie pasuje mi to :D BTW dłużej chyba siedziałem nad tym, żeby ta mała aplikacja miała sensowny minimalistyczny wygląd niż nad samym skryptem (przynajmniej skryptem w wersji dla normalnych przeglądarek (!==IE)) ;D

9. coldpeer napisał(a):
21 września 2007, 15:43:57

„czytnik RSS wymaga użycia PHP.”

czyżby?

10. Dziudek napisał(a):
21 września 2007, 16:28:10

@coldpeer – inaczej – „wymaga użycia języka server-side po to by ominąć zabezpieczenia przeglądarek” ;)

11. Coldpeer napisał(a):
21 września 2007, 16:32:10

;-)

12. Dziudek napisał(a):
21 września 2007, 16:36:03

@coldpeer – po prostu PHP jest najpopularniejszy i dlatego właśnie o nim wspomniałem (i go też użyję w następnym skrypcie opisywanym w artykule ;) )

13. Coldpeer napisał(a):
21 września 2007, 16:37:15

rozumiem, ale stwierdzenie „trzeba użyć PHP” jest nie na miejscu ;) raczej „można np. użyć PHP”

14. Dziudek napisał(a):
21 września 2007, 16:39:39

@Coldpeer – no w sumie tak, ale to są szczegóły nie mające wpływu na całość artykułu – jak znajdę w nocy czas to naniosę poprawki, bo teraz nie za bardzo mam siłę ;]

15. Dziudek napisał(a):
22 września 2007, 12:15:51

@Cooldpeer – poprawki naniesione ;] [co prawda znacznik DEL słabo widać, ale jest :D]

16. Coldpeer napisał(a):
22 września 2007, 12:17:12

gitara gra ;-) fakt, <del> jest mało wyraźny, może użyć czerwonego przekreślenia? :) swoją drogą, po co <del>, jak można te PHP całkowicie usunąć ;)

17. Dziudek napisał(a):
22 września 2007, 12:26:42

@Coldpeer – ale jak dają te znaczniki do dyspozycji to żal nie skorzystać ;D

18. Bartek napisał(a):
06 sierpnia 2008, 22:15:43

fajne, ale czy nie prościej i szybciej by było rzeczywiście zrobić to za pomocą PHP, a nie mootoolsa?

19. Dziudek napisał(a):
06 sierpnia 2008, 22:17:42

@Bartek – PHP może świetnie uzupełnić tą miniaplikację, ale czy da się to zrobić prościej ? 184 linijki kodu JS jak na ten poziom interaktywności jest moim zdaniem zadowalające ;)

Dodaj komentarz:

Textile Lite włączony ( szczegółowy opis znaczników ):
*strong* | # lista numerowana | * lista wypunktowana | _em_ | __italic__ | "link":http:// | bq. cytat.