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

Mount and unmount Content Script UI with MutationObserver #537

Open
hexpl0it opened this issue Mar 10, 2024 · 3 comments
Open

Mount and unmount Content Script UI with MutationObserver #537

hexpl0it opened this issue Mar 10, 2024 · 3 comments

Comments

@hexpl0it
Copy link

For my extension I need to mount my app immediately after a precise div. However, this div is not immediately available when the page is loaded.

To do this I used MutationObserver:

import ReactDOM from "react-dom/client";
import Toolbar from "./components/Toolbar";
import { useEffect } from "react";

function waitForElm(selector) {
  return new Promise((resolve) => {
    if (document.querySelector(selector)) {
      return resolve(document.querySelector(selector));
    }
    const observer = new MutationObserver((mutations) => {
      if (document.querySelector(selector)) {
        observer.disconnect();
        resolve(document.querySelector(selector));
      }
    });
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  });
}

export default defineContentScript({
  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: "inline",
      onMount: async (container) => {
        const elm = await waitForElm('[class^="ToolbarContainer__StyledHeader"]');
        const rootNode = document.createElement("div");
        elm.insertAdjacentElement("afterend", rootNode);
        const root = ReactDOM.createRoot(rootNode);
        root.render(<Toolbar />);
        return root;
      },
      onRemove: (root) => {
        root.unmount();
      },
    });

    ui.mount()
  },
});

However, this div can be removed following certain actions on the page. I would need to reassemble my component as soon as this div reappears. How can I do this?

@aklinker1
Copy link
Collaborator

aklinker1 commented Mar 10, 2024

You'll want to use the awaited element as the anchor tag, and use the append option to append your UI using insertAdjacentElement.

Something like this would work. I typed this up in github comments, so it probably isn't valid JS, and I haven't tested it. But hopefully it gives you a better idea of what you're looking for.

let anchor;
const ui  = createIntegratedUi(ctx, {
  position: "inline",
  anchor: () => anchor,
  append: (anchor, root) => anchor.insertAdjacentElement("afterend", root),
  onMount: async (container) => {
    const root = ReactDOM.createRoot(container);
    root.render(<Toolbar />);
    return root;
  },
  onRemove: (root) => {
    root.unmount();
  },
});
watchDomChanges(ctx, '[class^="ToolbarContainer__StyledHeader"]', {
  onAdd: (newAnchor) => {
    anchor = newAnchor;
    ui.mount();
  },
  onRemove: () => {
    ui.remove();
  },
});
function watchDomChanges(ctx, selector, callbacks) {
  const addMountListener = () => {
    const observer = new MutationObserver((mutations) => {
      const el = document.querySelector(selector)
      if (el) {
        observer.disconnect();
        unwatchCtx();
        callbacks.onAdd(el);
        addRemoveListener(el);
      }
    });
    const unwatchCtx = ctx.onInvalidated(observer.disconnect);
    observer.observe(document.body, {
      childList: true,
      subtree: true,
    });
  };
  const addRemoveListener = (el) => {
    const observer = new MutationObserver((mutations) => {
      if (mutations[0].removedNodes.includes(el)) {
        observer.disconnect();
        unwatchCtx();
        callbacks.onRemoved();
        addMountListener();
      }
    });
    const unwatchCtx = ctx.onInvalidated(observer.disconnect);
    observer.observe(el);
  };
  
  const initialEl = document.querySelector(selector);
  if (initialEl) {
    callbacks.onAdd(initialEl);
    addRemoveListener(initialEl);
  } else {
    addMountListener();
  }
}

@aklinker1 aklinker1 changed the title Mount and unmount with MutationObserver Mount and unmount Content Script UI with MutationObserver Mar 10, 2024
@aklinker1
Copy link
Collaborator

aklinker1 commented Mar 12, 2024

@hexpl0it If you get something that works, I'd like to add auto-mounting/unmounting to WXT, if you would like to contribute your implementation.

@aklinker1 aklinker1 added this to the v1.1 milestone Mar 12, 2024
@tesths
Copy link

tesths commented May 12, 2024

Thank you very much for this issue. I have recently encountered a similar problem.
I want to inject content script above a specific div on the website after it has loaded.
I have tried the methods, which are effective. Thanks for @aklinker1 .
I wonder if there are other methods that can achieve similar functions? Maybe similar effects can be achieved without using an anchor?

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

No branches or pull requests

3 participants