Skip to content
Snippets Groups Projects
neighbors.js 7.33 KiB
Newer Older
import * as Helpers from "../helpers";
import { createHTMLElement } from "../helpers";

export { NodeNeighborOverlay };

/**
 * Displays an overlay showing the neighbors of a node divided by the link type
 * that connects them.
 */
class NodeNeighborOverlay {
    constructor(graph, parentNode, infoOverlay, type = "link") {
        this.graph = graph;
        this.parentNode = parentNode;
        this.infoOverlay = infoOverlay;
        this.contentTab = null;
Matthias Konitzny's avatar
Matthias Konitzny committed
        this.tabContentPages = {}; // Page content - links to other nodes
        this.tabNavHandles = {}; // Top-level handles of the content pages
        this.tabPageVisibility = {}; // Visibility of each content page
    /**
     * Creates the visible elements of the overlay.
     * Must be called after the graph has been initialized.
     */
    create() {
        const bottomContainerDiv = Helpers.createDiv(
            "neighbor-container",
            this.parentNode
        );
        bottomContainerDiv.classList.add("fancy-scrollbar");
Harm Kube's avatar
Harm Kube committed
        // Create the collapsible of the entire menu
Matthias Konitzny's avatar
Matthias Konitzny committed
        const coll = Helpers.createDiv("button", bottomContainerDiv);
        coll.className = "neighbor-collapsible-title";
        coll.innerText = "Verwandte Inhalte";
        coll.style.textAlign = "center";
Harm Kube's avatar
Harm Kube committed

        // Div that displays the information about all the chapters
        const contentTab = Helpers.createDiv(
            "neighbor-content-tabs",
            bottomContainerDiv
        );
        //contentTab.classList.add("neighbor-collapsible-wrapper");
        this.contentTab = contentTab;

        coll.addEventListener("click", () => {
            if (contentTab.style.height === "0px") {
                contentTab.style.height = "auto";
            } else {
                contentTab.style.height = "0px";
        const colors =
            this.type === "link"
                ? this.graph.edgeColors
                : this.graph.nodeColors;
        for (const [cls, color] of Object.entries(colors)) {
            this.createCollapsibleTab(contentTab, cls, color);
Matthias Konitzny's avatar
Matthias Konitzny committed
    /**
     * Creates a new collapsible tab and content area for a specific node.
Matthias Konitzny's avatar
Matthias Konitzny committed
     * @param {HTMLElement} parent Parent of the new tab.
     * @param {string} name Name of the node type class
     * @param {string} color Color of the node type class
     */
    createCollapsibleTab(parent, name, color) {
        // Creating the collapsible tabs for the different chapters
        const collTab = Helpers.createDiv("button", parent);
        collTab.className = "neighbor-collapsible-section";
        collTab.innerText = name;
        collTab.type = name;
        this.tabNavHandles[name] = collTab;
        this.tabPageVisibility[name] = false;
Matthias Konitzny's avatar
Matthias Konitzny committed

        const collTabMarker = Helpers.createDiv(
            "neighbor-collapsible-marker-div",
            collTab
        );
        collTabMarker.style.borderColor = color;
        collTabMarker.style.backgroundColor = color;

        const openMarkerTabs = Helpers.createDiv(
            "neighbor-tab-open-status-marker",
            collTab
        );
        openMarkerTabs.innerText = "+";
        collTab.marker = openMarkerTabs;

        // Content of the different tabs
        const collTabContent = Helpers.createDiv(
            "neighbor-content-linksection",
            parent
        );
        collTabContent.classList.add("neighbor-collapsible-wrapper");
Matthias Konitzny's avatar
Matthias Konitzny committed
        collTabContent.type = name;

        const list = createHTMLElement("ul", collTabContent);
Matthias Konitzny's avatar
Matthias Konitzny committed
        list.style.margin = "0px";
        collTabContent.list = list;
Matthias Konitzny's avatar
Matthias Konitzny committed
        collTabContent.marker = openMarkerTabs;
        this.tabContentPages[name] = collTabContent;

        collTab.addEventListener("click", () => {
            this.toggleSectionVisibility(name);
    /**
     * Toggles the visibility of a specific section
     * @param {string} name Id of the section
     */
    toggleSectionVisibility(name) {
        if (this.tabPageVisibility[name]) {
            this.collapseSection(name);
        } else {
            this.expandSection(name);
        }
    }

    /**
     * Collapses the content area of a specific section
     * @param {string} name Id of the section
     */
    collapseSection(name) {
        this.tabPageVisibility[name] = false;
        const section = this.tabContentPages[name];
        section.style.height = "0px";
        section.marker.innerText = "+";
    }

    /**
     * Expands the content area of a specific section
     * @param {string} name Id of the section
     */
    expandSection(name) {
        this.tabPageVisibility[name] = true;
        const section = this.tabContentPages[name];
        section.style.height = `${section.scrollHeight}px`;
        section.marker.innerText = "-";
    }

Harm Kube's avatar
Harm Kube committed
     * Clears the images from all tab content pages and makes the object
     * invisible.
    clearTabContentPages() {
Harm Kube's avatar
Harm Kube committed
        for (const page of Object.values(this.tabContentPages)) {
            page.list.replaceChildren();
Harm Kube's avatar
Harm Kube committed
        }
     * Creates a new list element for the given target node.
     * @param target
Matthias Konitzny's avatar
Matthias Konitzny committed
     * @returns {HTMLLIElement}
    createReference(target) {
        const linkDiv = document.createElement("li");
Matthias Konitzny's avatar
Matthias Konitzny committed
        const linkText = document.createTextNode(target.name);
        linkDiv.className = "neighbor-content-link";
Harm Kube's avatar
Harm Kube committed
        linkDiv.appendChild(linkText);
        linkDiv.addEventListener("click", () => {
            this.graph.focusOnNode(target);
            this.infoOverlay.updateInfoOverlay(target);
        });
        return linkDiv;
    }

    /**
     * Updates all tabs to have content matching the given node.
     * @param node
     */
    updateTabs(node) {
Harm Kube's avatar
Harm Kube committed
        this.clearTabContentPages();
        for (const link of node.links) {
            const target = link.source === node ? link.target : link.source;
            const reference = this.createReference(target);
                this.tabContentPages[link.type].list.appendChild(reference);
                this.tabContentPages[target.type].list.appendChild(reference);
Harm Kube's avatar
Harm Kube committed
        }
        this.updatePageVisibility();
     * Updates the content page visibility on node change.
     * Hides all empty content pages.
    updatePageVisibility() {
        for (const page of Object.values(this.tabContentPages)) {
Matthias Konitzny's avatar
Matthias Konitzny committed
            if (!page.list.hasChildNodes()) {
                this.tabNavHandles[page.type].style.display = "none";
                page.style.display = "none";
            } else {
                this.tabNavHandles[page.type].style.display = "flex";
                page.style.display = "flex";
                if (this.tabPageVisibility[page.type]) {
                    page.style.height = `${page.list.scrollHeight}px`;
Matthias Konitzny's avatar
Matthias Konitzny committed
    /**
     * Toggle the visibility for a category
     * @param {string} type The name of the category that should be toggled
Matthias Konitzny's avatar
Matthias Konitzny committed
     */
    toggleCategory(type) {
        const page = this.tabContentPages[type];
        const handle = this.tabNavHandles[type];

        if (handle.style.display === "flex") {
            page.style.display = "none";
            handle.style.display = "none";
        } else if (page.list.hasChildNodes()) {
            page.style.display = "flex";
            handle.style.display = "flex";