11. díl - Dokončení editoru tabulek v JavaScriptu

JavaScript Základní konstrukce Dokončení editoru tabulek v JavaScriptu

ONEbit hosting Unicorn College Tento obsah je dostupný zdarma v rámci projektu IT lidem. Vydávání, hosting a aktualizace umožňují jeho sponzoři.

V minulé lekci, Editor tabulek v JavaScriptu, jsme rozpracovali editor tabulek. V dnešním JavaScript tutoriálu tuto webovou aplikaci dokončíme.

Index řádku aktivní buňky

Abychom si techniky "orientování se" v proměnných DOM osvojili, napíšeme si 2 funkce, které později využijeme. Jednou z nich je funkce indexRadkuAktivniBunky(), která vrátí index řádku (pořadí od nuly), kde se nachází uživatelem vybraná buňky (aktivniBunka). Nejprve si je třeba uvědomit, kde tento index získáme. Hledáme mezi řádky tabulky. Tabulka je představována elementem <table> a řádky jsou ihned v něm (buňky jsou až v řádcích, což může mást, dávejte si na to pozor).

Kde budeme hledat: tabulka.childNodes

A co hledáme, bude to trochu složitější. Hledáme řádek, ale my víme, že v aktivniBunka není element <td>, ale <input>, který je v <td>. Abychom se dostali na <td>, musíme z buňky dostat parentElement a ani to nám nebude stačit. Nepotřebujeme buňku, ale řádek a ten opět získáme pomocí parentElement.

Co hledáme: aktivniBunka.parentElement.parentElement

NodeList a indexOf

Jestli si vzpomínáte, tak v minulém dílu jsem zmínil, že funkce s DOM nikdy nevrací pole, ale NodeList nebo podobné specializované kolekce. A že tyto objekty jsou jen (prakticky) hloupější pole. Bohužel NodeList nemá metodu indexOf(), která by nám vrátila potřebný index prvku. Jsou dvě varianty. První je, že si ho cyklem proiterujeme sami a druhá je, že použijeme metodu indexOf() na objektu Array a zavoláme ji s kontextem NodeListu. Druhou část předchozího souvětí důkladně pochopíte u objektově orientovaného programování. Zatím nám stačí vědět, že zavoláme indexOf() na poli, ale "podvrhneme" mu NodeList. JavaScriptu je totiž jedno s čím pracuje, pokud to má vše co má mít a NodeList tuto vlastnost splňuje. Metody se volají se změněným kontextem tak, že je najdeme v prototypu objektu (v našem případě Array.prototype) a zavoláme na ně metodu call(). Jako první parametr ji předáme nový kontext a za ně přidáme parametry metody.

Výsledný kód by tak mohl vypadat následovně:

function indexRadkuAktivniBunky() {
        var cilHledani = tabulka.childNodes;
        var hledanyPrvek = aktivniBunka.parentElement.parentElement;
        return Array.prototype.indexOf.call(cilHledani, hledanyPrvek);
}

cilHledani máme to, v čem hledáme. V hledanyPrvek máme co hledáme. V cilHledani je NodeList a hledanyPrvek je element <tr>. Poté vrátíme hodnotu, kterou vrátí indexOf(), načež ji změníme kontext na NodeList a jako parametr, který má běžně, předáme onen element <tr>.

Index sloupce aktivní buňky

O něco jednodušší to bude se získáváním indexu sloupce aktivní buňky. Potřebujeme se dostat k řádku, kde se buňka nachází a získat její pozici. Buňku opět dostaneme pomocí parentElement na aktivniBunka a k řádku, kterému náleží přes parentElement.parentElement.

function indexSloupceAktivniBunky() {
        var bunkyVRadku = aktivniBunka.parentElement.parentElement.childNodes;
        var td = aktivniBunka.parentElement;
        return Array.prototype.indexOf.call(bunkyVRadku, td);
}

Doteď to možná bylo trochu nudné, protože se nic zajímavého nedělo. Nyní to však bude zajímavější. Již konečně začneme programovat funkce pro ovládání aplikace.

Přidání řádku nad vybraný

Metodu, která vytváří nový řádek (element <tr>), máme již hotovou. Umíme získat index řádku, na kterém leží vybraná buňka, a umíme před něj nový řádek i vložit. Pro přehlednost si nový řádek uložíme do proměnné, index řádku stejně tak a opět se budeme prokousávat DOMem. Řádky (<tr>) jsou vnořené přímo v elementu tabulky (<table>), takže to bude jednoduché. Metodu insertBefore() voláme na tabulce. První parametr je jasný, to je nový řádek. Druhý parametr musí být <tr>, ve kterém je vložena aktivní buňka. Dále se propracovávat nemusíme, řádky jsou přímo v tabulce, proto rovnou z NodeListu vybereme řádek podle indexu řádku aktivní buňky.

function PridejRadekNahoru() {
        var radek = vytvorRadek();
        var indexVybraneho = indexRadkuAktivniBunky();
        tabulka.insertBefore(radek, tabulka.childNodes[indexVybraneho]);
}

Ve funkci, která vytváří ovládací tlačítka, obslužme tlačítku pro přidání řádku nahoru událost onclick.

vytvorTlacitkoAVlozHo("Přidat řádek nahoru", document.body).onclick = PridejRadekNahoru;

Nyní se pustíme do trochu silnější kávy, ale vlastně to nebude nic nového.

Přidávání řádku za vybraný

Zde budeme postupovat prakticky stejně, ale doplatíme opět na to, že JavaScript neobsahuje žádnou metodu insertAfter() a my budeme muset řádek vkládat před aktuální + 1, abychom ho dostali na správnou pozici. Pamatujme, že u toho ještě musíme ošetřovat, zda takto nevkládáme za poslední, protože řádek poslední + 1 neexistuje. Použijeme podmínku ke zjištění, zda je poslední element tabulky (tabulka.lastChild) roven řádku, ve kterém je vybraná aktuální buňka. Pokud je, tak vkládáme nakonec jednoduše metodou appendChild(). V opačném případě vložíme řádek pomocí insertBefore() před řádek za řádkem s vybranou buňkou.

function PridejRadekDolu() {
        var radek = vytvorRadek();
        var indexVybraneho = indexRadkuAktivniBunky();
        if (tabulka.lastChild == tabulka.childNodes[indexVybraneho]) {
                tabulka.appendChild(radek)
        } else {
                tabulka.insertBefore(radek, tabulka.childNodes[indexVybraneho + 1])
        }
}

Nastavte obsluhu události danému tlačítku:

vytvorTlacitkoAVlozHo("Přidat řádek dolů", document.body).onclick = PridejRadekDolu;

Nyní již umíme přidávat řádky různě do tabulky, kde se nám hodí.

Přidávání řádků do tabulky v JavaScriptu

Přidání sloupce vlevo

Přidání sloupce již bude složitější. Tabulka v HTML totiž obsahuje řádky a v těch jsou buňky. A ejhle, v kódu nejsou žádné sloupce! Přidání sloupce budeme muset tedy řešit tak, že přidáme buňku do existujících řádků. Cyklem projedeme všechny řádky tabulky a do každého řádku před index vybrané buňky vložíme novou buňku.

Nyní je třeba si uvědomit co je kde a co budeme kde potřebovat. Budeme vkládat do řádku <tr> a ten je v <table>. Výraz tabulka.childNodes vrací pole řádků <tr> a my musíme vkládat v cyklu do každého z nich novou buňku.

Kam vkládáme: tabulka.childNodes[i]

Vkládáme novou buňku.

Co vkládáme: vytvorBunku()

A teď před co. Referenční element (ten, před který vkládáme) musí být uvnitř elementu, kam vkládáme. Jednoduše tedy začneme tím, že předáme ten samý element, do kterého vkládáme. Protože je to element zevnitř, logicky ho nalezneme uvnitř NodeListu childNodes a víme, že tento childNodes je pole prvků buněk <td> (Hurá! Ty hledáme!). Index tedy (který je pro daný cyklus konstantní) bude onen index buňky mezi sloupci. Pokud jste se ve výkladu ztratili, zkuste si to projít níže v kódu.

Před co vkládáme: tabulka.childNodes[i].childNodes[indexVybraneho]

Celé to bude vypadat následovně.

function PridejSloupecDoleva() {
        var indexVybraneho = indexSloupceAktivniBunky();
        for (var i = 0; i < tabulka.childNodes.length; i++) {
                tabulka.childNodes[i].insertBefore(vytvorBunku(), tabulka.childNodes[i].childNodes[indexVybraneho]);
        }
}

Pokud vám to přišlo složité nebo nepřehledné, tak než se pustíte do další části (protože ta je k orientaci ještě o něco těžší), doplňte si vhodně komentáře nebo si kód rozdělte do rozumně pojmenovaných proměnných.

Přidání sloupce vpravo

Jak již asi tušíte, bude to podobné, pouze se musíme opět vyrovnat s chybějící metodou insertAfter() jako jsme to udělal v metodě PridejRadekDolu(). Jediným rozdílem (opět) je, že musíme ověřit, že nevkládáme na konec. Logicky dojdeme k tomu, že ověřujeme vždy to, co vkládáme s posledním elementem rodiče ověřovaného elementu.

function PridejSloupecDoprava() {
        var indexVybraneho = indexSloupceAktivniBunky();
        for (var i = 0; i < tabulka.childNodes.length; i++) {
                if (tabulka.childNodes[i].childNodes[indexVybraneho] == tabulka.childNodes[i].lastElementChild) {
                        tabulka.childNodes[i].appendChild(vytvorBunku());
                } else {
                        tabulka.childNodes[i].insertBefore(vytvorBunku(), tabulka.childNodes[i].childNodes[indexVybraneho + 1]);
                }
        }
}

Přidejme obsluhy událostí tlačítkům pro přidávání sloupců a aplikaci vyzkoušejte.

vytvorTlacitkoAVlozHo("Přidat sloupec vlevo", document.body).onclick = PridejSloupecDoleva;
vytvorTlacitkoAVlozHo("Přidat sloupec vpravo", document.body).onclick = PridejSloupecDoprava;

Aplikaci vyzkoušejte. Po náročné práci si můžete zahrát třeba piškvorky ve vaší tabulce. Jednoduše si přidejte hodně sloupců a řádků :)

Piškvorky v naší tabulce

Možná při používání aplikace narazíme na problém. Tabulka může mít o sloupec nebo řádek více, než jsme chtěli. Přidáme funkce pro mazání řádku a sloupce.

Mazání řádku

Z tabulky odstraníme řádek, jehož element dostaneme tak, že vezmeme všechny elementy tabulky (childNodes) a vybereme ten s indexem vybrané buňky.

function smazRadek() {
        var indexVybraneho = indexRadkuAktivniBunky();
        tabulka.removeChild(tabulka.childNodes[indexVybraneho]);
}

Mazání sloupce

Mazání sloupce musíme zajistit tak, že smažeme v každém řádku (cyklem) buňku, jejíž index odpovídá indexu vybrané buňky.

function smazSloupec() {
        var indexVybraneho = indexSloupceAktivniBunky();
        for (var i = 0; i < tabulka.childNodes.length; i++) {
                tabulka.childNodes[i].removeChild(tabulka.childNodes[i].childNodes[indexVybraneho]);
        }
}

Zprovozníme tlačítka a máme hotovo.

vytvorTlacitkoAVlozHo("Odstranit řádek", document.body).onclick = smazRadek;
vytvorTlacitkoAVlozHo("Odstranit sloupec", document.body).onclick = smazSloupec;

Po tomto příkladu byste již měli ovládat DOM v JavaScriptu. Všechna ta kouzla od vytváření tabulky až po piškvorky byly vlastně jen hříčky s několika málo elementy, které bychom běžně psali v HTML. Všimněte si, že na začátku všeho máme HTML kód, který v body nemá jediný element.

V příští lekci, Obrázky a kreslení na canvas v JavaScriptu, se budeme věnovat práci s obrázky.


 

Stáhnout

Staženo 453x (2.23 kB)
Aplikace je včetně zdrojových kódů v jazyce JavaScript

 

 

Článek pro vás napsal Michal Žůrek (misaz)
Avatar
Jak se ti líbí článek?
12 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.
Miniatura
Předchozí článek
Editor tabulek v JavaScriptu
Miniatura
Všechny články v sekci
Základní konstrukce jazyka JavaScript
Miniatura
Následující článek
Cvičení k 11. lekci JavaScriptu
Aktivity (6)

 

 

Komentáře
Zobrazit starší komentáře (29)

Avatar
Pavel Šrytr
Člen
Avatar
Pavel Šrytr:26.2.2017 19:06

Jen tak ohrajově bych se chtěl zeptat, proč jsou funkce PridejRadekNahoru a PridejRadekDolu s velkými písmeny nazačátku ? Omlouvám se pokud jsem to přehlédl.

 
Odpovědět 26.2.2017 19:06
Avatar
Odpovídá na Pavel Šrytr
Michal Žůrek (misaz):26.2.2017 19:18

nevím, si je tam napiš malé abys to měl "dokonalé".

Odpovědět 26.2.2017 19:18
Nesnáším {}, proto se jim vyhýbám.
Avatar
Pavel Šrytr
Člen
Avatar
 
Odpovědět 27.2.2017 19:41
Avatar
Daniel Adámek:17.3.2017 17:06

stahl jsem si zip a nefunguje ani v jednom prohlizeci, v konzoli to pise aktivniBunka undefined...

 
Odpovědět 17.3.2017 17:06
Avatar
Odpovídá na Daniel Adámek
Michal Žůrek (misaz):17.3.2017 17:10

a klikl jsi do nějaké buňky?

Odpovědět 17.3.2017 17:10
Nesnáším {}, proto se jim vyhýbám.
Avatar
Lubiikpupiik
Člen
Avatar
Lubiikpupiik:5.9.2017 18:19

Může mi někdo poradit, proč mi to nefunguje? V konzoli to píše, že to nemůže přečis firstElementChild (v přesném znění: Uncaught TypeError: Cannot read property 'firstElementChild' of undefined)
Tady je kód:

var tabulka;
var vychoziVelikostX = 5;
var vychozivelikostY = 3;

var aktivniBunka;
function vytvorBunku(){
var td = document.createElement("td");
var tdInput = document.createElement("input");
tdInput.type = "text";
tdInput.onfocus = function(){
aktivniBunka = this;
};
td.appendChild(tdInput);
return td;
};

function vytvorVychoziTabulku(){
tabulka = document.createElement("table");
document.body.appendChild(tabulka);
for (var i = 0; i < vychozivelikostY; i++) {
                var tr = document.createElement("tr");
                tabulka.appendChild(tr);
                for (var j = 0; j < vychoziVelikostX; j++) {
                tr.appendChild(vytvorBunku());
                };
        };
};

function vytvorTlacitkoAVlozHo(popisek, rodic){
var btn = document.createElement("button");
btn.textContent = popisek;
rodic.appendChild(btn);
return btn;
};

function vytvorOvladaciTlacitka(){
vytvorTlacitkoAVlozHo("Přidej řádek nahoru", document.body).onclick = pridejRadekNahoru();
vytvorTlacitkoAVlozHo("Přidej řádek dolu", document.body).onclick = pridejRadekDolu();
vytvorTlacitkoAVlozHo("Přidej sloupec doleva", document.body);
vytvorTlacitkoAVlozHo("Přidej sloupec doprava", document.body);
vytvorTlacitkoAVlozHo("Smaž rádek", document.body);
vytvorTlacitkoAVlozHo("Smaž sloupec", document.body);

};

function vytvorRadek() {
        var novyRadek = document.createElement("tr");

        for (var i = 0; i < tabulka.firstElementChild.childNodes.length; i++) {
                novyRadek.appendChild(vytvorBunku());
        };
        return novyRadek;
};

function indexRadkuAktivniBunky() {
var cilHledani = tabulka.childNodes;
var hledanaPrvek = aktivniBunka.parentElement.parentElement;
return Array.prototype.indexOf.call(cilHledani, hledanaPrvek);
};

function indexSloupceAktivniBunky() {
        var bunkyVRadku = aktivniBunka.parentElement.parentElement.childNodes;
        var td = aktivniBunka.parentElement
        return Array.prototype.indexOf.call(bunkyVRadku, td)
};

function pridejRadekNahoru(){
var radek = vytvorRadek();
var indexVybraneho = indexRadkuAktivniBunky();
tabulka.insertBefore(radek, tabulka.childNodes[indexVybraneho]);
};

function pridejRadekDolu(){
var radek = vytvorRadek();
var indexVybraneho = indexRadkuAktivniBunky();
if (tabulka.lastChild = tabulka.childNodes[indexVybraneho]) {
tabulka.appendChild(radek);
} else{
tabulka.insertBefore(radek, tabulka.childNodes[indexVybraneho + 1]);
};
};
window.onload = function(){
vytvorOvladaciTlacitka();
vytvorVychoziTabulku();
};
 
Odpovědět 5.9.2017 18:19
Avatar
Odpovídá na Lubiikpupiik
Michal Žůrek (misaz):5.9.2017 18:36

v článku je napsané, abys použil řádek:

vytvorTlacitkoAVlozHo("Přidat řádek nahoru", document.body).onclick = PridejRadekNahoru;

v kódu však máš.

vytvorTlacitkoAVlozHo("Přidej řádek nahoru", document.body).onclick = pridejRadekNahoru()

máš tam závorky na konci. Když nastavuješ obsluhu události, nesmíš tam psát závorky, protože tím funkci okamžitě zavoláš a to nechceš. Ty ji chceš zavolat až po kliknutí na tlačítko, ne hned.

Editováno 5.9.2017 18:37
Odpovědět 5.9.2017 18:36
Nesnáším {}, proto se jim vyhýbám.
Avatar
Lubiikpupiik
Člen
Avatar
Odpovídá na Michal Žůrek (misaz)
Lubiikpupiik:5.9.2017 18:38

Jo paráda. Už chápu. Díky moc.

 
Odpovědět 5.9.2017 18:38
Avatar
frenky.pv
Člen
Avatar
frenky.pv:7.9.2017 14:49

Zkoušel jsem pár vylepšení.

  1. Třeba označit aktivní buňku. Do funkce vytvorBunku() jsem za řádek

tdInput.type = "text"
přidal:
tdInput.onfocus = function () {
if ( aktivniBunka ) aktivniBunka.pa­rentElement.id = '';
aktivniBunka = this
aktivniBunka.pa­rentElement.id = 'sel';
}

A do souboru se styly jsem přidal:
#sel {
border: 2px solid red;
}

Teď krásně vidím jakou buňku mám vybranou.

  1. Taky jsem zjednodušil funkce pro zjištění aktivní buňky

function indexRadkuAktiv­niBunky() {
if ( aktivniBunka ) return aktivniBunka.pa­rentElement.pa­rentElement.ro­wIndex;
return false;
}
function indexSloupceAk­tivniBunky() {
if ( aktivniBunka ) return aktivniBunka.pa­rentElement.ce­llIndex;
return false;
}

  1. Také jsem zjistil, že není třeba u funkcí PridejRadekDolu() dělat podmínky na poslední (koncový prvek). Když se použije tabulka.inser­tBefore(radek, tabulka.childNo­des[indexVybra­neho + 1]), tak to funguje vždy. Možná, že to v nějakém prohlížeči může vyhazovat chybu, pro čtení prvku mimo pole. Mě to fungovalo jak v IE, tak FF.
  1. A pro přidávání sloupců se dá i místo for použít i forEach, jen se to musí udělat přes funkci:

function sloupec( item, index ) {
tabulka.childNo­des[index].in­sertBefore( vytvorBunku(), tabulka.childNo­des[index].chil­dNodes[ indexVybraneho + 1] );
}
tabulka.childNo­des.forEach( sloupec );

 
Odpovědět 7.9.2017 14:49
Avatar
Odpovídá na frenky.pv
Michal Žůrek (misaz):7.9.2017 16:44

vyždycky to jde vylepšit/zjed­nodušit. Cílem článku nebylo udělat tutorial na excel, ale tutorial na obecné metody DOM.

2. ano, to by šlo, ale funguje to jenom v tabulkách. Chtěl jsem ukázat jak se to dělá v obecném elementu.
3. to jsem nevěděl, psal to zde už někde před tebou.
4. V době psaní článku jsi na childNodes volat forEach volat nemohl, protože childNodes není pole, ale instance NodeList. NodeList je takové hloupější pole a jedna z vad NodeListu (obdobné problémy mají/měli objekty HTMLCollection a FileList vrámci FileReader API) je, že neobsahuje běžné metody pole, jako je indexOf, forEach, pop, atd. Absence některých dává smysl, protože implementace operací nad DOM je na straně interpretu JS velmi složitá, nicméně některé se hodí. Oficiálně platný standard metody neobsahuje dodnes, nicméně je součástí návrhu a living standard jej obsahuje. Webvové prohlížeče je ve verzích vydaných cca koncem roku 2016 podporují. Protože forEach jsem i já shledával užitečný (i v dbě, kdy na childNodes zavolat nešel), přidával jsem si do kódu vlastní easy peasy polyfil toArray() na Object, který mi převedl objekt (tzn. NodeList, HTMLCollection a FileList) na klasické pole, samozřejmě za cenu, že tam stejně nebude fungovat třeba metoda splice.

Odpovědět 7.9.2017 16:44
Nesnáším {}, proto se jim vyhýbám.
Děláme co je v našich silách, aby byly zdejší diskuze co nejkvalitnější. Proto do nich také mohou přispívat pouze registrovaní členové. Pro zapojení do diskuze se přihlas. Pokud ještě nemáš účet, zaregistruj se, je to zdarma.

Zobrazeno 10 zpráv z 39. Zobrazit vše