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 · 9 comments
Open

Mount and unmount Content Script UI with MutationObserver #537

hexpl0it opened this issue Mar 10, 2024 · 9 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: any, selector: any, callbacks: any) {
  let prevAnchor: HTMLElement | undefined;

  const observer = new MutationObserver(() => {
    const el = document.querySelector(selector);
    if (el && !prevAnchor) {
      callbacks.onAdd(el);
    } else if (!el && prevAnchor) {
      callbacks.onRemove();
    }
    prevAnchor = el;
  });
  ctx.onInvalidated(() => observer.disconnect());
  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });
  const initialEl = document.querySelector(selector);
  if (initialEl) {
    callbacks.onAdd(initialEl);
    prevAnchor = initialEl;
  }
}

@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?

@aklinker1
Copy link
Collaborator

aklinker1 commented Sep 24, 2024

Think I'm gonna work on this soon. Here's what I'm thinking the API will look like:

const ui = createXyzUi({
  // ...
  anchor: "#some-anchor",
})

ui.autoMount();

Here's the types I expect:

type StopAutoMount = () => void;

interface UI {
  autoMount(options?: { once?: boolean }): StopAutoMount;
}

@1natsu172
Copy link
Contributor

I've been thinking about this Issue in the back of my mind for a long time. We get a lot of questions about dynamic UI mounts.

I just wonder what and how WXT should support. I have a feeling that autoMount will have a lot of responsibility since the timing and conditions are different for each application.

BTW: Actually, I have a personal library for dynamic mounting, lol.

@aklinker1
Copy link
Collaborator

I have a feeling that autoMount will have a lot of responsibility since the timing and conditions are different for each.

Hmm, I recommended this function to addressing a single use case: mounting a UI inside a dynamic element that gets added and removed.

If someone wants a custom implementation, like listening to focus events, or adding timeouts, they should write those themselves using ui.mount and ui.unmount. that's why we provide those APIs.

BTW: Actually, I have a personal library for dynamic mounting, lol.

Nice! We can probably use it, does it support listening for when the element is removed from the DOM?

@1natsu172
Copy link
Contributor

If someone wants a custom implementation, like listening to focus events, or adding timeouts, they should write those themselves using ui.mount and ui.unmount. that's why we provide those APIs.

If you are set in your mind on that policy, it's ok 👍 . Writing in the document that it is a single use case would avoid confusion users.

does it support listening for when the element is removed from the DOM?

Supported by detector option. However, this library doesn't manage DOM state. This is an async wrapper for pure mutationObserver and querySelector. So if you want accuracy based on DOM state, will need a separate DOM state manager.

Basically, it goes like this. (recently, major bump to v4, so if there any bugs it might not work properly 😅 )

// waiting anchor
const anchor = await waitElement(targetAnchor);
if (anchor) {
  ui.mount();
}
// waiting remove
const nowAnchor = await waitElement(targetAnchor, { detector: isNotExist });
if (nowAnchor === null) {
  ui.unmount()
}

@aklinker1
Copy link
Collaborator

I am onboard with adding functionality to WXT.

@1natsu172
Copy link
Contributor

Yeah, I’m willing to take this. I recently made my library support xpath for this issue :). I'm trying to write the test code first……but recently I've been so busy that I haven't had time. Need more time.

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

4 participants