Kawałek Kodu

Dzisiejszy wpis będą sponsorować dwa powiedzenia: "Kto pierwszy ten lepszy" oraz "Last but not least". Choć są raczej antagonistami, to jak mawiają ludzie: punkt widzenia zależy od punktu siedzenia, więc obydwa będą dziś wiodły prym. Może nie jednocześnie, ale raz jedno, a raz drugie. Będzie o sortowaniu wierszy tabeli, ale nie takim zwykłym - przecież byłoby nudno.

Order za każde miejsce.

Każdy wiersz zostanie dziś odznaczony orderem. W jaki sposób? Będzie trzeba to przekalkulować! Żeby nie czarować za bardzo, powiem Ci, że będziemy używać właściwości order z wyświetlania flex. Właściwość ta pozwala nam na ustawianie elementów rodzica z takim wyświetlaniem, w dowolnej kolejności, niezależnej od faktycznego umiejscowienia w drzewie DOM. I pamiętajmy o tym - ich fizyczna kolejność nie zmienia się, tylko tak wizualna.

No i niby wszystko fajnie. Załóżmy, że mamy tabelę z pięciomia wierszami - imionami. Wiersze nie są ustawione w kolejności alfabetycznej imion. Jeśli chcielibyśmy ustawieć je w takiej kolejności, to należałoby nadać każdemu elementowi odpowiedni styl - odpowiednią wartość order. Tyle, że może chcielibyśmy uzależnić kolejność od interakcji użytkownika. W tym momencie jest to jeszcze możliwe do uzyskania w czystym CSS. Potrzebujemy dodatkowego checkboxa. Jeśli checkbox miałby ustawioną pseudoklasę :checked, to dla każdego wiersza nadajemy odpowiednie style z order.

Zaczynamy od HTML dla tego przykładu:

<input type="checkbox"/>
<table>
  <tr><td>Romek</td></tr>
  <tr><td>Marek</td></tr>
  <tr><td>Adam</td></tr>
  <tr><td>Zigi</td></tr>
  <tr><td>Tomek</td></tr>
</table>

A CSS:

/* nasze wiersze są w <tbody>,
   więc nadajemy temu elementowi
   wyświetlanie flex,
   elementy podrzędne układamy
   wierszami z zawijaniem
*/
table tbody {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}

table tr {
  min-width: 100%;
}

table td {
  width: 100%;
  display: block;
  border: 1px solid #888;
}

/* i tu zaczynamy stylować
   nasze wiersze nadając im wartość
   order, która zmienia kolejność
   na posortowaną, jeśli checkbox
   czyli wyzwalacz sortowania
   jest wciśnięty
*/
input:checked~table tr:nth-child(1) {
  order: 3;
}

input:checked~table tr:nth-child(2) {
  order: 2;
}

input:checked~table tr:nth-child(3) {
  order: 1;
}

input:checked~table tr:nth-child(4) {
  order: 5;
}

input:checked~table tr:nth-child(5) {
  order: 4;
}

Ale chcielibyśmy też móc sortować w odwrotnej kolejności. Tu przydadzą nam się nie dwa checkboxy, ale dwa przyciski radio, a właściwie trzy, bo chcemy mieć kolejność naturalną, rosnącą oraz malejącą.

Nasz checkbox zmieniamy na wspomniane przyciski. Tabela pozostaje bez zmian:

<input type="radio" name="order" id="none" /><label for="none"> Brak</label>
<input type="radio" name="order" id="asc" /><label for="asc"> Asc</label>
<input type="radio" name="order" id="desc" /><label for="desc"> Desc</label>

+

/* tym razem mamy dwie grupy order
   w zależności, który przycisk radio
   jest wcisnięty
*/
#asc:checked ~ table tr:nth-child(1) {
  order: 3;
}

#asc:checked ~ table tr:nth-child(2) {
  order: 2;
}

#asc:checked ~ table tr:nth-child(3) {
  order: 1;
}

#asc:checked ~ table tr:nth-child(4) {
  order: 5;
}

#asc:checked ~ table tr:nth-child(5) {
  order: 4;
}

#desc:checked ~ table tr:nth-child(1) {
  order: 3;
}

#desc:checked ~ table tr:nth-child(2) {
  order: 4;
}

#desc:checked ~ table tr:nth-child(3) {
  order: 5;
}

#desc:checked ~ table tr:nth-child(4) {
  order: 1;
}

#desc:checked ~ table tr:nth-child(5) {
  order: 2;
}

Sterowanie sortowaniem jest o tyle "wadliwe", że pomimo zaznaczonej opcji widzimy również dwie pozostałe. Nie jest jak w programach do obsługi baz danych - brak strzałki lub jest strzałka w górę (sortowanie rosnąco) lub jest strzałka w dół (sortowanie malejąco). O tym za chwilę, bo to jeszcze nie koniec wybrzydzania.
Chcielibyśmy również aby działanie było niezależne od zawartości wierszy, tj. nie chcemy modyfikować arkusza styli, kiedy ilość lub zawartość wierszy zmienia się.

Remedium na uniezależnienie się od ilości i zawartości wierszy w kontekscie arkusza styli CSS jest użycie zmiennych CSS oraz funkcji calc. Popatrz:

<table>
  <tbody>
    <tr style="--name:23;">
      <td>Romek</td>
    </tr>
    <tr style="--name:1;">
      <td>Adam</td>
    </tr>
    <tr style="--name:2;">
      <td>Bartek</td>
    </tr>
    <tr style="--name:30;">
      <td>Zigi</td>
    </tr>
    <tr style="--name:26;">
      <td>Tomek</td>
    </tr>
  </tbody>
</table>

Nadaliśmy każdemu wierszowi zmienną CSS o wartości położenia początkowej litery w polskim alfabecie. Generując HTML możesz również wygenerować wartości tych zmiennych. Oczywiście jeśli wyrazy nie różnią się pierwszymi literami, to należy nadać im kolejność wedle kolejnej litery. Teraz już widać, że zmienne można użyć jako wartości dla order. Dla sortowania rosnącego wystarczy nam teraz jedna linijka dla wszystkich wierszy!

#asc:checked  ~ table tr{
  order: var(--name);
}

A co, w przypadku sortowania malejącego? Czy musimy dodawać drugą zmienną CSS? Oczywiście, że nie. Właściwość order może przyjmować również wartości ujemne, tak więc wystarczy przemnożyć wyrażenie przez -1:

#desc:checked ~ table tr{
  order: calc(-1 * var(--name));
}

A jeśli będziemy chcieli sortować więcej niż jedną kolumnę, tak aby sortowania poszczególnych kolumn współpracowały ze sobą? Czyli przykładowo chcemy sortować malejąco po pierwszej kolumnie, a następnie rosnąco po drugiej, ale wyniki ułożone tym pierszym sortowaniem. Czyli nie w sposób niezależny - taki gdzie sortowanie ostatniej kolumny resetowałoby sortowania poprzednich mając najwyższy priorytet.

Musimy wartość dla order obliczać po prostu na podstawie tylu zmiennych CSS, ile mamy kolumn. Ilość reguł CSS rośnie jednak wykładniczno i stanowi wartość wariacji z powtórzeniami, czyli nk, gdzie n to ilość stanów sortowania (czyli 3, bo: brak, rosnąco i malejąco), a k to ilość kolumn. Jedna reguła nam odpadnie, bo nie musimy jej ustalać dla braku sortowania wszystkich kolumn jednocześnie, czyli bedąc dokładnym: nk-1.

Musimy również pamiętać o ważnej rzeczy. Mianowicie każda kolumna licząc od lewej ma mieć wyższy priorytet sortowania, do sąsiada z prawej, tak aby na wartość order wciąż miała większy wpływ sortowana kolumna na lewo. Ponieważ order będziemy liczyć jako sumę zmiennych CSS, chodzi o to, aby:

  • wartość zmiennej CSS z kolumny lewej była większa od wartości kolumny prawej,
  • ale również suma dla danego wiersza, który ma być wyżej przy sortowaniu malejącym, nie była mniejsza od sumy dla wiersza poniżej.

Przykładowo, jeśli druga kolumny przyjmuje wartości zmiennych z zakresu 1-20, to zakres dla pierwszej kolumny nie może mieć części wspólnej z tym pierwszym, bo w pewnym momencie dla dwóch wierszy możemy uzyskać nieprawidłową wartość order - wartość drugiej kolumny będzie mięc większy wpływ na sumę. Najprościej będzie to osiągnąć przemnażając wartość order dla lewej kolumny, przez liczbę o kilka rzędów wielkości większą od maksymalnej wartości zmiennej (przemnożonej) z kolumny prawej.

No dobra. Ostateczny kod. Aaaa! Jeszcze jedno. Wcześniej wybrzydzaliśmy działanie przełącznika sortowania. Nie bez kozery w poprzednim odcinku pisałem o cycle button, bo w dzisiejszym przykładzie go wykorzystamy. Kto nie czytał, ten gapa - zapraszam do tamtego wpisu.

<input type="radio" name="order-name" id="none-name" autofocus checked />
<label for="asc-name">Imię</label>
<input type="radio" name="order-name" id="asc-name" />
<label for="desc-name">Imię</label>
<input type="radio" name="order-name" id="desc-name" />
<label for="none-name">Imię</label>

<input type="radio" name="order-points" id="none-points" autofocus checked />
<label for="asc-points">Punkty</label>
<input type="radio" name="order-points" id="asc-points" />
<label for="desc-points">Punkty</label>
<input type="radio" name="order-points" id="desc-points" />
<label for="none-points">Punkty</label>

<table>
  <tbody>
    <tr style="--name:23;--points:100">
      <td>Romek</td>
      <td>100</td>
    </tr>
    <tr style="--name:1;--points:209">
      <td>Adam</td>
      <td>209</td>
    </tr>
    <tr style="--name:1;--points:55">
      <td>Adam</td>
      <td>55</td>
    </tr>
    <tr style="--name:30;--points:34">
      <td>Zigi</td>
      <td>34</td>
    </tr>
    <tr style="--name:26;--points:2">
      <td>Tomek</td>
      <td>2</td>
    </tr>
  </tbody>
</table>

+ CSS:

/* ukrywamy oryginalne
   radio
*/
input[type="radio"] {
  height: 0;
  width: 0;
  position: absolute;
}

/* label 50% dostępnej
   szerokości
*/
label {
  display: none;
  width: 50%;
  float: left;
}

/* radiohack
*/
input[type="radio"]:checked+label {
  display: block;
}

/* ikonki trybu sortowania
   w <label>
*/
label[for^="asc"]:after {
  content: "\223C";
}

label[for^="desc"]:after {
  content: "\25B4";
}

label[for^="none"]:after {
  content: "\25BE";
}

/* tabela na całą szerokość
   i czyścimy float po
   <label>
*/
table {
  clear: both;
  width: 100%;
}

table tbody {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
}

table tr {
  min-width: 100%;
}

table td {
  width: 50%;
  display: block;
  float: left;
  box-sizing: border-box;
  border: 1px solid #888;
}

/* i tu się zaczyna
   roller coaster, czyli
   nasze wariacje bez powtórzeń
   dla możliwych trybów sortowania
   dwóch kolumn
*/

/* kolumna 1 asc, kolumna 2 brak
*/
#asc-name:checked ~ table tr {
  order: calc(10000 * var(--name));
}

/* kolumna 1 desc, kolumna 2 brak
*/
#desc-name:checked ~ table tr {
  order: calc(-10000 * var(--name));
}

/* kolumna 1 asc, kolumna 2 asc
*/
#asc-name:checked ~ #asc-points:checked ~ table tr {
  order: calc(10000 * var(--name) + var(--points));
}

/* kolumna 1 desc, kolumna 2 asc
*/
#desc-name:checked ~ #asc-points:checked ~ table tr {
  order: calc(-10000 * var(--name) + var(--points));
}

/* kolumna 1 asc, kolumna 2 desc
*/
#asc-name:checked ~ #desc-points:checked ~ table tr {
  order: calc(10000 * var(--name) + -1 * var(--points));
}

/* kolumna 1 desc, kolumna 2 desc
*/
#desc-name:checked ~ #desc-points:checked ~ table tr {
  order: calc(-10000 * var(--name) + -1 * var(--points));
}

/* kolumna 1 brak, kolumna 2 asc
*/
#asc-points:checked ~ table tr {
  order: calc(var(--points));
}

/* kolumna 1 brak, kolumna 2 desc
*/
#desc-points:checked ~ table tr {
  order: calc(-1 * var(--points));
}

Oczywiście metodę można stosować do przestawiania również innych elementów na ekranie, niekoniecznie wierszy tabeli.

Zachowując naturalną kolejność, zapraszam Cię do kolejnego wpisu. Do przeczytania!

 

Przydatne linki:
Cykliści jednak wszystkiemu niewinni, czyli o cycle button, elemencie, który (nie)istnieje w HTML.
Właściwość order w CSS.