MDN’s new design is in Beta! A sneak peek: https://blog.mozilla.org/opendesign/mdns-new-design-beta/

Szczegóły widoku drzewa

W tej części będzie opisane więcej cech wyglądu drzewa.

Tworzenie własnego hierarchicznego widoku

W ostatnim artykule tworzyliśmy prosty widok drzewa, który implementował tylko minimum funkcjonalności. Teraz przyjrzyjmy się innym, dodatkowym funkcjom, których możemy zaimplementować. Sprawdzimy tutaj, jak stworzyć hierarchicznie ustawione pozycje, używające widoku. Jest to dość podstępny proces pociągający za sobą utrzymywanie ścieżek pozycji potomnych i także, które wiersze są otwarte, a które zostały zamknięte.

Zagnieżdżanie poziomu

Każdy wiersz w drzewie ma zagnieżdżony poziom. Najwyższy wiersz jest na poziomie 0, dzieci tego wiersza są na poziomie 1, a z kolei kolejne dzieci mamy na poziomie 2 itd. Drzewo wypyta widok o każdy wiersz, wywołując metodę getLevel w celu znalezienia poziomu danego wiersza. Widok zwróci 0 dla najwyższych, zewnętrznych wierszy, wyższe wartości dla wierszy wewnętrznych. Drzewo użyje tych informacji w celu ustalenia hierarchicznej struktury wierszy.

Dodatkowo do metody getLevel, jest jeszcze funkcja hasNextSibling, która daje wiersz zwracająca wartość true, jeżeli znajduje się kolejny wiersz na tym samym poziomie. Funkcja ta jest używana w szczególności podczas rysowania zagnieżdżonych linii w głąb drzewa.

Metoda getParentIndex jest zwraca źródłowy wiersz dla rzędu, jeżeli znajduje się kolejny rząd na tym samym poziomie. Wszystkie z tych metod muszą być zastosowane przez widok dla dzieci do podtrzymania własności.

Kontenery

Są tam też trzy funkcje isContainer, isContainerEmpty i isContainerOpen do posługiwania się źródłowymi pozycjami drzewa.

  • Naturalnie, isContainer powinna zwrócić wartość true, jeżeli wiersz jest pojemnikiem i może zawierać kolejne wiersze.
  • Metoda isContainerEmpty powinna zwrócić wartość true, jeżeli wiersz jest pustym pojemnikiem, jak np. pusty folder.
  • Metoda isContainerOpen służy do ustalenia, które z pozycji są otwarte, a które zamknięte.

Zwróć uwagę ze metody te nie zostaną użyte, jeżeli isContainer nie wskaże tego, że dany element jest pojemnikiem.

Pojemnik może zostać przedstawiony inaczej, jako nie kontener. Na przykład może mieć ikonę folderu obok siebie. Schemat może zostać użyty do stylizacji elementów opierając się na różnych własnościach, takich jak to czy wiersz jest otwarty czy nie. Opisane jest to w dalszej części. Niepusty pojemnik będzie wyświetlony obok, tak, że użytkownik będzie mógł go otwierać i zamykać by zobaczyć zawartość wiersza.

Kiedy użytkownik kliknie by otworzyć wiersz drzewo wywoła funkcję toggleOpenState. Następnie widok powinien wykonać wszelkie konieczne operacje, by uzyskać zawartość wiersza i wypełnić drzewo nowymi wierszami.

Przegląd metod

Przegląd metod potrzebnych do użycia hierarchicznego widoku:

getLevel(row)
hasNextSibling(row, afterIndex)
getParentIndex(row)
isContainer(row)
isContainerEmpty(row)
isContainerOpen(row)
toggleOpenClose(row)

Argument afterIndex wykona funkcję hasNextSibling w celu optymalizacji, żeby znaleźć następny element. Na przykład, klient mógłby już wiedzieć gdzie dany element prawdopodobnie mógłby się znajdować. Wyobraź sobie sytuację gdzie wiersz ma pod wiersze, a te pod wiersze maja pochodne wiersze i niektóre z nich są otwarte.

Przykład własnego hierarchicznego widoku

Zobaczmy na prostym przykładzie, z pobranej tablicy budowane jest drzewo. Przeanalizujmy to kawałek, po kawałku.

<window onload="init();"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

<tree id="elementList" flex="1">
  <treecols>
    <treecol id="element" label="Element" primary="true" flex="1"/>
  </treecols>
  <treechildren/>
</tree>

</window>

Używamy prostego drzewa bez danych w treechildren. Funkcja 'init' jest wywoływana, gdy okno jest ładowane w celu uruchomienia drzewa. To w prosty sposób umieszcza własny widok odzyskując drzewo i umieszczeniu własności widoku.

function init() {
  document.getElementById("elementList").view = treeView;
}

Własny widok drzewa będzie musiał wprowadzić pewna liczbę metod, z których najważniejsze będą sprawdzane indywidualnie. Drzewo to wesprze tylko pojedynczy główny poziom z wewnętrznym pochodnym poziomem, ale może to być dodatkowo rozszerzone bez większych problemów. Najpierw zdefiniujemy dwie struktury do przetrzymywania danych dla drzewa, pierwsza będzie przetrzymywała mapę zawartą między głównym, a pochodną pozycją, a druga tablicę widocznych elementów. Pamiętaj, że własny widok musi posiadać informacje o tym, które są widoczne.

var treeView = {
  childData : {
    Solids: ["Silver", "Gold", "Lead"],
    Liquids: ["Mercury"],
    Gases: ["Helium", "Nitrogen"]
  },

  visibleData : [
    ["Solids", true, false],
    ["Liquids", true, false],
    ["Gases", true, false]
  ],

Struktura childData zawiera tablicę pochodnych pozycji trzech głównych węzłów. Tablica visibleData zaczyna się tylko trzema widocznymi pozycjami, trzy szczytowe poziomy. Elementy będą dodawane i usuwane z tej tablicy, gdy pozycje będą otwierane i zamykane. Zasadniczo, gdy główny wiersz będzie otwarty, pochodne pozycje zostaną pobrane z childData i wstawione do tablicy visibleData. Na przykład, jeżeli wiersz płynów będzie otwarty, korespondująca tablica chilldData, która zawiera tylko pojedynczy pochodny element <tt>Merkury</tt>, będzie umieszczony w tablicy visibleData za płynami, ale przed gazami. To zwiększy rozmiar tablicy o jeden. Dwie wartości boolean w każdym wierszu w strukturze visibleData wskazują na to czy wiersz jest pojemnikiem i czy jest otwarty. Oczywiście nowo wstawione pochodne pozycje będą miały obie wartości ustawione na false.

Implementowanie interfejsu widoku drzewa

Następnie potrzeba wprowadzić interfejs widoku drzewa. Najpierw proste funkcje:

  treeBox: null,
  selection: null,

  get rowCount()                     { return this.visibleData.length; },
  setTree: function(treeBox)         { this.treeBox = treeBox; },
  getCellText: function(idx, column) { return this.visibleData[idx][0]; },
  isContainer: function(idx)         { return this.visibleData[idx][1]; },
  isContainerOpen: function(idx)     { return this.visibleData[idx][2]; },
  isContainerEmpty: function(idx)    { return false; },
  isSeparator: function(idx)         { return false; },
  isSorted: function()               { return false; },
  isEditable: function(idx, column)  { return false; },

Funkcja rowCount zwróci długość tablicy visibleData. Zauważ to, że powinno zwrócić liczbę aktualnie widocznych wierszy, a nie ich całkowitą ilość. Więc na początku trzy pozycje są widoczne i w rowCount powinny być trzy, nawet, jeżeli sześć rzędów jest ukrytych.

Funkcje getCellText, isContainer i isContainerOpen, zawracają tylko odpowiedni element z tablicy visibleData. W końcu pozostałe funkcje mogą zwrócić wartość false ponieważ nie potrzeba tych cech. Gdybyśmy mieli wiersz, który by nie miał żadnych pochodnych pozycji, chcielibyśmy wprowadzić funkcję isContainerEmpty to zwróciłaby ona wartość true dla tych elementów.

  getParentIndex: function(idx) {
    if (this.isContainer(idx)) return -1;
    for (var t = idx - 1; t >= 0 ; t--) {
      if (this.isContainer(t)) return t;
    }
  },

Funkcją getParentIndex będzie musiała odnaleźć główny przedmiot zadanego indeksu. W naszym prostym przykładzie, mamy tylko dwa poziomy, więc wiemy, że pojemniki nie posiadają głównych elementów, więc otrzymujemy -1. W takim wypadku cofamy się wstecz szukając wiersz będącego pojemnikiem. Następna funkcja jest getLevel:

  getLevel: function(idx) {
    if (this.isContainer(idx)) return 0;
    return 1;
  },

Funkcja getLevel jest prosta. Zwraca wartość 0 dla pojemnika, 1 dla nie-pojemnika. Gdybyśmy chcieli dołożyć dodatkowy poziom elementów pochodnych, wiersz te miałyby poziom 2.

  hasNextSibling: function(idx, after) {
    var thisLevel = this.getLevel(idx);
    for (var t = idx + 1; t < this.visibleData.length; t++) {
      var nextLevel = this.getLevel(t)
      if (nextLevel == thisLevel) return true;
      else if (nextLevel < thisLevel) return false;
    }
  },

Funkcja theNextSibling zwraca true, jeżeli wiersz jest tego samego poziomu, co podany wiersz. Powyższy kod używa metody "brute force", która po prostu powtarzana jest, poszukując jednego wiersza, zwraca wartość true jeżeli wiersz jest tego samego poziomu, false jeżeli niższego poziomu. W tym prostym przykładzie ta metoda działa świetnie, lecz bardziej rozbudowane drzewo lepiej użyć innego, bardziej efektywnego sposobu.

Otwieranie i zamykanie wiersza

Ostatnia funkcja to toggleOpenState, jest najbardziej złożona. Wymaga to modyfikacji tablicy, visibleData, gdy wiersz jest otwarty lub zamknięty.

  toggleOpenState: function(idx) {
    var item = this.visibleData[idx];
    if (!item[1]) return;

    if (item[2]) {
      item[2] = false;

      var thisLevel = this.getLevel(idx);
      var deletecount = 0;
      for (var t = idx + 1; t < this.visibleData.length; t++) {
        if (this.getLevel(t) > thisLevel) deletecount++;
        else break;
      }
      if (deletecount) {
        this.visibleData.splice(idx + 1, deletecount);
        this.treeBox.rowCountChanged(idx + 1, -deletecount);
      }
    }
    else {
      item[2] = true;

      var label = this.visibleData[idx][0];
      var toinsert = this.childData[label];
      for (var i = 0; i < toinsert.length; i++) {
        this.visibleData.splice(idx + i + 1, 0, [toinsert[i], false]);
      }
      this.treeBox.rowCountChanged(idx + 1, toinsert.length);
    }
  },

Najpierw musimy sprawdzić czy wiersz jest pojemnikiem. Jeżeli nie, funkcja po prostu powróci, ponieważ nie pojemniki nie mogą być otwarte ani zamknięte. Od trzeciej pozycji tablicy (indeks 2) przetrzymywane są informacje czy wiersz jest otwarty czy nie, używamy dwóch ścieżek kodu, pierwszej do zamykania wiersza, a druga do otwierania wiersza. Zbadajmy każdy blok kodu, lecz zerknijmy najpierw na drugi blok służący do otwierania wiersza.

      item[2] = true;

      var label = this.visibleData[idx][0];
      var toinsert = this.childData[label];
      for (var i = 0; i < toinsert.length; i++) {
        this.visibleData.splice(idx + i + 1, 0, [toinsert[i], false]);
      }
      this.treeBox.rowCountChanged(idx + 1, toinsert.length);

Pierwsza linia czyni w tablicy, wiersz otwartym, więc z stąd funkcja toggleOpenState będzie wiedziała, który wiersz ma być zamknięty. Następnie szukamy danych w childData dla danego wiersza. Wynikiem jest to, że 'toinsert' będzie wypełnione jednym z elementów tablicy, na przykład ["Silver", "Gold", "Lead"], jeżeli wiersz zostanie otwarty. Następnie używamy funkcji, splice tablicy, by wstawić nowy rząd do każdej pozycji. Dla stałych elementów, zostaną wstawione trzy pozycje.

Używamy funkcji rowCountChanged do poinformowania drzew, że dodaliśmy kilka wierszy do podstawowych danych. Drzewo zostanie przerysowane według potrzeby, a w wyniku pochodne wiersze ukażą się wewnątrz pojemnika. Inne implementowane funkcje powyżej, jak getLevel i isContainer są używane przez drzewo do określenia tego jak wyrysować drzewo.

Funkcja rowCountChanged pobiera dwa argumenty, indeks gdzie pierwszy wiersz był wstawiony i numer wierszy do wstawienia. W powyższym kodzie, zauważ, że początkowy wiersz określony jest jako ‘idx’ plus jeden, którym będzie pierwsza pochodna pozycja pod główną. Drzewo użyje tej informacji i doda przestrzeń dla odpowiedniej liczy wierszy i umieści je w niej. Upewnij się czy liczba jest prawidłowa, w przeciwnym wypadku drzewo zostanie narysowane niepoprawnie lub spróbuje narysować więcej wierszy niż jest to konieczne.

Poniższy kod jest używany do usuwania wierszy, gdy wiersz jest zamknięty.

      item[2] = false;

      var thisLevel = this.getLevel(idx);
      var deletecount = 0;
      for (var t = idx + 1; t < this.visibleData.length; t++) {
        if (this.getLevel(t) > thisLevel) deletecount++;
        else break;
      }
      if (deletecount) {
        this.visibleData.splice(idx + 1, deletecount);
        this.treeBox.rowCountChanged(idx + 1, -deletecount);
      }

Najpierw, element jest zamknięty w tablicy. Następnie będziemy przeszukiwać wiersze, aż natkniemy się na któryś tego samego poziomu. Wszystkie o wyższym poziomie będą musiały być usunięte, lecz wiersz na tym samym poziomie, będzie kolejnym pojemnikiem, który nie powinien być usuniety.

W końcu używamy funkcji splotu (splice) w celu usunięcia wierszy z tablicy visibleData a wywołania funkcji rowCountChanged żeby przerysować drzewo ponownie. Podczas usuwania wierszy, będziesz musiał dostarczyć odwrotny licznik wierszy do usunięcia.

Pełen przykład

Jest tam kilka innych funkcji widoku, które możemy implementować, jednak nie będą one miały nić do wykonania w tym przykładzie. Będą one dodane pod koniec następującego przykładu:

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<window onload="init();"
        xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

<tree id="elementList" flex="1">
  <treecols>
    <treecol id="element" label="Element" primary="true" flex="1"/>
  </treecols>
  <treechildren/>
</tree>

<script>
<![CDATA[

var treeView = {
  childData : {
    Solids: ["Silver", "Gold", "Lead"],
    Liquids: ["Mercury"],
    Gases: ["Helium", "Nitrogen"]
  },

  visibleData : [
    ["Solids", true, false],
    ["Liquids", true, false],
    ["Gases", true, false]
  ],

  treeBox: null,
  selection: null,

  get rowCount()                     { return this.visibleData.length; },
  setTree: function(treeBox)         { this.treeBox = treeBox; },
  getCellText: function(idx, column) { return this.visibleData[idx][0]; },
  isContainer: function(idx)         { return this.visibleData[idx][1]; },
  isContainerOpen: function(idx)     { return this.visibleData[idx][2]; },
  isContainerEmpty: function(idx)    { return false; },
  isSeparator: function(idx)         { return false; },
  isSorted: function()               { return false; },
  isEditable: function(idx, column)  { return false; },

  getParentIndex: function(idx) {
    if (this.isContainer(idx)) return -1;
    for (var t = idx - 1; t >= 0 ; t--) {
      if (this.isContainer(t)) return t;
    }
  },
  getLevel: function(idx) {
    if (this.isContainer(idx)) return 0;
    return 1;
  },
  hasNextSibling: function(idx, after) {
    var thisLevel = this.getLevel(idx);
    for (var t = idx + 1; t < this.visibleData.length; t++) {
      var nextLevel = this.getLevel(t)
      if (nextLevel == thisLevel) return true;
      else if (nextLevel < thisLevel) return false;
    }
  },
  toggleOpenState: function(idx) {
    var item = this.visibleData[idx];
    if (!item[1]) return;

    if (item[2]) {
      item[2] = false;

      var thisLevel = this.getLevel(idx);
      var deletecount = 0;
      for (var t = idx + 1; t < this.visibleData.length; t++) {
        if (this.getLevel(t) > thisLevel) deletecount++;
        else break;
      }
      if (deletecount) {
        this.visibleData.splice(idx + 1, deletecount);
        this.treeBox.rowCountChanged(idx + 1, -deletecount);
      }
    }
    else {
      item[2] = true;

      var label = this.visibleData[idx][0];
      var toinsert = this.childData[label];
      for (var i = 0; i < toinsert.length; i++) {
        this.visibleData.splice(idx + i + 1, 0, [toinsert[i], false]);
      }
      this.treeBox.rowCountChanged(idx + 1, toinsert.length);
    }
  },

  getImageSrc: function(idx, column) {},
  getProgressMode : function(idx,column) {},
  getCellValue: function(idx, column) {},
  cycleHeader: function(col, elem) {},
  selectionChanged: function() {},
  cycleCell: function(idx, column) {},
  performAction: function(action) {},
  performActionOnCell: function(action, index, column) {},
  getRowProperties: function(idx, column, prop) {},
  getCellProperties: function(idx, column, prop) {},
  getColumnProperties: function(column, element, prop) {},
};

function init() {
  document.getElementById("elementList").view = treeView;
}

]]></script>

</window>

Następnie, zobaczymy więcej szczegółów o obiekcie pola drzewa.

Autorzy i etykiety dokumentu

Etykiety: 
 Autorzy tej strony: fscholz, teoli, Mgjbot, Ptak82
 Ostatnia aktualizacja: fscholz,