momente şi schiţe de informatică şi matematică
anti point—and—click

Dezvoltarea unei aplicaţii interactive peste orare şcolare

jQuery | orar şcolar
2021 apr

În seria [1]-[4] am pus la punct programe R prin care se produce o repartizare pe zile şi apoi, o repartizare pe orele zilei, a încadrării pe clase a profesorilor unei şcoli. Evocăm ideea de bază: pe orarul unei şcoli avem 5 variabile, dintre care 3 sunt date ale încadrării furnizate (profesor, clasă, obiect) şi două sunt variabile „libere” (şi independente) – anume, ziua şi ora din zi în care trebuie plasat fiecare cuplu profesor-clasă, astfel încât să nu apară suprapuneri; iar în R (cu tidyverse) – care modelează lucrul vectorial cu seturi de date – putem asocia imediat cuplurilor profesor-clasă, o etichetare cu zile, respectiv cu ore 1..7 ale zilei.

Am impus o singură condiţie: distribuţia pe zile a orelor fiecărui profesor (dacă are măcar 10 ore) să fie „omogenă”; ca urmare, timpii de obţinere a rezultatelor sunt foarte scurţi. Dar pentru a ţine seama şi de alte condiţii, repartiţia pe zile obţinută trebuie ajustată – folosind eventual o aplicaţie interactivă precum /recast din [5]; la fel, orarele zilnice obţinute trebuie ajustate, pentru a reduce ferestrele apărute (şi a satisface diverse cerinţe individuale).

Presupunem dat orarul pe o zi al unei şcoli cu două schimburi (sau orarele pe două zile, pentru o şcoală cu un singur schimb) – poate fi orarul produs prin programele R menţionate (şi atunci, sigur necesită operaţii de ajustare), sau poate fi un orar arbitrar pe care încă am vrea să-l „ajustăm”.
Vizăm aici proiectarea şi realizarea efectivă (folosind jQuery) a unei aplicaţii interactive prin care să putem ajusta orarul furnizat, lucrând „în paralel” pe cele două schimburi (sau zile); avem a ne ocupa nu de „ce face” o instrucţiune sau alta dintr-un limbaj sau altul, ci mai degrabă de logica şi fluenţa lucrurilor şi asamblărilor.

Pentru dezvoltarea unei aplicaţii interactive avem nevoie de un terminal simplu (există bineînţeles şi terminale inutil de sofisticate) – pentru „linia de comandă”, vizând comenzi, programe utilitare, interpretoare puse la dispoziţie de sistem (Ubuntu-Linux) – şi de un editor de text simplu dar competent (Gedit); intenţionăm să deschidem aplicaţia într-un browser (Firefox, sau Chrome) şi implicit, avem de folosit limbajele asociate acestuia (javaScript, HTML, CSS). Iar pentru a depăşi diversele dileme care apar inevitabil pe parcurs, legate de comenzi sau de limbaje de programare – ne putem folosi ad-hoc de sistemul de documentare existent pe Ubuntu-Linux şi deasemenea, de facilităţile de căutare şi documentare oferite de Internet.

Fişierele de lucru; conturarea aplicaţiei

Întâi creem directorul de lucru al aplicaţiei şi înscriem fişierele de a căror dezvoltare urmează să ne ocupăm:

vb@Home:~/21apr/LRC/STMT$ mkdir 2recast;  cd 2recast/
vb@Home:~/21apr/LRC/STMT/2recast$ touch pairRecast.{html,css,js}

Întrebare colaterală: într-o şcoală avem nivelele 9..12, fiecare cu câte 7 clase A..G; cum generăm denumirile claselor?
Putem folosi (ca şi în comanda redată mai sus) construcţii "brace expansion":
vb@Home:~/21apr/LRC/STMT/2recast$ echo {9..12}{A..G}
9A 9B 9C 9D 9E 9F 9G 10A 10B 10C 10D 10E 10F 10G 11A 11B 11C 11D 11E 11F 11G \
12A 12B 12C 12D 12E 12F 12G

Întrebare colaterală: de ce folosim în programe denumiri englezeşti (v. "pair", "recast", etc.)?
Ghidurile de „bune practici” recomandă să întrebuinţăm engleza, chiar şi în comentariile din programe… Dar atenţie: este insuficient pentru un informatician, să cunoască o singură limbă (mai ales dacă această singură limbă este engleza) – în acest caz, de exemplu declaraţia <meta charset="utf-8"> nu prea mai are sens, iar chestiunile referitoare la fonturi devin inaccesibile. Deasemenea, cu totul insuficient este să vizezi un singur limbaj de programare…

Adăugăm (în directorul creat mai sus, sau într-un subdirector al acestuia) fişierele externe de care vom avea nevoie: jquery-1.11.1.min.js şi jquery.ui.widget.min.js.

În bara de adresă a browser-ului va trebui să indicăm fişierul HTML, deci în pairRecast.html specificăm fişierele de care avem nevoie:

<!doctype html>
<html>
<head>
    <title>Orar interactiv (două schimburi)</title>
    <meta charset="utf-8">
    <link rel="stylesheet" href="pairRecast.css">
    <script src="jquery-1.11.1.min.js"></script>
    <script src="jquery.ui.widget.min.js"></script>
    <script src="pairRecast.js"></script>
</head>

Browser-ul va citi întâi secţiunea <head> şi va încărca pe rând fişierele specificate aici, constituind treptat o anumită structură de date ("Document Object Model", sau DOM) care va fi asociată aplicaţiei noastre.

Pentru introducerea orarelor prevedem ca de obicei, elemente <textarea> – dar renunţăm la ideea uzuală de a adăuga un buton "Load" prevăzut cu un handler de click prin care să se preia în aplicaţie, datele pastate în casetele arătate în fereastra browser-ului la deschiderea aplicaţiei. Dacă e să se ocupe de orar – utilizatorul ştie să citească şi să scrie, deci va putea înscrie orarele direct în fişierul HTML:

<body>
<div id="pair-container">
    <div id="Sch1">
        <textarea rows="4" cols="30">
P01,-,-,-,12E,11A,12B,-
P02,-,-,-,-,11B,12A,-
P04,11F,12F,12G,-,-,-,-
<!-- etc. -->
        </textarea>
    </div>
    <div id="Sch2">
        <textarea rows="4" cols="30">
P01,9B,9D,9G,-,-,-
P02,10B,10C,9C,9A,-,-
P03,10D,10G,6Z,9F,9G,-
<!-- etc. -->
        </textarea>   
    </div>
</div>

Aici, "P01" etc. sunt numele profesorilor (este convenabil să fie „scurtate”). Pentru a indica o „oră liberă” trebuie folosit '-'; atenţie la notaţia claselor…

Prevăzând câte un atribut id celor trei diviziuni, ne uşurăm specificarea anumitor proprietăţi CSS; dar în codul JS (mult mai delicat de modificat, faţă de HTML şi CSS) vom putea accesa uşor diviziunile respective şi fără a ne folosi de id – încât valorile id iniţiale pot fi schimbate, sau chiar eliminate (modificând şi fişierul CSS).

Lucrul cel mai important în dezvoltarea unei aplicaţii constă în a prevedea (cât mai devreme) ce operaţii este necesar sau util să furnizezi. De obicei, instrumentele de lucru oferite de aplicaţie sunt constituite prin codul JS al acesteia (ferindu-le cumva de modificări); dar preferăm să le explicităm direct în HTML, sugerând din start operaţiile posibile (atenţionând că nu trebuie modificat nimic, exceptând atributul title):

<div id="Operations">
    <p><input value="9A"><button title="...">Mark</button></p>
    <p><button title="...">Gaps</button> <span></span> | <span></span></p>
    <p><button title="...">Intersect</button></p>
    <p><button title="...">SWAP</button></p>
    <p><button title="...">Undo</button></p>
    <p><a title="salvează orarele (CSV)">Export</a></p>
</div>

Este de înţeles că datele din <textarea> vor fi redate prin două tabele HTML; dacă în <input> este înscrisă o clasă, Mark va restrânge tabelul HTML al schimbului în care se află acea clasă, la liniile care o conţin (evidenţiind orarul clasei, uneori se pot vedea mai uşor ce modificări ar fi de făcut); Gaps va evidenţia liniile cu ferestre, fie numai din schimbul de care ţine clasa vizibilă în <input>, fie din ambele schimburi dacă <input> este deja vid (se va vedea mai uşor, cam ce „reparaţii” de ferestre s-ar putea face); Intersect restrânge tabelele la profesorii care au ore în ambele schimburi (pentru a decide mai uşor la care profesori s-ar putea lega sau distanţa schimburile).

SWAP şi Undo sunt operaţiile esenţiale, permiţând mutarea unei clase pe un loc liber de pe aceeaşi linie şi respectiv, refacerea situaţiei dinaintea ultimei mutări efectuate; desigur, mutarea unei clase dintr-o coloană în alta necesită de obicei multiple schimbări de clase între cele două coloane, pentru ca în final să nu apară vreo suprapunere, pe o coloană sau alta. În sfârşit, prin Export se va putea salva (ca fişier CSV) conţinutul curent al celor două tabele HTML.

Încheiem pairRecast.html cu un element <script> în care „lansăm” aplicaţia, instanţiind widget-ul jQuery pe care urmează să-l definim în fişierul pairRecast.js:

<script>
    $(function() {
        $('#pair-container').pairRecast();
    });
</script>
</body>
</html>

Prin încărcarea fişierului jquery-1.11.1.min.js, în browser este injectat obiectul JS jQuery; dintr-un script propriu, putem exploata funcţionalităţile acestui nou obiect prin jQuery(...), sau folosind scurtătura $(...).
$(function(){...}) înregistrează în jQuery funcţia anonimă indicată ca parametru, urmând ca aceasta să fie pusă în execuţie după ce browser-ul va fi terminat de constituit DOM-ul fişierului HTML; $('#pair-container').pairRecast() va depista nodul (înscris deja în DOM) atributat cu id="pair-container" şi (însuşindu-l ca „obiect jQuery”) îi va aplica funcţia pairRecast(), presupusă deocamdată, a fi constructorul widget-ului modelat în fişierul "pairRecast.js" (deja încărcat).

În cadrul widget-ului respectiv, diviziunea pe care este montat – cea cu id="pair-container" – va fi accesată prin this.element, de la care apoi vom putea accesa subdiviziunile care conţin orarele, fără a mai utiliza atributele id ale acestora.

Poziţionarea elementelor vizibile ale aplicaţiei

Pentru a alătura orizontal tabelele HTML corespunzătoare celor două schimburi, putem folosi CSS grid, specificând în pairRecast.css următoarele proprietăţi, pentru cele trei diviziuni din fişierul HTML:

#pair-container {
    display: grid;
    width: 500px;
    grid-template-columns: auto auto;
    grid-auto-columns: max-content;
    grid-auto-flow: column;
    justify-content: start;
    grid-column-gap: 10px;
}
#Sch1 { grid-column: 1; }
#Sch2 { grid-column: 2; }

În dreapta celor două tabele, alăturăm bara butoanelor de operare, prevăzând-o cu proprietatea position:fixed – încât ea să rămână vizibilă când am derula în sus sau în jos, tabelele HTML din stânga (astfel putem opera imediat, pe oricare linie):

#Operations {
   position: fixed;
   top: 5em;
   left: 510px; 
}

Desigur, vor mai fi de adăugat pe parcurs anumite proprietăţi CSS pentru butoane, pentru elementele <td>, etc. Dar nu-i cazul să „stilăm” şi elementele <textarea>, dat fiind că acestea nu vor fi vizualizate…

În final, decupând din fereastra browser-ului, aplicaţia noastră arată cam aşa:

Desigur, am putea abate cumva scopul aplicaţiei şi spre ideea unui simplu joc: participanţii primesc un acelaşi „orar” cu două schimburi, iar premiul revine celui care aranjează orele astfel încât pe fiecare schimb să avem zero ferestre, iar orele aflate la câte un acelaşi profesor în ambele schimburi să fie sau legate (contând ca numărul acestor situaţii să fie cât mai mare), sau să aibă cât mai mare distanţă între schimburi.

De exemplu, pe imaginea redată mai sus avem 0 ferestre pe fiecare schimb (cum se vede în dreapta butonului 'Gaps'); "P10" are orele din primul schimb legate de cele din al doilea (ora 7 din primul schimb coincide cu ora 1 din al doilea); la "P08" schimburile sunt la distanţă de 8 ore libere, între ele.

Schema funcţională a aplicaţiei

Formulăm pairRecast.js în forma IIFE (Immediately Invoked Function Expression) – la încărcarea în browser, funcţia din paranteză este pusă imediat în execuţie (iar în interiorul acesteia, '$' devine o referinţă la obiectul jQuery actual, permiţând folosirea metodelor acestuia):

(function($) {
    /* ... */  // constante şi funcţii comune instanţelor widget-ului
    $.widget("vb.pairRecast", {  // 'widget' este definit de jquery.ui.widget.min.js
        _create: function() { 
            /* ... */
            this._set_handlers();  // ataşează handlere de 'click', pe butoane
        },
        _set_handlers: function() {
            /* ... */
        }
    });
})(jQuery);

Sarcina principală a widget-ului pe care urmează să-l definim este aceea de a asigura funcţionarea convenită mai sus pentru butoanele din diviziunea '#Operations'.

Prevedem întâi o serie de funcţii auxiliare, de care ne vom servi pentru instanţierea widget-ului (în cadrul metodei interne _create()) şi deasemenea, în corpul metodei proprii _set_handlers(); aici, doar le descriem (codul este publicat la [5]):

function gaps(ore) primeşte un tablou JS precum ['P01','-','9A','-','11D','-','8F','-'] şi returnează numărul de ferestre din orarul individual respectiv (în cazul exemplificat, 2: înainte şi după ora de la 11D);

function SWAP(td1, td2) primeşte referinţe la două obiecte jQuery, reprezentând elemente <td> de pe o aceeaşi linie a tabelului HTML şi interschimbă conţinuturile acestora (asigurând mutarea unei clase dintr-o coloană în alta);

function set_orar(CSV) primeşte orarul în format CSV şi îl returnează ca tablou JS conţinând ca sub-tablouri orarele individuale (precum cel exemplificat mai sus);

function set_spread(orar) primeşte orarul (cum este cel returnat de set_orar()) şi returnează un „dicţionar” care specifică pentru fiecare clasă din orar, lista indecşilor sub-tablourilor din orar corespunzătoare profesorilor cu ore la clasa respectivă;

function set_html(orar) returnează un tablou JS cu două componente: prima structurează ca element HTML <table> orarul primit (ambalând corespunzător cu taguri <tr> şi <td>); ultima componentă este numărul de ferestre rezultat prin cumularea pe parcurs a valorilor returnate de gaps() pentru fiecare dintre liniile orarului;

function intersect(orar1, orar2) primeşte orarele celor două schimburi (sau zile) şi returnează un tablou conţinând perechile de indecşi corespunzători în cele două orare unui aceluiaşi profesor (vizând profesorii care au ore în ambele schimburi).

În _create() (metodă specifică obiectelor "widget" introduse de ui.widget.js) asumăm că orarele sunt conţinute în elemente <textarea> din diviziuni al căror părinte comun este diviziunea pe care este instanţiat widget-ul (referită intern ca obiect jQuery prin this.element); instituim referinţe interne (this.orar1, this.spread1 etc.) la tablourile şi „dicţionarele” returnate pentru cele două orare de către funcţiile set_orar() şi set_spread(); apoi, inserăm în DOM tabelele HTML returnate de set_html() şi păstrăm în this.Sch1 şi this.Sch2 referinţe la obiectele jQuery asociate acestor tabele; prin this.BAR vom referi panoul de butoane care urmează imediat după tabelele HTML. În this.Hist vom înregistra fiecare mutare de clasă efectuată prin operaţia SWAP, în unul sau celălalt dintre cele două tabele HTML.

        _create: function() { 
            let twos = this.element.children(),
                sch1 = twos.eq(0),  // DIV cu orarul primului schimb (în TEXTAREA)
                sch2 = twos.eq(1);  // DIV orar schimbul 2
            let csv1 = sch1.find('textarea').val(),
                csv2 = sch2.find('textarea').val();
            if(!(csv1 && csv2)) {
                alert("În 'pairRecast.html' lipseşte orarul unui schimb");
                return;
            }
            this.orar1 = set_orar(csv1);
            this.orar2 = set_orar(csv2);
            this.spread1 = set_spread(this.orar1);
            this.spread2 = set_spread(this.orar2);
            let html1 = set_html(this.orar1),
                html2 = set_html(this.orar2),
                scor1 = html1.pop(),
                scor2 = html2.pop();
            this.Sch1 = sch1.empty().append($(html1.join(""))); 
            this.Sch2 = sch2.empty().append($(html2.join("")));
            this.BAR = this.element.next();  // bara cu butoanele acţiunilor
            let scor = this.BAR.find('span');
            this.Scor1 = scor.eq(0).text(scor1);  // afişează "scor" pe schimburi
            this.Scor2 = scor.eq(2).text(scor2);
            this.Hist = [[], [], 0];  // păstrează mutările SWAP, pe schimburi
            this._set_handlers();  // ataşează handlere de 'click', pe butoane
        },

Construcţia widget-ului este încheiată prin apelarea metodei this._set_handlers(); aceasta instituie întâi variabile „locale” prin care se vor accesa mai uşor tabelele HTML şi bara butoanelor – de exemplu, după let got1 = this.Sch1.find('table'), tabelul HTML corespunzător primului schimb va putea fi accesat ca obiect jQuery prin got1 (dar numai în interiorul metodei _set_handlers()); apoi, prevede unul după altul câte un handler de click pe elementele de interacţiune (5 butoane şi un link) prevăzute în diviziunea referită de bar = this.BAR.

Mutarea unei clase dintr-o coloană în alta

Exemplificăm aici realizarea operaţiei SWAP – operaţia principală a aplicaţiei, mai delicat de implementat decât celelalte.

Vrem să modelăm următoarea regulă: se poate muta o clasă pe un loc liber (indicat prin '-') aflat pe aceeaşi linie (din acelaşi tabel) cu clasa.

Subtilitatea principală decurge din faptul că în orar fiecare clasă trebuie să apară pe fiecare coloană cel mult o singură dată; dacă ar apărea de două ori, atunci profesorii pe liniile cărora apare clasa respectivă ar trebui să intre într-o aceeaşi oră la acea clasă – ceea ce în principiu, trebuie exclus (ar fi posibil ca excepţie, doar în cazul unui „cuplaj”; dar aici – v. [1] de exemplu – excludem explicitarea cuplajelor).
Deci schimbând coloana clasei, avem de căutat a doua apariţie a clasei respective pe noua coloană, trebuind să o mutăm pe coloana de pe care tocmai am îndepărtat-o; dacă prin această a doua mutare, locul în care ar veni clasa este liber – atunci am terminat, dar altfel trebuie să repetăm pentru clasa existentă pe acel loc.

O a doua subtilitate decurge din faptul că unele clase au mai puţin de 6 ore; în acest caz, clasa nu va putea fi mutată din coloana 3 de exemplu, în coloana 6 (ar rezulta o „fereastră” în ora a 3-a, pentru clasa respectivă) – dar este posibilă o excepţie: putem muta clasa din coloana 1 în coloana 6, acceptând ca programul de lucru al clasei să înceapă de la a doua oră a zilei.

Mai întâi, trebuie prevăzută o modalitate de a indica clasa şi locul în care vrem să o mutăm; dar evităm mecanismul uzual "drag and drop" (amintindu-ne cum am pus dama în priză, din cauza mouse-ului, într-o partidă de şah) – preferăm o manieră simplă şi clară: la click pe o clasă din tabelul HTML marcăm celula care o conţine (la fel, pentru următorul click, pe '-'), adăugându-i una sau alta dintre aceste două proprietăţi CSS (de plasat în pairRecast.css):

#pair-container td.gap-source, td.gap-dest { background: yellow; }

În principiu, proprietatea .gap_source va fi aplicată celulei <td> care conţine clasa pe care s-a făcut click, iar .gap_dest locului indicat pentru mutarea clasei.

Bineînţeles că nu este necesar să încărcăm fiecare celulă cu câte un handler de marcare; dimpotrivă, putem folosi un singur asemenea handler pentru celulele ambelor tabele:

let got12 = got1.add(got2);  // combină tabelele într-un singur obiect jQuery

    got12.on('click', function(event) {
        if(got12.find('td.gap-dest').length > 0)
            got12.find('td').removeClass('gap-source')
                            .removeClass('gap-dest');
        let target = $(event.target);
        if(target.is('td') && target.index() > 0) {
            let ql = target.text();
            if(ql == '-')
                target.addClass('gap-dest');
            else
                target.addClass('gap-source');
        }
    });

SWAP trebuie să se asigure întâi că există un singur <td class="gap-source"> şi un singur <td class="gap-dest"> (marcând clasa care trebuie mutată şi respectiv, locul liber pe care trebuie mutată); în caz contrar, se încheie imediat, cu eliminarea finală a atributelor 'class' de pe toate celulele celor două tabele.

Consultând dicţionarul this.Spread1 al instanţei curente a widget-ului, stabilim în care schimb se află clasa conţinută de <td class="gap-source"> – pentru a-l indica în this.Hist, alături de indecşii rândului din tabelul HTML corespunzător acelui schimb şi ai celor două <td>-uri, angajate în efectuarea mutării; pe baza acestei înregistrări din this.Hist se va putea eventual reconstitui, prin operaţia Undo, starea anterioară efectuării mutării respective.

Dar trebuie să luăm seama la acest aspect: în interiorul handler-ului, "this" referă de regulă butonul pe care este montat acel handler şi nu instanţa curentă a widget-ului, cum avem nevoie; de aceea, introducem în prealabil definirii handler-ului o referinţă „locală”, fie Self, la "this"-ul widget-ului.

După ce – folosind funcţia auxiliară SWAP() – se schimbă între ele conţinuturile celor două <td>-uri marcate, urmează să se aplice repetat SWAP între clase aflate pe coloanele celor două <td>-uri (identificându-le prin metodele lui jQuery, parent(), siblings(), find(), filter() şi index()) – în scopul de a evita dubla apariţie a unei aceleiaşi clase pe o coloană sau alta.

let Self = this;  // pentru a referi din interiorul handler-ului, instanţa widget-ului

bar.find('button:contains(SWAP)').on('click', function(event) {
    let td1 = got12.find('td.gap-source');
    if(td1.length == 1) {
        let td2 = td1.parent().find('td.gap-dest');
        if(td2.length == 1) {
            let clasa = td1.text(),
                hst = Self.spread1[clasa] ? 0 : 1;
            let id1 = td1.index(), 
                id2 = td2.index();
                Self.Hist[hst].push([td1.parent().index(), id1, id2].join(', '));
                let sig = '-';
                SWAP(td1, td2);
                do { 
                     td1 = td2.parent().siblings()
                                       .find("td:contains("+td2.text()+")")
                                       .filter(function() {
                                            return $(this).index() == id2;});
                     td2 = td1.parent().find("td")
                                       .filter(function() {
                                            return $(this).index() == id1;})
                     if(!(td1.length && td2.length)) {
                        alert('Lipseşte clasă pe coloană!\n(a folosi "Undo")\n');
                        break;
                     }
                     SWAP(td1, td2);
                     td2 = td1;
                 } while(td2.text() != sig);
            if(hst == 0)
                Self.Scor1.next().text(get_scor(0));
            else
                Self.Scor2.next().text(get_scor(1));
            Self.Hist[2] = hst;  // memorează schimbul pe care s-a operat SWAP
        }
    }
    got12.find('td').removeClass('gap-source')
                    .removeClass('gap-dest')
                    .removeClass('highlight');
});

După ce se efectuează toate mutările necesare între cele două coloane, se determină „scorul” rezultat (noul număr de ferestre) şi se înscrie corespunzător schimbului, în elementele <span> referite de la Self.Scor1, respectiv Self.Scor2; „scorul” este returnat de funcţia get_scor(), care poate fi adăugată în _set_handlers() de exemplu imediat după handler-ul "SWAP" redat mai sus:

function get_scor(sch) {  // numărul actual de ferestre, din tabelul HTML al schimbului
    let grid = got12[sch], 
        scor = 0;
    for(let i=0, rl=grid.rows.length; i < rl; i++) {
        let line = [];
        for(let j=0, cl=grid.rows[0].cells.length; j < cl; j++)
            line.push(grid.rows[i].cells[j].innerHTML);
        scor += gaps(line);  // v. funcţia auxiliară gaps()
    }
    return scor;
};

Uneori mai procedăm şi mecanic: încercăm să eliminăm o fereastră la un profesor, mutând una dintre clasele sale pe locul liber respectiv; dacă astfel, numărul total de ferestre apărut în dreapta butonului 'Gaps' se micşorează – atunci păstrăm mutarea făcută, dar dacă se măreşte, atunci mai bine revenim prin Undo la starea precedentă şi încercăm cu alta, dintre clasele profesorului respectiv (şi poate prin aceste manevre găsim una, prin a cărei mutare să micşorăm numărul total de ferestre).

În cadrul operaţiei Undo n-avem decât să preluăm ultima înregistrare din this.Hist, obţinând indecşii de schimb, de linie a tabelului HTML şi de elemente <td> între care s-a petrecut ultima mutare de clasă; apoi, setăm pe cele două <td>-uri proprietăţile "gap-source" şi respectiv "gap-dest" corespunzător inversării mutării şi apelăm handler-ul de click pentru SWAP:

bar.find('button:contains(Undo)').on('click', function(event) {
    let hst = Self.Hist[2],
        Shst = Self.Hist[hst],  // istoricul mutărilor, pentru schimbul curent
        got = $(got12[hst]);
    if(Shst.length > 0) {
        let last = Shst.pop().split(',');  // înregistrarea pentru ultima mutare
        let tr = got.find('tr').eq(last[0])
                    .find('td').eq(last[2]).addClass('gap-source')
                    .end()
                    .eq(last[1]).addClass('gap-dest');
        bar.find('button:contains(SWAP)').click();  // efectuează mutarea "inversă"
        Shst.pop();
    }
});

Desigur, am avut grijă în final să eliminăm din this.Hist (folosind pop()), înregistrarea mutării inverse tocmai efectuate.

vezi Cărţile mele (de programare)

docerpro | Prev | Next