NOVINKA - Online rekvalifikační kurz Python programátor. Oblíbená a studenty ověřená rekvalifikace - nyní i online.
Hledáme nové posily do ITnetwork týmu. Podívej se na volné pozice a přidej se do nejagilnější firmy na trhu - Více informací.

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:

Editor tabulek
localhost

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:

Editor tabulek
localhost

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

 

Předchozí článek
Editor tabulek v JavaScriptu
Všechny články v sekci
Základní konstrukce jazyka JavaScript
Přeskočit článek
(nedoporučujeme)
Striktní operátory a přetypování v JavaScriptu
Článek pro vás napsal Michal Žůrek - misaz
Avatar
Uživatelské hodnocení:
1127 hlasů
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