28 grudnia 2007

Prezentacja dużych tabel na stronach HTML

W czym tkwi problem? Otóż chciałbym mieć komponent, który umożliwi prezentację dużej tabeli na stronie HTML, przy czym tabela ta, jest zarówno bardzo długa, jak i bardzo szeroka. Jedną z możliwości, właśnie tą, o której dziś będę pisał jest wyświetlanie tylko pewnego "okna" tej tabeli wraz z nagłówkami widocznych wierszy i kolumn. Naturalnie trzeba też umożliwić przewijanie, tj. zmianę pozycji "okna". Jeden rysunek wart jest tysiąca słów, a więc ideę tę prezentuję poniżej w postaci graficznej:


Zatem zamiast wyświetlać całą tabelę wyświetlamy tylko pewien jej wycinek i dodajemy guziki do przewijania. Powinniśmy także prezentować informację o aktualnie wyświetlanym fragmencie, czyli np. jak to pokazano na powyższej ilustracji napis "2-4 / 5" co oznacza, że aktualnie wyświetlone są kolumny od 2. do 4. a cała tabela ma 5 kolumn.

Moim ostatecznym celem jest implementacja komponentu JSF, ewentualnie w postaci kompozycji Facelets, ale w dzisiejszym artykule poprzestanę na warstwie prezentacji, czyli JavaScript + HTML = DHTML. W jaki sposób zrealizujemy mechanizm ukrywania kolumn i wierszy, które nie należą do wyświetlanego "okna"? Naturalnie poprzez dodanie stylu style="display: none;" do odpowiednich elementów <tr> i <td>. Przewijanie "okna" zaimplementujemy przy pomocy jednej klasy JavaScript. Przy okazji polecam świetny artykuł o klasach w języku JavaScript – "3 ways to define a JavaScript class". JavaScript będzie użyty tylko do zmiany pozycji "okna" poprzez odpowiednie żonglowanie wartościami display, stylów elementów <tr> i <td> natomiast inicjalizacja wstępnej pozycji "okna" musi być zakodowana w tabeli HTML. Tabela zaprezentowana uprzednio na ilustracji (z dodanymi elementami <input type="checkbox" /> w komórkach) powinna więc wyglądać tak:

<table id="myTable">
<tr>
<td>&nbsp;</td>
<td style="display:none;">Arek</td>
<td>Borys</td>
<td>Czarek</td>
<td>Darek</td>
<td style="display:none;">Ewa</td>
</tr>
<tr style="display:none;">
<td>1</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr style="display:none;">
<td>2</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr>
<td>3</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr>
<td>4</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr>
<td>5</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
<tr style="display:none;">
<td>6</td>
<td style="display:none;"><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td><input type="checkbox" /></td>
<td style="display:none;"><input type="checkbox" /></td>
</tr>
</table>

Aby kod JavaScript, który za chwilkę zaprezentuję działał poprawnie ważne jest żeby określić styl style="display:none;" dla wszystkich elementów <td> z ukrytej kolumny tabeli, nawet wtedy gdy dana komórka znajduje się w ukrytym wierszu. Wiersz może bowiem stać się widoczny w skutek przewijania "okna" w górę lub w dół, natomiast wszystkie komórki danej kolumny powinny pozostać ukryte. A oto klasa JavaScript implementująca przewijanie tabeli:

function TableRewinder
(tableName, vCols, vRows, leftOffset, topOffset, leftHeader, topHeader) {

// wartość atrybutu id tabeli, na której operujemy
this.tableName = tableName;

// ile kolumn, wliczając okno i nagłówek ma tabela
this.visibleCols = vCols;

// ile wierszy, wliczając okno i nagłówek ma tabela
this.visibleRows = vRows;

// przesunięcie okna względem pierwszej kolumny treści tabeli
this.leftOffset = leftOffset;

// przesunięcie okna względem pierwszego wiersza treści tabeli
this.topOffset = topOffset;

// ile kolumn tabeli stanowi lewy nagłówek
this.leftHeader = leftHeader;

// ile wierszy tabeli stanowi górny nagłówek
this.topHeader = topHeader;

this.getTable = function() {
return document.getElementById(this.tableName);
};

this.visibleLeftmost = function() {
return this.leftHeader + this.leftOffset;
};

this.visibleRightmost = function() {
return this.leftOffset + this.visibleCols - 1;
};

this.visibleTopmost = function() {
return this.topHeader + this.topOffset;
};

this.visibleBottommost = function() {
return this.topOffset + this.visibleRows - 1;
};

this.canGoLeft = function() {
return this.visibleLeftmost() > this.leftHeader;
};

this.canGoRight = function() {
return this.visibleRightmost() < this.getTable().rows[0].cells.length - 1;
};

this.canGoUp = function() {
return this.visibleTopmost() > this.topHeader;
};

this.canGoDown = function() {
return this.visibleBottommost() < this.getTable().rows.length - 1;
};

this.toggleRows = function(hideRowNum, showRowNum) {
this.getTable().rows[hideRowNum].style.display = 'none';
this.getTable().rows[showRowNum].style.display = '';
};

this.toggleColumns = function(hideColNum, showColNum) {
for(i = 0 ; i < this.getTable().rows.length; i++) {
rowColumns = this.getTable().rows[i].cells;

rowColumns[hideColNum].style.display = 'none';
rowColumns[showColNum].style.display = '';
}
};

this.rewindLeft = function() {
if(this.canGoLeft()) {
this.toggleColumns(this.visibleRightmost(), this.visibleLeftmost() - 1);

this.leftOffset -= 1;
}
};

this.rewindRight = function() {
if(this.canGoRight()) {
this.toggleColumns(this.visibleLeftmost(), this.visibleRightmost() + 1);

this.leftOffset += 1;
}
};

this.rewindUp = function() {
if(this.canGoUp()) {
this.toggleRows(this.visibleBottommost(), this.visibleTopmost() - 1);

this.topOffset -= 1;
}
};

this.rewindDown = function() {
if(this.canGoDown()) {
this.toggleRows(this.visibleTopmost(), this.visibleBottommost() + 1);

this.topOffset += 1;
}
};
}

Kod jest nieco rozwlekły, ale to dobrze robi jego czytelności. Znaczenie poszczególnych parametrów konstruktora klasy (funkcji TableRewinder) zostało opisane nad odpowiadającymi im zmiennymi klasowymi. Jak widać, klasa działa również z tabelami, które mają liczbę wierszy czy kolumn nagłówka inną niż 1. Poniżej kod, który utworzy instancję odpowiednią dla przedstawionej powyżej tabeli HTML oraz przykład użycia tej instancji:

var myTableRewinder = new TableRewinder('myTable', 4, 4, 1, 2, 1, 1);

<span onclick="myTableRewinder.rewindLeft()">lewo</span>
<span onclick="myTableRewinder.rewindRight()">prawo</span>
<span onclick="myTableRewinder.rewindUp()">góra</span>
<span onclick="myTableRewinder.rewindDown()">dół</span>

Po złożeniu zaprezentowanych elementów w jedną stronę DHTML otrzymujemy poniższy efekt. Nie ładny, bo bez odpowiednich CSS’ów, ale działa! Jak dobrze pójdzie w jednym z kolejnych artykułów opakuje to w komponent JSF, który wygeneruje coś ładniejszego.

Brak komentarzy: