IT rekvalifikácia. Seniorní programátori zarábajú až 6 000 €/mesiac a rekvalifikácia je prvým krokom. Zisti, ako na to!

20. diel - Dokončenie editora tabuliek v JavaScripte

V minulej lekcii, Editor tabuliek v JavaScripte, sme rozpracovali editor tabuliek, ktorý umožní užívateľovi tabuľky rozširovať a upravovať.

V dnešnom tutoriále základov JavaScriptu dokončíme rozpracovaný editor tabuliek. Táto časť bude zameraná predovšetkým na manipuláciu s elementmi DOM. Do projektu totiž doplníme funkcie, ktoré nám umožnia vkladať stĺpce a riadky na vybrané miesto tabuľky. Nakoniec si ukážeme, ako zaistiť ich zmazanie.

Zistenie pozície aktívnej bunky

Z minulosti máme v našom editore implementovanú možnosť výberu bunky v tabuľke. Do premennej activeCell ukladáme element <input>, ktorý užívateľ označil. Aby sme mohli tabuľku upravovať na vybranom mieste, budeme najskôr potrebovať zistiť túto pozíciu aktívnej bunky. Vytvoríme si teda dve funkcie, ktoré neskôr využijeme. Jednou z nich bude funkcia activeCellRowIndex(), ktorá vráti index riadku aktívnej bunky počnúc 0. Druhá bude funkcia activeCellColumnIndex(), ktorá vráti zodpovedajúci index stĺpca aktívnej bunky.

Funkcia activeCellRowIndex()

Najprv si vysvetlíme, ako index riadka získame. Pripomeňme si hierarchiu potomkov vygenerovanej tabuľky:

<body>
  <table>
    <tr>
      <td>
        <input type="text">
      </td>
      <td>
        <input type="text">
      </td>
      <td>
        <input type="text">
      </td>
      <!-- Here are two more row cells with nested input elements -->
    </tr>
    <tr>
      <!-- The second and third rows are created in the same way -->
    </tr>
  </table>
</body>

Hľadáme v tabuľke, ktorá je predstavovaná elementom <table> a riadky sú jej priamymi potomkami. Pripravíme si teda kolekciu, do ktorej vložíme všetky uzly, ktoré sú priamymi potomkami elementu tabuľky. Pomocou vlastnosti childNodes volanej na našej tabuľke získame takto všetky jej elementy <tr>. V kolekcii uzlov typu NodeList budeme hľadať element <tr> zodpovedajúci aktívnej bunke.

Pretože máme v premennej activeCell uložený element <input>, vráti nám vlastnosť parentElement ako jej nadradený element zodpovedajúci bunku tabuľky (<td>). Aby sme sa dostali k zodpovedajúcemu riadku (elementu <tr>), musíme teda zavolať parentElement dvakrát.

Na začiatku funkcie activeCellRowIndex() budeme mať tento kód:

function activeCellRowIndex() {
    let tableNodes = table.childNodes; // list of all table nodes
    let foundRow = activeCell.parentElement.parentElement; // the row with the active cell

    // Here we add the code to get the index of the searched line
}

Už vieme, že vlastnosť childNodes zahŕňa všetky priamo podriadené uzly rodičovského elementu. V našom prípade by sme mohli využiť aj vlastnosť children, ktorá vráti kolekciu obsahujúcu iba HTML elementy.

Metóda indexOf()

Aby sme nemuseli prechádzať celú kolekciu uzlov a zakaždým porovnávať jeden jej prvok s hľadaným riadkom, predstavíme si metódu indexOf(). Táto metóda nám vráti priamo index riadku s aktívnou bunkou. Voláme ju na poli, v ktorom má index prvku hľadať a ako parameter ju odovzdávame hľadaný prvok.

Ide o metódu pre prácu s poľom, my ju však potrebujeme využiť na kolekciu NodeList. Ukážme si dva spôsoby, ako tento problém vyriešiť.

Prevod kolekcie NodeList na pole

Ako prvú uložíme našu kolekciu do nového poľa a zavoláme na ňom metódu indexOf():

function activeCellRowIndex() {
    let tableNodes = table.childNodes;
    let foundRow = activeCell.parentElement.parentElement;

    let arrayNodes = Array.from(tableNodes);
    return arrayNodes.indexOf(foundRow);
}

Tento zápis je zrozumiteľný, ale duplikuje kolekciu uzlov do poľa.

Zmena kontextu metódy indexOf()

Ďalšie riešenie predstavuje možnosť zavolať metódu indexOf() na poli, ale "podvrhnúť" mu NodeList. JavaScriptu je totiž jedno, s čím pracuje, pokiaľ má všetko potrebné.

Metódy sa volajú so zmeneným kontextom tak, že ich nájdeme v prototype objektu (v našom prípade Array.prototype) a zavoláme na ne metódu call(). Ako prvý parameter jej odovzdáme nový kontext a zaň pridáme parametre pôvodnej metódy.

Kód vyššie upravíme nasledovne:

function activeCellRowIndex() {
    let tableNodes = table.childNodes;
    let foundRow = activeCell.parentElement.parentElement;

    return Array.prototype.indexOf.call(tableNodes, foundRow);
}

Ide o zložitejšiu syntax, pomocou ktorej sa však vyhneme vytváraniu ďalšej kolekcie.

Funkcia activeCellColumnIndex()

Podobne budeme postupovať pri získavaní indexu stĺpca aktívnej bunky. Vytvoríme si kolekciu uzlov daného riadku a premennú pre hľadaný element <td> aktívnej bunky.

Aby sme v kolekcii mali naozaj uzly vybraného riadku, musíme vziať aktívnu bunku a zreťazeným volaním vlastnosti parentElement sa dostať k nadradenému elementu <tr>. Až potom naň zavoláme vlastnosť childNodes. Nakoniec znovu zavoláme metódu indexOf() so zmeneným kontextom:

function activeCellColumnIndex() {
    let rowNodes = activeCell.parentElement.parentElement.childNodes;
    let foundCell = activeCell.parentElement;

    return Array.prototype.indexOf.call(rowNodes, foundCell);
}

Z aktuálnej bunky (elementu <input>) sme sa vlastností parentElement dostali k nadradenej bunke az nej k zodpovedajúcemu riadku. Z neho sme pomocou childNodes vytvorili kolekciu vnorených uzlov. Hľadanú bunku potom reprezentuje priamo rodič aktívne bunky.

Pridanie riadku

Teraz si doplníme funkcie pre obsluhu vytvorených tlačidiel. Začneme pridaním riadku.

Vytvoríme funkciu addRow(), ktoré v parametri určíme, či chceme vložiť nový riadok nad vybraný riadok alebo pod neho. Parameter pomenujeme top a budeme mu odovzdávať hodnoty true alebo false. Vo funkcii zavoláme funkciu createRow(), ktorú už máme pripravenú. Nový riadok si uložíme do premennej. Ďalej získame a uložíme index riadku.

Doplníme podmienku testujúcu parameter top, aby sme vedeli, kam riadok vložiť. Na vloženie nového riadka nad vybraný riadok využijeme metódu insertBefore(). Pripomeňme, že metóda insertBefore() berie v prvom parametri vkladaný prvok a v druhom zadávame prvok, pred ktorý chceme nový prvok vložiť.

Pred vložením riadka pod vybraný riadok v ďalšej podmienke najskôr zistíme, či ide o posledný riadok tabuľky. Pokiaľ áno, vložíme nový riadok metódou appendChild(). V opačnom prípade použijeme metódu insertBefore() a navýšime selectedIndex o 1.

Celý kód vyzerá takto:

function addRow(top) {
    let newRow = createRow();
    let selectedIndex = activeCellColumnIndex();

    if (top) {
        table.insertBefore(newRow, table.childNodes[selectedIndex]); // inserts a new row above the row with the active cell
    } else {
        if (table.lastChild == table.childNodes[selectedIndex]) {
            table.appendChild(newRow); // if the selected row is the last one, we use the appendChild() method
        } else {
            table.insertBefore(newRow, table.childNodes[selectedIndex + 1]); // unless the selected row is not the last row, we use the insertBefore() method
        }
    }
}

Nakoniec vo funkcii createControlButtons() nastavíme obsluhu kliknutí prvým dvom tlačidlám:

createButton("Insert row below").onclick=function () {
        addRow(false);
};

createButton("Insert row above").onclick=function () {
        addRow(true);
};

Pretože udalosti onclick odovzdávame funkciu s parametrami, museli sme použiť anonymnú funkciu a do nej volanie metódy addRow() vložiť.

Novú funkcionalitu si môžeme vyskúšať v živej ukážke nižšie, nezabudnime však predtým označiť nejakú bunku tabuľky kliknutím:

Table editor
localhost

Pridanie stĺpcov

Ako ďalšiu vytvoríme funkciu na pridávanie stĺpcov vľavo a vpravo. Pridanie stĺpca je však oproti pridaniu riadku trochu zložitejšie. Tabuľka v HTML totiž obsahuje riadky a v tých sú bunky. Zaujímavé je že, priamo v štruktúre HTML kódu nie sú definované stĺpce ako také. Pridanie stĺpca budeme musieť teda riešiť tak, že pridáme bunku do každého z existujúcich riadkov. To dosiahneme iteráciou. Cyklom prejdeme všetky riadky tabuľky a do každého riadku pred index vybranej bunky vložíme novú bunku.

Inak bude funkcia addColumn() fungovať na podobnom princípe ako funkcia addRow(). Zistíme a uložíme v nej index stĺpca aktívnej bunky. Doplníme cyklus, ktorý prebehne toľkokrát, koľko je v tabuľke riadkov. To zistíme z vlastnosti length na table.childNodes. Na začiatku cyklu zavolaním funkcie createCell() získame novú bunku a uložíme ju do premennej. V podmienke zistíme, či ju máme vložiť doľava alebo doprava od aktívnej bunky. To určujeme v parametri funkcie podobne ako pri pridávaní riadku.

Novú bunku budeme vkladať postupne na každý riadok tabuľky, teda na pozíciu table.childNodes[i], kde i obsahuje aktuálnu iteráciu cyklu. Vloženie bunky doľava zaistíme opäť metódou insertBefore(). Jej prvým parametrom bude newCell. Druhý parameter určí pozíciu bunky, pred ktorou chceme novú bunku vložiť. Túto pozíciu získame vyhľadaním aktuálneho riadku (table.childNodes[i]), na ktorý zavoláme childNodes[selectedIndex], aby sme v ňom našli aktívnu bunku.

Pri vložení novej bunky doprava využijeme rovnaké pozície. Podobne ako pri vkladaní riadku musíme v tomto prípade otestovať, či nejde o posledný element tabuľky. Ak áno, zavoláme metódu appendChild(). V opačnom prípade použijeme metódu insertBefore() a navýšime selectedIndex o 1:

function addColumn(left) {
    let selectedIndex = activeCellColumnIndex();

    for (let i = 0; i < table.childNodes.length; i++) {
        let newCell = createCell();

        if (left) {
            table.childNodes[i].insertBefore(newCell, table.childNodes[i].childNodes[selectedIndex]);
        } else {
            if (table.childNodes[i].childNodes[selectedIndex] == table.childNodes[i].lastElementChild) {
                table.childNodes[i].appendChild(newCell);
            } else {
                table.childNodes[i].insertBefore(newCell, table.childNodes[i].childNodes[selectedIndex+ 1]);
            }
        }
    }
}

Vo funkcii createControlButtons() nastavíme obsluhu kliknutí ďalším dvom tlačidlám:

createButton("Insert column to left").onclick=function () {
    addColumn(true);
};

createButton("Insert column to right").onclick=function () {
    addColumn(false);
};

Mazanie

Nakoniec do aplikácie pridáme funkcie na mazanie riadku a stĺpca. Mazanie riadkov a stĺpcov je dôležité pre úpravu tabuľky, keď niektoré informácie už nie sú relevantné alebo boli zadané omylom. Predstavme si, že máme napríklad tabuľku s údajmi o projekte a potrebujeme odstrániť riadok, ktorý obsahuje zastarané alebo neplatné informácie o projektových úlohách.

Mazanie riadku

Vybraný riadok z tabuľky odstránime pomocou metódy removeChild(). V parametri jej teda zadáme riadok, ktorý chceme zmazať. Pretože ide o riadok, kde sa nachádza aktívna bunka, získame si našou funkciou activeCellRowIndex() jeho index. Tento index potom využijeme na výber z podriadených elementov tabuľky (childNodes):

function removeRow() {
    let selectedRow = activeCellRowIndex();
    table.removeChild(table.childNodes[selectedRow]); // deletes the row with the given index
}

Mazanie stĺpca

Mazanie stĺpca musíme zaistiť tak, že zmažeme pomocou cyklu na každom riadku jednu bunku, ktorej index zodpovedá indexu vybranej bunky. Metódu removeChild() budeme teda v cykle volať na aktuálny riadok (table.childNodes[i]) a v parametri dvakrát zavoláme vlastnosť childNodes, prvýkrát s indexom cyklu, druhýkrát s indexom vybranej bunky:

function removeColumn() {
    let selectedIndex = activeCellColumnIndex();
    for (let i = 0; i < table.childNodes.length; i++) {
        table.childNodes[i].removeChild(table.childNodes[i].childNodes[selectedIndex]); // deletes the cell with the given index in all rows
    }
}

Sprevádzkováme obsluhu posledných dvoch tlačidiel a máme hotovo:

createButton("Remove row").onclick = removeRow;
createButton("Remove column").onclick = removeColumn;

Pretože posledné dve funkcie nášho editora neprijímajú žiadny parameter, nemuseli sme ich v udalosti onclick zodpovedajúcich tlačidiel volať pomocou anonymnej funkcie.

Aplikáciu si môžeme vyskúšať, začneme opäť kliknutím do tabuľky:

Table editor
localhost

Po tomto príklade by sme už mali ovládať DOM v JavaScripte. Všimnime si, že na začiatku všetkého máme HTML kód, ktorý v bodoch nemá jediný element.

V budúcej lekcii, Striktné operátory a pretypovanie v JavaScripte, si znovu povieme niečo o podmienkach a ukážeme si niektoré úskalia pretypovania, než sa pustíme do ďalšej väčšej témy - práca s grafikou.


 

Mal si s čímkoľvek problém? Stiahni si vzorovú aplikáciu nižšie a porovnaj ju so svojím projektom, chybu tak ľahko nájdeš.

Stiahnuť

Stiahnutím nasledujúceho súboru súhlasíš s licenčnými podmienkami

Stiahnuté 3x (2.01 kB)
Aplikácia je vrátane zdrojových kódov v jazyku JavaScript

 

Predchádzajúci článok
Editor tabuliek v JavaScripte
Všetky články v sekcii
Základné konštrukcie jazyka JavaScript
Preskočiť článok
(neodporúčame)
Striktné operátory a pretypovanie v JavaScripte
Článok pre vás napísal Michal Žůrek - misaz
Avatar
Užívateľské hodnotenie:
4 hlasov
Autor se věnuje tvorbě aplikací pro počítače, mobilní telefony, mikroprocesory a tvorbě webových stránek a webových aplikací. Nejraději programuje ve Visual Basicu a TypeScript. Ovládá HTML, CSS, JavaScript, TypeScript, C# a Visual Basic.
Aktivity