Skip to content

Commit

Permalink
feat: add support for event selectors
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-berger committed Jul 2, 2024
1 parent 9af74e7 commit 702baff
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 45 deletions.
2 changes: 1 addition & 1 deletion packages/client-api/src/init-element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
69 changes: 37 additions & 32 deletions packages/client-api/src/user-config/get-script-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Promise<any>> = {};
const assetPathCache: Record<string, string> = {};

/**
* Map of module paths to asset paths.
* Map of asset paths to promises that resolve to the module.
*/
const modulePathToAssetPath: Record<string, string> = {};
const moduleCache: Record<string, Promise<any>> = {};

/**
* Abstraction over loading and invoking user-defined scripts.
Expand All @@ -28,21 +28,7 @@ export function getScriptManager() {

async function loadScriptForFn(fnPath: string): Promise<any> {
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(
Expand All @@ -51,24 +37,43 @@ async function callFn(
context: ElementContext,
): Promise<any> {
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<any> {
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<string> {
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): {
Expand Down
39 changes: 27 additions & 12 deletions packages/client/src/app/template-element.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ export interface TemplateElementProps {
context: ElementContext;
}

interface ElementEventListener {
eventType: string;
eventCallback: (event: Event) => Promise<any>;
selectorElement: Element;
}

export function TemplateElement(props: TemplateElementProps) {
const config = props.context.parsedConfig;
const logger = createLogger(`#${config.id}`);
Expand All @@ -19,15 +25,13 @@ export function TemplateElement(props: TemplateElementProps) {
const element = createRootElement();

// Currently active event listeners.
let listeners: { type: string; fn: (event: Event) => Promise<any> }[] =
[];
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'));
Expand All @@ -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,
});
}
});
}

Expand Down

0 comments on commit 702baff

Please sign in to comment.