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:
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:
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