În partea I am discutat contextul aplicaţiei, evidenţiind că javascript nu permite "Save/Load" şi conturând ca soluţie pentru "salvarea / încărcarea" datelor, folosirea elementelor <textarea>. Acum vom trata problema în liniile ei esenţiale (nu implicăm lista obiectelor, statistici pe obiecte, etc.): trebuie asigurată posibilitatea de a introduce/încărca numele şi mediile la obiectele din catalog, obţinând tabelul mediilor generale; în plus - posibilitatea de a ordona "interactiv" acest tabel.

Situaţia şcolară a clasei      [sit-sco.zip]

  Catalog (tastaţi/copiaţi din fişier-text Nume Prenume medii, unul pe rând)


  

Utilizatorul are la dispoziţie un <textarea> în care poate introduce Nume Prenume şi mediile la obiectele din catalog pentru fiecare elev (putând şi "salva"/"încărca" datele, cum am arătat în I: selectează conţinutul, CTRL + C pentru a copia datele, apoi CTRL + V pentru a le înscrie într-un fişier-text propriu, respectiv înapoi în <textarea>). Apoi, trebuie click pe butonul Load. Apoi, click pe butonul Mediile generale va produce tabloul elevilor şi mediilor generale; click pe antetul uneia dintre coloane va reordona corespunzător liniile tabelului.

^TOP^
Fişierele HTML şi CSS ale aplicaţiei

Aplicaţia este constituită din fişierele sit-sco.html, sit-sco.css şi sit-sco.js (vezi secţiunea <head>).

<!-- "sit-sco.html" -->
<head>
   <meta http-equiv="Content-Type" content="text/html;charset=utf-8">
   <link rel="stylesheet" href="sit-sco.css" type="text/css">
   <script type="text/javascript" src="sit-sco.js"></script> 
   <title>Situaţia şcolară anuală</title>
</head>
<body>
   <p class="psit">  <b>Catalog</b> <span class="ishelp">(tastaţi/copiaţi din fişier-text <i>Nume Prenume medii</i>, unul pe rând)</span></p>
   <div class="tdiv">
      <textarea id="catalog" rows="8" cols="100"></textarea><br>
      <button type="button" class="sbutt" onclick="load_cat();">Load</button>
   </div>

   <p class="psit">  <button type="button" class="sbutt" onclick="med_gen('medii');">Mediile generale</button></p>
   <div id="medii" class="tdiv"> </div>
</body>

Elementele <button> Load şi Medii generale au asociate câte un "handler" - funcţiile load_cat(), respectiv med_gen() - care va fi "tras" de click-ul pe buton. Click pe "Medii generale" va duce la apelarea funcţiei "med_gen('medii')"; aceasta va genera în DOM-ul documentului (deci în memorie) tabelul cu mediile generale.

În unele browsere, tabelul generat poate fi vizualizat ca şi cum ar face parte (dar nu face parte) din sursa HTML iniţială (de exemplu, în Firefox avem un meniu View Generated Source).

Diversele atribute class ("psit", "ishelp", "tdiv", etc.) permit stilarea flexibilă (prin intermediul fişierului CSS) a elementelor HTML implicate. Stilurile prevăzute pentru <table> (şi elementele subordonate) diferă foarte puţin de cele deja folosite anterior la "orarul clasei":

/* "sit-sco.css" */
#medii table {
  border-collapse: collapse; 
  font-family: verdana, arial, sans-serif; font-size: 12px; font-weight: normal;
  color: #363636; background: #FFD800; 
}
td, th { padding: .2em; }
thead th, tfoot td { font-size: 11px; padding-top: 3px; background: #FFFFFF; }
thead th { text-align: left; }
tfoot td { text-align: center; border-top: 1px solid #03476F; font-weight: bold;}
tr.altern { background: #FFD866; }

.ishelp { font-size: 0.8em; color:#448; }
.sbutt { cursor: pointer; font-weight: bold; }
.tdiv {margin-bottom: 2em; margin-left: 2em; }
.psit { margin-bottom: 0.2em; }

^TOP^
Funcţiile javascript necesare aplicaţiei

Fişierul sit-sco.js poate fi constituit pe următoarea structură principială:

        var CATAL = []; // tablou global pentru Elevi-medii 
        var SORT_ANT; // coloana după care s-a făcut ultima sortare (pentru "reverse") 

        function load_cat() { // apelată de click pe butonul Load
           // încarcă în tabloul CATAL, liniile de date din <textarea> cu ID = "catalog"
        }

        function med_gen(dest) { // apelată de click pe butonul Medii generale
           // tabel = createElement('table') // creează apoi, 'thead', 'tfoot', 'tbody'
           // pentru fiecare linie din CATAL:
           //    creează TR şi TD pentru Nume-Prenume,
           //    calculează media-generală a mediilor de pe linia respectivă
           //    appendChild() - TD la TR, TR la TBODY
           // tabel.appendChild(TBODY);
           // dest.appendChild(tabel); // înscrie tabelul în DOM, la ID = dest
        }

        function sort_table(scol) { // rangul coloanei după care sortează (Nume/Medie)
                            // apelată de click pe antetul coloanei (<TH><A onclick=...>)
           // rows[] = Array local al liniilor din TBODY (pentru tabelul existent al mediilor generale)
           // ordonează rows[] după valorile din coloana indicată 
           // creează un nou TBODY având ca TR-uri rândurile (ordonate) din rows[]
           // înlocuieşte vechiul TBODY cu noul TBODY // table.replaceChild(tbody1, tbody0)    
        }

Sensul considerării unei variabile globale SORT_ANT se vede imaginând următoarea desfăşurare: se face click pe butonul Load, încât se constituie tabloul CATAL; apoi, se face click pe butonul Medii generale, rezultând tabelul Numelor şi mediilor generale; în acest tabel, elevii apar în ordinea în care existau în CATAL - deci, nu neapărat ordonaţi în vreun fel.

Făcând acum click pe antetul "Nume Prenume", se lansează sort_table(0) care realizează schimbările necesare în tabel încât elevii să fie redaţi în ordine alfabetică. Făcând imediat, un al doilea click tot pe "Nume Prenume", se apelează iarăşi sort_table(0), realizând de această dată ordonarea inversă celei obţinute la primul click - ori de data aceasta (la al doilea click) putem scuti prelucrările de ordonare specifice, dat fiind că putem invoca direct funcţia nativă reverse().

Memorând "primul click" (de fapt, rangul coloanei respective) în SORT_ANT, vom putea şti dacă este suficient să se invoce reverse(), sau trebuie efectiv folosită procedura de ordonare.

^TOP^
Funcţia load_cat() şi… verificarea datelor

Funcţia load_cat() este cea mai simplă (dacă nu implicăm şi verificări…):

function load_cat() {
   var tarea = document.getElementById('catalog');
   var trows = tarea.value.replace(/^\s+/mg,"").replace(/\s+$/mg,"");
   if(trows) CATAL = trows.split(/[\n\r]+/g); else alert("Catalogul este vid!");
}

Variabila tarea salvează o referinţă la nodul din DOM corespunzător elementului <textarea> cu ID-ul "catalog"; trows memorează ca şir de caractere valoarea existentă în acest <textarea> (după ce se elimină toate spaţiile iniţiale/finale, folosind replace()). Apoi, split(/[\n\r]+/g) separă şirul trows în subşiruri care corespund rândurilor din <textarea> (ca şi în alte limbaje, "\n" este caracterul de "sfârşit de rând", introdus de obicei prin apăsarea tastei ENTER) - iar aceste subşiruri devin componentele tabloului global CATAL.

Verificarea datelor. Pentru simplitate - nu am implicat nicio verificare (pentru a accepta numai şiruri care să reprezinte Nume Prenume şi respectiv "medii" de forma: dd.dd, unde d este o cifră zecimală). În general este necesară verificarea datelor introduse de utilizator (şi este absolut necesară, dacă datele se transmit la server - ceea ce nu este cazul aici). Dar nici n-ar trebui exagerat în privinţa procedurilor de verificare: utilizatorul nu este imbecil şi înţelege totuşi despre ce este vorba! E drept că este frecventă - şi este citată cu groază de către programatori - şi situaţia opusă: utilizatorul - mai ales cel cu educaţie bazată pe point-and-click - face click la întâmplare şi la repezeală pe orice se poate, ignoră complet orice text explicativ ca şi cum n-ar şti şi el să citească, iar dacă-i vorba să "bage" nişte date atunci tastează ceva la întâmplare şi dă cu ciocanul pe ENTER…; nu-i de mirare că primim astfel de recomandări: "Programatorule - fii scurt: evită textul, comunică numai prin iconuri şi imagini; şi ai grijă: nu trebuie să ai încredere în utilizator!" şi analog: "Hei, Dom'profesor - fii scurt, lasă filosofia şi corelările şi dă doar formula că aia i-o cere la bacalaureat!".

^TOP^
Funcţia med_gen() de constituire în DOM a tabelului

Funcţia med_gen(dest) construieşte în DOM, ca "child" al nodului "dest", elementul <table> corespunzător tabelului mediilor generale (Nume, Media generală); ea este foarte asemănătoare cu funcţia set_DOM_orar(orar, div_id) - redată anterior în "Exemplu: orarul clasei, varianta dinamică" - care primind tabloul orelor "orar" construia <table> ca "child" al nodului indicat de "div_id".

function med_gen(dest) {
   if(CATAL.length == 0) { alert("înscrieţi întâi Catalogul!"); return false; }
   var dest = document.getElementById(dest); 
   dest.innerHTML = ''; // iniţializare necesară - permite "reîncărcarea" datelor

   var tabel = document.createElement('table'); 
   tabel.setAttribute('id', 'situatie');  // pentru accesare din sort_table()

   var TR = document.createElement('tr'); // nodurile TR, TH, TD vor fi apoi "clonate",
   var TH = document.createElement('th'); // evitând recrearea lor
   var TD = document.createElement('td');
   var row, cel; 

   var thead = document.createElement('thead'); // secţiunea <thead> a tabelului
   row = TR.cloneNode(true); var antet = ['Nume Prenume', 'media'];
   for(var i = 0; i < 2; i++) { 
      cel = TH.cloneNode(true); 
      cel.innerHTML = "<a onclick='sort_table(" + i + ");' href='javascript:;'>" + antet[i] + "</a>"; 
      row.appendChild(cel); // click va apela sort_table(scol), cu scol=0 sau scol=1
   }
   thead.appendChild(row); 
   tabel.appendChild(thead); // amânăm crearea <tfoot>, pentru a calcula şi media clasei

   var tbody = document.createElement('tbody'); // secţiunea <tbody>
   var mgcl = 0; // media generală a clasei (va fi înscrisă în <tfoot>)
   var n = CATAL.length;
   for(var i = 0; i < n; i++) {
       var el = CATAL[i]; 
       var nume = el.replace(/^(\D+).+/,"$1");
       var medii = el.replace(/^\D+(.+)$/,"$1").split(/\s+/g);   // alert(medii);
       var mg = 0; var no = medii.length;
       for(var m = 0; m < no; m++)
          mg += parseFloat(medii[m]);
       mg /= no; mgcl += mg;
       row = TR.cloneNode(true); 
       cel = TD.cloneNode(true); cel.innerHTML = nume; row.appendChild(cel);
       cel = TD.cloneNode(true); 
       cel.innerHTML = mg.toPrecision(4).substring(0,4); // cu 2 zecimale exacte
       row.appendChild(cel);
       if(i & 1) row.setAttribute('class', 'altern'); // alternez background rânduri
       tbody.appendChild(row);
   }

   mgcl /= n; // media clasei va fi adăugată la subsol
   var tfoot = document.createElement('tfoot'); // secţiunea <tfoot>
   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);
   dest.appendChild(tabel); // înscrie tabelul în document (în DOM)

   SORT_ANT = -1; // încă nu s-a apelat la sortare după vreo coloană
   indicatii(dest); // adaugă "indicaţii" pentru utilizator
}

<thead> şi <tfoot> trebuie inserate înaintea lui <tbody> (tabel.appendChild(tfoot) înainte de tabel.appendChild(tbody)), dar această regulă nu ne împiedică să creem întâi <tbody> şi apoi <tfoot> (ceea ce a fost necesar aici, pentru a obţine întâi media generală a clasei şi a o înscrie apoi în <tfoot>).

Cele două elemente <th> din <thead> conţin câte un link ("ancoră"), precum <a onclick = "sort_table(0);" href = "javascript:;">Nume Prenume</a>. Atributul href trebuie să specifice resursa vizată (dacă este un URL - de exemplu href = "http://www.google.com" - atunci la click pe link-ul respectiv va fi încărcată pagina indicată de URL); aici, nu este vizat protocolul HTTP, ci "protocolul javascript:", deci click pe link-ul respectiv este "inoperant" şi de fapt clik-ul va lansa funcţia indicată de atributul onclick (ordonarea tabelului după coloana de rang 0).

Elementele tabloului CATAL sunt şiruri precum "Ionescu Ion 7.67 8.67 9 10 10 10 10 10 8.67 7.67 8.67 9 10 10 10 10 10 8.67" (Nume Prenume şi apoi mediile la obiecte); la crearea elementului <tbody> se preia câte un asemenea şir prin var el = CATAL[i];, se extrage numele, apoi se extrage şirul mediilor şi se constituie (cu split) tabloul mediilor la obiecte.

Numele este extras prin var nume = el.replace(/^(\D+).+/,"$1");; aici, /^(\D+).+/ este o expresia regulată (un regex), adică un şablon construit după anumite specificaţii standard, care permite 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".

În final, se iniţializează SORT_ANT cu un rang de coloană inexistent - încât la prima apelare a funcţiei sort_table() să se facă efectiv ordonarea tabelului (şi nu să se invoce reverse()); de asemenea, se apelează o funcţie indicatii() care ar adăuga sub tabel, nişte mici "indicaţii de utilizare" a tabelului.

^TOP^
Funcţia sort_table() de reconstrucţie în DOM a tabelului ordonat

Ideea pe care o folosim se întâlneşte în diverse locuri - google table sort javascript - de obicei, în contextul mai complicat al creerii unui widget complet - obiect înzestrat cu majoritatea operaţiilor de care utilizatorul ar putea avea nevoie (de exemplu, pe lângă ordonarea care ne interesează aici: redimensionarea / repoziţionarea coloanelor folosind mouse-ul, etc.).

Funcţia med_gen() a creat tabelul cu două coloane "Nume Prenume" şi "medii" şi i-a setat atributul ID cu valoarea "situatie" (prin tabel.setAttribute('id', 'situatie');) - încât tabelul poate fi acum accesat din funcţia noastră. Un <table> poate conţine mai multe secţiuni <tbody>, iar getElementsByTagName('tbody') furnizează un tablou conţinând referinţele corespunzătoare fiecăreia; reţinem în tbody0 referinţa la unicul <tbody> din tabloul nostru şi încărcăm referinţele la rândurile <tr> din acest <tbody>, în tabloul rows.

Constituim apoi tabloul arr_col, în care fiecare componentă este o pereche formată din indicele rândului curent din rows şi valoarea din coloana de rang scol 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.

arr_col este deci un tablou de hash-uri, în care: arr_col[i].oldr = i este rangul rândului curent din tabloul rows, iar arr_col[i].valc = rows[i].getElementsByTagName('td')[scol].firstChild.nodeValue este valoarea de pe rândul respectiv din coloana de rang scol.

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_ANT - 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 rows[arr_col[i].oldr]; după înlocuirea vechiului cu noul <tbody>, tabelul corespunde ordinii dorite.

function sort_table(scol) { // rangul coloanei după care sortează (aici, 0 sau 1)
   var table = document.getElementById('situatie');
   var tbody0 = table.getElementsByTagName('tbody')[0]; // vechiul TBODY
   var rows = tbody0.getElementsByTagName('tr'); // rândurile din vechiul TBODY

   var arr_col = [];  // valorile din coloana de sortat ("array of hashes")
   for (var i = 0, len = rows.length; i < len; i++) {
      arr_col[i] = {}; // "hash" { rând curent (old) => valoarea din coloană }
      arr_col[i].oldr = i;
      arr_col[i].valc = rows[i].getElementsByTagName('td')[scol].firstChild.nodeValue;
   }

   if (scol == SORT_ANT) { // dacă s-a sortat anterior după coloana 'scol' (ASC), 
      arr_col.reverse();  // atunci e suficientă inversarea rândurilor (DESC)
   }
   else {
      SORT_ANT = scol;  // memorează rangul coloanei de sortat (pentru "reverse") 
      if (scol == 0) arr_col.sort(hash_cmp_lex); // ordonează lexicografic numele, 
      else arr_col.sort(hash_cmp_num);  // ordonează numeric mediile 
   }

   var tbody1 = document.createElement('tbody');  // constituie noul TBODY al tabelului
   for (var i=0, len = arr_col.length; i < len; i++) {
      var myrw = rows[arr_col[i].oldr]; // rândul de pe vechea poziţie
      var cls = i&1 ? 'altern' : ''; // setează/resetează 'altern' pe rândul respectiv
      myrw.setAttribute('class', cls);
      tbody1.appendChild(myrw.cloneNode(true)); // adaugă rândul pe noua poziţie 
   }

   table.replaceChild(tbody1, tbody0); // înlocuieşte în tabel, vechiul cu noul TBODY
}

function hash_cmp_lex(a, b) { // ordonează lexicografic
   var aVal = a.valc, bVal = b.valc;  
   return (aVal == bVal ? 0 : (aVal > bVal ? 1 : -1));
}

function hash_cmp_num(a, b) { // ordonează numeric
   var aVal = parseFloat(a.valc), bVal = parseFloat(b.valc);
   return (aVal - bVal);
}

Privind funcţiile native sort() şi reverse(), iată vreo două exemple semnificative:
["Foo", "Bar", "bar", "Baz"].sort() produce tabloul ordonat lexicografic ["Bar", "Baz", "Foo", "bar"];
["Foo", "Bar", "bar", "Baz"].reverse() produce tabloul "inversat" ["Baz", "bar", "Bar", "Foo"];
[30, 7, 300, 31].sort() produce tabloul sortat lexicografic [30, 300, 31, 7]
iar [30, 7, 300, 31].sort( function(a,b) {return a - b;} ), în care am implicat şi o funcţie de comparare a elementelor - produce tabloul ordonat 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.

^TOP^
Bibliografie

Expresii regulatebrainjar.com/dhtml/tablesortCore JavaScript Guide 1.5