type ShadowRootUiOptions = {
name: string;
position?: string;
alignment?: string;
zIndex?: number;
mode?: ShadowRootMode;
inheritStyles?: boolean;
isolateEvents?: string[];
css?: string;
viewport?: boolean;
scopeRoot?: boolean;
onMount(container: HTMLElement): T;
onRemove?(mounted?: T): void;
};
export interface ShadowRootContentScriptUi {
shadowHost: HTMLElement;
shadow: ShadowRoot;
uiContainer: HTMLElement;
mounted?: T;
mount(): void;
remove(): void;
}
function installStyles(shadow: ShadowRoot, localCss = '', scopeRoot = false): void {
// CSS CSS ShadowRoot
const css = [globalThis.__fluentReadUserscriptCss || '', localCss].filter(Boolean).join('\n');
if (!css) return;
const style = document.createElement('style');
style.setAttribute('data-fluent-read-userscript-styles', 'true');
// Options :root / :root.dark ShadowRoot
// host document
style.textContent = scopeRoot
? css.replace(/:root((?:(?:\.[\w-]+)|(?:\[[^\]]+\]))*)/gu, (_match, suffix: string) => (
suffix ? `:host(${suffix})` : ':host'
))
: css;
shadow.appendChild(style);
}
/** userscript runtime WXT Shadow UI */
export async function createShadowRootUi(
_ctx: unknown,
options: ShadowRootUiOptions,
): Promise {
const shadowHost = document.createElement('div');
shadowHost.setAttribute('data-fluent-read-userscript-host', options.name);
// ShadowRoot
shadowHost.style.cssText = [
'all: initial !important',
'display: block !important',
'position: fixed !important',
'left: 0 !important',
'top: 0 !important',
options.viewport ? 'width: 100vw !important' : 'width: 0 !important',
options.viewport ? 'height: 100vh !important' : 'height: 0 !important',
options.viewport ? 'overflow: hidden !important' : 'overflow: visible !important',
'pointer-events: auto !important',
`z-index: ${options.zIndex ?? 2_147_483_647} !important`,
].join(';');
const shadow = shadowHost.attachShadow({mode: options.mode || 'open'});
installStyles(shadow, options.css, options.scopeRoot);
const container = document.createElement('div');
container.setAttribute('data-fluent-read-userscript-container', options.name);
container.style.cssText = options.viewport
? 'all: initial; width: 100%; height: 100%; overflow: hidden; pointer-events: auto;'
: 'all: initial; width: 0; height: 0; overflow: visible; pointer-events: auto;';
shadow.appendChild(container);
// ShadowRoot
// Element Plus select Shadow DOM
//
for (const eventName of options.isolateEvents || []) {
shadow.addEventListener(eventName, (event) => event.stopPropagation());
}
let isMounted = false;
const ui: ShadowRootContentScriptUi = {
shadowHost,
shadow,
uiContainer: container,
mount() {
if (isMounted) return;
isMounted = true;
(document.documentElement || document.body).appendChild(shadowHost);
ui.mounted = options.onMount(container);
},
remove() {
if (!isMounted) return;
isMounted = false;
options.onRemove?.(ui.mounted);
ui.mounted = undefined;
shadowHost.remove();
},
};
return ui;
}