Home / Blog

Replacing the Next Button Arrow with a Lock Icon when disabled in Articulate Storyline

By Nathan Stoker · September 17, 2024 · 6 min read

Replacing the Next Button Arrow with a Lock Icon when disabled in Articulate Storyline

Understanding the Need

Consider a typical eLearning course with a series of knowledge checks or interactive slides. The "Next" button is a vital navigation tool, leading learners through the content. However, there might be situations where this button needs to be temporarily disabled. Perhaps a learner hasn't completed a required action, or certain conditions haven't been met. While simply graying out the button is functional, it can leave learners wondering why they can't proceed. A more elegant solution involves incorporating a visual cue, such as changing the button's icon, to clearly indicate its disabled state.

A JavaScript Solution

Today, we'll explore a JavaScript snippet that seamlessly integrates into your Articulate Storyline projects, enabling you to dynamically change the icon on a "Next" button based on its enabled/disabled state. This enhancement provides clear feedback to the learner, preventing confusion and frustration.

 (function() {
    try {
        console.log("SVG Mask Script starting execution");

        // Flag to ensure the script runs only once
        let scriptInitialized = false;

        function getIconSvgFromAccText(accText) {
            console.log(`Searching for icon with acc-text: "${accText}"`);
            const container = document.querySelector(`[data-acc-text="${accText}"]`);
            if (!container) {
                console.error(`Element with data-acc-text '${accText}' not found.`);
                return null;
            }
            console.log(`Found container for acc-text: "${accText}"`, container);

            // Try to find SVG content
            const svgElement = container.querySelector('svg');
            if (svgElement) {
                console.log(`Found SVG element for acc-text: "${accText}"`, svgElement);
                console.log('SVG innerHTML:', svgElement.innerHTML);
                
                // Clone the SVG to avoid modifying the original
                const clonedSvg = svgElement.cloneNode(true);
                
                // Ensure the SVG has the correct size
                clonedSvg.setAttribute('width', '18');
                clonedSvg.setAttribute('height', '18');
                
                // Find all <use> elements
                const useElements = clonedSvg.querySelectorAll('use');
                for (const useElement of useElements) {
                    const href = useElement.getAttribute('href');
                    if (href && href.startsWith('#')) {
                        const targetId = href.slice(1);
                        const targetElement = document.getElementById(targetId);
                        if (targetElement) {
                            // Replace the <use> element with the actual content
                            useElement.outerHTML = targetElement.outerHTML;
                        }
                    }
                }
                
                // Convert SVG to a data URL
                const svgString = new XMLSerializer().serializeToString(clonedSvg);
                console.log('Serialized SVG string:', svgString);
                
                const dataUrl = `data:image/svg+xml;base64,${btoa(svgString)}`;
                console.log('Created data URL for SVG:', dataUrl);
                
                return dataUrl;
            }

            console.error(`No suitable SVG found for acc-text: "${accText}"`);
            return null;
        }

        // Function to initialize the button
        function initializeButton(button, lockedIconAccText) {
            console.log(`Initializing button with locked icon acc-text: "${lockedIconAccText}"`);
            const viewContent = button.querySelector(".view-content");
            if (!viewContent) {
                console.error("Could not find .view-content within the button");
                return;
            }
            console.log("Found .view-content", viewContent);

            const textSpan = viewContent.querySelector("[data-ref='label']");
            const iconSpan = viewContent.querySelector(".btn-icon");
            
            if (!textSpan) console.warn("Could not find [data-ref='label'] within .view-content");
            if (!iconSpan) console.warn("Could not find .btn-icon within .view-content");

            console.log("Text span:", textSpan);
            console.log("Icon span:", iconSpan);

            // Apply styles to the button
            button.style.display = "flex";
            button.style.alignItems = "center";
            button.style.justifyContent = "center";
            button.style.gap = "2px";

            // Reset any existing margins or paddings
            viewContent.style.margin = "0";
            viewContent.style.padding = "0";
            if (textSpan) {
                textSpan.style.margin = "0";
                textSpan.style.padding = "0";
            }

            // Style the icon container
            if (iconSpan) {
                iconSpan.style.display = "flex";
                iconSpan.style.alignItems = "center";
                iconSpan.style.justifyContent = "center";
                iconSpan.style.width = "18px";
                iconSpan.style.height = "18px";
                iconSpan.style.flexShrink = "0";
                iconSpan.style.margin = "0";
                iconSpan.style.padding = "0";
            }

            // Function to toggle the lock/arrow icon based on the button state
            function toggleButtonIcon() {
                console.log("Toggling button icon");
                console.log("Button classList:", button.classList);
                if (button.classList.contains("cs-disabled")) {
                    console.log("Button is disabled. Applying custom lock icon.");
                    const lockedSvgDataUrl = getIconSvgFromAccText(lockedIconAccText);
                    if (lockedSvgDataUrl) {
                        console.log("Successfully retrieved locked icon SVG data URL");
                        if (iconSpan) {
                            iconSpan.innerHTML = ''; // Clear existing content
                            iconSpan.style.width = '18px';
                            iconSpan.style.height = '18px';
                            iconSpan.style.backgroundColor = 'rgba(186, 187, 186, 1)'; // Set the color you want
                            iconSpan.style.webkitMask = `url("${lockedSvgDataUrl}") no-repeat center / contain`;
                            iconSpan.style.mask = `url("${lockedSvgDataUrl}") no-repeat center / contain`;
                            console.log("Applied mask to icon span");
                        } else {
                            console.error("Icon span not found, cannot apply mask");
                        }
                    } else {
                        console.error("Failed to retrieve locked icon SVG data URL");
                    }
                } else {
                    console.log("Button is enabled. Restoring original arrow icon.");
                    if (iconSpan) {
                        iconSpan.style.backgroundColor = ''; // Reset background color
                        iconSpan.style.webkitMask = ''; // Reset mask
                        iconSpan.style.mask = ''; // Reset mask
                        iconSpan.innerHTML = `
                            <svg class="cs-icon next-icon" width="10" height="18" viewBox="0 -1 10 18" focusable="false">
                                <path transform="rotate(180, 5, 8)" d="M2.81685219,7.60265083 L9.00528946,1.41421356 L7.5910759,-1.27897692e-13 L1.55431223e-13,7.5910759 L0.0115749356,7.60265083 L1.55431223e-13,7.61422577 L7.5910759,15.2053017 L9.00528946,13.7910881 L2.81685219,7.60265083 Z" stroke="none" fill-rule="evenodd"></path>
                            </svg>`;
                    } else {
                        console.error("Icon span not found, cannot restore original icon");
                    }
                }
            }

            // Initialize the icon for the button
            toggleButtonIcon();

            // Set up a MutationObserver to watch for changes to the classList of the button
            const observer = new MutationObserver(function(mutations) {
                mutations.forEach(function(mutation) {
                    if (mutation.type === "attributes" && mutation.attributeName === "class") {
                        console.log("Button class changed. Checking button state...");
                        toggleButtonIcon();
                    }
                });
            });

            // Start observing the button for class changes
            observer.observe(button, {
                attributes: true,
                attributeFilter: ['class']
            });
        }

        // Main script execution
        function initializeNextLockScript(options = {}) {
            console.log("Initializing Next Lock Script");
            if (scriptInitialized) {
                console.log("Script already initialized. Skipping initialization.");
                return;
            }

            const defaultOptions = {
                lockedIconAccText: "Custom Locked Icon"
            };

            const { lockedIconAccText } = { ...defaultOptions, ...options };

            // Check if Next or Submit button exists
            const nextButton = document.querySelector("#next");
            const submitButton = document.querySelector("#submit");

            console.log("Next button:", nextButton);
            console.log("Submit button:", submitButton);

            // Check if the buttons exist and are visible
            const isButtonVisible = (button) => {
                if (!button) return false;
                const style = window.getComputedStyle(button);
                return style.display !== "none" && style.visibility !== "hidden";
            };

            if (isButtonVisible(nextButton)) {
                console.log("Next button is visible, initializing...");
                initializeButton(nextButton, lockedIconAccText);
            } else {
                console.log("Next button is not visible or doesn't exist");
            }

            if (isButtonVisible(submitButton)) {
                console.log("Submit button is visible, initializing...");
                initializeButton(submitButton, lockedIconAccText);
            } else {
                console.log("Submit button is not visible or doesn't exist");
            }
            
            if (!isButtonVisible(nextButton) && !isButtonVisible(submitButton)) {
                console.log("No visible Next or Submit button found. Exiting script.");
                return;
            }

            // Set the flag to prevent re-initialization
            scriptInitialized = true;
            console.log("Script initialization complete");
        }

        // MutationObserver to monitor DOM changes
        const observer = new MutationObserver(() => {
            const nextButton = document.querySelector("#next");
            const submitButton = document.querySelector("#submit");

            if (nextButton || submitButton) {
                initializeNextLockScript({
                    lockedIconAccText: "Custom Locked Icon"
                });
                observer.disconnect(); // Stop observing once the buttons are found and initialized
            }
        });

        // Start observing the document for any changes in child elements
        observer.observe(document.body, {
            childList: true,
            subtree: true
        });

        console.log("SVG Mask Script finished execution");
    } catch (error) {
        console.error("An error occurred in the SVG Mask Script:", error);
    }
})();

Understanding the code

First Things First: Have We Been Here Before?

We kick things off with a little check. We're basically asking, "Hey, has this code already run once?" This is important because we don't want to accidentally do things twice and mess up our button.

Next Up: Finding Our Target

Now it's time to find the "Next" button we want to work on. We use some fancy JavaScript to locate it on the page.

Style It Up!

Once we've got our button in our sights, we start giving it a makeover. We apply some CSS styles to make it look just right. We're talking about things like its layout, spacing, and making sure everything lines up nicely when we toggle.

The Icon Transformation

Now, let's get to the exciting part – where we add some visual flair to our "Next" button. We've got a special function called toggleNextButtonIcon that's responsible for dynamically swapping out the icon. It works like a clever little switch:

  • Disabled State: When the "Next" button is disabled (meaning it's not clickable), we replace the standard arrow icon with a visual cue that signifies it's currently unavailable. In our code, we've chosen a lock icon to represent this state.

  • Enabled State: As soon as the button becomes enabled again, the original arrow icon magically reappears, inviting the user to click and proceed.

Personalize Your Icon

Want to get creative? You can easily customize the disabled icon to match your course's aesthetic. Here's how:

  1. Explore the Google Material Icon library or make your own: Head over to the Google Material Icon library and browse their extensive collection of icons or save an SVG in Adobe Illustrator.

  2. Choose your replacement: Find or create an icon that visually communicates the "disabled" or "unavailable" state. It could be a "block" symbol, a "pause" icon, or anything that resonates with your design.

  3. Paste it in Storyline off the stage: Right click on the icon, give it an Accessibility Alt Text of "Custom Locked Icon".

Important Note: Remember that the icon space is limited to 18x18 pixels. So, if your chosen icon looks too cramped or unclear at that size, consider opting for a simpler one.

The Mutation Observer: Our On-the-Fly Icon Changer

Now, this part might sound a bit sci-fi, but it's actually pretty cool. We set up something called a "Mutation Observer". Think of it like a super-attentive guard. It constantly watches the "Next" button, waiting for any changes to its status. The moment the button's status changes (like from disabled to enabled), the observer springs into action and calls our icon-changing function. This ensures the icon is always accurate, even if the button's state changes dynamically.

Sadly, all this awesome button magic might not work on mobile devices yet. But, I'm working on it!

Integration and Impact

By seamlessly integrating this code into your Articulate Storyline project, you introduce a subtle yet powerful enhancement that significantly benefits your learners. The dynamic icon change serves as an intuitive visual cue, preventing confusion and frustration when navigating through your course. It reinforces the button's state, aiding learners who rely on screen readers or other assistive technologies. Moreover, the clean and well-structured code promotes maintainability, making it easier to adapt and customize for future projects.

Conclusion

This seemingly minor enhancement can have a profound impact on the overall effectiveness of your eLearning interactions. It exemplifies how JavaScript can be leveraged within Articulate Storyline to craft responsive and user-friendly interfaces. Remember, paying close attention to such details contributes to a more polished and engaging learning experience for your audience.