Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhancement request: In sequence diagram, top-level actors are "sticky" #4653

Closed
jon-freed opened this issue Jul 20, 2023 · 2 comments · Fixed by #5241
Closed

Enhancement request: In sequence diagram, top-level actors are "sticky" #4653

jon-freed opened this issue Jul 20, 2023 · 2 comments · Fixed by #5241

Comments

@jon-freed
Copy link

jon-freed commented Jul 20, 2023

In sequence diagrams, please make the top-level row of actors be "sticky" so that they remain at the top of the view port when a user scrolls down the diagram.

Tasks:

  1. To simplify the code, the solution should include giving the "g" elements for the top-level row of actors their own class.
  2. Those "g" elements also need to be moved to the bottom of the SVG so they will remain on top during scrolling. (z-level, not vertical placement. However, z-level CSS won't work, which is why the "g" elements have to be moved.)
  3. CSS "position:sticky" does not work for elements in an SVG, so the solution should listen for the scroll event on the closest scrolling element.
  4. The top level row of actors needs some kind of opaque background so users can see them while other elements scroll up behind them.

Here is some hacky code that does 1-4. There may be better ways to do this.

<script>
async function makeMermaidSequenceActorsSticky() {
    document.querySelectorAll('div[data-mermaidgraphtype="sequence"] > svg').forEach((mermaidSeqSvg) => {
        const closestScrollableAncestor = (mermaidSeqSvg) => {
            let currentElement = element.parentElement;
            while (currentElement !== null) {
                if (currentElement.tagName === "BODY") {
                    return window;
                }
                const overflowYStyle = window.getComputedStyle(currentElement).overflowY;
                const canScrollVertically =
                    currentElement.scrollHeight > currentElement.clientHeight &&
                    (overflowYStyle === "scroll" || overflowYStyle === "auto" || overflowYStyle === "visible");
                if (canScrollVertically) {
                    return currentElement;
                }
                currentElement = currentElement.parentElement;
            }
            // If no scrollable ancestor is found, return null
            return null;
        };
        if (!closestScrollableAncestor) {
            return;
        }

        const mermaidSequenceActorGroups = [];
        // Get all the "g" elements within the SVG
        const allGElements = mermaidSeqSvg.querySelectorAll("g");

        // Iterate through the "g" elements and save references to the top-level actors
        for (let i = 0; i < allGElements.length; i++) {
            const gElement = allGElements[i];
            const isActorManClass = gElement.classList.contains("actor-man");
            const hasRootId = gElement.id.startsWith("root-");
            const boundingBox = gElement.getBBox();
            const isAtTheTop = boundingBox.y >= -10 && boundingBox.y <= 10; // this is what determines what is at the top
            if ((isActorManClass || hasRootId) && isAtTheTop) {
                // Add the "g" element to the actor groups array
                const parentSvg = gElement.closest("svg");
                mermaidSequenceActorGroups.push({
                    parentSvg: parentSvg,
                    gElement: gElement,
                    initialTopOffset: gElement.getBoundingClientRect().top - parentSvg.getBoundingClientRect().top,
                });
                gElement.classList.add("mermaidSequenceActorGroups");
                // Move the g element to the bottom of the svg so it will stay on top when sticky
                parentSvg.appendChild(gElement);
            }
        }

        // Iterate through the actor groups and find the extremes for the left, top, right, and bottom
        let actorsLeft;
        let actorsTop;
        let actorsRight;
        let actorsBottom;
        for (let i = 0; i < mermaidSequenceActorGroups.length; i++) {
            const gElement = mermaidSequenceActorGroups[i].gElement;
            const bbox = gElement.getBBox();
            actorsLeft = !actorsLeft ? bbox.x : actorsLeft < bbox.x ? actorsLeft : bbox.x;
            actorsTop = !actorsTop ? bbox.y : actorsTop < bbox.y ? actorsTop : bbox.y;
            actorsRight = !actorsRight ? bbox.width : actorsRight > bbox.x + bbox.width ? actorsRight : bbox.x + bbox.width;
            actorsBottom = !actorsBottom ? bbox.height : actorsBottom > bbox.y + bbox.height ? actorsBottom : bbox.y + bbox.height;
        }
        let actorsWidth = actorsRight - actorsLeft;
        let actorsHeight = actorsBottom - actorsTop;

        // For the first actor g element....
        for (let i = 0; i < 1; i++) {
            // Add an opaque background rectangle to the actor g element
            let gElement = mermaidSequenceActorGroups[i].gElement;
            let parentSvg = mermaidSequenceActorGroups[i].parentSvg;
            const scaleFactor = 1.1; // 1.1 == 10% scale factor (you can adjust this as needed)
            const rectElement = document.createElementNS("http://www.w3.org/2000/svg", "rect");
            const scaledWidth = actorsWidth * scaleFactor;
            const scaledHeight = actorsHeight * scaleFactor;
            const centerX = actorsLeft + actorsWidth / 2;
            const centerY = actorsTop + actorsHeight / 2;
            const newX = centerX - scaledWidth / 2;
            const newY = centerY - scaledHeight / 2;
            rectElement.setAttribute("x", newX.toString());
            rectElement.setAttribute("y", newY.toString());
            rectElement.setAttribute("width", scaledWidth.toString());
            rectElement.setAttribute("height", scaledHeight.toString());
            const backgroundColor = getComputedStyle(parentSvg).backgroundColor;
            // Function to convert rgba string to rgb
            function rgbaToRgb(rgbaString) {
                const parts = rgbaString.match(/[\d.]+/g);
                if (parts && parts.length === 4) {
                    return `rgb(${parts[0]}, ${parts[1]}, ${parts[2]})`;
                }
                return rgbaString;
            }
            // Check if the background color is transparent and update it
            const newBackgroundColor = backgroundColor === "rgba(0, 0, 0, 0)" ? "white" : rgbaToRgb(backgroundColor);
            rectElement.setAttribute("fill", newBackgroundColor);
            gElement.insertBefore(rectElement, gElement.firstChild);
        }

        let containerScrollEvents = 0;
        // When the page is scrolled....
        closestScrollableAncestor.addEventListener("scroll", function (c) {
            containerScrollEvents++;
            setTimeout(endOfScrollingProcessing, 75);
            function endOfScrollingProcessing() {
                // After the scrolling ends...
                containerScrollEvents--;
                if (containerScrollEvents === 0) {
                    for (let i = 0; i < mermaidSequenceActorGroups.length; i++) {
                        const actor = mermaidSequenceActorGroups[i].gElement;
                        const svgElement = mermaidSequenceActorGroups[i].parentSvg;
                        const svgTop = mermaidSequenceActorGroups[i].parentSvg.getBoundingClientRect().top;
                        if (svgTop < 0) {
                            // Get the current top position of the svgElement relative to the scrolling container
                            let svgTopOffset = svgElement.getBoundingClientRect().top;
                            if (closestScrollableAncestor.getBoundingClientRect) {
                                svgTopOffset =
                                    svgElement.getBoundingClientRect().top - closestScrollableAncestor.getBoundingClientRect().top;
                            }
                            const scaleFactor = actor.getCTM()?.d;
                            // Calculate the new top offset for the actor , considering the scaling factor
                            const newTopOffset = mermaidSequenceActorGroups[i].initialTopOffset + (svgTopOffset * -1) / scaleFactor;
                            // Apply the translation to the actor
                            actor.style.translate = "0px " + newTopOffset.toString() + "px";
                        } else {
                            actor.style.translate = "0px 0px";
                        }
                    }
                }
            }
        });
    });
}
</script>
@github-actions github-actions bot added the Status: Triage Needs to be verified, categorized, etc label Jul 20, 2023
@sidharthv96
Copy link
Member

sidharthv96 commented Aug 2, 2023

This is out of scope for mermaid as it only gives back the SVG text.
How the text is integrated on the website is left to the providers.

However, we could add a class into the top boxes, so it's easier to select them with a query.
PRs to add the class for top boxes in sequence renderer are welcome.

@Ronid1
Copy link
Contributor

Ronid1 commented Jan 25, 2024

I would like to work on this. Will add a class to the top boxes and upload it in the next few days.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants