Lekce 21 - Dokončení editoru tabulek v JavaScriptu
V minulé lekci, Editor tabulek v JavaScriptu, jsme rozpracovali editor HTML tabulek, který uživateli umožňuje tabulku rozšiřovat a upravovat.
V dnešním tutoriálu základů JavaScriptu dokončíme rozpracovaný editor tabulek. Tato část bude zaměřena především na manipulaci s elementy DOM. Do projektu totiž doplníme funkce, které nám umožní vkládat sloupce a řádky na vybrané místo tabulky. Nakonec si ukážeme, jak zajistit jejich smazání.
Zjištění pozice aktivní buňky
Z dřívějška máme v našem editoru implementovanou možnost
výběru buňky v tabulce. Do proměnné aktivniBunka
ukládáme element <input>
, který uživatel označil.
Abychom mohli tabulku upravovat na vybraném místě, budeme nejprve potřebovat
zjistit pozici této aktivní buňky. Vytvoříme si tedy dvě
funkce, které později využijeme. První z nich bude funkce
indexRadkuAktivniBunky()
, která vrátí index řádku aktivní
buňky počínaje 0
. Druhá bude funkce
indexSloupceAktivniBunky()
, která vrátí odpovídající index
sloupce aktivní buňky.
Funkce
indexRadkuAktivniBunky()
Nejprve si vysvětlíme, jak index řádku získáme. Připomeňme si hierarchii potomků vygenerované tabulky:
<body> <table> <tr> <td> <input type="text"> </td> <td> <input type="text"> </td> <td> <input type="text"> </td> <!-- Zde jsou další dvě buňky řádku s vnořenými elementy typu input --> </tr> <tr> <!-- Stejným způsobem je vytvořen druhý a třetí řádek --> </tr> </table> </body>
Tabulka je reprezentována elementem <table>
.
Řádky (<tr>
) jsou jejími
přímými potomky. Každý řádek má zase
jako přímé potomky jednotlivé buňky
(<td>
). Na začátku funkce
indexRadkuAktivniBunky()
budeme mít tento kód:
function indexRadkuAktivniBunky() { let uzlyTabulky = tabulka.childNodes; let hledanyRadek = aktivniBunka.parentElement.parentElement; // Zde doplníme kód pro získání indexu hledaného řádku }
Do proměnné uzlyTabulky
vkládáme pomocí vlastnosti
childNodes
všechny přímé potomky elementu tabulky. Získáme
tak kolekci jednotlivých řádků (elementů
<tr>
) tabulky. Kolekce je typu NodeList
a my v
ní budeme hledat řádek obsahující aktivní buňku.
Hledaný řádek získáme z aktivní buňky pomocí vlastnosti
parentElement
. Protože máme v proměnné
aktivniBunka
uložený element <input>
, vrátí
nám vlastnost aktivniBunka.parentElement
jako nadřazený element
příslušnou buňku (<td>
), ve které je
<input>
vnořený. Abychom se dostali k řádku (elementu
<tr>
), ve kterém se aktivní buňka nachází, je nutné
zavolat parentElement
dvakrát.
Vlastnost childNodes
zahrnuje všechny přímo
podřízené uzly rodičovského elementu. V našem případě bychom mohli
využít také vlastnost children
, která vrací kolekci
obsahující pouze HTML elementy. Uzel je základní jednotka DOM, která může
představovat element, text nebo komentář. HTML element je specifický typ
uzlu, který vždy odpovídá konkrétní značce/tagu v HTML.
Metoda indexOf()
Abychom nemuseli procházet celou kolekci uzlů a pokaždé porovnávat jeden
její prvek s hledaným řádkem, představíme si metodu
indexOf()
. Tato metoda nám vrátí přímo index řádku s
aktivní buňkou. Voláme ji na poli, v němž se má index
prvku hledat a jako parametr ji předáváme hledaný
prvek.
Jedná se o metodu pro práci s polem, my ji však
potřebujeme využít na kolekci NodeList
. Ukážeme si dva
způsoby, jak tento problém vyřešit.
Převod kolekce NodeList
na pole
Nejprve uložíme naši kolekci do nového pole pomocí metody
Array.from()
. Poté na tomto poli zavoláme metodu
indexOf()
pro získání požadovaného indexu:
function indexRadkuAktivniBunky() { let uzlyTabulky = tabulka.childNodes; let hledanyRadek = aktivniBunka.parentElement.parentElement; let uzlyPole = Array.from(uzlyTabulky); return uzlyPole.indexOf(hledanyRadek); }
Tento zápis je srozumitelný, ale vytváří duplicitní kolekci uzlů ve formě pole. To bývá nevhodné z hlediska efektivity, protože zbytečně spotřebovává paměť a zvyšuje výpočetní náročnost.
Změna kontextu metody
indexOf()
Další řešení představuje možnost zavolat metodu indexOf()
na poli, ale "podvrhnout" mu NodeList
. JavaScriptu na typu objektu
nezáleží, pokud má potřebné vlastnosti a strukturu. Tento přístup se
nazývá volání s upraveným kontextem. Upravený kód
vypadá takto:
function indexRadkuAktivniBunky() { let uzlyTabulky = tabulka.childNodes; let hledanyRadek = aktivniBunka.parentElement.parentElement; return Array.prototype.indexOf.call(uzlyTabulky, hledanyRadek); }
Metodu voláme se změněným kontextem tak, že ji
vyhledáme v prototypu objektu (v našem případě
Array.prototype
) a použijeme na ní metodu call()
.
Té jako první parametr předáváme nový kontext a jako
druhý parametr argumenty původní metody. Jde o
složitější syntaxi, díky které jsme se však vyhnuli vytváření další
kolekce.
Funkce
indexSloupceAktivniBunky()
Podobně budeme postupovat při získávání indexu sloupce aktivní buňky.
Vytvoříme si kolekci uzlů daného řádku a proměnnou pro
hledaný element <td>
aktivní buňky:
function indexSloupceAktivniBunky() { let uzlyRadku = aktivniBunka.parentElement.parentElement.childNodes; let hledanaBunka = aktivniBunka.parentElement; return Array.prototype.indexOf.call(uzlyRadku, hledanaBunka); }
Abychom v kolekci měli opravdu uzly vybraného řádku,
musíme vzít aktivní buňku (element <input>
) a
zřetězeným voláním vlastnosti parentElement
se dostat k
nadřazenému elementu <tr>
. Teprve pak na něj voláme
vlastnost childNodes
a získáváme kolekci jednotlivých buňek v
daném řádku. Nakonec znovu voláme metodu indexOf()
se
změněným kontextem a získáváme tím index hledané buňky.
Přidávání řádků
Nyní si doplníme funkce pro obsluhu vytvořených tlačítek, začneme přidáváním řádků.
Funkce pridejRadekNahoru()
Funkce pro přidávání řádku nad vybranou buňku bude vypadat následovně:
function pridejRadekNahoru() { let novyRadek = vytvorRadek(); let indexVybraneho = indexRadkuAktivniBunky(); tabulka.insertBefore(novyRadek, tabulka.childNodes[indexVybraneho]); }
Pomocí naší již vytvořené funkce vytvorRadek()
vytváříme nový řádek a ukládáme ho do proměnné. Dále si ukládáme
index řádku, ve kterém se nachází aktivní (označená)
buňka. Nad ten pak vkládáme nově vytvořený řádek metodou
insertBefore()
. Tato metoda přijímá jako první parametr
element, který má být vložen do HTML struktury, a jako druhý parametr uzel,
před který se má tento element umístit. V našem případě je to řádek,
ve kterém se nachází aktivní buňka.
Funkce pridejRadekDolu()
Doplníme funkci přidávající řádek pod vybranou buňku:
function pridejRadekDolu() { let novyRadek = vytvorRadek(); let indexVybraneho = indexRadkuAktivniBunky(); if (tabulka.lastChild == tabulka.childNodes[indexVybraneho]) { tabulka.appendChild(novyRadek); } else { tabulka.insertBefore(novyRadek, tabulka.childNodes[indexVybraneho + 1]); } }
Ve funkci máme navíc podmínku, kterou zjišťujeme, zda je vybrána buňka
na posledním řádku tabulky. Pokud tomu tak je, použijeme metodu
appendChild()
, která jednoduše vloží nový řádek na konec
tabulky.
Pokud však aktivní buňka není na posledním řádku, vložíme nový
řádek za aktuálně vybraný řádek do tabulky pomocí
metody insertBefore()
. K tomu stačí navýšit index
vybraného řádku o jedna. Tím se zajistí, že se nový prvek
vloží přímo za řádek obsahující aktivní buňku a ne před něj.
Nakonec se přesuneme do naší funkce vytvorOvladaciTlacitka()
a nastavíme zde obsluhu kliknutí prvním dvěma tlačítkům:
vytvorTlacitkoAVlozHo("Přidat řádek dolů").onclick = pridejRadekDolu; vytvorTlacitkoAVlozHo("Přidat řádek nahoru").onclick = pridejRadekNahoru;
Novou funkcionalitu si můžeme vyzkoušet v živé ukázce níže. Nezapomeňme však nejprve označit nějakou buňku tabulky kliknutím na ni:
Přidávání sloupců
Jako další vytvoříme funkce pro přidávání sloupců vlevo a vpravo. Přidávání sloupců je oproti přidávání řádků trochu složitější. Tabulka v HTML totiž obsahuje řádky, které obsahují buňky, ale sloupce jako takové v ní definovány nejsou. Přidávání sloupců tedy budeme muset řešit tak, že přidáme buňku do každého z existujících řádků. Pomocí cyklu projdeme všechny řádky tabulky a do každého řádku vložíme novou buňku buď před, nebo za označenou buňku.
Funkce
pridejSloupecDoleva()
Do našeho kódu doplníme funkci pro přidávání sloupců nalevo od aktivní buňky:
function pridejSloupecDoleva() { let indexVybraneho = indexSloupceAktivniBunky(); for (let i = 0; i < tabulka.childNodes.length; i++) { let novaBunka = vytvorBunku(); tabulka.childNodes[i].insertBefore(novaBunka, tabulka.childNodes[i].childNodes[indexVybraneho]); } }
Nejprve zjišťujeme index sloupce, ve kterém se nachází
aktivní buňka a ukládáme ho do proměnné
indexVybraneho
. Následně cyklem procházíme jednotlivé řádky
tabulky. Kolik má tabulka řádků zjišťujeme zápisem
tabulka.childNodes.length
. Do proměnné novaBunka
ukládáme nově vytvořenou buňku pomocí funkce
vytvorBunku()
.
Následně opět metodou insertBefore()
tuto novou buňku
vkládáme do řádku. Místo, před které se nová buňka vkládá, určujeme
zápisem tabulka.childNodes[i].childNodes[indexVybraneho]
, kde
childNodes[i]
nám vrací vždy ten řádek, ve kterém se
právě cyklus nachází, z něj se voláním
childNodes[indexVybraneho]
dostaneme už na konkrétní
buňku, před kterou se nová bude vkládat.
Funkce
pridejSloupecDoprava()
U přidání sloupce směrem doprava budeme opět muset kontrolovat, zda se nejedná o poslední sloupec tabulky:
function pridejSloupecDoprava() { let indexVybraneho = indexSloupceAktivniBunky(); for (let i = 0; i < tabulka.childNodes.length; i++) { let novaBunka = vytvorBunku(); if (tabulka.childNodes[i].childNodes[indexVybraneho] == tabulka.childNodes[i].lastElementChild) { tabulka.childNodes[i].appendChild(novaBunka); } else { tabulka.childNodes[i].insertBefore(novaBunka, tabulka.childNodes[i].childNodes[indexVybraneho + 1]); } } }
Do funkce pridejSloupecDoprava()
jsme tedy doplnili podmínku,
abychom zjistili, zda je vybraná buňka (indexVybraneho
) v
aktuálním řádku (tabulka.childNodes[i]
) poslední buňkou
tohoto řádku (tabulka.childNodes[i].lastElementChild
). Pokud ano,
přidáme novou buňku metodou appendChild()
. V opačném
případě použijeme metodu insertBefore()
a
indexVybraneho
zvýšíme o jedna.
Naše čtyři funkce pro přidávání sloupců a řádků by mohly být sloučeny do dvou univerzálnějších. Tento přístup by byl z hlediska dobrých programátorských praktik, zejména zásady DRY (neopakuj se), vhodnější. Zvolili jsme však rozdělení kódu do více specifických funkcí z důvodu přehlednosti a snadnějšího nastavování událostí kliknutí pro jednotlivá tlačítka.
Ve funkci vytvorOvladaciTlacitka()
nastavíme obsluhu kliknutí
dalším dvěma tlačítkům:
vytvorTlacitkoAVlozHo("Přidat sloupec vlevo").onclick = pridejSloupecDoleva; vytvorTlacitkoAVlozHo("Přidat sloupec vpravo").onclick = pridejSloupecDoprava;
Mazání
Nakonec do aplikace přidáme funkce pro mazání řádku a sloupce.
Mazání řádku
Funkce pro mazání řádků bude vypadat následovně:
function smazRadek() { let indexVybraneho = indexRadkuAktivniBunky(); tabulka.removeChild(tabulka.childNodes[indexVybraneho]); }
Řádek odstraňujeme pomocí metody removeChild()
, která jako
parametr očekává element určený k odstranění. V našem případě jde o
řádek obsahující aktivní buňku, jehož index získáváme funkcí
indexRadkuAktivniBunky()
a ukládáme ho do proměnné
indexVybraneho
. Následně tuto proměnnou využíváme k výběru
správného elementu z kolekce podřízených elementů
(childNodes
) tabulky.
Mazání sloupce
Nyní doplníme funkci pro mazání sloupců:
function smazSloupec() { let indexVybraneho = indexSloupceAktivniBunky(); for (let i = 0; i < tabulka.childNodes.length; i++) { tabulka.childNodes[i].removeChild(tabulka.childNodes[i].childNodes[indexVybraneho]); } }
Cyklem procházíme všechny řádky tabulky a mažeme v nich ty buňky,
jejichž index odpovídá indexu té označené. K mazání opět používáme
metodu removeChild()
. Tu voláme na
tabulka.childNodes[i]
, tedy aktuálním řádku, ve kterém se
právě nacházíme v cyklu, a předáváme jí parametr
tabulka.childNodes[i].childNodes[indexVybraneho]
. Tento parametr
označuje buňku ke smazání, tedy buňku se stejným indexem, jaký má
právě označená buňka.
Zprovozníme obsluhu posledních dvou tlačítek a máme hotovo:
vytvorTlacitkoAVlozHo("Odstranit řádek").onclick = smazRadek; vytvorTlacitkoAVlozHo("Odstranit sloupec").onclick = smazSloupec;
Aplikaci si můžeme vyzkoušet, opět ale musíme nejprve označit nějakou buňku kliknutím do tabulky:
Po tomto příkladu bychom již měli ovládat DOM v JavaScriptu. Všimněme si, že na začátku všeho máme HTML kód, který v body nemá jediný element.
V příští lekci, Striktní operátory a přetypování v JavaScriptu, si znovu řekneme něco o podmínkách a ukážeme si některá úskalí přetypování, než se pustíme do dalšího většího tématu - práce s grafikou.
Měl jsi s čímkoli problém? Stáhni si vzorovou aplikaci níže a porovnej ji se svým projektem, chybu tak snadno najdeš.
Stáhnout
Stažením následujícího souboru souhlasíš s licenčními podmínkami
Staženo 2120x (3.24 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript