Skip to content

Instantly share code, notes, and snippets.

@SKevo18
Last active August 19, 2024 18:16
Show Gist options
  • Save SKevo18/f7cf35d4d7ad002dc995c258bb235554 to your computer and use it in GitHub Desktop.
Save SKevo18/f7cf35d4d7ad002dc995c258bb235554 to your computer and use it in GitHub Desktop.
Pojmová mapa v JavaScripte na MediaWiki pomocou `vis-network`
const tippyInstancia = tippy(document.createElement("div"));
/**
* Vytvorí JSON pojmovej mapy z obsahu stránky.
* @param {HTMLElement} poznamkyElement - Element obsahujúci poznámky.
* @returns {object} JSON reprezentácia pojmovej mapy.
*/
function vytvoritDataMapy(poznamkyElement) {
const mapa = { vrcholy: [], hrany: [] };
const nadpisy = Array.from(poznamkyElement.querySelectorAll("h1, h2, h3, h4, h5, h6"));
const posledneNadpisy = [];
const korenovyNadpis = document.getElementById("firstHeading")?.innerText || "Hlavná téma";
mapa.vrcholy.push({
id: 1,
label: korenovyNadpis,
color: generovatFarbu(0),
tooltip: "",
});
nadpisy.forEach((nadpis, index) => {
const aktualnyLevel = parseInt(nadpis.tagName.substring(1), 10);
const idVrchola = index + 2;
const nazov = nadpis.querySelector(".mw-headline")?.innerText || `Nadpis ${idVrchola}`;
const idRodica = posledneNadpisy.slice(0, aktualnyLevel).reverse().find(id => id !== undefined) || 1;
mapa.vrcholy.push({
id: idVrchola,
label: nazov,
color: generovatFarbu(idRodica),
tooltip: obsahNadpisu(nadpis),
});
mapa.hrany.push({ from: idRodica, to: idVrchola });
posledneNadpisy[aktualnyLevel] = idVrchola;
posledneNadpisy.splice(aktualnyLevel + 1);
});
return mapa;
}
/**
* Získa obsah nadpisu a všetkých nasledujúcich elementov, pokiaľ nepríde po ďalší nadpis.
* @param {HTMLElement} nadpisElement - HTML element nadpisu.
* @returns {string} Obsah nadpisu a všetkých nasledujúcich elementov v HTML formáte.
*/
function obsahNadpisu(nadpisElement) {
const novyNadpis = nadpisElement.cloneNode(true);
novyNadpis.innerHTML = novyNadpis.querySelector(".mw-headline").outerHTML;
novyNadpis.style.cssText = "margin-top: 0; text-align: center;";
const obsahElement = document.createElement("div");
obsahElement.appendChild(novyNadpis);
function pridavajObsah(element) {
if (!element || ["H1", "H2", "H3", "H4", "H5", "H6"].includes(element.tagName)) {
return;
}
if (element.querySelector(":scope > h1, h2, h3, h4, h5, h6")) {
pridavajObsah(element.firstElementChild);
} else {
obsahElement.appendChild(element.cloneNode(true));
pridavajObsah(element.nextElementSibling);
}
}
pridavajObsah(nadpisElement.nextElementSibling);
return skratitElement(obsahElement).innerHTML;
}
/**
* Skráti HTML elementu na maximálnu dĺžku.
* @param {HTMLElement} element - Element na skrátenie.
* @param {number} maxDlzka - Maximálna dĺžka.
* @returns {HTMLElement} Nový skrátený element.
*/
function skratitElement(element, maxDlzka = 2000) {
const novyElement = document.createElement(element.tagName);
let pocitadloDlzok = 0;
for (const child of element.childNodes) {
pocitadloDlzok += child.textContent.length;
if (pocitadloDlzok > maxDlzka) {
novyElement.appendChild(document.createTextNode("..."));
break;
}
novyElement.appendChild(child.cloneNode(true));
}
return novyElement;
}
/**
* Vygeneruje zobrazenie pojmovej mapy.
* @param {HTMLElement} elementMapy - Element, do ktorého sa vloží zobrazenie pojmovej mapy.
* @param {object} jsonMapy - JSON reprezentácia pojmovej mapy.
* @returns {object} Objekt pojmovej mapy (vis-network).
*/
function vykreslitMapu(elementMapy, jsonMapy) {
const jeSirokeZobrazenie = window.innerWidth > 768;
const dataSiete = {
nodes: new vis.DataSet(jsonMapy.vrcholy),
edges: new vis.DataSet(jsonMapy.hrany),
};
const nastavenia = {
autoResize: true,
interaction: {
dragNodes: false,
dragView: true,
zoomView: false,
hover: true,
},
nodes: {
shape: "box",
widthConstraint: { maximum: 200 },
margin: 10,
labelHighlightBold: false,
},
edges: {
width: 1.0,
arrows: { to: { enabled: true } },
},
layout: {
hierarchical: {
enabled: true,
direction: jeSirokeZobrazenie ? "UD" : "LR",
sortMethod: "directed",
nodeSpacing: jeSirokeZobrazenie ? 220 : 100,
levelSeparation: jeSirokeZobrazenie ? 100 : 250,
shakeTowards: "roots",
},
},
physics: {
hierarchicalRepulsion: { nodeDistance: 150 },
},
height: "400px",
};
const pojmova_mapa = new vis.Network(elementMapy, dataSiete, nastavenia);
pojmova_mapa.on("hoverNode", (parametre) => {
const obsah = jsonMapy.vrcholy.find((vrchol) => vrchol.id === parametre.node)?.tooltip;
if (!parametre.node || !obsah) return;
tippyInstancia.setProps({
triggerTarget: elementMapy,
maxWidth: "90vw",
maxHeight: "70vh",
allowHTML: true,
arrow: false,
interactive: true,
appendTo: document.body,
getReferenceClientRect: () => ({
width: 0,
height: 0,
left: window.innerWidth / 2,
right: window.innerWidth / 2,
top: window.innerHeight - 10,
}),
});
tippyInstancia.setContent(`
<div style="padding: 1rem; font-size: 12px; color: white; max-height: 60vh; overflow-y: auto;">
${obsah}
</div>
`);
tippyInstancia.show();
});
let poslednaKliknutaBunkaId;
let casPoslednehoKliknutiaNaBunku;
pojmova_mapa.on("click", (parametre) => {
if (!parametre.nodes.length) return;
const bunkaId = parametre.nodes[0];
function navigovat() {
const titulokBunky = jsonMapy.vrcholy.find((vrchol) => vrchol.id === bunkaId)?.label;
if (!titulokBunky) return;
window.location.hash = `#${titulokBunky.replaceAll(" ", "_")}`;
tippyInstancia.hide();
}
const aktualnyCas = new Date().getTime();
if (!jeSirokeZobrazenie && poslednaKliknutaBunkaId === bunkaId && aktualnyCas - casPoslednehoKliknutiaNaBunku < 500) {
setTimeout(navigovat, 50);
} else if (jeSirokeZobrazenie) {
navigovat();
}
poslednaKliknutaBunkaId = bunkaId;
casPoslednehoKliknutiaNaBunku = aktualnyCas;
});
pojmova_mapa.on("blurNode", () => {
if (!tippyInstancia.state.isEnabled) {
tippyInstancia.hide();
}
});
const info = document.createElement("small");
info.style.fontSize = "12px";
info.innerHTML = jeSirokeZobrazenie
? "Kliknite na mapu pre interakciu s ňou. Kliknutím na vrchol sa presuniete na príslušnú sekciu."
: "Kliknite na mapu pre interakciu s ňou. Dvojitým kliknutím na vrchol sa presuniete na príslušnú sekciu.";
elementMapy.parentNode.insertBefore(info, elementMapy.nextSibling);
// Zabezpečenie, že mapa je interaktívna len keď je zameraná
pojmova_mapa.on("afterDrawing", () => {
elementMapy.tabIndex = 0;
elementMapy.style.outline = "none";
elementMapy.addEventListener("focus", () => pojmova_mapa.setOptions({ interaction: { dragNodes: true, dragView: true } }));
elementMapy.addEventListener("blur", () => pojmova_mapa.setOptions({ interaction: { dragNodes: false, dragView: false } }));
});
return pojmova_mapa;
}
/**
* Generuje náhodné číslo z čísla (seed). Rovnaký seed vždy vygeneruje rovnaké "náhodné" číslo v určenom rozsahu.
*
* https://stackoverflow.com/a/63599906
*
* @param {number} seed Číslo, z ktorého sa generuje náhodné číslo.
* @param {number[]} rozsah Rozsah, v ktorom sa náhodné číslo generuje.
* @returns {number} Náhodné číslo.
*/
function nahodneCislo(seed, rozsah = [0, 255]) {
seed = String(seed)
.split("")
.reduce((c, n) => (n != 0 ? c * n : c * c));
let od = rozsah[0];
let po = rozsah[1];
while (seed < rozsah[0] || seed > rozsah[1]) {
if (seed > rozsah[1]) seed = Math.floor(seed / po--);
if (seed < rozsah[0]) seed = Math.floor(seed * od++);
}
return seed;
}
/**
* Generuje náhodnú farbu z čísla. Rovnaké číslo vždy vygeneruje rovnakú farbu.
*
* @param {number} cislo Číslo, z ktorého sa generuje farba.
* @returns {string} Farba v RBG formáte.
*/
function generovatFarbu(seed = 1, svetla = true) {
let od = svetla ? 125 : 0;
let po = svetla ? 255 : 125;
r = nahodneCislo(seed + 11, [od, po]);
g = nahodneCislo(seed + 12, [od, po]);
b = nahodneCislo(seed + 13, [od, po]);
return `rgb(${r}, ${g}, ${b})`;
}
window.addEventListener("load", () => {
const poznamkyElement = document.querySelector("#mw-content-text .mw-parser-output");
const elementMapy = document.getElementById("mapa");
if (elementMapy && poznamkyElement) {
const jsonMapy = vytvoritDataMapy(poznamkyElement);
vykreslitMapu(elementMapy, jsonMapy);
} else {
console.log("Na stránke sa nenachádza pojmová mapa.");
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment