Change single instance of icon

Bring this old post back: Changing a single instance of an icon

I need to change single instances of icons throughout the site. I do not want to use replaceIcon() and override all instances of that icon. I see in the past they made this possible for specific areas of the site, or that is how it reads, but not throughout the site.

For instance, I want to change ONE of the cog icons in search to better reflect what happens when the user clicks it. I do not want to change all cog icons.

Thanks!

1 Like

Have a look at this PR, you might be able to do something similar with just CSS:

3 Likes

This seems hacky, how does it do when you tab through/keyboard only?

2 Likes

when one is in the desert and there is apparently no water, hacks can come in very handy!

2 Likes

Alas, we are not in the desert… we are paying to use a platform to host a public site.

2 Likes

Which icon is this specifically (where does it appear)? if it seems like there’s a reasonable case others could find useful, we can create an alias for it to allow it to be replaced separately.

2 Likes

Here’s one example where the cog doesn’t reflect the button’s action of opening the expanded search page:

1 Like

We use a “sliders” icon here by default (and it’s the only occurrence of this icon in Discourse by default) — so it would be safe to use replaceIcon() in this case

image

Because we don’t have an API to replace individual icons, the best way we can support this today is by evaluating requests to add new aliases that group similar use cases.

So for example, we use d-liked as an alias for heart so replaceIcon() can be used against d-liked to change it in the like context, rather than replacing every occurrence of the heart icon throughout the app.

It would be nice to replace any single occurrence though, that’s not an unusual situation in themes — hopefully someday we’ll have an API for that.

awesome, it looks like the discourse team changed ours to the sertting cog so I have it fixed up! Thank you :pray:

2 Likes

Okay I seem to understand now what’s the verdict on this. Sad to know it’s not available until an API update so I’ll have to use the method I’ve done for the time being.

@awesomerobot Here’s an updated code of what I’ve done to change the create topic icon which is a + without global change to all +

<script type="text/discourse-plugin" version="0.8">
    api.onPageChange(() => {
        const button = document.querySelector("#create-topic");

        if (button) {
            // Temporarily hide the button while we replace the icon
            button.style.display = "none";

            // Look for the SVG inside the button
            const oldIcon = button.querySelector("svg");
            if (oldIcon) {
                // Remove the existing icon (SVG)
                oldIcon.remove();
            }

            // Create a new SVG for the Font Awesome icon (fa-feather-pointed)
            const newIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
            newIcon.setAttribute("class", "fa d-icon d-icon-feather-pointed svg-icon svg-string");
            newIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg");
            newIcon.innerHTML = '<use href="#feather-pointed"></use>';

            // Insert the new icon inside the button
            button.insertBefore(newIcon, button.firstChild);

            // Show the button again after icon replacement
            button.style.display = "block";
        }
    });
</script>

Have to refine it a bit more for multiple icon changes

<script type="text/discourse-plugin" version="0.8">
    api.onPageChange(() => {
        // List of button selectors and corresponding custom SVG content
        const iconsToReplace = [
            { selector: "#create-topic", newSVG: `
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
                    <path d="M12 2L15 5H13V9H11V5H9L12 2ZM3 12L5 10V13H9V15H5V18L3 16V12ZM21 12L19 10V13H15V15H19V18L21 16V12Z"/>
                </svg>
            ` },
            { selector: "#some-other-button", newSVG: `
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
                    <path d="M21 3H3C2.44772 3 2 3.44772 2 4V20C2 20.5523 2.44772 21 3 21H21C21.5523 21 22 20.5523 22 20V4C22 3.44772 21.5523 3 21 3ZM20 19H4V5H20V19Z"/>
                </svg>
            ` },
            { selector: "#another-button", newSVG: `
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
                    <path d="M12 2L8 6H10V10H14V6H16L12 2ZM12 12L8 16H10V20H14V16H16L12 12Z"/>
                </svg>
            ` }
        ];

        iconsToReplace.forEach(icon => {
            const button = document.querySelector(icon.selector);

            if (button) {
                // Temporarily hide the button while we replace the icon
                button.style.display = "none";

                // Look for the existing SVG inside the button
                const oldIcon = button.querySelector("svg");
                if (oldIcon) {
                    // Remove the existing icon (SVG)
                    oldIcon.remove();
                }

                // Create a new SVG element and set its content
                const newIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
                newIcon.innerHTML = icon.newSVG; // Set the custom SVG content

                // Insert the new SVG icon inside the button
                button.insertBefore(newIcon, button.firstChild);

                // Show the button again after icon replacement
                button.style.display = "block";
            }
        });
    });
</script>

You can also use iconHTML instead of manually added the icon.
Something like this should work:

import { apiInitializer } from "discourse/lib/api";
import { iconHTML } from "discourse/lib/icon-library";

export default apiInitializer((api) => {
  api.onPageChange(() => {
    const btnIcon = document.querySelector("#create-topic .d-icon");
    if (btnIcon) {
      btnIcon.outerHTML = iconHTML("plus");
    }
  });
});
1 Like

This could end up a little flaky.

I believe api.onPageChange is firing on the route change.

That’s well before rendering.

Therefore there’s a risk the elements won’t be rendered before you attempt to find and override them.

You may be lucky but it’s not guaranteed. Hence awesomerobot raising that it would be nice if there was a dedicated api …

(@Don your solution has a similar issue I believe)

2 Likes

Yeah, you’re right! This is not too stable. Maybe it can be more stable with requestAnimationFrame or MutationObserver? :thinking: but yeah the best would be a dedicated api like you mentioned.

2 Likes

Thanks for pointing that out — you’re absolutely right. Using api.onPageChange alone can be unreliable since it fires on route change, not after DOM rendering. That means there’s a race condition risk where the #create-topic button might not exist yet when the script tries to access it.

 api.onPageChange(() => {
        const targetSelector = "#create-topic";

        const tryEnhanceButton = () => {
            const button = document.querySelector(targetSelector);

            if (button) {
                // Disconnect observer after finding the element
                observer.disconnect();

                // Temporarily hide the button while we replace the icon
                button.style.display = "none";

                // Remove old icon if exists
                const oldIcon = button.querySelector("svg");
                if (oldIcon) oldIcon.remove();

                // Create and insert new Font Awesome SVG icon
                const newIcon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
                newIcon.setAttribute("class", "fa d-icon d-icon-feather-pointed svg-icon svg-string");
                newIcon.setAttribute("xmlns", "http://www.w3.org/2000/svg");
                newIcon.innerHTML = '<use href="#feather-pointed"></use>';

                button.insertBefore(newIcon, button.firstChild);

                // Show the button again
                button.style.display = "block";
            }
        };

        // Observe changes to the DOM and try to enhance the button when it's added
        const observer = new MutationObserver(() => tryEnhanceButton());
        observer.observe(document.body, { childList: true, subtree: true });

        // Try immediately as well in case it's already there
        tryEnhanceButton();
    });

@Don I’ve included MutationObserver to ensure the script only runs once the target element actually exists, it disconnects itself after doing its job to avoid unnecessary overhead. It still attempts an immediate check in case the element already exists.

I’m unable to fetch the svg I want from the Discourse icon system, although i registered it admin SVG icon subset as it seems it’s in fontawesome but not in discourse.

1 Like