Last active
August 19, 2024 18:16
-
-
Save SKevo18/f7cf35d4d7ad002dc995c258bb235554 to your computer and use it in GitHub Desktop.
Pojmová mapa v JavaScripte na MediaWiki pomocou `vis-network`
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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