Linux şi aplicaţii Web în 24 de ore  
introducere în Linux şi iniţiere în dezvoltarea de aplicaţii Web

Linux şi aplicaţii Web în 24 de ore

Aplicaţie Web simplă: situaţia şcolară

«  Dezvoltarea de aplicaţii Web simple   ::   Contents   ::   Aplicaţie Web tipică angajând PHP şi MySQL  »

Aplicaţie Web simplă: situaţia şcolară

Punerea problemei

La sfârşit de semestru (inclusiv, de an şcolar) diriginţii au de raportat o “situaţia şcolară” pentru clasa respectivă, conţinând mediile generale obţinute de elevi şi o situaţie statistică pe grupe de medii. Să realizăm o aplicaţie Web SitSco, care să poată fi utilizată în acest scop.

Pentru “SitSco” s-ar impune să angajăm o bază de date, păstrând datele elevilor şi pentru anii următori şi putând evoca oricând situaţiile înregistrate anterior. Dar deocamdată vom folosi numai ceea ce pune la dispoziţie un browser (în mod obişnuit: HTML, javaScript, CSS) - vrând acum să ilustrăm posibilităţi de folosire din programe javaScript, a obiectelor (proprietăţi şi metode) existente în DOM; din javaScript nu se pot accesa fişiere de pe hard-disk (cel puţin, nu în mod obişnuit), încât implicând numai browserul (nu şi programe pe server) nu putem salva pe disc, nici încărca de pe disc (deci nici nu putem angaja o “bază de date” propriu-zisă).

Este de înţeles de ce javaScript nu prevede posibilităţi de a accesa fişiere de pe hard-disk (există de doi-trei ani şi biblioteci care extind javaScript în acest sens, dar deocamdată este vorba de experimente controversate). Când un utilizator accesează din browserul său o aplicaţie de pe Internet, browserul descarcă de pe serverul respectiv şi pune în execuţie programele javaScript aferente acelei aplicaţii; ori dacă programul javaScript respectiv ar putea salva un fişier pe hard-disk-ul utilizatorului - atunci evident că s-ar crea pericolul ca fişierele de pe hard-disk-ul utilizatorului să fie deteriorate.

În contextul de lucru precizat mai sus, pentru a încărca date dintr-un fişier-text şi pentru a salva date într-un fişier-text utilizatorul va putea folosi obişnuita operaţie Copy&Paste, asupra conţinutului curent al unui element textarea pus la dispoziţie de către “SitSco”.

Datele de încărcat sunt linii de text conţinând numele şi prenumele elevului şi mediile acestuia la disciplinele din catalogul şcolar; aplicaţia trebuie să calculeze media generală pentru fiecare elev şi să adauge în pagina Web un tabel conţinând numele, prenumele şi media generală, tabel care să poată fi ordonat de către utilizator fie după nume, fie după medie (şi care să poată fi apoi “salvat”).

Pentru preluarea în vederea prelucrării, a datelor introduse în <textarea> vom folosi (ca şi mai înainte în aplicaţia “CMMDC”) metodele .replace() şi .split() ale obiectului javaScript String(). Pentru crearea, inserarea şi sortarea tabelului mediilor generale vom folosi metode oferite de DOM: .createElement(), .cloneNode(), appendChild(), replaceChild() şi altele.

Amintim că am creat deja un “virtual host”, având ca DocumentRoot subdirectorul Proiecte/web-public şi ca ServerName “proiecte.home”. Creem fişierele necesare (anume, în /home/user/Proiecte/web-public) astfel:

cd Proiecte/web-public
touch sitsco.html Js/sitsco.js Css/sitsco.css

şi ne pregătim să le edităm încărcându-le în gEdit. Putem deja testa în browser faptul că vom putea lansa aplicaţia tastând în bara de adresă http://proiecte.home/sitsco.html.

Fişierul HTML

Ne putem scuti de a mai scrie declaraţia <!DOCTYPE şi secţiunea <head>: deschidem fişierul pe care l-am creat anterior “cmmdc.html”, selectăm până la <body>, copiem (fie folosind meniul Edit/Copy din gEdit, fie folosind direct combinaţia de două taste CTRL + C), apoi revenim în fişierul “sitsco.html” deschis şi acesta în gEdit şi folosim “Paste” (combinaţia de două taste CTRL + V). Bineînţeles că înlocuim “cmmdc.js” cu “sitsco.js” (şi analog pentru “cmmdc.css”) şi eventual, rescriem valoarea atributului “content” din meta-declaraţia pentru “Description”.

Operaţia evidenţiată mai sus selectare, Copy&Paste este una generală, permisă inclusiv de către browsere; de regulă, se poate selecta şi copia astfel orice text dintr-o pagină Web deschisă în browser (inclusiv de exemplu, conţinutul unui element <table>). Ea este desigur, binecunoscută; dar am evidenţiat-o pentru că este implicată esenţial în aplicaţia pe care o dezvoltăm aici.

În elementul <body> din fişierul sitsco.html prevedem următorul conţinut:

<p>
    <textarea id="catalog" rows="4" cols="70">
Albuleţ Ionuţ 7.67 8.67 9 10 6.67 10 10 10 6.67 7.67 8.67 9 10 9 10 10 10
Zarea Alina 8 9 8 9 8 9 8 9 8 9 8 9 8 9 8 9 8 9 8 9 8 9
Şofron Steluţa 7.67 8.67 9 10 6.67 10 10 10 10 7.67 8.67 9 10 6.67 10 10
    </textarea>

    <button id="b-relativ"
            onclick="med_gen('catalog', 'medii-generale', 'medii-chart')">
        Medii Generale
    </button>
</p>

<table>
<tr>
    <td><div id="medii-generale"></div></td>
    <td><div id="medii-chart"></div></td>
</tr>
</table>

În <textarea> am înscris nişte date, servind testării pe parcursul dezvoltării. Am prevăzut un “ID” pe <button> în scopul stilării CSS - putem intra imediat în Css/sitsco.css pentru a încerca:

#b-relativ {
    position: relative;
    top: -2em;
    cursor: pointer;
}

După ce salvăm “sitsco.html” şi “sitsco.css” putem accesa din browser http://proiecte.home/sitsco.html; datorită setării proprietăţilor de stil position (cu valoarea “relative”) şi top, butonul apare deplasat în sus (cu valoarea 2em;) faţă de poziţia implicită în care ar fi fost plasat.

Pe buton am montat un “handler de click”, med_gen(); această funcţie va fi lansată când utilizatorul va face click pe buton şi ea va trebui să identifice obiectele DOM corespunzătoare celor trei identificatori transmişi, să extragă şi să prelucreze datele din <textarea>, constituind şi adăugând în pagină tabelul şi diagrama statistică cerute.

Am implicat un element <table> (cu un singur rând şi două coloane) pur şi simplu pe post de container pentru tabelul mediilor generale şi respectiv (în a doua coloană) diagrama statistică.

Fişierul JS (angajând metode DOM)

În sitsco.js înscriem întâi prototipul funcţiei care va constitui tabelul mediilor generale şi diagrama statistică aferentă:

function med_gen( catalog, medii, chart ) {

}

Parametrii ‘catalog’, ‘medii’ şi ‘chart’ sunt valorile atributului ID pentru <textarea> şi pentru două diviziuni destinate înscrierii tabelului mediilor generale şi respectiv, diagramei statistice. Primul lucru de făcut în cadrul funcţiei, constă în obţinerea unor referinţe la obiectele DOM corespunzătoare parametrilor primiţi; pentru aceasta folosim metoda document.getElementById(id_HTML) şi fiindcă avem să o folosim de mai multe ori - este mai comod să introducem o “scurtătură” (întâlnită în bibliotecile javaScript prototype.js şi jQuery.js, ca “selector de elemente”) definind în prealabil (în exteriorul funcţiei “med_gen()”):

function $( id_element ) {
    return document.getElementById( id_element );
}

Să notăm în treacăt că denumirile proprietăţilor şi metodelor din DOM sunt suficient de sugestive (“get Element By Id”), încât de multe ori nu este necesară vreo explicaţie suplimentară. Este de remarcat că atunci când este cazul, metodele DOM nu returnează obiecte, ci de regulă referinţe la obiectele respective.

Folosind “$()”, obţinerea referinţelor la obiectele DOM corespunzătoare parametrilor primiţi decurge astfel:

function med_gen( catalog, medii, chart ) {
    var catalog = $(catalog),
        ob_medii = $(medii),
        ob_chart = $(chart);

    alert(catalog.value); // pentru testare

}

Putem proba imediat, reîncărcând în browser - va trebui să se alerteze textul conţinut de <textarea> (valoare a proprietăţii value a obiectului DOM corespunzător).

Următorul lucru pe care trebuie să-l facă med_gen() este analiza şi internalizarea în format potrivit prelucrării, a valorii catalog.value. Mai întâi, dacă textul respectiv este nul (utilizatorul a selectat şi a şters, apoi a făcut click pe buton) atunci trebuie returnat un mesaj corespunzător:

if( catalog.value.length == 0 ) {
     alert("Introduceţi linii ca: Popa Jan 9.50 8.50 8 9 10");
     return false;
}

Dacă este cazul, dintr-un handler de click trebuie să se iasă prin return false;, fiindcă altfel browserul propagă evenimentul ‘click’ (spre exteriorul elementului respectiv, din aproape în aproape).

Când lungimea textului - indicată de proprietatea length pentru String()-ul respectiv - este nenulă, atunci eliminăm de pe fiecare linie eventualele spaţii (propriu-zise) iniţiale sau finale şi pur şi simplu folosim split() pentru a obţine un tablou al liniilor respective (fiecare element al tabloului reprezentând datele unui elev):

var CATALOG = catalog.value.replace(/^\s+/mg, '')
                           .replace(/\s+$/mg, '')
                           .split(/[\n\r]+/g);

alert('[' + CATALOG + ']'); // pentru testare
alert( CATALOG[0] );        // pentru testare

Fiindcă am văzut un manual de C++ (pentru “informatică-intensiv”) care foloseşte mereu if(condiţie) return 0; else {...} - subliniem că nu este necesar “else” după “if”-ul de mai sus.

Putem iarăşi proba, reîncărcând în browser (şi eventual, introducând diverse formate de date în <textarea>); după aceasta, putem elimina (sau doar comenta) instrucţiunile alert().

Putem trece la modelarea prelucrării intenţionate. Putem începe prin a crea în DOM elementele (nodurile) necesare.

Un element <table> are de regulă trei secţiuni: <thead> (“antetul de tabel”), <tfoot> (subsolul tabelului) şi <tbody>.

În cazul nostru, antetul va fi format dintr-un rând <tr> (“table row”) conţinând două elemente <th> (“table head”) pentru denumirile coloanelor (“Nume şi prenume”, respectiv “Media”); subsolul tabelului va consta într-un rând cu o singură coloană, pe care vom înregistra media generală a clasei; iar <tbody> va trebui să conţină câte un TR pentru fiecare elev, constituit dintr-un TD pentru numele elevului şi un TD conţinând media generală corespunzătoare.

Adăugăm în sitsco.js următoarea secvenţă, prin care se vor crea obiecte DOM corespunzătoare elementelor de tabel menţionate şi deasemenea, variabile prin care să putem referi ulterior în program aceste obiecte:

var tabel = document.createElement('table'),
    thead = document.createElement('thead'),
    tbody = document.createElement('tbody'),
    tfoot = document.createElement('tfoot');

var TR = document.createElement('tr'),
    TH = document.createElement('th'),
    TD = document.createElement('td');

Desigur, vor fi de creat mai multe astfel de elemente TR (câte unul pentru antet şi subsol, apoi câte unul pentru fiecare elev), TD şi TH - dar vom putea refolosi variabilele respective, datorită metodei .cloneNode() (prin care se creează o copie a nodului respectiv - operaţie mai puţin costisitoare decât cea de creare a unui nou nod).

Pentru inserarea nodurilor folosim o regulă simplă: inserăm pe cale ierarhică, de la ultimul spre primul. De exemplu, în mod tipic vom constitui un nod TD prin var cell = TD.cloneNode() şi-i vom înscrie conţinutul prin cell.innerHTML = conţinutul_elementului; apoi, putem insera cell prin row.appendChild(cell), unde row referă TR-ul în care trebuie pus TD-ul creat; mai departe, inserăm la fel row în tbody, iar pe acesta din urmă în obiectul referit de tabel.

Secţiunile de tabel trebuie inserate (în obiectul referit de table) neapărat în ordinea thead, tfoot şi abia la sfârşit, tbody.

Toate aceste operaţii de inserare ierarhică se petrec în memorie (nu “apare” nimic în pagina Web); abia în final, obiectul referit de tabel va fi adăugat (vizibil) în pagina Web, prin “agăţare” de elementul al cărui identificator s-a specificat iniţial: ob_medii.appendChild(tabel);. Browserul întâi constituie DOM-ul documentului (în memorie) şi abia după aceea se ocupă de vizualizarea conţinutului.

Secţiunea <thead> a tabelului mediilor generale

Mai departe în sitsco.js, definim conţinutul obiectului referit de variabila thead, instituită mai sus. Secvenţa respectivă ar consta în câteva “clonări” (de TR, TH), înregistrări de conţinut (prin ”.innerHTML”) şi “appendare” - n-ar fi greu de scris, dar mai trebuie să nu scăpăm din vedere ansamblul aplicaţiei. Să ne amintim că ne propusesem să oferim utilizatorului posibilitatea de a ordona tabelul după nume, sau după medie.

Maniera standard prin care se asigură posibilitatea ordonării după o coloană sau alta a unui tabel constă în transformarea antetului de coloană într-un link către o funcţie care să realizeze sortarea dorită. Cu alte cuvinte, trebuie creat un element <a onclick="sort_table(rang_coloană);" href="">nume_coloană</a>, care trebuie inserat apoi în TH-ul coloanei.

Dar trebuie sesizat faptul că sort_table() trebuie să primească şi o referinţă la tabelul pe care să opereze; nu putem să-i transmitem pur şi simplu referinţa folosită în program (apelând prin sort_table(tabel, 0)), pentru că ‘tabel’ referă un obiect care încă nu există în pagina Web. Soluţia cea mai simplă constă în a defini un identificator pentru obiectul referit de “tabel”, prin tabel.setAttribute('id', 'situatie') şi a pasa valoarea respectivă: sort_table('situatie', 0).

var n = CATALOG.length; //numărul de elevi
var i;
var row, cel; //pentru diverse clone TR, TD

tabel.setAttribute('id', 'situatie');

//constituie antetul tabelului

var antet = ['Nume şi Prenume', 'media'];

row = TR.cloneNode(true);
for(i = 0; i < 2; i++) {
    cel = TH.cloneNode(true);
    // handlerul de click, pentru sortare după coloană
    cel.innerHTML = "<a onclick='sort_table(" +
                           '\"situatie\",' + i + ");' " +
                           "href='#'>" + antet[i] + "</a>";
    row.appendChild(cel);
}
thead.appendChild(row);
tabel.appendChild(thead);

ob_medii.appendChild(tabel); //pentru testare (apoi ştergeţi)

Numai în scopul testării acestei porţiuni, am adăugat ultima linie de mai sus (prin care se înscrie în pagină tabelul construit până la acest moment, având numai secţiunea de <thead>).

Desigur, putem adăuga tot acum în sitsco.js - dar în exteriorul funcţiei “med_gen()” - şi funcţia sort_table():

function sort_table( id_table, rang_col ) {
    var table = document.getElementById(id_table);

    alert(table.tagName); //pentru testare

}

Salvând fişierul şi reîncărcând “sitsco.html” în browser, putem constata că lucrurile încep să funcţioneze: click pe butonul “Medii generale” creează tabelul mediilor generale (numai antetul, în acest moment), iar click pe antetul uneia dintre coloane lansează “sort_table()”, care în acest moment doar alertează “TABLE” (valoarea de “tagName” a tabelului creat). Ştergem ultima linie pe care am scris-o în corpul funcţiei “med_gen()” şi putem continua.

Secţiunea <tbody> a tabelului mediilor generale

Creând întâi tbody, vom putea determina uşor media generală a clasei, care este necesară apoi în tfoot.

var mgcl = 0;  //media generală a clasei

for(i = 0; i < n; i++) {
    var elev = CATALOG[i];
    row = TR.cloneNode(true);

    //extrage numele şi mediile elevului
    var nume = elev.replace(/^(\D+).+/,"$1");
    var medii = elev.replace(/^\D+(.+)$/,"$1").split(/\s+/g);

    //media generală a elevului
    var mg = 0; var no = medii.length;
    for(var m = 0; m < no; m++)
        mg += parseFloat(medii[m]);
    mg /= no;
    mgcl += mg;

    //inserează în 'row' TD-ul cu numele
    cel = TD.cloneNode(true);
    cel.innerHTML = nume;
    row.appendChild(cel);

    //inserează în 'row' TD-ul cu media
    cel = TD.cloneNode(true);
    cel.innerHTML = mg.toPrecision(4).substring(0,4);
    row.appendChild(cel);

    //alternează background-ul rândurilor
    if(i & 1) row.setAttribute('class', 'altern');

    tbody.appendChild(row);
}

//'tbody' este complet; completăm acum 'tfoot'
mgcl /= n; //media generală a clasei

row = TR.cloneNode(true);
cel = TD.cloneNode(true);
cel.setAttribute('colspan', '2');
cel.innerHTML = "media: " + mgcl.toPrecision(5).substring(0,5);
row.appendChild(cel);
tfoot.appendChild(row);

tabel.appendChild(tfoot);
tabel.appendChild(tbody);

ob_medii.appendChild(tabel);

Am intercalat comentarii şi sublinieri unde am crezut de cuviinţă şi am grupat secvenţele pe verticală; este un obicei practicat pretutindeni pentru uşurarea înţelegerii unui program-sursă. Probabil, numai două chestiuni ar mai fi de explicat aici.

Numele s-a extras prin var nume = elev.replace(/^(\D+).+/,"$1");. Aici, /^(\D+).+/ este o expresie regulată, adică un şablon construit după anumite specificaţii standard, permiţând selectarea unei anumite secvenţe de caractere dintr-un şir; ^ şi $ specifică “de la primul” şi respectiv, “până la ultimul” caracter din şir; \d şi \D specifică un caracter care este şi respectiv, nu este o cifră zecimală, iar . “ţine loc” de oricare caracter; \D+ specifică o secvenţă de unul sau mai multe caractere succesive care sunt non-cifre (iar .+ corespunde unei secvenţe de unul sau mai multe caractere oarecare). Parantezele asigură memorarea grupului de caractere încadrat, el putând apoi fi extras prin $n unde n este rangul grupului (1 pentru primul grup parantezat).

Astfel, var nume = "Ionescu Ion 7.67 8.67" . replace(/^(\D+).+/,"$1"); va memora în $1 secvenţa de non-cifre “Ionescu Ion ” de la începutul şirului (ignorând cifrele care urmează) şi va transfera rezultatul în variabila “nume”.

Al doilea lucru de explicat ţine de CSS. Am folosit if(i & 1) row.setAttribute('class', 'altern'); pentru ca la vizualizarea în browser a tabelului rândurile consecutive să difere prin “background” (procedeu de redare numit sugestiv “zebra table”). Ca să şi funcţioneze aşa, trebuie să intrăm acum în sitsco.css şi să adăugăm:

#medii-generale table  tr.altern {
    background: #eee; /* sau altă culoare */
}

Salvând fişierele şi relansând din browser http://proiecte.home/sitsco.html ar trebui să constatăm obţinerea tabloului mediilor generale (inclusiv, dedesubtul lui, a mediei generale a clasei).

Diagrama statistică (folosind Google Chart)

Are sens să definim sort_table(id_tabel, rang_col) în afara funcţiei med_gen(): nefăcând parte din contextul unei alte funcţii, ea va putea fi refolosită oricând pentru alte tabele. În schimb, diagrama statistică pe care vrem să o obţinem este strâns legată de datele existente în tabelul mediilor generale obţinut - prin urmare cel mai firesc este să obţinem această diagramă chiar în cadrul funcţiei med_gen(). Deci să extindem corpul funcţiei med_gen() cu o funcţie care dacă este apelată, să producă diagrama statistică necesară.

Pentru reprezentarea grafică a unor statistici asupra mediilor vom apela un serviciu-web oferit de Google, anume Google Chart. Google Chart API răspunde la un URL corespunzător, returnând o imagine în format PNG. În URL trebuie specificate: dimensiunea imaginii, tipul diagramei şi datele necesare (alte atribute sunt opţionale: culori, etichete, titlu, etc.).

Identificatorii prevăzuţi pentru atribute încep de obicei cu ch (de la “chart”); astfel, chs=300x100 identifică “size” (dimensiune), asignând 300 pixels pentru width (lăţime) şi 100 pentru height (înălţime); chd=t:20,30,40,10 identifică datele de construcţie a diagramei, specificând ca tip de codificare a acestora text: valori numerice pozitive între 0.0 şi 100.0 (“floating point”), separate prin virgulă; cht=p specifică tipul diagramei (“p” pentru “pie”, adică diagramă circulară obişnuită; “p3” pentru diagramă circulară “3D”; iar “v” pentru diagramă Venn; etc.).

URL-ul poate fi transmis şi din bara de adresă a browserului:

http://chart.apis.google.com/chart?chs=400x150&chd=t:20,30,40,10&cht=p3

unde ?``chs=... indică parametrii cererii, separaţi prin ``& (formula obişnuită pentru QUERY STRING).

Dar mai obişnuit este ca diagrama returnată să fie inclusă într-un document HTML existent (cum avem nevoie în cazul nostru); în acest scop, URL-ul necesar trebuie specificat în atributul src al unui element <img>. Adăugăm în corpul funcţiei med_gen():

to_chart(); //a comenta linia, dacă nu se vrea diagrama statistică

function to_chart() {
    ob_chart.innerHTML = ''; //elimină diagrama anterioară, dacă există
    var size = '400x200', i;
    var charturl = 'http://chart.apis.google.com/chart?cht=p3&chs=' + size +
                          '&chtt=Statistică|pe+grupe+de+medii&chd=t:';
    var labels = ['5-6', '6-7', '7-8', '8-9', '9-10'];
    var data = [0, 0, 0, 0, 0];

    var t = $('situatie');
    var tds = t.getElementsByTagName('tbody')[0].getElementsByTagName('td');
    for(i = 0; tds[i]; i += 2) {
        media = tds[i+1].innerHTML;
        switch(media.charAt(0)) {
            case '5': data[0]++; break;
            case '6': data[1]++; break;
            case '7': data[2]++; break;
            case '8': data[3]++; break;
            case '9': data[4]++; break;
            case '1': data[4]++; break; //media 10 (nu avem media 1)
        }
    }

    for(i = 0; i < 5; i++) labels[i] += ' (' + data[i] + ')';

    var chart = document.createElement('img');
    chart.setAttribute('src', charturl + data.join(',') +
                              '&chl=' + labels.join('|'));
    chart.setAttribute('alt', 'diagrama');

    ob_chart.appendChild(chart);
}; //se încheie funcţia 'med_gen()'

var tds = t.getElementsByTagName('tbody')[0] obţine o referinţă la obiectul corespunzător secţiunii tbody[0] din tabelul referit de var t = $('situatie') (un tabel HTML poate avea mai multe secţiuni <tbody>).

În tabloul javaScript data[] s-au contorizat mediile de 5, 6, etc. şi apoi pentru setarea atributului src am transformat acest tablou în şir de valori separate prin virgulă, folosind data.join(',').

Odată cu adăugarea în pagina Web a diagramei returnate de Google Chart funcţia med_gen() este încheiată. Salvând fişierele şi reîncărcând aplicaţia în browser putem constata funcţionarea aşteptată, până în acest moment.

Mai rămâne de adăugat funcţia table_sort(); am evidenţiat mai sus că această funcţie ar putea folosi oricând, pentru oricare tabel - încât o dezvoltăm separat, într-un fişier propriu (posibil de inclus ulterior în alte programe în care am avea nevoie să sortăm un tabel).

O funcţie de ordonare

A rămas de realizat funcţia sort_table(id_tabel, rang_col); ea va fi declanşată atunci când utilizatorul va face click pe antetul uneia dintre coloanele tabelului mediilor generale obţinut anterior, trebuind să asigure reordonarea elementelor TR astfel încât acestea să apară fie în ordinea alfabetică a numelor, fie în ordinea mediilor.

Pentru a crea posibilitatea refolosirii acestei funcţii (în alte programe), o vom înscrie într-un fişier separat Js/ordonare.js. Acesta trebuie specificat desigur, în secţiunea <head> din sitsco.html:

   <script type="text/javascript" src="Js/sitsco.js"></script>
   <script type="text/javascript" src="Js/ordonare.js"></script>
   <title>Situaţia şcolară anuală</title>
</head>

Să ne amintim că adăugasem în sitsco.js o funcţie “sort_table()” care conţinea (“pentru testare”) alert(table.tagName);. Acum desigur, o ştergem: o selectăm, folosim “Cut” şi pastăm în noul fişier “ordonare.js” - iar după salvarea fişierelor modificate astfel (“sitsco.html”, “sitsco.js” şi “ordonare.js”) putem şi testa în browser: click pe un antet de coloană trebuie să alerteze “TABLE”. După aceasta, ştergem conţinutul funcţiei sort_table().

Presupunem că tabelul (al cărui ID se transmite în vederea ordonării) are o singură secţiune <tbody>; vom obţine o referinţă la obiectul DOM corespunzător acestei secţiuni prin:

var tbody0 = table.getElementsByTagName('tbody')[0];

(unde ‘table’ referă obiectul corespunzător ID-ului transmis). Încărcăm referinţele la rândurile TR din obiectul vizat de “tbody0”, într-un tablou javaScript ‘rows’[]:

var rows = tbody0.getElementsByTagName('tr');

Ceea ce avem de făcut este să ordonăm tabloul rows[] după valorile din coloana precizată la apel, să creem un nou element var tbody1 = document.createElement('tbody'); în care să plasăm elementele TR din tabloul tocmai ordonat rows[] şi în final, să înlocuim în pagina Web obiectul referit de “tbody0” cu “tbody1”:

table.replaceChild(tbody1, tbody0);

Desigur că pentru ordonarea tabloului rows[] este de preferat să nu interschimbăm conţinuturile respective (rows[0] referă primul TR din tabel, rows[1] pe al doilea TR, etc.), ci rangurile (sau indecşii) acestora. În acest scop, constituim tabloul arr_col, în care fiecare componentă este o pereche formată din indicele rândului curent din rows[] şi valoarea din coloana de rang “rang_col” de pe acel rând.

Putem formula o asemenea pereche prin { rând_curent: 2, valoare_col: “Popescu Giorgică” } unde acoladele încadrează elementele perechii, virgula le separă, iar : permite să disociem între identificatorii de elemente ale perechii şi valorile propriu-zise. La prima vedere, identificatorii elementelor din pereche nu sunt necesari (perechea propriu-zisă ar fi [2, “Popescu Giorgică”] - numai că… ar fi vorba atunci de un tablou de valori!); de fapt, identificatorii chiar devin esenţiali în operaţiile fireşti de atribuire şi de selectare: var P = { rând: 2, val_col: “Ion” }; alert(P.val_col); P.val_col = “Geo”; alert(P). Variabila P de aici este ceea ce se cheamă în diverse limbaje, o variabilă de tip hash, iar în javaScript este asimilată cu obiect.

arr_col este deci un tablou de hash-uri (sau de obiecte javaScript), în care: arr_col[i].idx_row = i este rangul rândului curent din tabloul rows[], iar arr_col[i].val_col = rows[i].getElementsByTagName(‘td‘)[rang_col].firstChild.nodeValue este valoarea de pe rândul respectiv din coloana de rang rang_col.

Ordonăm tabloul arr_col folosind funcţia nativă sort() şi o funcţie de comparare corespunzătoare (sau, în funcţie de valoarea din variabila globală SORT_ANTERIOR - folosind arr_col.reverse()). Apoi, creem un nou <tbody>, în care înscriem rândurile existente în rows[], dar în ordinea rezultată în arr_col[], deci în ordinea rows[arr_col[i].idx_row]; după înlocuirea vechiului cu noul <tbody>, tabelul corespunde ordinii dorite.

Sensul considerării variabilei globale SORT_ANTERIOR ţine de faptul că sort_table() este destinată să fie un “handler de click”. Dacă utilizatorul a făcut click pe antetul unei coloane a tabelului din pagina Web respectivă, se declanşează operaţia de sortare; dar dacă imediat după redarea rezultatului în pagină, utilizatorul face un nou click pe acelaşi antet (ceea ce este un obicei frecvent) - păi atunci nu este necesară ordonarea efectivă a tabloului, ci este suficientă inversarea rândurilor tabloului, realizată de metoda nativă pentru tablouri javaScript reverse()).

var SORT_ANTERIOR = -1; //coloana precedentei ordonări

/*
ordonează <table> cu ID=id_table, după coloana de rang=rang_col,
alfabetic dacă alpha_num==true, numeric dacă alpha_num==false
*/

function sort_table(id_table, rang_col, alpha_num) {
    var table = document.getElementById(id_table);
    var tbody0 = table.getElementsByTagName('tbody')[0];
    var rows = tbody0.getElementsByTagName('tr');

    var arr_col = []; // reţine pentru fiecare coloană:
                      // indicele TR şi conţinutul TD
    for (var i = 0, len = rows.length; i < len; i++) {
        arr_col[i] = {};
        arr_col[i].idx_row = i;
        arr_col[i].val_col = rows[i]
                             .getElementsByTagName('td')[rang_col]
                             .firstChild.nodeValue;
    }

    if (rang_col == SORT_ANTERIOR) {
       // la cereri consecutive de ordonare pe aceeaşi coloană,
        arr_col.reverse(); // inversează rândurile
    }
    else {
        SORT_ANTERIOR = rang_col;
        // ordonează arr_col[] (lexicografic, sau numeric)
        if (alpha_num) arr_col.sort(hash_cmp_lex);
        else arr_col.sort(hash_cmp_num);
    }

    // <tbody> cu TR aşezate în ordinea din arr_col[]
    var tbody1 = document.createElement('tbody');
    for (var i=0, len = arr_col.length; i < len; i++) {
        var rows_after_sort = rows[arr_col[i].idx_row];
        var cls = i&1 ? 'altern' : '';
        rows_after_sort.setAttribute('class', cls);
        tbody1.appendChild(rows_after_sort.cloneNode(true));
    }
    table.replaceChild(tbody1, tbody0);

    // funcţiile de comparare, folosite intern
    // Compară obiecte arr_col[i] şi arr_col[j],
    // după valoarea câmpului val_col

    function hash_cmp_lex(a, b) {
        var aVal = a.val_col, bVal = b.val_col;
        return aVal.localeCompare(bVal);
    }

    function hash_cmp_num(a, b) {
        var aVal = parseFloat(a.val_col),
            bVal = parseFloat(b.val_col);
        return (aVal - bVal);
    }
}

Privind funcţiile native sort() şi reverse(), iată vreo două exemple semnificative:

["Foo", "Bar", "bar", "Baz"].sort()   lexicografic: ["Bar", "Baz", "Foo", "bar"]
["Foo", "Bar", "bar", "Baz"].reverse()  "inversează": ["Baz", "bar", "Bar", "Foo"]
[30, 7, 300, 31].sort()   lexicografic [30, 300, 31, 7]
[30, 7, 300, 31].sort( function(a,b) {return a - b;} )   numeric [7, 30, 31, 300]

Folosirea “pseudoprotocolului” javascript: permite verificarea imediată a unor astfel de exemplificări: tastaţi în bara de adresă a browserului javascript:alert([30, 7, 300, 31].sort(function(a,b){return a - b;})); - răspunsul va fi o fereastră de alertare, conţinând tabloul sortat.

Putem folosi pentru mici teste şi “Error Console” (a vedea meniul Firefox “Tools”). În imaginea alăturată este reprodus testul sortării obişnuite pentru cazul când numele ar începe cu litere “nestandard”; vedem că “Z...” este înainte de “Ş...”.

Pentru sortarea corectă a şirurilor care conţin şi alte caractere decât cele standard, trebuie folosită metoda localeCompare(), proprie obiectului javaScript String() - cum atestează următoarea imagine (şi cum am folosit în hash_cmp_lex()):

_images/sort-cons224.png

Ca să şi probăm lucrurile, încărcăm desigur http://proiecte.home/sitsco.html; dar… nu ajunge! În sitsco.js apelam sort_table() numai cu doi parametri:

for(i = 0; i < 2; i++) {
    cel = TH.cloneNode(true);
    // handlerul de click, pentru sortare după coloană
    cel.innerHTML = "<a onclick='sort_table(" +
                           '\"situatie\",' + i + ");' " +
                           "href='#'>" + antet[i] + "</a>";
    row.appendChild(cel);
}

ori aici am adăugat şi parametrul ‘alpha_num’, astfel că onclick trebuie rescris pentru a invoca sort_table(‘situatie’, 0, true) în cazul primei coloane (ordonare alfabetică după nume) şi respectiv sort_table(‘situatie’, 1, false) în cazul celei de-a doua coloane (ordonare numerică după medii).

Fişierele acestei aplicaţii sunt oferite în arhiva sitsco.zip anexată (şi vedeţi acolo şi mica modificare evidenţiată mai sus).

«  Dezvoltarea de aplicaţii Web simple   ::   Contents   ::   Aplicaţie Web tipică angajând PHP şi MySQL  »