Skip to content

Commit

Permalink
feat: footer actions
Browse files Browse the repository at this point in the history
  • Loading branch information
VojtechVidra committed Jan 9, 2024
1 parent 33216a4 commit 3604216
Show file tree
Hide file tree
Showing 16 changed files with 369 additions and 132 deletions.
5 changes: 1 addition & 4 deletions examples/react-nextjs/components/LocalFlows.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ export const LocalFlows = () => {
element: "#start-flow-1",
title: "Hey!",
body: "This is a demo of FlowsJS. <br/>Click the button below to continue.",
arrow: true,
placement: "right-start",
},
],
Expand Down Expand Up @@ -73,8 +72,7 @@ export const LocalFlows = () => {
element: ".lorem-ipsum-paragraph",
title: "This is a paragraph",
body: "This paragraph doesn't make sense.",
arrow: true,
options: [],
hideNext: true,
wait: {
element: ".home-nav-link",
},
Expand All @@ -88,7 +86,6 @@ export const LocalFlows = () => {
title: "This is a paragraph",
body: "This paragraph doesn't make sense.",
element: "#start-flow-1",
arrow: true,
},
],
location: "/about",
Expand Down
1 change: 0 additions & 1 deletion examples/react-nextjs/next.config.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
eslint: { ignoreDuringBuilds: true },
reactStrictMode: false,
};

module.exports = nextConfig;
11 changes: 7 additions & 4 deletions examples/vanilla-js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,13 @@ window.FlowsJS?.init({
element: "#start-flow-6",
title: "Hello from Flow 6!",
body: "Lorem ipsum dolor sit amet consectetur adipisicing elit. Dicta iure quae soluta quam, eius ullam placeat nobis harum fugiat excepturi?",
options: [
{ text: "Variant A", action: 0 },
{ text: "Variant B", action: 1 },
],
hideNext: true,
footerActions: {
right: [
{ text: "Variant A", action: 0 },
{ text: "Variant B", action: 1 },
],
},
},
[
[{ element: ".flow-6-A-text", title: "You selected variant A" }],
Expand Down
24 changes: 11 additions & 13 deletions public/flows.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@
line-height: var(--flows-base-line-height);
}

.flows-footer {
display: flex;
justify-content: space-between;
margin-top: 12px;
gap: 8px;
}
.flows-footer div {
display: flex;
gap: 8px;
}

.flows-button {
background-color: var(--flows-background-subtle);
border: var(--flows-border);
Expand Down Expand Up @@ -139,12 +150,6 @@
right: var(--flows-tooltip-padding);
}

.flows-tooltip-footer {
display: flex;
justify-content: flex-end;
margin-top: 12px;
gap: 8px;
}
.flows-back-wrap {
flex: 1;
}
Expand Down Expand Up @@ -195,10 +200,3 @@
top: var(--flows-tooltip-padding);
right: var(--flows-tooltip-padding);
}

.flows-modal-footer {
display: flex;
justify-content: center;
margin-top: 12px;
gap: 8px;
}
52 changes: 52 additions & 0 deletions src/document-change.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { endFlow, startFlow } from "./public-methods";
import { FlowsContext } from "./flows-context";
import { locationMatch } from "./form";

let prevPathname: string | null = null;
export const handleDocumentChange = (): void => {
const pathname = window.location.pathname + window.location.search;
const locationChanged = prevPathname !== pathname;
if (locationChanged) {
FlowsContext.getInstance().onLocationChange?.(pathname, FlowsContext.getInstance());

FlowsContext.getInstance().instances.forEach((state) => {
const step = state.currentStep;
if (!step) return;
if ("element" in step) {
const el = document.querySelector(step.element);
if (!el) endFlow(state.flowId, { variant: "cancel" });
}
if (step.wait) {
if (Array.isArray(step.wait)) {
const matchingWait = step.wait.find((wait) => {
if (wait.location) return locationMatch({ location: wait.location, pathname });
return false;
});
if (matchingWait) state.nextStep(matchingWait.action).render();
} else if (
step.wait.location &&
locationMatch({ location: step.wait.location, pathname })
) {
state.nextStep().render();
}
}
});

Object.values(FlowsContext.getInstance().flowsById ?? {}).forEach((flow) => {
if (!flow.location) return;
if (locationMatch({ location: flow.location, pathname })) startFlow(flow.id);
});
}
prevPathname = pathname;

FlowsContext.getInstance().instances.forEach((state) => {
if (state.waitingForElement) state.render();

const step = state.currentStep;
if (step && "element" in step) {
const el = document.querySelector(step.element);
const targetChanged = state.flowElement?.target && el !== state.flowElement.target;
if (targetChanged) state.render();
}
});
};
6 changes: 4 additions & 2 deletions src/flow-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,11 @@ export class FlowState {
get stepHistory(): FlowStepIndex[] {
try {
const data = JSON.parse(window.localStorage.getItem(this.storageKey) ?? "") as unknown;
if (!Array.isArray(data)) return [];
if (!Array.isArray(data) || !data.length) throw new Error();
return data as FlowStepIndex[];
} catch {
return [];
this.stepHistory = [0];
return [0];
}
}

Expand Down Expand Up @@ -164,6 +165,7 @@ export class FlowState {
*/
unmount(): this {
if (!this.flowElement) return this;
this.waitingForElement = false;
this.flowElement.cleanup?.();
this.flowElement.element.remove();
return this;
Expand Down
11 changes: 9 additions & 2 deletions src/flows-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export class FlowsContext {
tracking?: (event: TrackingEvent) => void;
onSeenFlowIdsChange?: (seenFlowIds: string[]) => void;
rootElement?: string;
onLocationChange?: (pathname: string, context: FlowsContext) => void;

updateFromOptions(options: FlowsInitOptions): void {
if (options.projectId) this.projectId = options.projectId;
Expand All @@ -74,9 +75,8 @@ export class FlowsContext {
this.tracking = options.tracking;
this.seenFlowIds = [...(options.seenFlowIds ?? [])];
this.onSeenFlowIdsChange = options.onSeenFlowIdsChange;
this.onLocationChange = options.onLocationChange;
this.rootElement = options.rootElement;
this.userId = options.userId;
this.userProperties = this.userProperties ?? options.userProperties;
this.flowsById = {
...this.flowsById,
...options.flows?.reduce(
Expand All @@ -87,9 +87,16 @@ export class FlowsContext {
{} as Record<string, Flow>,
),
};
this.updateUser(options.userId, options.userProperties);
this.startInstancesFromLocalStorage();
}

updateUser = (userId?: string, userProperties?: UserProperties): this => {
this.userId = userId ?? this.userId;
this.userProperties = userProperties ?? this.userProperties;
return this;
};

addFlowData(flow: Flow): this {
if (!this.flowsById) this.flowsById = {};
this.flowsById[flow.id] = flow;
Expand Down
23 changes: 17 additions & 6 deletions src/form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,15 @@ export const formWaitMatch = ({
if (!wait.form) return false;
if (!form.matches(wait.form.element)) return false;
const allValuesMatch = wait.form.values.every((valueDef) => {
const input = form.querySelector(valueDef.element);
if (!input || !("value" in input)) return false;
return input.value === valueDef.value;
const el = form.querySelector(valueDef.element);
if (
!el ||
!("value" in el) ||
typeof el.value !== "string" ||
typeof valueDef.value !== "string"
)
return false;
return new RegExp(valueDef.value).test(el.value);
});
return allValuesMatch;
};
Expand All @@ -29,9 +35,14 @@ export const changeWaitMatch = ({
if (!someElementIsTarget) return false;
const allValuesMatch = wait.change.every((changeDef) => {
const el = document.querySelector(changeDef.element);
if (!el || !("value" in el) || typeof el.value !== "string") return false;
if (changeDef.value instanceof RegExp) return changeDef.value.test(el.value);
return el.value === changeDef.value;
if (
!el ||
!("value" in el) ||
typeof el.value !== "string" ||
typeof changeDef.value !== "string"
)
return false;
return new RegExp(changeDef.value).test(el.value);
});
return allValuesMatch;
};
Expand Down
2 changes: 2 additions & 0 deletions src/global.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
type Child = string | HTMLElement | DocumentFragment;
interface Props {
className?: string;
href?: string;
target?: string;
dangerouslySetInnerHTML?: { __html: string };
}

Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,13 @@ import type {
FlowTooltipStep,
FlowModalStep,
FlowWaitStep,
WaitStepOptions,
} from "./types";
import { isValidFlow, isValidFlowsOptions, validateFlow, validateFlowsOptions } from "./validation";
import { init as _init } from "./init";

export * from "./public-methods";
export const init: (options: FlowsOptions) => void = _init;
export const init: (options: FlowsOptions) => Promise<void> = _init;
export { isValidFlow, isValidFlowsOptions, validateFlow, validateFlowsOptions };
export type {
Flow,
Expand All @@ -27,4 +28,5 @@ export type {
FlowTooltipStep,
FlowModalStep,
FlowWaitStep,
WaitStepOptions,
};
69 changes: 15 additions & 54 deletions src/init.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
import { handleDocumentChange } from "./document-change";
import { FlowsContext } from "./flows-context";
import { changeWaitMatch, formWaitMatch, locationMatch } from "./form";
import { changeWaitMatch, formWaitMatch } from "./form";
import { addHandlers } from "./handlers";
import { ready } from "./lib/ready";
import { log } from "./log";
import { endFlow, startFlow } from "./public-methods";
import type { FlowsInitOptions } from "./types";
import { validateFlowsOptions } from "./validation";

let observer: MutationObserver | null = null;

export const init = (options: FlowsInitOptions): void => {
export const init = (options: FlowsInitOptions): Promise<void> =>
new Promise((res) => {
ready(() => {
_init(options);
res();
});
});

const _init = (options: FlowsInitOptions): void => {
const validationResult = validateFlowsOptions(options);
if (validationResult.error)
log.error(
Expand Down Expand Up @@ -39,7 +49,7 @@ export const init = (options: FlowsInitOptions): void => {

FlowsContext.getInstance().instances.forEach((state) => {
const step = state.currentStep;
if (!step || !("wait" in step)) return;
if (!step?.wait) return;
if (Array.isArray(step.wait)) {
const matchingWait = step.wait.find((wait) => {
if (wait.element) return eventTarget.matches(wait.element);
Expand Down Expand Up @@ -95,7 +105,7 @@ export const init = (options: FlowsInitOptions): void => {

FlowsContext.getInstance().instances.forEach((state) => {
const step = state.currentStep;
if (!step || !("wait" in step)) return;
if (!step?.wait) return;
if (Array.isArray(step.wait)) {
const matchingWait = step.wait.find((wait) => formWaitMatch({ form: eventTarget, wait }));
if (matchingWait) state.nextStep(matchingWait.action).render();
Expand All @@ -110,7 +120,7 @@ export const init = (options: FlowsInitOptions): void => {

FlowsContext.getInstance().instances.forEach((state) => {
const step = state.currentStep;
if (!step || !("wait" in step)) return;
if (!step?.wait) return;
if (Array.isArray(step.wait)) {
const matchingWait = step.wait.find((wait) =>
changeWaitMatch({ target: eventTarget, wait }),
Expand All @@ -134,55 +144,6 @@ export const init = (options: FlowsInitOptions): void => {
}
};

let prevPathname: string | null = null;
const handleDocumentChange = (): void => {
const pathname = window.location.pathname + window.location.search;
const locationChanged = prevPathname !== pathname;
if (locationChanged) {
options.onLocationChange?.(pathname, FlowsContext.getInstance());

FlowsContext.getInstance().instances.forEach((state) => {
const step = state.currentStep;
if (!step) return;
if ("element" in step) {
const el = document.querySelector(step.element);
if (!el) endFlow(state.flowId, { variant: "cancel" });
}
if ("wait" in step) {
if (Array.isArray(step.wait)) {
const matchingWait = step.wait.find((wait) => {
if (wait.location) return locationMatch({ location: wait.location, pathname });
return false;
});
if (matchingWait) state.nextStep(matchingWait.action).render();
} else if (
step.wait.location &&
locationMatch({ location: step.wait.location, pathname })
) {
state.nextStep().render();
}
}
});

Object.values(context.flowsById ?? {}).forEach((flow) => {
if (!flow.location) return;
if (locationMatch({ location: flow.location, pathname })) startFlow(flow.id);
});
}
prevPathname = pathname;

FlowsContext.getInstance().instances.forEach((state) => {
if (state.waitingForElement) state.render();

const step = state.currentStep;
if (step && "element" in step) {
const el = document.querySelector(step.element);
const targetChanged = state.flowElement?.target && el !== state.flowElement.target;
if (targetChanged) state.render();
}
});
};

observer?.disconnect();
observer = new MutationObserver(handleDocumentChange);
observer.observe(document, {
Expand Down
8 changes: 8 additions & 0 deletions src/lib/ready.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- any is correct here
export function ready(fn: (...args: any[]) => any): void {
if (document.readyState !== "loading") {
fn();
return;
}
document.addEventListener("DOMContentLoaded", fn);
}
Loading

0 comments on commit 3604216

Please sign in to comment.