postiz/apps/extension/src/pages/content/main.content.tsx

166 lines
5.1 KiB
TypeScript

import {
FC,
Fragment,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react';
import { ProviderList } from '@gitroom/extension/providers/provider.list';
import { createPortal } from 'react-dom';
import { ActionComponent } from '@gitroom/extension/pages/content/elements/action.component';
// Define a type to track elements with their action types
interface ActionElement {
element: HTMLElement;
actionType: string;
}
export const MainContent: FC = () => {
return <MainContentInner />;
};
export const MainContentInner: FC = (props) => {
const [actionElements, setActionElements] = useState<ActionElement[]>([]);
const actionSetRef = useRef(new Map<HTMLElement, string>());
const provider = useMemo(() => {
return ProviderList.find((p) => {
return p.baseUrl.indexOf(new URL(window.location.href).hostname) > -1;
});
}, []);
useEffect(() => {
if (!provider) return;
// Helper to scan DOM for existing matching elements
const scanDOMForExistingMatches = () => {
const action = { selector: provider.element, type: 'post' };
const matches = document.querySelectorAll(action.selector);
matches.forEach((match) => {
const htmlMatch = match as HTMLElement;
if (!actionSetRef.current.has(htmlMatch)) {
actionSetRef.current.set(htmlMatch, action.type);
}
});
// Update state
const elements: ActionElement[] = [];
actionSetRef.current.forEach((actionType, element) => {
elements.push({ element, actionType });
});
setActionElements(elements);
};
// Initial scan before observing
scanDOMForExistingMatches();
const observer = new MutationObserver((mutationsList) => {
let addedSomething = false;
let removedSomething = false;
for (const mutation of mutationsList) {
if (mutation.type === 'childList') {
for (const node of mutation.addedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as HTMLElement;
const action = { selector: provider.element, type: 'post' };
if (
el.matches?.(action.selector) &&
!actionSetRef.current.has(el)
) {
actionSetRef.current.set(el, action.type);
addedSomething = true;
}
if (el.querySelectorAll) {
const matches = el.querySelectorAll(action.selector);
matches.forEach((match) => {
const htmlMatch = match as HTMLElement;
if (!actionSetRef.current.has(htmlMatch)) {
actionSetRef.current.set(htmlMatch, action.type);
addedSomething = true;
}
});
}
}
}
for (const node of mutation.removedNodes) {
if (node.nodeType === Node.ELEMENT_NODE) {
const el = node as HTMLElement;
if (actionSetRef.current.has(el)) {
actionSetRef.current.delete(el);
removedSomething = true;
}
const action = { selector: provider.element, type: 'post' };
if (el.querySelectorAll) {
const matches = el.querySelectorAll(action.selector);
matches.forEach((match) => {
const htmlMatch = match as HTMLElement;
if (actionSetRef.current.has(htmlMatch)) {
actionSetRef.current.delete(htmlMatch);
removedSomething = true;
}
});
}
}
}
}
if (mutation.type === 'attributes') {
const el = mutation.target;
if (el instanceof HTMLElement) {
const action = { selector: provider.element, type: 'post' };
const matchesNow = el.matches(action.selector);
const wasTracked = actionSetRef.current.has(el);
if (matchesNow && !wasTracked) {
actionSetRef.current.set(el, action.type);
addedSomething = true;
} else if (!matchesNow && wasTracked) {
actionSetRef.current.delete(el);
removedSomething = true;
}
}
}
}
if (addedSomething || removedSomething) {
const elements: ActionElement[] = [];
actionSetRef.current.forEach((actionType, element) => {
elements.push({ element, actionType });
});
setActionElements(elements);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeOldValue: true,
});
return () => observer.disconnect();
}, []);
return actionElements.map((actionEl, index) => (
<Fragment key={index}>
{createPortal(
<ActionComponent
target={actionEl.element}
keyIndex={index}
actionType={actionEl.actionType}
provider={provider}
wrap={true}
/>,
actionEl.element
)}
</Fragment>
));
};