Files
2025-08-02 16:30:27 +02:00

128 lines
3.5 KiB
TypeScript

import { SectionsWidget } from './SectionsWidget';
import { render, createPortal } from 'react-dom';
import { Fragment, memo, useEffect, useState } from 'react';
import { Provider as CacheContextProvider, type Cache } from './cache';
const EVENT_MOUNT = 'wpj:sections-autocomplete:mount';
export type RuntimeProps = {
runtimeTarget: HTMLElement;
onInit?: () => void;
};
export type WidgetProps = {
// Buttons
onAddSection?: (sectionId: number) => void;
onAddWithSubsections?: (topsectionId: number) => void;
// Row click
onSelect?: (idSection: number) => void;
shouldHideAfterSelect?: boolean;
};
export type MountEvent = CustomEvent<{
mountTarget: HTMLElement;
} & WidgetProps>;
let CacheInitialized = false;
export function RuntimeComponent({ options }: { options: RuntimeProps }) {
const { runtimeTarget, onInit } = options;
const [portals, setPortals] = useState<MountEvent['detail'][]>([]);
const [cache, setCache] = useState<Cache>({
topLevelSections: [],
_query: {},
_fetchTopLevelSections: fetchTopLevelSections,
});
function fetchTopLevelSections() {
if (CacheInitialized) {
return;
}
const url = new URL('/admin/autocomplete/sections', `${window.location.protocol}//${window.location.host}`);
fetch(url.href)
.then(r => r.json())
.then(sections => {
setCache(p => ({ ...p, topLevelSections: sections }));
CacheInitialized = true;
})
.catch(alert);
}
useEffect(() => {
function mountEventHandler(ev: Event | MountEvent) {
if (ev.type !== EVENT_MOUNT) {
return;
}
const mountEvent = ev as MountEvent;
setPortals(previous => [...previous, mountEvent.detail]);
}
runtimeTarget.addEventListener(EVENT_MOUNT, mountEventHandler);
if (onInit) {
onInit();
}
return () => {
runtimeTarget.removeEventListener(EVENT_MOUNT, mountEventHandler);
};
}, []);
return (
<CacheContextProvider value={[cache, setCache]}>
<PortalMounter portals={portals} />
</CacheContextProvider>
);
}
const PortalMounter = memo(({ portals }: { portals: MountEvent['detail'][] }) => {
return (
<>
{portals.map((props, index) => (
<Fragment key={index}>
{createPortal(<SectionsWidget {...props} />, props.mountTarget, index.toString())}
</Fragment>
))}
</>
);
});
export function prepareRuntime(runtimeTarget?: HTMLElement) {
if (!runtimeTarget) {
const elem = document.createElement('meta');
document.body.insertAdjacentElement('beforeend', elem);
runtimeTarget = elem;
}
runtimeTarget.dataset.sectionsAutocompleteRuntime = '';
return new Promise<HTMLElement>((resolve) => {
const options = { runtimeTarget, onInit: () => { resolve(runtimeTarget!) } } as RuntimeProps;
render(
<RuntimeComponent options={options} />,
options.runtimeTarget,
);
});
}
export function mountAutocomplete(target: HTMLElement, options: WidgetProps = { shouldHideAfterSelect: true }) {
const event: MountEvent = new CustomEvent(EVENT_MOUNT, {
detail: {
mountTarget: target,
...options,
},
bubbles: true,
});
const runtimeElement = document.querySelector('[data-sections-autocomplete-runtime]');
if (!runtimeElement) {
throw new Error('SectionsAutocomplete runtime not found');
}
runtimeElement.dispatchEvent(event);
}