momente şi schiţe de informatică şi matematică
To attain knowledge, write. To attain wisdom, rewrite.

Un bookmarklet pentru calculul dinamic al mediei şcolare

Firefox | bookmarklet | javaScript | modelare obiectuală
2009 jul

Mai înainte—Un plugin jQuery pentru calculul dinamic al mediei şcolare—am arătat deosebirea dintre "a calcula" şi "a calcula cu două zecimale" şi am dezvoltat două mici aplicaţii: prima nu folosea biblioteci externe şi producea un formular "dichisit", de ancorat într-un loc predeterminat - motiv pentru care am şi calificat-o ca fiind statică; a doua, implica biblioteca jQuery şi permitea folosirea dinamică (unde şi cât este necesar) a unui formular simplu şi practic. Ambele aplicaţii realizează calculul dinamic al mediei şcolare, în sensul că media este evaluată şi afişată după fiecare tastare de notă.

Pentru a folosi într-un document una sau alta dintre cele două aplicaţii menţionate, scriptul respectiv trebuie inclus în mod explicit (folosind <script>), în cadrul acelui document. Ne ocupăm aici de o a treia posibilitate: înregistrăm scriptul respectiv în Bookmarks-ul browserului, astfel că el va putea fi utilizat direct (fără includere explicită) din oricare document încărcat (ba chiar şi direct din browser).

Bookmarks (semne de carte)

Peste tot aici vizăm Firefox; dar meniurile si conceptele referite sunt valabile mutatis mutandis şi în alte browsere. Accesăm din bara de meniu View/Sidebar/Bookmarks:

bookm1

După aceea, click-dreapta pe Bookmarks Toolbar instanţiază un meniu care oferă două opţiuni esenţiale: New Bookmark... (care ne va permite să înregistrăm un nou "semn de carte") şi Properties (prin care vom putea modifica un bookmark existent):

bookm2

De obicei, Bookmarks se foloseşte pentru a ne asigura posibilitatea de a accesa un anumit site fără să mai trebuiască să tastăm adresa respectivă în bara de adresă a browserului (sau să-l căutăm prin intermediul unui formular Search). După click New Bookmark, putem completa formularul Add Bookmark - de exemplu:

bookm3

După ce am adăugat astfel "semnul de carte", n-avem decât să deschidem Bookmarks şi să-l găsim pe cel dorit (pentru aceasta este suficient de obicei să tastam în caseta Search din Bookmarks, primele două-trei litere ale numelui bookmark-ului):

bookm4

Click pe bookmark-ul găsit astfel, va accesa site-ul respectiv; iar click-dreapta - va permite să optăm asupra destinaţiei paginii Web accesate: fie într-un nou tab al browserului, fie într-o nouă fereastră.

Unde se va deschide pagina Web?

Este foarte interesant că în anumite condiţii, click pe bookmark-ul respectiv (sau pe opţiunea Open - vezi imaginea de mai sus) va deschide pagina Web corespunzătoare chiar în cadrul marcat de Bookmarks (nu într-un nou tab, nu într-o nouă fereastră şi nu în interiorul vreunui document deja deschis în browser):

bookm5

Regiunea de pe ecran marcată de Bookmarks nu este o "fereastră" obişnuită; nu poate fi maximizată, nici salvată ca atare, nici încărcată prin modalităţi uzuale cu un anumit conţinut, etc. Dar experimentul tocmai reprodus arată că zona-cadru Bookmarks este mai mult decât un container destinat listei de bookmark-uri creată intern de către browser, putând eventual (în anumite condiţii) să fie folosită şi pentru documente "externe".

Aceasta ne va permite de exemplu, să instanţiem formularul nostru pentru media şcolară chiar în cadrul Bookmarks - făcându-l astfel accesibil din oricare document deschis în browser şi în acelaşi timp, evitând orice dependenţă cu vreun document existent (în particular, va fi accesibil într-o aceeaşi poziţie-ecran, independent de cum este scrollat documentul).

Bookmarklet

Pe lângă protocolul http:// (folosit în bara de adresă pentru a accesa o pagină Web), browserele recunosc şi pseudo-protocolul javascript: prin care se lansează în execuţie o secvenţă de instrucţiuni javaScript scrisă direct în bara de adresă:

bookm7

La fel cum se pot crea bookmark-uri cu link-uri de pagini Web - aşa cum am exemplificat mai sus - tot aşa se pot crea bookmark-uri şi pentru scripturi javaScript:

bookm6

Înregistrând bookmark-ul, căutându-l apoi în lista Bookmarks şi făcând click pe el - browserul va executa secvenţa de cod asociată. Codul asociat prin javascript: poate să fie chiar destul de complex (nu doar o singură instrucţiune, ca în exemplul nostru) şi bookmark-urile de acest tip au primit o denumire specială: bookmarklet - rezultată prin combinarea termenilor "bookmark" şi applet (program Java inclus într-o pagină HTML).

Transformarea aplicaţiei media şcolară într-un bookmarklet

Un bookmarklet nu poate să fie prea lung: maximum 508 caractere sub IE 6.0, 2083 caractere sub IE 7.0 şi până pe la 20000 de caractere pentru Firefox. Însă - în treacăt fie spus - lungimea n-ar fi o problemă, fiindcă în loc de a pune sub javascript: însuşi scriptul lung respectiv, putem scrie o secvenţă relativ scurtă prin care să se creeze în document un element <script> al cărui atribut src să aibă ca valoare adresa necesară pentru încărcarea scriptului real (adică bookmarklet-ul revine la a fi în realitate un stub - secvenţă care serveşte pentru legarea de un program extern).

În cazul nostru - mai ales că problema calculării mediei este suficient de elementară - încercăm să evităm biblioteci precum jQuery şi evităm să referim alte scripturi. Trebuind să scriem cât mai concis posibil, în primul rând vom folosi pentru variabile identificatori de câte o singură literă (contrar recomandărilor binecunoscute, de stil de programare).

Strategia obişnuită pentru realizarea unui bookmarklet constă în următorii paşi: întâi se lucrează ca şi când ar fi vorba de o aplicaţie obişnuită (implicând fişiere JS, HTML, CSS - realizate toate în stilul obişnuit de comentare şi spaţiere); odată pusă la punct aplicaţia respectivă, urmează să eliminăm comentariile şi toate spaţiile care nu sunt strict necesare (şi eventual, va mai trebui să codificăm anumite caractere); în final - rămâne eventual să ambalăm scriptul respectiv în "paranteze de invocare", asigurând lansarea în execuţie imediat după încărcare.

Modelarea modernă a aplicaţiei media dinamică

De fapt, maniera pe care o folosim mai jos a devenit deja "clasică". Definim un obiect G care în primul rând, conţine trei containere - respectiv pentru Note, Teză şi Medie; acestea sunt ele însele nişte obiecte (şi anume nişte obiecte predefinite), încât putem folosi proprietăţile şi metodele specifice lor (în cadrul obiectului G definit de noi).

Note şi Teză sunt "de tip" <input>; ca obiect, un <input> are predefinite nişte metode de interacţiune - onClick(), onKeypress(), onKeyup(), onMouseover(), etc. iar acestea se pot "redefini" pentru a răspunde în modul dorit unor evenimente precum apăsarea unei taste sau click în cadrul containerului aferent, sau în exteriorul lui, etc.

Redefinim în obiectul G, metodele onKeyup() pentru <input>-urile Note şi Teză, în aşa fel încât fiecare tastare (sau ştergere) de notă/teză să declanşeze procedura de calcul şi înscriere a mediei.

După definirea astfel a obiectului G, adăugăm o funcţie care să-l creeze conform specificaţiilor lui şi să-l anexeze documentului. În final, includem totul în "paranteze de invocare" (...)();, astfel că toate variabilele implicate rămân ne-vizibile din afară şi pe de altă parte, funcţia finală (care şi ea trebuie inclusă în paranteze de invocare) va fi pusă în execuţie imediat după încărcarea scriptului.

// fişierul mediadyn.js modelează calculul dinamic al mediei şcolare

(function() {
    // ne punem la dispoziţie un nod <input> şi un nod <span>
    var x = document.createElement('input'),
        y = document.createElement('span');

    var G = function() {
        this.N = x.cloneNode(1); // clonează un <input> pentru Note
        this.T = x.cloneNode(1); // <input> pentru Teza
        this.N.setAttribute('size',6); this.T.setAttribute('size',1);
        this.M = y.cloneNode(1); // <span> pentru media calculată
        
        var F = this; // păstrează o referinţă la obiectul G

        this.N.onkeyup = function() { // la tastarea unei note, calculează media
            var d = 0, n = F.N.value, r = n.length, k = r;
            for(var i = 0; i < k; i++) { // însumează notele înregistrate în Note
                var q = parseInt(n.charAt(i));
                if(!q) { d += 9; r--; } // '0' poate urma numai după '1' (nota '10')
                else d += q;
            }
            d = parseInt((d / r)*100) / 100; // media, cu două zecimale exacte
            setTimeout(function() { // scrie rezultatul în <span>-ul rezervat mediei
                F.M.innerHTML = ' ' + d;
            }, 10);
            // dacă există Teză, recalculează media (declanşând onkeyup() pentru Teză)
            if(F.T.value)
                setTimeout(function() { F.T.onkeyup();}, 10);
        };

        this.T.onkeyup = function() { // la tastarea tezei, calculează media
            var t = F.T.value || '0';
            if(t == '0') F.N.onkeyup(); // recalculează media, dacă Teza a fost ştearsă
            else {
                var d = parseFloat(F.M.innerHTML); // preia media din <span>
                d = (3 * d + parseInt(t)) / 4;
                d = parseInt(d * 100) / 100; // media şcolară, cu două zecimale exacte
                setTimeout(function() { // scrie rezultatul în <span>-ul rezervat mediei
                    F.M.innerHTML = ' ' + d;
                }, 10);
            }
        };
    };
    
    (function () { // instanţiază obiectul definit mai sus şi completează structura HTML
        var w = new G(); 
        var p = document.createElement('p'); // <p> care va ambala Note, Teză, Media
        p.style.backgroundColor='#448'; p.style.color='#FFF';
        p.style.fontWeight='bold'; p.style.fontSize='12px';
        p.style.width="220px";
        var s = y.cloneNode(1); s.innerHTML = "Note"; 
        p.appendChild(s); p.appendChild(w.N);
        s = y.cloneNode(1); s.innerHTML = "Teza";
        p.appendChild(s); p.appendChild(w.T);
        p.appendChild(w.M); // <p>Note<input />Teza<input /><span></span></p>
        document.body.appendChild(p);
    })();
    
})();

Faţă de comentariile de mai sus… probabil că un singur lucru ar fi totuşi de adăugat : this referă (ca şi în multe alte limbaje) instanţa curentă a obiectului; dar onkeyup() este o metodă proprie obiectului predefinit "input", încât în blocul aferent care redefineşte în G această metodă, this va referi nu instanţa curentă G, ci instanţa curentă de "input". Din acest motiv, a fost necesară salvarea într-o variabilă F a unei referinţe la G (folosind în interiorul funcţiilor onkeyup() nu this, ci copia F a sa).

Pentru verificare şi punere la punct, putem folosi următorul fişier HTML (desigur, cât mai simplu - având în vedere intenţia de a transforma scriptul de mai sus într-un bookmarklet):

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
          "http://www.w3.org/TR/html4/strict.dtd">
<html lang="ro"> <title>Media dinamică file:///home/vb/lar/root/mediadyn.html</title>

<body> </body>

<script type="text/javascript" src="mediadyn.js"></script>
</html>

Este important locul în care plasăm <script>-ul care încarcă mediadyn.js; dacă-l plasam în <head> (şi nu după <body>, cum am procedat), atunci scriptul nostru s-ar fi încărcat (şi pus în execuţie) înainte de a se fi creat <body> - încât execuţia lui ar fi eşuat la document.body.appendChild(p) (având document.body=null).

N.B. Întâlnesc mereu persoane care nu ştiu să citească; s-au deprins (prost) să rezume toate deprinderile elementare (a citi, a scrie, a vorbi, a gândi) la CLICK şi nu înţeleg de fel că s-ar afla ceva în spatele "iconiţelor" sau al manevrelor uzuale. Zic eu acuşi, "încărcăm şi executăm fişierul de mai sus"… dar să nu creadă cineva că cele două fişiere redate mai sus s-au scris singure şi dintr-o bucată! Totdeauna trebuie ceva muncă - şi de documentare, şi de modificare şi reverificare, sau "punere la punct" cum se zice - până ce în sfârşit, se ajunge la o formă funcţională acceptabilă.

Încărcăm în browser fişierul HTML tocmai redat şi probăm funcţionalitatea asigurată:

bookm9

Am ales să reproducem tocmai acest exemplu de calcul, fiindcă rezultă media şcolară 7.49 (adică oficial, media 7 şi nu 8). Dar (fiindcă tot ne-a scăpat să o facem anterior în Un plugin jQuery pentru calculul dinamic al mediei şcolare) să reproducem calculul respectiv şi pe un "calculator" obişnuit:

bookm8

Tastând Enter în caseta calculatorului, rezultă media 7.50 (şi nu 7.49), adică media oficială ar fi 8 (şi nu 7)! Dar nu ar trebui să fie cineva surprins: este diferenţă şi teoretică şi practică între a calcula (calculatoarele folosesc modelul floating-point) şi a calcula cu două zecimale exacte (legea şcolii noastre pretinde ca media şcolară să fie calculată "cu două zecimale exacte" şi apoi, rotunjită - ceea ce era corect pentru vechea regulă de calcul (când se împărţea la 2 suma dintre media notelor şi teză), dar devine incorect pentru regula actuală mai sofisticată).

Transformarea în bookmarklet

Mai întâi, folosim programul wc ("word-count") pentru o statistică elementară asupra fişierului mediadyn.js:

bookm10

Opţiunile -lmc corespund respectiv numărului de linii (59 caractere "new-line"), numărului total de caractere (= 2698) şi numărului de octeţi ocupaţi de fişier pe disk (= 2736).

Desigur, statistica respectivă şi chiar instrumentele cu care se poate obţine - depind şi de sistemul de operare; de exemplu, numărul de octeţi ocupaţi pe disk va fi ceva mai mare sub DOS/Windows - probabil, cu 59 de octeţi mai mult, fiindcă un caracter "New Line" este reprezentat pe 1 octet în Linux, dar pe 2 octeţi în DOS. Iar diferenţa dintre numărul de octeţi şi numărul de caractere (2736 versus 2698) provine din faptul că în comentariile incluse în mediadyn.js am folosit şi caractere "nestandard" (precum ă, ş, ţ etc.), iar acestea se reprezintă de obicei pe câte doi octeţi (şi nu pe unul singur, precum caracterele din setul standard).

Putem fi siguri pe baza datelor statistice obţinute, că eliminând ceea ce se poate elimina (comentarii şi spaţii nesemnificative) vom putea reduce suficient dimensiunea scriptului - astfel încât să-l putem transforma direct în bookmarklet (desigur, nu şi pentru IE 6.0, care totuşi este prea restrictiv cu cei maximum 508 octeţi permişi pentru un bookmarklet).

Pentru eliminarea comentariilor, spaţiilor şi pentru alte chestiuni de compresie - cel mai bine este să folosim unul dintre programele utilitare existente (deja verificate şi bine puse la punct până acum) - vezi de exemplu YUI Compressor. Să precizăm că unele dintre aceste programe înlocuiesc automat identificatorii "lungi" prin identificatori de câte o singură literă (aşa că n-ar fi fost nevoie să ne impunem folosirea de variabile de câte o singură literă, cum am procedat din capul locului în mediadyn.js).

Redăm aici una dintre variantele rezultate astfel, în ideea următoare: textul poate fi selectat, copiat şi pastat în fereastra de adăugare a bookmark-ului (în caseta Location javascript:):

(function(){var x=document.createElement('input'),y=document.createElement('span');var G=function(){this.N=x.cloneNode(1);this.T=x.cloneNode(1);this.N.setAttribute('size',6);this.T.setAttribute('size',1);this.M=y.cloneNode(1);var F=this;this.N.onkeyup=function(){var d=0,n=F.N.value,r=n.length,k=r;for(var i=0;i<k;i++){var q=parseInt(n.charAt(i));if(!q){d+=9;r--;}
else d+=q;}
d=parseInt((d/r)*100)/100;setTimeout(function(){F.M.innerHTML=' '+d;},10);if(F.T.value)
setTimeout(function(){F.T.onkeyup();},10);};this.T.onkeyup=function(){var t=F.T.value||'0';if(t=='0')F.N.onkeyup();else{var d=parseFloat(F.M.innerHTML);d=(3*d+parseInt(t))/4;d=parseInt(d*100)/100;setTimeout(function(){F.M.innerHTML=' '+d;},10);}};};(function(){var w=new G();var p=document.createElement('p');p.style.backgroundColor='#448';p.style.color='#FFF';p.style.fontWeight='bold';p.style.fontSize='12px';p.style.width="220px";var s=y.cloneNode(1);s.innerHTML="Note";p.appendChild(s);p.appendChild(w.N);s=y.cloneNode(1);s.innerHTML="Teza";p.appendChild(s);p.appendChild(w.T);p.appendChild(w.M);document.body.appendChild(p);})();})();

Terminând şi această operaţie, putem trece în mod normal (că s-ar putea să mai trebuiască unele modificări, precum conversii de anumite caractere) să înregistrăm bookmarklet-ul: accesăm View/Sidebar/Bookmarks, apoi click-dreapta pe Bookmarks Toolbar şi alegem New Bookmark:

bookm11

Am tastat javascript: în Location şi apoi am pastat codul compresat obţinut anterior. Am înscris chiar în numele bookmark-ului textul explicativ "a tasta fără spaţiu", fiindcă am evitat din capul locului să încărcăm formularul cu mesaje explicative sau de atenţionare; este drept pe de altă parte, că majoritatea persoanelor tastează nota şi apoi spaţiu, deşi acest spaţiu chiar nu este necesar, fiindcă notele sunt de la 1 la 10).

În sfârşit, fâcând click pe bookmark-ul tocmai înregistrat, putem proba funcţionalitatea bookmarklet-ului nostru:

bookm12

Indiferent ce document am deschide în browser şi indiferent cum am scrolla - formularul de calcul "mediaDyn" rămâne mereu accesibil pentru utilizare, într-o aceeaşi poziţie de ecran (şi bineînţeles că el poate fi închis când nu mai este necesar şi poate fi deschis din nou, accesându-l din lista bookmark-urilor).

Simplificarea click-and-drag-to-Bookmark reduce posibilităţile

Industria - vizând cum se şi cuvine, categoria cea mai largă de utilizatori - promovează o metodă de instalare a unui bookmarklet mult mai simplă şi directă, decât cea pe care am descris-o şi am folosit-o mai sus: utilizatorul nu are decât să "apuce" link-ul oferit şi să-l tragă cu mouse-ul peste meniul sau iconiţa "Bookmarks". Pentru a permite utilizatorului să folosească acest procedeu, trebuie furnizat un link <a> de forma următoare:

<a href='javascript:(function(){... scriptul respectiv ...})();'>nume bookmarklet<a> 
(click-and-drag-to-Bookmarks)

Abia acum apar în mod explicit două "mici" probleme, a căror evitare a fost posibilă (cel puţin aparent) anterior, pentru metoda folosită mai sus: e vorba de codificarea caracterelor în cadrul şirului atribuit prin href (şir care conţine scriptul nostru) şi apoi, de folosirea ghilimelelor şi a apostrofului.

Atributul href are ca valoare un şir de caractere (semnificând fie o adresă sau o referinţă, fie în cazul de acum - un script de executat prin pseudo-protocolul javascript:); dar şirurile de caractere trebuie încadrate corect fie între ghilimele, fie între apostrofuri. Să observăm că în mediadyn.js am folosit în unele locuri " şi în altele '; prin urmare, nu putem încadra direct întregul script nici între " şi nici între ' (situaţie în care soluţia obişnuită ar fi "escaparea" caracterelor din interior, ceea ce revine la prefixarea fiecăruia cu un caracter \).

Însă, ceea ce nouă ne-a scăpat şi n-am putut anticipa iniţial (ar fi trebuit să folosim din start un singur tip de încadrare a şirurilor, fie ghilimele pentru toate şirurile, fie apostrofuri pentru toate şirurile implicate în script) - nu a scăpat compresorului pe care l-am folosit: codul minimizat furnizat de YUI Compressor pentru scriptul nostru (spre deosebire de cel reprodus mai sus, obţinut folosind un alt compressor) nu conţine decât un singur tip de caractere de încadrare, anume ghilimele.

Cealaltă problemă, a codificării caracterelor ne-alfanumerice, apare datorită faptului că href are menirea principală de a indica un URL şi atunci, trebuie respectate nişte reguli specifice pentru URL encoding. În principal, cu câteva excepţii, orice caracter ne-alfanumeric trebuie înlocuit cu o secvenţă de forma %cod-hexazecimal; de exemplu, caracterul < nu are voie sa apară ca atare în URL, trebuind codificat prin secvenţa %3c. Din fericire, avem si pentru aceasta soluţii "de-a gata"; de exemplu, putem angaja într-un mic script funcţia javaScript escape(sourceString).

În cazul nostru, pare a fi suficientă codificarea caracterului < prin %3c - ceea ce am operat direct, putând pune la dispoziţie pentru proba click-drag-and-drop-to-your-Bookmark următorul link:

mediaDyn drag and drop to your Bookmarks (trageţi-l cu mouse-ul în Bookmarks)

Acuma... ce-ar fi de reproşat acestui procedeu (afară de faptul că… este folosit de toată lumea)? Există oare, motive să nu-l agreezi? Păi să comparăm rezultatele şi să vedem…

Prin primul procedeu (cel "complicat", plecând de la View/Sidebar/Bookmarks, apoi click-dreapta Bookmarks Toolbar, apoi New Bookmark..., scrie numele, scrie javascript: şi pastează codul minimizat) am obţinut un rezultat care chiar ne place: formularul de calcul apare mereu în acelaşi loc (şi anume chiar în cadrul rezervat listei de bookmark-uri, ceea ce trebuie gândit ca fiind mai degrabă o facilitate, decât un defect), iar formularul respectiv poate fi utilizat în timp ce lucrezi indiferent la care document deschis în browser.

În schimb, după drag-and-drop-to-Bookmark pe link-ul tocmai furnizat mai sus, iată cam ce obţinem:

bookm13

Formularul a fost adăugat la sfârşitul documentului curent (este drept că exact aşa zicea însuşi scriptul, în final: document.body.appendChild(p);); pentru a şi folosi formularul, trebuie să derulezi documentul până la sfârşit; fiecare nou click (accidental sau nu) pe bookmark-ul rămas selectat în zona Bookmarks a ecranului are drept consecinţă o nouă adăugare (inutilă) a formularului la sfârşitul documentului; formularul este adăugat fireşte, numai în tab-ul curent (pentru a-l folosi din alt tab, bookmarklet-ul trebuie relansat şi în tab-ul respectiv).

Majoritatea acestor "defecte" pot fi reparate, modificând corespunzător scriptul; mai precis - lungind scriptul cu o serie de instrucţiuni, cum ar fi de exemplu o secvenţă de verificare iniţială a existenţei formularului (caz în care el n-ar mai trebui recreat). Dar suntem de părere că defectele apărute se datorează exact manierei de folosire a bookmarklet-ului (reducând totul la o manevră simplistă ca "drag-and-drop"); simplificările au în mod firesc drept consecinţă, diminuarea posibilităţilor.

vezi Cărţile mele (de programare)

docerpro | Prev | Next