@funnelsgrove/runtime 0.1.25 → 0.1.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -6,6 +6,8 @@ export declare const BUILDER_PREVIEW_DEFINITION_PATCH = "builder.preview.definit
|
|
|
6
6
|
export declare const BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED = "builder.preview.variableValuesChanged";
|
|
7
7
|
export declare const BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED = "builder.preview.paywallPlansChanged";
|
|
8
8
|
export declare const BUILDER_PREVIEW_THEME_CHANGED = "builder.preview.themeChanged";
|
|
9
|
+
export declare const BUILDER_PREVIEW_EDIT_MODE_CHANGED = "builder.preview.editModeChanged";
|
|
10
|
+
export declare const BUILDER_PREVIEW_TEXT_EDITED = "builder.preview.textEdited";
|
|
9
11
|
export type BuilderPreviewReadyMessage = {
|
|
10
12
|
type: typeof BUILDER_PREVIEW_READY;
|
|
11
13
|
stepId: string;
|
|
@@ -80,3 +82,15 @@ export type BuilderPreviewThemeChangedMessage = {
|
|
|
80
82
|
type: typeof BUILDER_PREVIEW_THEME_CHANGED;
|
|
81
83
|
cssVariables: Record<string, string>;
|
|
82
84
|
};
|
|
85
|
+
export type BuilderPreviewEditModeChangedMessage = {
|
|
86
|
+
type: typeof BUILDER_PREVIEW_EDIT_MODE_CHANGED;
|
|
87
|
+
enabled: boolean;
|
|
88
|
+
};
|
|
89
|
+
export type BuilderPreviewTextEditedMessage = {
|
|
90
|
+
type: typeof BUILDER_PREVIEW_TEXT_EDITED;
|
|
91
|
+
stepId: string;
|
|
92
|
+
tag: string;
|
|
93
|
+
index: number;
|
|
94
|
+
value: string;
|
|
95
|
+
previousValue: string;
|
|
96
|
+
};
|
|
@@ -6,3 +6,5 @@ export const BUILDER_PREVIEW_DEFINITION_PATCH = 'builder.preview.definitionPatch
|
|
|
6
6
|
export const BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED = 'builder.preview.variableValuesChanged';
|
|
7
7
|
export const BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED = 'builder.preview.paywallPlansChanged';
|
|
8
8
|
export const BUILDER_PREVIEW_THEME_CHANGED = 'builder.preview.themeChanged';
|
|
9
|
+
export const BUILDER_PREVIEW_EDIT_MODE_CHANGED = 'builder.preview.editModeChanged';
|
|
10
|
+
export const BUILDER_PREVIEW_TEXT_EDITED = 'builder.preview.textEdited';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getChoiceTargetsFromEdges, isManifestStepId, resolveInitialStepId, resolveNextStepId, } from './route-resolver.js';
|
|
2
|
+
const terminalStepIdsByOrder = ['paywall', 'subscription-started', 'manage-subscription'];
|
|
2
3
|
export const getFunnelStepSequence = (manifest) => manifest.steps.map((step) => step.id);
|
|
3
4
|
export const getFunnelStepIdsByEdgeOrder = (graph) => {
|
|
4
5
|
const stepIds = graph.steps.map((step) => step.id).filter(Boolean);
|
|
@@ -24,6 +25,9 @@ export const getFunnelStepIdsByEdgeOrder = (graph) => {
|
|
|
24
25
|
for (const entryPointStepId of graph.entryPointStepIds || []) {
|
|
25
26
|
visit(entryPointStepId);
|
|
26
27
|
}
|
|
28
|
+
for (const terminalStepId of terminalStepIdsByOrder) {
|
|
29
|
+
visit(terminalStepId);
|
|
30
|
+
}
|
|
27
31
|
return orderedStepIds.length > 0 ? orderedStepIds : stepIds;
|
|
28
32
|
};
|
|
29
33
|
export const getFunnelEntryPoints = (manifest) => (manifest.entryPoints || []).map((entryPoint) => (Object.assign(Object.assign({}, entryPoint), { stepId: entryPoint.stepId })));
|
|
@@ -44,6 +44,9 @@ type PreviewBridgeMessage = {
|
|
|
44
44
|
} | {
|
|
45
45
|
kind: 'themeChanged';
|
|
46
46
|
cssVariables: Record<string, string>;
|
|
47
|
+
} | {
|
|
48
|
+
kind: 'editModeChanged';
|
|
49
|
+
enabled: boolean;
|
|
47
50
|
} | {
|
|
48
51
|
kind: 'goToStep';
|
|
49
52
|
stepId: string;
|
|
@@ -52,6 +55,7 @@ export declare const parsePreviewQuickEditPatch: (value: unknown) => PreviewQuic
|
|
|
52
55
|
export declare const parsePreviewBridgeMessage: (value: unknown) => PreviewBridgeMessage | null;
|
|
53
56
|
export declare const applyPreviewThemeVariables: (cssVariables: Record<string, string>) => void;
|
|
54
57
|
export declare const applyPreviewQuickEditPatch: (patch: PreviewQuickEditPatch) => void;
|
|
58
|
+
export declare const setPreviewInlineEditMode: (enabled: boolean, getActiveStepId: () => string) => void;
|
|
55
59
|
export declare function emitPreviewVariableValues(stepId: FunnelStepId, values: Record<string, string>): void;
|
|
56
60
|
export declare function usePreviewVariableValues(stepId: FunnelStepId, values: Record<string, string>): void;
|
|
57
61
|
export type PreviewBridgeOptions = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { useEffect, useRef } from 'react';
|
|
3
|
-
import { BUILDER_PREVIEW_ACTIVE_STEP_CHANGED, BUILDER_PREVIEW_DEFINITION_PATCH, BUILDER_PREVIEW_GO_TO_STEP, BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_CHANGED, BUILDER_PREVIEW_THEME_CHANGED, BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED, } from '../config/builder-preview.protocol.js';
|
|
3
|
+
import { BUILDER_PREVIEW_ACTIVE_STEP_CHANGED, BUILDER_PREVIEW_DEFINITION_PATCH, BUILDER_PREVIEW_EDIT_MODE_CHANGED, BUILDER_PREVIEW_GO_TO_STEP, BUILDER_PREVIEW_PAYWALL_PLANS_CHANGED, BUILDER_PREVIEW_READY, BUILDER_PREVIEW_RUNTIME_MODE_CHANGED, BUILDER_PREVIEW_TEXT_EDITED, BUILDER_PREVIEW_THEME_CHANGED, BUILDER_PREVIEW_VARIABLE_VALUES_CHANGED, } from '../config/builder-preview.protocol.js';
|
|
4
4
|
import { isPreviewFrameRuntime } from '../services/preview-frame.service.js';
|
|
5
5
|
import { logger } from '../services/logger.js';
|
|
6
6
|
import { applyPreviewDefinitionPatch, applyPreviewPaywallPlansPatch } from './preview-definition-overrides.js';
|
|
@@ -160,6 +160,12 @@ export const parsePreviewBridgeMessage = (value) => {
|
|
|
160
160
|
const cssVariables = parseThemeCssVariables(value.cssVariables);
|
|
161
161
|
return cssVariables ? { kind: 'themeChanged', cssVariables } : null;
|
|
162
162
|
}
|
|
163
|
+
if (messageType === BUILDER_PREVIEW_EDIT_MODE_CHANGED) {
|
|
164
|
+
return {
|
|
165
|
+
kind: 'editModeChanged',
|
|
166
|
+
enabled: value.enabled === true,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
163
169
|
if (messageType === BUILDER_PREVIEW_GO_TO_STEP) {
|
|
164
170
|
const stepId = typeof value.stepId === 'string' ? value.stepId.trim() : '';
|
|
165
171
|
return stepId ? { kind: 'goToStep', stepId } : null;
|
|
@@ -249,6 +255,136 @@ export const applyPreviewQuickEditPatch = (patch) => {
|
|
|
249
255
|
}
|
|
250
256
|
}
|
|
251
257
|
};
|
|
258
|
+
const INLINE_EDIT_STYLE_ID = 'builder-preview-inline-edit-style';
|
|
259
|
+
const INLINE_EDITABLE_SELECTOR = 'h1, h2, h3, h4, h5, h6, p, span, button, a, li, label, blockquote';
|
|
260
|
+
let inlineEditCleanup = null;
|
|
261
|
+
const isInlineEditableElement = (element) => {
|
|
262
|
+
var _a;
|
|
263
|
+
if (!(element instanceof HTMLElement) || !element.matches(INLINE_EDITABLE_SELECTOR)) {
|
|
264
|
+
return false;
|
|
265
|
+
}
|
|
266
|
+
return element.children.length === 0 && Boolean((_a = element.textContent) === null || _a === void 0 ? void 0 : _a.trim());
|
|
267
|
+
};
|
|
268
|
+
const emitPreviewTextEdited = (input) => {
|
|
269
|
+
if (typeof window === 'undefined' || window.parent === window) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const stageRoot = document.querySelector('.step-stage-transition');
|
|
273
|
+
if (!(stageRoot instanceof HTMLElement)) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const tag = input.element.tagName.toLowerCase();
|
|
277
|
+
const index = Array.from(stageRoot.querySelectorAll(tag)).indexOf(input.element) + 1;
|
|
278
|
+
if (index < 1) {
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
window.parent.postMessage({
|
|
282
|
+
type: BUILDER_PREVIEW_TEXT_EDITED,
|
|
283
|
+
stepId: input.stepId,
|
|
284
|
+
tag,
|
|
285
|
+
index,
|
|
286
|
+
value: input.value,
|
|
287
|
+
previousValue: input.previousValue,
|
|
288
|
+
}, '*');
|
|
289
|
+
};
|
|
290
|
+
const enablePreviewInlineEditMode = (getActiveStepId) => {
|
|
291
|
+
const style = document.createElement('style');
|
|
292
|
+
style.id = INLINE_EDIT_STYLE_ID;
|
|
293
|
+
style.textContent = [
|
|
294
|
+
`.step-stage-transition :is(${INLINE_EDITABLE_SELECTOR}):hover { outline: 1.5px dashed rgba(59, 130, 246, 0.85); outline-offset: 2px; cursor: text; }`,
|
|
295
|
+
'.step-stage-transition [contenteditable="true"] { outline: 1.5px solid rgba(59, 130, 246, 0.95); outline-offset: 2px; cursor: text; }',
|
|
296
|
+
].join('\n');
|
|
297
|
+
document.head.appendChild(style);
|
|
298
|
+
let session = null;
|
|
299
|
+
const endSession = (commit) => {
|
|
300
|
+
if (!session) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
const { element, previousValue, cancelled } = session;
|
|
304
|
+
session = null;
|
|
305
|
+
element.removeAttribute('contenteditable');
|
|
306
|
+
const value = element.textContent || '';
|
|
307
|
+
if (!commit || cancelled) {
|
|
308
|
+
element.textContent = previousValue;
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (value !== previousValue) {
|
|
312
|
+
emitPreviewTextEdited({
|
|
313
|
+
stepId: getActiveStepId(),
|
|
314
|
+
element,
|
|
315
|
+
previousValue,
|
|
316
|
+
value,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
const handleClick = (event) => {
|
|
321
|
+
const target = event.target;
|
|
322
|
+
if (!(target instanceof Element)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const stageRoot = document.querySelector('.step-stage-transition');
|
|
326
|
+
if (!(stageRoot instanceof HTMLElement) || !stageRoot.contains(target)) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
// Edit mode is modal: clicks select text to edit instead of driving the
|
|
330
|
+
// funnel (button taps, navigation).
|
|
331
|
+
event.preventDefault();
|
|
332
|
+
event.stopPropagation();
|
|
333
|
+
if ((session === null || session === void 0 ? void 0 : session.element) === target) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
endSession(true);
|
|
337
|
+
if (!isInlineEditableElement(target)) {
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
session = {
|
|
341
|
+
element: target,
|
|
342
|
+
previousValue: target.textContent || '',
|
|
343
|
+
cancelled: false,
|
|
344
|
+
};
|
|
345
|
+
target.setAttribute('contenteditable', 'true');
|
|
346
|
+
target.focus();
|
|
347
|
+
};
|
|
348
|
+
const handleKeyDown = (event) => {
|
|
349
|
+
if (!session) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
if (event.key === 'Enter') {
|
|
353
|
+
event.preventDefault();
|
|
354
|
+
endSession(true);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (event.key === 'Escape') {
|
|
358
|
+
event.preventDefault();
|
|
359
|
+
session.cancelled = true;
|
|
360
|
+
endSession(false);
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
const handleFocusOut = () => {
|
|
364
|
+
endSession(true);
|
|
365
|
+
};
|
|
366
|
+
document.addEventListener('click', handleClick, true);
|
|
367
|
+
document.addEventListener('keydown', handleKeyDown, true);
|
|
368
|
+
document.addEventListener('focusout', handleFocusOut, true);
|
|
369
|
+
return () => {
|
|
370
|
+
var _a;
|
|
371
|
+
endSession(true);
|
|
372
|
+
document.removeEventListener('click', handleClick, true);
|
|
373
|
+
document.removeEventListener('keydown', handleKeyDown, true);
|
|
374
|
+
document.removeEventListener('focusout', handleFocusOut, true);
|
|
375
|
+
(_a = document.getElementById(INLINE_EDIT_STYLE_ID)) === null || _a === void 0 ? void 0 : _a.remove();
|
|
376
|
+
};
|
|
377
|
+
};
|
|
378
|
+
export const setPreviewInlineEditMode = (enabled, getActiveStepId) => {
|
|
379
|
+
if (typeof document === 'undefined') {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
inlineEditCleanup === null || inlineEditCleanup === void 0 ? void 0 : inlineEditCleanup();
|
|
383
|
+
inlineEditCleanup = null;
|
|
384
|
+
if (enabled) {
|
|
385
|
+
inlineEditCleanup = enablePreviewInlineEditMode(getActiveStepId);
|
|
386
|
+
}
|
|
387
|
+
};
|
|
252
388
|
function emitPreviewActiveStepChanged(stepId) {
|
|
253
389
|
if (typeof window === 'undefined' || window.parent === window) {
|
|
254
390
|
return;
|
|
@@ -288,9 +424,16 @@ export function usePreviewVariableValues(stepId, values) {
|
|
|
288
424
|
}
|
|
289
425
|
export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableStepId, setRuntimeMode, shouldLockToInitialStep, }) {
|
|
290
426
|
const previewReadySentRef = useRef(false);
|
|
427
|
+
const activeStepIdRef = useRef(activeStepId);
|
|
291
428
|
useEffect(() => {
|
|
429
|
+
activeStepIdRef.current = activeStepId;
|
|
292
430
|
emitPreviewActiveStepChanged(activeStepId);
|
|
293
431
|
}, [activeStepId]);
|
|
432
|
+
useEffect(() => {
|
|
433
|
+
return () => {
|
|
434
|
+
setPreviewInlineEditMode(false, () => '');
|
|
435
|
+
};
|
|
436
|
+
}, []);
|
|
294
437
|
useEffect(() => {
|
|
295
438
|
if (!isPreviewFrameRuntime() || previewReadySentRef.current) {
|
|
296
439
|
return;
|
|
@@ -323,6 +466,10 @@ export function usePreviewBridge({ activeStepId, onGoToStep, resolveRenderableSt
|
|
|
323
466
|
applyPreviewThemeVariables(action.cssVariables);
|
|
324
467
|
return;
|
|
325
468
|
}
|
|
469
|
+
if (action.kind === 'editModeChanged') {
|
|
470
|
+
setPreviewInlineEditMode(action.enabled, () => activeStepIdRef.current);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
326
473
|
if (action.kind === 'runtimeModeChanged') {
|
|
327
474
|
setRuntimeMode(action.mode);
|
|
328
475
|
return;
|