Spis treści:

Kategoria:HTMLJavaScript


Ocena na stronie w postaci gwiazdek - JavaScript

Ikona do artykułu o systemie oceny na stronie w postaci gwiazdek

Lajki, gwiazdki, plusy - system ocen na stronach

Internet ma pewną właściwość, która z pewnością ma wpływ na jego popularność. Właściwie każdy może w nim umieszczać informacje o sobie, swoich zainteresowaniach, wypowiadać się na jakiś temat będąc w dużej mierze anonimowym. Taka internetowa komunikacja ma jednak swoją wadę - nie mamy możliwości zbadania reakcji odbiorcy komunikatów czy też czytelników artykułów. W rozmowie na żywo możemy dostrzec niezadowolenie, grymas na twarzy lub uśmiech. W sieci tego nie ma. Tworzy się substytuty w postaci lajków, pozwala się użytkownikom komentować pod artykułami, umieszcza się kolorowe gwiazdki obok przepisów kulinarnych i produktów. Taka ocena jest przydatna dla samego autora, w przypadku artykułów, ale także dla innych użytkowników szukających dobrego produktu.

Dzisiejszy artykuł będzie o gwiazdkowym systemie oceny treści, który najłatwiej pokazać obrazkowo. Popatrzmy na przykład:

Ocena artykułu:

Domyślne ustawienia:

Zadowolenie:

Dobór tematu:

Ocena innych (zablokowane):

Przed przystąpieniem do lektury można się zapoznać z możliwościami tego stosunkowo prostego rozwiązania i sprawdzić, czy będzie ono spełniało pożądane funkcje. Komponent, biorąc pod uwagę bardzo mały rozmiar skryptu i całkowitą niezależność od innych bibliotek, pozwala na sporą konfigurację:

  • można zmienić liczbę gwiazdek lub
  • zmienić sposób wyświetlania z gwiazdki na dowolny inny element HTML,
  • zablokować ocenianie,
  • nałożyć style CSS,
  • obsłużyć zdarzenie zmiany oceny swoim skryptem.

Aby osiągnąć taki efekt, uzyskać prostotę obsługi bez dużej ingerencji w istniejący kod, trzeba było wszystko dobrze obmyślić.

Założenia projektowe

Zanim pokażę implementację i wyjaśnię sposób działania skupię się na wstępnych założeniach. Po pierwsze założyłem, że liczba dostępnych gwiazdek może się różnić: ktoś zechce trzygwiazdkowy system, ktoś, pewnie większość pięciogwiazdkowy, jeszcze inny uzna, że 6 gwiazdek będzie ideałem. Postanowiłem, że liczba ta będzie konfigurowalna. Po drugie, wygląd gwiazdek również może się komuś nie podobać. A gdyby ktoś zamiast gwiazdek zechciał serduszka, kciuki wyciągnięte w górę lub kółeczka? pomyślałem, że zrealizuję to przy pomocy szablonów HTML. Dzięki temu można zdefiniować dowolny wycinek HTML, który będzie reprezentował pojedynczą gwiazdkę. W praktyce elementy te będą raczej proste, ale nie wykluczam złożonych struktur, włącznie z elementami wideo. Teoretycznie wszystko to, co da się zrobić w HTML, może reprezentować jedną gwiazdkę. Po trzecie, być może nie wszyscy będą mieli możliwość oceniania. To znów element konfiguracyjny. Jeżeli zatem chcemy, aby oceniali tylko użytkownicy zalogowani, możemy w prosty sposób zablokować komponent. Czwarty punkt to style. Skoro gwiazdkami są dowolne elementy HTML, dostajemy możliwość wprowadzania dowolnych stylów CSS. Wszystko ze sobą współgra w taki sam sposób, jak na zwykłej stronie. I ostatnie, być może najważniejsze: zmiana wartości w komponencie może być w różny sposób obsługiwana. Jedni zachcą zapamiętać wartość lokalnie, inni skomunikować się z serwerem i zapisać w bazie danych. To również konfiguracja. Warto zwrócić przy okazji uwagę na to, że komponent może się zintegrować z ukrytym polem (element input typu hidden). Uzyskujemy dzięki temu możliwość tradycyjnej obsługi wysyłania formularzy.

Liczba elementów konfiguracyjnych może przerażać, ale nie jest tak źle. W zasadzie żaden z elementów konfiguracyjnych nie jest wymagany, bo:

  • domyślnie ustawi się 5 gwiazdek,
  • domyślnie zaznaczone będą wszystkie,
  • jeżeli nie podamy funkcji obsługującej kliknięcie, stracimy tylko możliwość przechwycenia zdarzenia, w dalszym ciągu będziemy mogli pobrać wartość z elementu input typu hidden,
  • domyślnie użyty zostanie szablon o identyfikatorze stars-default, a jeżeli takiego nie ma, symbol ♥.

Uznałem także, że konfiguracja powinna być prosta i intuicyjna, zgodna z zasadą oddzielenia logiki od interfejsu. Zachowanie się elementu reprezentującego ocenę zostanie zdefiniowane w atrybucie. Atrybut będzie się nazywał data-star, a zawartość będzie umieszczana w znanym formacie JSON. O tym jednak za chwilę.

Zasada działania

W kolejnych podpunktach przedstawię najważniejsze elementy całego rozwiązania. Jak tworzone są gwiazdki, jak obsługiwane jest rysowanie i zdarzenia, jaki wpływ na komponent mają poszczególne ustawienia konfiguracyjne, jak wszystko jest zorganizowane?

Sposób korzystania z komponentu

Dobrą praktyką jest takie pisanie kodu, aby w jak najmniejszym stopniu zależał od innych fragmentów. Można oczywiście tak napisać kod, żeby automatycznie wstawiał gwiazdki we wszystkich komponentach z atrybutem data-star, ale wybrałem nieco inne rozwiązanie. Funkcję instalacyjną umieściłem w swojej przestrzeni (obiekcie) pdprogs, w sekcji rating. Tam znajduje się funkcja apply.

Funkcja ta może przyjąć jeden argument reprezentujący selektor, ale może też być wywołana bez argumentów. Ten selektor definiuje obszar dokumentu HTML, w którym zostaną zainstalowane gwiazdki (przypomnę, że gwiazdki instalowane są dla elementów z atrybutem data-star). Jeżeli selektor zwróci na większą liczbę elementów, wybrany zostanie tylko pierwszyDrobna zmiana w kodzie pozwoli zainstalować komponent oceny we wszystkich elementach. Jeżeli funkcja apply zostanie wywołana bez parametrów, komponenty zostaną zainstalowane w całym dokumencie. Po umieszczeniu skryptu instalacja sprowadzi się do wywołania następującej funkcji:

pdprogs.rating.apply('.ExampleSection');

Implementacja metody instalującej

To proste wywołanie funkcji apply będzie wystarczające do instalacji. Nie zamierzam na tym poprzestać i skupię się na wnętrzu metody. Sam kod tego wnętrza może wyglądać następująco:

function (selector) {
    var element = selector ? document.querySelector(selector) : document,
        components = element.querySelectorAll('[data-star]');
    for (var i = 0; i < components.length; i++) {
        applyContext(components[i]);
    }
}

Najpierw sprawdzamy, co jest naszym obszarem poszukiwań: jeżeli selektor jest ustawiony, wybieramy pierwszy element na podstawie tego selektora, jeżeli argument jest pusty, rozważamy cały dokument. Drugi etap to odnalezienie wszystkich elementów posiadających atrybut data-star. Dla każdego z nich wywołujemy już właściwą metodę instalującą. Każde wywołanie applyContext to jeden zestaw gwiazdek. Rozszerzenie funkcji o możliwość obsługi wielu elementów spełniających warunek selektora nie powinno być problemem.

Odczyt konfiguracji

Odczyt konfiguracji może się wydawać skomplikowany. Definicja znajduje się w atrybucie data-star. Wprawdzie samo pobranie atrybutu sprowadza się do wywołania funkcji getAttribute, ale nie to jest najtrudniejsze. Mamy tam przecież jakiś fragment JSON, dane tekstowe trzeba jakoś zamienić na liczbowe. Zezwoliłem również na przekazywanie własnych obiektów, praktycznie dowolnych, do kontekstu komponentu. Byłoby to trudne, gdyby nie pewien trik. Popatrzmy na następujący fragment kodu:

var extractData = function (element) {
    var attr = element.getAttribute('data-star');
    return Function('return {' + attr + '}')();
}

Pozwoliłem sobie jawnie wywołać konstruktor funkcji i przekazać w nim instrukcję return sklejoną z fragmentem obiektu JSON. Taką funkcję wprost zwracającą obiekt JavaScript wystarczy już tylko wywołać i konfiguracja jest gotowa. Sama funkcja odczytująca będzie dostępna pod postacią zmiennej extractData. Warto zwrócić uwagę na fakt, że konfiguracja może przekazywać dowolne dane do funkcji obsługi zdarzenia pod postacią pola tag. Jeżeli zatem chcemy obsłużyć wszystkie kliknięcia jedną funkcją, możemy spokojnie przekazać dowolne dane i na ich podstawie zdecydować o konkretnej akcji.

Wartości domyślne

Uruchomienie aplikacji z wieloma parametrami nie jest trywialne. Wyobraźmy sobie, że wsiadając do samochodu musimy ustawić fotel, kierownicę, lusterka, ale także wysokość zawieszenia, ułożenie skrzyni biegów, sprzęgło, i kilka innych parametrów. Jazda samochodem byłaby z pewnością bardziej irytująca. Podobnie jest z aplikacją. To dlatego część ustawień jest zdefiniowana odgórnie. Dzięki temu, w najprostszym przypadku, możemy wsiąść i pojechać. Uruchomić aplikację i zobaczyć jakiś efekt. Implementacja jest bardzo prosta. Popatrzmy na poniższy listing:

var data = extractData(component);

data.items = data.items || 5;
data.template = data.template || '#stars-default';
data.default = component.value || data.items;

...
var clone = template ? document.importNode(template.content, true) : document.createTextNode('\u2665');

Metoda extractData została opisana w poprzednim podpunkcie.

Wewnętrzny kontekst komponentu

W celu łatwiejszego zarządzania stanem komponentu z gwiazdkami utworzyłem dodatkowy obiekt. Obiekt ten będzie zawierał wszystkie informacje niezbędne do poprawnego funkcjonowania systemu ocen. Wprawdzie wszystkie podstawowe informacje da się wydobyć z DOM, ale byłoby to po pierwsze nieefektywne, a po drugie mniej czytelne. W tym celu każdy komponent dostaje swój kontekst z kilkoma informacjami:

  • value - bieżąca wartość komponentu,
  • lastHover - bieżąca wartość zaznaczenia (najechanie kursorem myszy zmienia tylko wartość wyświetlaną, dopiero kliknięcie akceptuje wybór),
  • total - łączna liczba gwiazdek,
  • component - komponent, dla którego zainstalowano moduł gwiazdek,
  • items - tablica elementów reprezentujących pojedynczą gwiazdkę.

Popatrzmy na sposób tworzenia kontekstu zaprezentowany poniżej:

var template = document.querySelector(data.template),
    ctx = {
        value: data.default,
        lastHover: data.default,
        total: data.items,
        component: component,
        items: []
    };
for (var j = 0; j < data.items; j++) {
    //utwórz klon szablonu i wstaw go na stronę
    var clone = template ? document.importNode(template.content, true) : document.createTextNode('\u2665'),
        wrapper = document.createElement('span');
    wrapper.appendChild(clone);
    component.parentElement.insertBefore(wrapper, component);
    ctx.items[j] = wrapper;
    ...
}

Większość z tych wartości da się uzyskać później (na przykład total będzie równe items.length), ale wygodniej jest to wszystko mieć w jednym miejscu. Przyda się to podczas obsługi zdarzeń. Zdarzenia są bowiem silnikiem całego systemu oceny.

Dynamiczne elementy DOM

Zanim przejdę do zdarzeń, pochylę się chwilę nad modyfikacjami obiektowego modelu dokumentu (DOM). Algorytm podczas przetwarzania strony dość mocno ingeruje w istniejącą strukturę. Po pierwsze, pobierany jest szablon:

var template = document.querySelector(data.template);

Następnie, jeżeli oczywiście szablon został odnaleziony, tworzona jest kopia jego zawartości:

var clone = template ? document.importNode(template.content, true):(domyślny element)

Jeżeli szablonu nie ma, wykorzystywany jest element domyślny, tworzony przy pomocy funkcji createTextNode obiektu document. Na tym etapie mamy jasno określoną zawartość pojedynczego elementu reprezentującego gwiazdkę. W zasadzie już ten element mógłby być wstawiony do dokumentu, ale postanowiłem zrobić coś więcej. Pojedynczy element będzie opakowany dodatkowym elementem w celu uzyskania większej kontroli nad stylem. Taki element opakowujący może posiadać jakieś style lub atrybuty pozwalające jednoznacznie go zidentyfikować pośród wielu innych elementów na stronie. W moim przypadku jest to span, ale nic nie stoi na przeszkodzie, aby nadać mu jakąś unikatową klasę. Do tego elementu wstawiamy nasz klon:

wrapper.appendChild(clone);

Całość umieszczam w DOM tuż przed elementem input reprezentującym gwiazdkę:

component.parentElement.insertBefore(wrapper, component);

To tyle. Elementy są gotowe do przechwytywania zdarzeń.

Obsługa zdarzeń

Gwiazdki powinny reagować na ruch myszy. Po najechaniu na jedną z gwiazdek ocena powinna się zmienić, po opuszczeniu obszaru oceny wrócić do zaakceptowanej wartości. Kliknięcie powinno ustawiać nową wartość. Mamy zatem minimum trzy zdarzenia, które trzeba obsłużyć:

for (var j = 0; j < data.items; j++) {
...
    wrapper.addEventListener('mouseenter', function (v, ctx) {
        if (ctx.lastHover !== v) {
            ctx.lastHover = v;
            onChanged(ctx);
        }
    }.bind(this, j + 1, ctx));
    wrapper.addEventListener('mouseleave', function (v, ctx) {
        if (ctx.lastHover !== ctx.value) {
            ctx.lastHover = ctx.value;
            onChanged(ctx);
        }
    }.bind(this, j + 1, ctx));
    wrapper.addEventListener('click', function (v, ctx) {
        if (ctx.value !== v) {
            ctx.value = v;
            component.value = v;
            if (data.click)
                data.click.bind(component, { value: v, tag: data.tag })();
        }
    }.bind(this, j + 1, ctx));
...
}

Wszystkie funkcje obsługi zdarzeń wywoływane są z jawnym przekazaniem argumentów. Wywołanie funkcji bind z takim zestawem argumentów sprawi, że wszystkie metody dostaną:

  • this ustawione przez zdarzenie kliknięcia,
  • pierwszy parametr ustawiony na j + 1, to znaczy numer porządkowy gwiazdki,
  • drugi parametr ustawiony na kontekst, o którym pisałem wcześniej.

Przy takim układzie sama obsługa zdarzeń nie powinna być już trudna. Dla zdarzenia najechania kursorem myszy na gwiazdkę, mouseenter, ustawiamy wartość lastHover na numer porządkowy gwiazdki i wywołujemy własne zdarzenie onChanged. Dla zdarzenia opuszczenia gwiazdki, mouseleave, ustawiamy wartość lastHover na numer zaakceptowanej gwiazdki i znów wywołujemy własne zdarzenie onChanged. Warto zauważyć, że zdarzenia wywoływane są tylko w przypadku zmiany. Zdarzenie kliknięcia nie jest dużo bardziej skomplikowane: modyfikujemy wartość w kontekście, wartość elementu input, a następnie wywołujemy zdefiniowane w konfiguracji zdarzenie click z przekazanym w postaci this klikniętym komponentem oraz z ograniczonym kontekstem zawierającym wartość i dodatkowe dane. Te informacje powinny wystarczyć do obsłużenia zdecydowanej większości scenariuszy.

Nie wspomniałem jeszcze o tajemniczej funkcji onChanged. Wbrew pozorom jest ona wyjątkowo prosta, a jej zadaniem jest ustawienie klasy selected na zaznaczonych gwiazdkach. Popatrzmy na listing:

onChanged = function (ctx) {
    for (var j = 0; j < ctx.total; j++)
        ctx.items[j].className = j < ctx.lastHover ? 'selected' : '';
}

Można nieco zoptymalizować tę funkcję zmieniając tylko te elementy, którym ten styl się zmienia. Uznałem, że na potrzeby przykładu nie jest to konieczne.

Trochę o stylach

Pokazany na początku artykułu przykład wykorzystuje gotowy plik ze stylami Bootstrap. Ikonka gwiazdki również pochodzi z tej biblioteki, a dokładniej z czcionki dostarczanej z tą biblioteką.

Przejdźmy do wyglądu. Jeżeli chcemy, aby gwiazdki miały kolor szary, stosujemy zwykły CSS:

.ExampleSection span{
    color: #AAA;
}

Jeżeli natomiast chcemy, aby gwiazdki po zaznaczeniu były żółte, możemy napisać coś takiego:

.ExampleSection .selected{
    color: #d9bc1a;
}

Tu mała uwaga: założyłem, że gwiazdki znajdują się w elemencie klasy .ExampleSection. Jeżeli trudno za pomocą prostego selektora wskazać elementy, zawsze można nieco zmodyfikować element opakowujący wrapper opisany nieco wcześniej lub umieścić jakiś charakterystyczny element w szablonie.

Podsumowanie

W sieci można znaleźć wiele różnych bibliotek, które w lepszy lub gorszy sposób pozwalają zamienić zwykły interfejs użytkownika w kolorowy, a zwykłe pole do wpisywania oceny w piękne gwiazdki lub serduszka. Celem artykułu było pokazanie, że całe te biblioteki nie są wcale skomplikowane. Sprawdzając inne rozwiązania zauważyłem ponadto coś szczególnego - zdecydowana większość z nich korzysta z biblioteki jQuery. To dlatego jednym z założeń było napisanie biblioteki od podstaw. Zdarza się, że do projektu dołącza się dużą bibliotekę tylko dlatego, że inny komponent jej wymaga. Mam nadzieję, że przedstawione rozwiązanie, być może po drobnych modyfikacjach, zajmie tę niszę. Jeżeli zainteresowanie, wyrażone w komentarzach, będzie duże, być może wrócę do tematu. Zachęcam wobec tego do zgłaszania swoich uwag, sugestii i pytań.

Kategoria:HTMLJavaScript

, 2015-06-01

Komentarze:

Arek (2016-02-23 21:26:09)
Jestem laikiem w JS.
Czy mógłbym prosić o informację w jaki sposób przerobić powyższy skrypt "domyślne ustawienia" aby po kliknięciu np. w ostatnią gwiazdkę zaktualizować dany rekord w bazie MySQL ?
Dodaj komentarz
Wyślij
Ostatnie komentarze
Dzieki za te informacje. były kluczowe
Dobrze wyjaśnione, dzięki !
a z innej strony - co gdybym ciąg znaków chciał mieć rozbity nie na wiersze a na kolumny? Czyli ciąg ABCD: 1. kolumna: A, 2. kolumna: B, 3. kolumna: C, 4 kolumna: D?
Ciekawy artykuł.
Czy można za pomocą EF wysłać swoje zapytanie?
Czy lepiej do tego użyć ADO.net i DataTable?