From 702baff28e32dcf78a104dcc9b5b67789fc22877 Mon Sep 17 00:00:00 2001 From: Lars Berger Date: Tue, 2 Jul 2024 18:38:07 +0800 Subject: [PATCH] feat: add support for event selectors --- packages/client-api/src/init-element.ts | 2 +- .../src/user-config/get-script-manager.ts | 69 ++++++++++--------- .../src/app/template-element.component.ts | 39 +++++++---- 3 files changed, 65 insertions(+), 45 deletions(-) diff --git a/packages/client-api/src/init-element.ts b/packages/client-api/src/init-element.ts index de1221ef..a84bfceb 100644 --- a/packages/client-api/src/init-element.ts +++ b/packages/client-api/src/init-element.ts @@ -92,7 +92,7 @@ export async function initElement( }); }); - // Import the scripts for the element. + // Preload the scripts used for the element's events. runWithOwner(args.owner, () => { createEffect(async () => { try { diff --git a/packages/client-api/src/user-config/get-script-manager.ts b/packages/client-api/src/user-config/get-script-manager.ts index 2fefdfed..8d054363 100644 --- a/packages/client-api/src/user-config/get-script-manager.ts +++ b/packages/client-api/src/user-config/get-script-manager.ts @@ -7,14 +7,14 @@ import type { ElementContext } from '../element-context.model'; const logger = createLogger('script-manager'); /** - * Map of asset paths to promises that resolve to the module. + * Map of module paths to asset paths. */ -const modulesByPath: Record> = {}; +const assetPathCache: Record = {}; /** - * Map of module paths to asset paths. + * Map of asset paths to promises that resolve to the module. */ -const modulePathToAssetPath: Record = {}; +const moduleCache: Record> = {}; /** * Abstraction over loading and invoking user-defined scripts. @@ -28,21 +28,7 @@ export function getScriptManager() { async function loadScriptForFn(fnPath: string): Promise { const { modulePath } = parseFnPath(fnPath); - - const assetPath = - modulePathToAssetPath[modulePath] ?? - (modulePathToAssetPath[modulePath] = convertFileSrc( - await join(await homeDir(), '.glzr/zebar', modulePath), - )); - - logger.info( - `Loading script at path '${assetPath}' for function path '${fnPath}'.`, - ); - - const importPromise = import(assetPath); - modulesByPath[assetPath] = importPromise; - - return importPromise; + return resolveModule(modulePath); } async function callFn( @@ -51,24 +37,43 @@ async function callFn( context: ElementContext, ): Promise { const { modulePath, functionName } = parseFnPath(fnPath); - const assetPath = modulePathToAssetPath[modulePath]; - const foundModule = modulesByPath[assetPath!]; + const foundModule = await resolveModule(modulePath); + const fn = foundModule[functionName]; + + if (!fn) { + throw new Error( + `No function with the name '${functionName}' at function path '${fnPath}'.`, + ); + } + + return fn(event, context); +} - if (!foundModule) { - throw new Error(`No script found at function path '${fnPath}'.`); +async function resolveModule(modulePath: string): Promise { + const assetPath = await getAssetPath(modulePath); + const foundModule = moduleCache[assetPath]; + + if (foundModule) { + return foundModule; } - return foundModule.then(foundModule => { - const fn = foundModule[functionName!]; + logger.info(`Loading script at path '${assetPath}'.`); + return (moduleCache[assetPath] = import(assetPath)); +} - if (!fn) { - throw new Error( - `No function with the name '${functionName}' at function path '${fnPath}'.`, - ); - } +/** + * Converts user-defined path to a URL that can be loaded by the webview. + */ +async function getAssetPath(modulePath: string): Promise { + const foundAssetPath = assetPathCache[modulePath]; + + if (foundAssetPath) { + return foundAssetPath; + } - return fn(event, context); - }); + return (assetPathCache[modulePath] = convertFileSrc( + await join(await homeDir(), '.glzr/zebar', modulePath), + )); } function parseFnPath(fnPath: string): { diff --git a/packages/client/src/app/template-element.component.ts b/packages/client/src/app/template-element.component.ts index ab5eeb9f..f192aeeb 100644 --- a/packages/client/src/app/template-element.component.ts +++ b/packages/client/src/app/template-element.component.ts @@ -10,6 +10,12 @@ export interface TemplateElementProps { context: ElementContext; } +interface ElementEventListener { + eventType: string; + eventCallback: (event: Event) => Promise; + selectorElement: Element; +} + export function TemplateElement(props: TemplateElementProps) { const config = props.context.parsedConfig; const logger = createLogger(`#${config.id}`); @@ -19,15 +25,13 @@ export function TemplateElement(props: TemplateElementProps) { const element = createRootElement(); // Currently active event listeners. - let listeners: { type: string; fn: (event: Event) => Promise }[] = - []; + let listeners: ElementEventListener[] = []; // Update the HTML element when the template changes. createEffect(() => { - clearEventListeners(); // @ts-ignore - TODO element.innerHTML = config.template; - addEventListeners(); + updateEventListeners(); }); onMount(() => logger.debug('Mounted')); @@ -40,21 +44,32 @@ export function TemplateElement(props: TemplateElementProps) { return element; } - function clearEventListeners() { - listeners.forEach(({ type, fn }) => - element.removeEventListener(type, fn), + function updateEventListeners() { + // Remove existing event listeners. + listeners.forEach(({ eventType, eventCallback, selectorElement }) => + selectorElement.removeEventListener(eventType, eventCallback), ); listeners = []; - } - function addEventListeners() { config.events.forEach(eventConfig => { - const callFn = (event: Event) => + const eventCallback = (event: Event) => scriptManager.callFn(eventConfig.fn_path, event, props.context); - element.addEventListener(eventConfig.type, callFn); - listeners.push({ type: eventConfig.type, fn: callFn }); + // Default to the root element if no selector is provided. + const selectorElement = eventConfig.selector + ? element.querySelector(eventConfig.selector) + : element; + + if (selectorElement) { + selectorElement.addEventListener(eventConfig.type, eventCallback); + + listeners.push({ + eventType: eventConfig.type, + eventCallback, + selectorElement, + }); + } }); }