Skip to content

Commit

Permalink
Appear event handling
Browse files Browse the repository at this point in the history
  • Loading branch information
peterszerzo committed Jul 2, 2024
1 parent f74c865 commit fe2b581
Show file tree
Hide file tree
Showing 2 changed files with 204 additions and 47 deletions.
143 changes: 99 additions & 44 deletions packages/journey-manager/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ interface LoadStep {
}

/**
* Click step
* Step with additional query
*/
export interface StepWithQuery {
/**
Expand All @@ -91,6 +91,16 @@ export interface StepWithQuery {
urlCondition?: UrlCondition;
}

/**
* Step with query and found elements
*/
export type StepWithQueryAndElements = StepWithQuery & {
/**
* Elements found
*/
elements?: HTMLElement[];
};

const debounce = (func: () => void, timeout = 300): (() => void) => {
let timer: NodeJS.Timer | null = null;
return () => {
Expand Down Expand Up @@ -126,6 +136,44 @@ const matchesUrlCondition = (urlCondition: UrlCondition): boolean => {
return false;
};

const withElements = async (
steps: StepWithQuery[],
): Promise<StepWithQueryAndElements[]> => {
const targets = await Promise.all(
steps
.filter(
({ urlCondition }) =>
urlCondition == null || matchesUrlCondition(urlCondition),
)
.map(async (step) => {
try {
return {
...step,
elements: await find(step.query),
};
} catch (e) {
return step;
}
}),
);
return targets;
};

const withElementsSync = (
steps: StepWithQuery[],
): StepWithQueryAndElements[] => {
return filterMap(steps, (step) => {
if (step.urlCondition != null && !matchesUrlCondition(step.urlCondition)) {
return null;
}
const elements = getAll(step.query);
if (elements.length === 0) {
return null;
}
return { ...step, elements };
});
};

const localStorageKey = (conversationId: string): string =>
`jb-triggered-steps-${conversationId}`;

Expand Down Expand Up @@ -317,17 +365,18 @@ export const run = async (props: RunProps): Promise<RunOutput> => {
* Handle load steps
*/

const loadSteps: LoadStep[] = Object.entries(triggers).reduce(
(prev: LoadStep[], [stepId, trigger]: [StepId, Trigger]) => {
const loadSteps: LoadStep[] = filterMap(
Object.entries(triggers),
([stepId, trigger]: [StepId, Trigger]) => {
if (trigger.event === "pageLoad") {
return [
...prev,
{ stepId, urlCondition: trigger.urlCondition, once: trigger.once },
];
return {
stepId,
urlCondition: trigger.urlCondition,
once: trigger.once,
};
}
return prev;
return null;
},
[],
);

let previousUrl = window.location.toString();
Expand Down Expand Up @@ -355,52 +404,43 @@ export const run = async (props: RunProps): Promise<RunOutput> => {

handleLoadSteps();

const clickSteps: StepWithQuery[] = Object.entries(triggers).reduce(
(prev: StepWithQuery[], [stepId, trigger]: [StepId, Trigger]) => {
const clickSteps: StepWithQuery[] = filterMap(
Object.entries(triggers),
([stepId, trigger]: [StepId, Trigger]) => {
if (trigger.event === "click" && trigger.query != null) {
const newEntry: StepWithQuery = {
return {
stepId,
query: decode(trigger.query),
urlCondition: trigger.urlCondition,
once: trigger.once,
};
return [...prev, newEntry];
}
return prev;
return null;
},
);

const appearSteps: StepWithQuery[] = filterMap(
Object.entries(triggers),
([stepId, trigger]: [StepId, Trigger]) => {
if (trigger.event === "appear" && trigger.query != null) {
return {
stepId,
query: decode(trigger.query),
urlCondition: trigger.urlCondition,
once: trigger.once,
};
}
return null;
},
[],
);

const handleGlobalClickForAnnotations = async (ev: any): Promise<void> => {
const targets = await Promise.all(
clickSteps
.filter(
({ urlCondition }) =>
urlCondition == null || matchesUrlCondition(urlCondition),
)
.map(async ({ stepId, query }) => {
try {
return {
stepId,
query,
elements: await find(query),
};
} catch (e) {
return { stepId, query };
}
}),
);
const targets = await withElements(clickSteps);
const node = ev.target;
const clickStep:
| (StepWithQuery & {
/**
*
*/
elements?: HTMLElement[];
})
| undefined = targets.find(({ elements }) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
(elements ?? []).some((element: HTMLElement) => element.contains(node)),
const clickStep: StepWithQueryAndElements | undefined = targets.find(
({ elements }) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
(elements ?? []).some((element: HTMLElement) => element.contains(node)),
);
if (clickStep != null) {
sendStep(clickStep.stepId, clickStep.once ?? false);
Expand Down Expand Up @@ -493,7 +533,22 @@ export const run = async (props: RunProps): Promise<RunOutput> => {
* Change detection
*/

const documentObserver = new MutationObserver(() => {
const documentObserver = new MutationObserver((mutations) => {
// If any of the added nodes are inside matches on appear events, trigger those events
const targets = withElementsSync(appearSteps);
mutations.forEach((mutation) => {
targets.forEach(({ stepId, once, elements }) => {
if (
(elements ?? []).some((element) => {
return [...mutation.addedNodes].some((addedNode) => {
return element.contains(addedNode);
});
})
) {
sendStep(stepId, once ?? false);
}
});
});
debouncedSetHighlights();
// If the document changed for any reason (click, popstate event etc.), check if the URL also changed
// If it did, handle page load events
Expand Down
108 changes: 105 additions & 3 deletions packages/website/src/components/Prototyping.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { type FC, useRef, useEffect } from "react";
import { type FC, useRef, useEffect, useState } from "react";

const runJourneyManager = async (): Promise<unknown> => {
const { run } = await import("@nlxai/journey-manager");
Expand All @@ -18,14 +18,60 @@ const runJourneyManager = async (): Promise<unknown> => {
fontFamily: "'Neue Haas Grotesk'",
},
},
triggers: {},
triggers: {
// Page load
"5c539d6c-ea44-455d-a138-0cb8094c09b2": {
event: "pageLoad",
},
// Page load: URL condition
"63e5b924-7e12-47fc-a24c-655119fd32d9": {
event: "pageLoad",
urlCondition: {
operator: "contains",
value: "#tab2",
},
},
// Appear step
"9d668597-207d-4d1d-9136-1f6c7c89b49f": {
event: "appear",
query: {
parent: null,
options: null,
name: "QuerySelector",
target: "#error",
},
},
// Click step
"1cf7eeb6-1906-47a2-ac79-4bf1a1e8849e": {
event: "click",
query: {
parent: null,
name: "QuerySelector",
options: null,
target: "#click",
},
},
// Click step: once
"2e952b0c-0b03-4a99-b5d1-1e3fdd77839b": {
event: "click",
once: true,
query: {
parent: null,
options: null,
name: "QuerySelector",
target: "#click-once",
},
},
},
});
return null;
};

export const Prototyping: FC<unknown> = () => {
const isRun = useRef(false);

const [showError, setShowError] = useState<boolean>(false);

useEffect(() => {
if (isRun.current) {
return;
Expand All @@ -37,5 +83,61 @@ export const Prototyping: FC<unknown> = () => {
});
}, []);

return <p>Hello</p>;
useEffect(() => {
const timeout = setTimeout(() => {
setShowError(true);
}, 3000);
return () => {
clearTimeout(timeout);
};
}, [setShowError]);

const [tab, setTab] = useState<string | null>(null);

useEffect(() => {
const tabFromHash = window.location.hash.replace("#", "");
setTab(tabFromHash === "" ? "tab1" : tabFromHash);
}, []);

return (
<div>
<h1 className="text-xl">Test page</h1>
{showError ? (
<div className="p-2 rounded text-red-600 bg-red-50" id="error">
Error
</div>
) : null}
<div>
<button id="click">Trigger click</button>
<button id="click-once">Trigger click once</button>
</div>
<div className="flex gap-1">
<a
href="#tab1"
onClick={() => {
setTab("tab1");
}}
>
Tab 1
</a>
<a
href="#tab2"
onClick={() => {
setTab("tab2");
}}
>
Tab 2
</a>
<a
href="#tab3"
onClick={() => {
setTab("tab3");
}}
>
Tab 3
</a>
</div>
{tab == null ? null : <p>{tab}</p>}
</div>
);
};

0 comments on commit fe2b581

Please sign in to comment.