Skip to content

7 - Popups with Floating-UI

Warning

This is just an example of how to put popups using a third party library. We do not give support to Floating-UI or any other third party library.

This example uses pure javascript along with the third party library Floating-UI to create a popup that shows a thumbnail over available nodes.

It's important that your popups that are going to show thumbnails on their content, independently of which library you use to do it, use a timeout to create the popup once the mouse has stopped moving over a node. This will prevent downloading too many thumbnails when moving the mouse over the map, and just download the one that you really are going to use.

Floating-UI has a method to autoUpdate the positioning of the popup when some global events like resizing or scrolling happens, But you would need to use zooming and panning triggers to track also map changes.

If you end using Floating-UI autoUpdate function, you can use the MapViewerNode object as a virtual dom element, but you must set instanced_nodes flag to true to make sure the MapViewerNode object is updated.

Map Viewer

Code

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8" />
    <style>
        .popup {
            background-color: white;
            color: black;
            border: 1px solid black;
            padding: 1px;
            width: max-content;
            position: absolute;
            pointer-events: none;
            text-align: center;
            top: 0;
            left: 0;
        }

        .popup.hidden {
            visibility: hidden;
            pointer-events: none;
        }

        .popup-thumb {
            width: 150px;
            height: 150px;
        }
        .popup img {
            width: 100%;
        }
    </style>
</head>
<body>
<div id="viewer-container"></div>

<script src="https://cdn.jsdelivr.net/npm/@floating-ui/core@1.6.9"></script>
<script src="https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.13"></script>
<script src="https://tk3d.tk3dapi.com/dvm/v1/lib/stable/dvm.js"></script>
<script>
    const input_options = {
        container: "viewer-container",
        styles_by_groups: true,
        instanced_nodes: true
    };

    // ---- LOADING MODULE ----
    DVM.loadModule("map_viewer", input_options)
        .then(function(viewer) {
            window.viewer = viewer;
            start(viewer);
        })
        .catch(function(err) {
            console.error(err);
        });

    function start(viewer) {
        // Just for the example purpose
        viewer.flags.scroll_with_mod_key = true;
        // Floating-UI use this global variable when using its UMD build
        const { computePosition, autoUpdate, autoPlacement } = FloatingUIDOM;

        const venue_id = "eu-gb-00021-football"
        const map_id = "blockmap";

        // Element that will do the popup functionality
        const popup = document.createElement("div");
        popup.id = "popup";
        popup.className = "popup hidden";
        popup.innerHTML = `<div class="popup-title"></div><div class="popup-thumb"></div>`;
        const popup_title = popup.querySelector(".popup-title");
        const popup_thumb = popup.querySelector(".popup-thumb");

        let popup_node = null;
        let popup_cleanup = null;
        let popup_timeout = null;

        viewer.subscribe("enter", (obj) => {
            const node = obj.nodes[0];
            if (node && (node.state === "available" || node.state === "selected")) {
                setupPopup(node);
            }
        });

        viewer.subscribe("leave", (_obj) => {
            removePopup();
        });

        viewer.subscribe("zooming", (_obj) => {
            updatePopup();
        });

        viewer.subscribe("panning", (_obj) => {
            updatePopup();
        });
        viewer.subscribe("load_success", (_obj) => {
            setRandomAvailability()
        })

        // Load the map
        viewer.loadMap({ venue_id, map_id })
            .then((_obj) => {
                // Successfully loaded
                console.log("LOADED!");
            })
            .catch((err) => {
                // Error while loading
                console.error(err);
            });

        function updatePopup() {
            // Tracking changes on the map that may cause the node to go off-screen
            if (popup_node && popup_node.on_screen) {
                showPopup();
                updatePopupPosition(popup_node);
            } else {
                hidePopup();
            }
        }

        function updatePopupPosition(node) {
            computePosition(node, popup, {
                middleware: [autoPlacement()]
            }).then(({ x, y }) => {
                Object.assign(popup.style, {
                    left: `${x}px`,
                    top: `${y}px`
                });
            });
        }

        function setupFloatingUI(node) {
            // Track scrolls, resizes and whatever floating-ui tracks
            popup_cleanup = autoUpdate(node, popup, () => {
                updatePopupPosition(node);
            });
        }

        function clearFloatingUI() {
            if (popup_cleanup) {
                // Stop floating-ui tracking
                popup_cleanup();
            }
            popup_cleanup = null;
        }

        function setupPopup(node) {
            clearPopupTimeout();
            popup_node = node;
            popup_title.innerText = "Node: " + node.id;

            // This setTimeout prevents making too many calls just moving the mouse over the map
            popup_timeout = setTimeout(() => {
                viewer.getThumbnail({venue_id, view_id: node.id }).then((thumb) => {
                    if (node === popup_node) {
                        popup_thumb.innerHTML = "";
                        popup_thumb.appendChild(thumb)
                    }
                });
                appendPopup();
                setupFloatingUI(node);
                showPopup();
            }, 200);
        }

        function removePopup() {
            clearPopupTimeout();
            clearFloatingUI();
            hidePopup();
            popup_node = null;
            popup_title.innerText = "";
            popup_thumb.innerHTML = "";
            if (popup.parentElement) {
                popup.parentElement.removeChild(popup);
            }
        }

        function clearPopupTimeout() {
            if (popup_timeout) {
                clearTimeout(popup_timeout);
                popup_timeout = null;
            }
        }

        function showPopup() {
            popup.classList.remove("hidden");
        }

        function hidePopup() {
            popup.classList.add("hidden");
        }

        function appendPopup() {
            document.body.appendChild(popup);
        }

        // Just for the purpose of having some random availability on the map
        function setRandomAvailability(prob = 0.6) {
            if (viewer && viewer.isLoaded()) {
                viewer.getTypesList().forEach((type) => {
                    const availability = [];
                    viewer.getNodesByType(type).forEach((node) => {
                        if (Math.random() < prob) {
                            availability.push(node.id);
                        }
                    });
                    viewer.setAvailability(type, availability);
                });
            }
        }
    }
</script>
</body>
</html>