@builder.io/sdk-svelte 0.0.4 → 0.0.6
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.
- package/package/--package.json +3 -2
- package/package/components/render-content/render-content.svelte +13 -7
- package/package/functions/track.d.ts +35 -5
- package/package/functions/track.js +5 -4
- package/package/index.d.ts +1 -0
- package/package/index.js +1 -0
- package/package.json +3 -2
- package/src/blocks/BaseText.svelte +27 -0
- package/src/blocks/button/button.svelte +36 -0
- package/src/blocks/button/component-info.ts +42 -0
- package/src/blocks/columns/columns.svelte +148 -0
- package/src/blocks/columns/component-info.ts +254 -0
- package/src/blocks/custom-code/component-info.ts +31 -0
- package/src/blocks/custom-code/custom-code.svelte +66 -0
- package/src/blocks/embed/component-info.ts +53 -0
- package/src/blocks/embed/embed.svelte +51 -0
- package/src/blocks/embed/helpers.ts +8 -0
- package/src/blocks/form/component-info.ts +282 -0
- package/src/blocks/form/form.svelte +332 -0
- package/src/blocks/fragment/component-info.ts +10 -0
- package/src/blocks/fragment/fragment.svelte +12 -0
- package/src/blocks/image/component-info.ts +170 -0
- package/src/blocks/image/image.helpers.ts +75 -0
- package/src/blocks/image/image.svelte +145 -0
- package/src/blocks/img/component-info.ts +21 -0
- package/src/blocks/img/img.svelte +51 -0
- package/src/blocks/input/component-info.ts +78 -0
- package/src/blocks/input/input.svelte +34 -0
- package/src/blocks/raw-text/component-info.ts +15 -0
- package/src/blocks/raw-text/raw-text.svelte +16 -0
- package/src/blocks/section/component-info.ts +50 -0
- package/src/blocks/section/section.svelte +34 -0
- package/src/blocks/select/component-info.ts +61 -0
- package/src/blocks/select/select.svelte +34 -0
- package/src/blocks/submit-button/component-info.ts +32 -0
- package/src/blocks/submit-button/submit-button.svelte +15 -0
- package/src/blocks/symbol/component-info.ts +43 -0
- package/src/blocks/symbol/symbol.svelte +100 -0
- package/src/blocks/text/component-info.ts +24 -0
- package/src/blocks/text/text.svelte +11 -0
- package/src/blocks/textarea/component-info.ts +48 -0
- package/src/blocks/textarea/textarea.svelte +19 -0
- package/src/blocks/util.ts +6 -0
- package/src/blocks/video/component-info.ts +109 -0
- package/src/blocks/video/video.svelte +102 -0
- package/src/components/render-block/block-styles.svelte +62 -0
- package/src/components/render-block/render-block.helpers.ts +27 -0
- package/src/components/render-block/render-block.svelte +228 -0
- package/src/components/render-block/render-component-with-context.svelte +38 -0
- package/src/components/render-block/render-component.svelte +35 -0
- package/src/components/render-block/render-repeated-block.svelte +53 -0
- package/src/components/render-block/types.ts +7 -0
- package/src/components/render-blocks.svelte +104 -0
- package/src/components/render-content/components/render-styles.svelte +80 -0
- package/src/components/render-content/index.ts +1 -0
- package/src/components/render-content/render-content.svelte +360 -0
- package/src/components/render-inlined-styles.svelte +26 -0
- package/src/constants/builder-registered-components.ts +42 -0
- package/src/constants/device-sizes.ts +28 -0
- package/src/constants/target.ts +2 -0
- package/src/context/builder.context.ts +13 -0
- package/src/context/types.ts +23 -0
- package/src/functions/camel-to-kebab-case.ts +2 -0
- package/src/functions/evaluate.ts +52 -0
- package/src/functions/event-handler-name.ts +6 -0
- package/src/functions/extract-text-styles.ts +39 -0
- package/src/functions/fast-clone.ts +5 -0
- package/src/functions/get-block-actions-handler.ts +19 -0
- package/src/functions/get-block-actions.ts +25 -0
- package/src/functions/get-block-component-options.ts +12 -0
- package/src/functions/get-block-properties.ts +11 -0
- package/src/functions/get-block-tag.ts +7 -0
- package/src/functions/get-builder-search-params/fn.test.ts +19 -0
- package/src/functions/get-builder-search-params/index.ts +48 -0
- package/src/functions/get-content/ab-testing.ts +132 -0
- package/src/functions/get-content/fn.test.ts +35 -0
- package/src/functions/get-content/index.ts +77 -0
- package/src/functions/get-content/types.ts +36 -0
- package/src/functions/get-fetch.ts +14 -0
- package/src/functions/get-global-this.ts +15 -0
- package/src/functions/get-processed-block.test.ts +33 -0
- package/src/functions/get-processed-block.ts +54 -0
- package/src/functions/get-react-native-block-styles.ts +30 -0
- package/src/functions/if-target.ts +20 -0
- package/src/functions/is-browser.ts +3 -0
- package/src/functions/is-editing.ts +7 -0
- package/src/functions/is-iframe.ts +5 -0
- package/src/functions/is-previewing.ts +14 -0
- package/src/functions/mark-mutable.ts +7 -0
- package/src/functions/on-change.test.ts +23 -0
- package/src/functions/on-change.ts +29 -0
- package/src/functions/register-component.ts +67 -0
- package/src/functions/register.ts +46 -0
- package/src/functions/sanitize-react-native-block-styles.ts +65 -0
- package/src/functions/set-editor-settings.ts +16 -0
- package/src/functions/set.test.ts +19 -0
- package/src/functions/set.ts +26 -0
- package/src/functions/track.ts +124 -0
- package/src/functions/transform-block.ts +6 -0
- package/src/helpers/ab-tests.ts +22 -0
- package/src/helpers/cookie.ts +102 -0
- package/src/helpers/css.ts +33 -0
- package/src/helpers/flatten.ts +24 -0
- package/src/helpers/localStorage.ts +38 -0
- package/src/helpers/nullable.ts +4 -0
- package/src/helpers/sessionId.ts +34 -0
- package/src/helpers/time.ts +4 -0
- package/src/helpers/url.test.ts +16 -0
- package/src/helpers/url.ts +12 -0
- package/src/helpers/uuid.ts +17 -0
- package/src/helpers/visitorId.ts +38 -0
- package/src/index-helpers/blocks-exports.ts +10 -0
- package/src/index-helpers/top-of-file.ts +4 -0
- package/src/index.ts +13 -0
- package/src/scripts/init-editing.ts +102 -0
- package/src/types/builder-block.ts +58 -0
- package/src/types/builder-content.ts +39 -0
- package/src/types/can-track.ts +3 -0
- package/src/types/components.ts +188 -0
- package/src/types/deep-partial.ts +7 -0
- package/src/types/element.ts +60 -0
- package/src/types/targets.ts +6 -0
- package/src/types/typescript.ts +7 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { RegisteredComponent } from '../context/types.js';
|
|
2
|
+
import type { ComponentInfo, Input } from '../types/components.js';
|
|
3
|
+
import { fastClone } from './fast-clone.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @deprecated. Use the `customComponents` prop in RenderContent instead to provide your custom components to the builder SDK.
|
|
7
|
+
*/
|
|
8
|
+
export const components: RegisteredComponent[] = [];
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @deprecated. Use the `customComponents` prop in RenderContent instead to provide your custom components to the builder SDK.
|
|
12
|
+
*/
|
|
13
|
+
export function registerComponent(component: any, info: ComponentInfo): void {
|
|
14
|
+
components.push({ component, ...info });
|
|
15
|
+
|
|
16
|
+
console.warn(
|
|
17
|
+
'registerComponent is deprecated. Use the `customComponents` prop in RenderContent instead to provide your custom components to the builder SDK.'
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
return component;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const createRegisterComponentMessage = ({
|
|
24
|
+
component: _,
|
|
25
|
+
...info
|
|
26
|
+
}: RegisteredComponent) => ({
|
|
27
|
+
type: 'builder.registerComponent',
|
|
28
|
+
data: prepareComponentInfoToSend(info),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const serializeValue = (value: object): any =>
|
|
32
|
+
typeof value === 'function' ? serializeFn(value) : fastClone(value);
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Input attributes that are functions must be converted to strings before being serialized to JSON.
|
|
36
|
+
*/
|
|
37
|
+
// eslint-disable-next-line @typescript-eslint/ban-types
|
|
38
|
+
const serializeFn = (fnValue: Function) => {
|
|
39
|
+
const fnStr = fnValue.toString().trim();
|
|
40
|
+
// we need to account for a few different fn syntaxes:
|
|
41
|
+
// 1. `function name(args) => {code}`
|
|
42
|
+
// 2. `name(args) => {code}`
|
|
43
|
+
// 3. `(args) => {}`
|
|
44
|
+
const appendFunction =
|
|
45
|
+
!fnStr.startsWith('function') && !fnStr.startsWith('(');
|
|
46
|
+
|
|
47
|
+
return `return (${
|
|
48
|
+
appendFunction ? 'function ' : ''
|
|
49
|
+
}${fnStr}).apply(this, arguments)`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const prepareComponentInfoToSend = ({
|
|
53
|
+
inputs,
|
|
54
|
+
...info
|
|
55
|
+
}: ComponentInfo): ComponentInfo => ({
|
|
56
|
+
...fastClone(info),
|
|
57
|
+
inputs: inputs?.map(
|
|
58
|
+
(input): Input =>
|
|
59
|
+
Object.entries(input).reduce(
|
|
60
|
+
(acc, [key, value]) => ({
|
|
61
|
+
...acc,
|
|
62
|
+
[key]: serializeValue(value),
|
|
63
|
+
}),
|
|
64
|
+
{} as Input
|
|
65
|
+
)
|
|
66
|
+
),
|
|
67
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { BuilderBlock } from '../types/builder-block.js';
|
|
2
|
+
import type { DeepPartial } from '../types/deep-partial.js';
|
|
3
|
+
import { isBrowser } from './is-browser.js';
|
|
4
|
+
|
|
5
|
+
export interface InsertMenuItem {
|
|
6
|
+
name: string;
|
|
7
|
+
icon?: string;
|
|
8
|
+
item: DeepPartial<BuilderBlock>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface InsertMenuConfig {
|
|
12
|
+
name: string;
|
|
13
|
+
priority?: number;
|
|
14
|
+
persist?: boolean;
|
|
15
|
+
advanced?: boolean;
|
|
16
|
+
items: InsertMenuItem[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const registry: { [key: string]: any[] } = {};
|
|
20
|
+
|
|
21
|
+
export function register(type: 'insertMenu', info: InsertMenuConfig): void;
|
|
22
|
+
export function register(type: string, info: any): void;
|
|
23
|
+
export function register(type: string, info: any) {
|
|
24
|
+
let typeList = registry[type];
|
|
25
|
+
if (!typeList) {
|
|
26
|
+
typeList = registry[type] = [];
|
|
27
|
+
}
|
|
28
|
+
typeList.push(info);
|
|
29
|
+
if (isBrowser()) {
|
|
30
|
+
const message = {
|
|
31
|
+
type: 'builder.register',
|
|
32
|
+
data: {
|
|
33
|
+
type,
|
|
34
|
+
info,
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
try {
|
|
38
|
+
parent.postMessage(message, '*');
|
|
39
|
+
if (parent !== window) {
|
|
40
|
+
window.postMessage(message, '*');
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
console.debug('Could not postmessage', err);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
const propertiesThatMustBeNumber = new Set(['lineHeight']);
|
|
2
|
+
const displayValues = new Set(['flex', 'none']);
|
|
3
|
+
|
|
4
|
+
const SHOW_WARNINGS = false;
|
|
5
|
+
|
|
6
|
+
type Styles = Record<string, string | number>;
|
|
7
|
+
|
|
8
|
+
const normalizeNumber = (value: number): number | undefined => {
|
|
9
|
+
if (Number.isNaN(value)) {
|
|
10
|
+
return undefined;
|
|
11
|
+
} else if (value < 0) {
|
|
12
|
+
// TODO: why are negative values not allowed?
|
|
13
|
+
return 0;
|
|
14
|
+
} else {
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const sanitizeReactNativeBlockStyles = (styles: Styles): Styles => {
|
|
20
|
+
return Object.keys(styles).reduce<Styles>((acc, key): Styles => {
|
|
21
|
+
const propertyValue = styles[key];
|
|
22
|
+
|
|
23
|
+
if (key === 'display' && !displayValues.has(propertyValue as string)) {
|
|
24
|
+
if (SHOW_WARNINGS) {
|
|
25
|
+
console.warn(
|
|
26
|
+
`Style value for key "display" must be "flex" or "none" but had ${propertyValue}`
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return acc;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
propertiesThatMustBeNumber.has(key) &&
|
|
34
|
+
typeof propertyValue !== 'number'
|
|
35
|
+
) {
|
|
36
|
+
if (SHOW_WARNINGS) {
|
|
37
|
+
console.warn(
|
|
38
|
+
`Style key ${key} must be a number, but had value \`${styles[key]}\``
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
return acc;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof propertyValue === 'string') {
|
|
45
|
+
// `px` units need to be stripped and replaced with numbers
|
|
46
|
+
// https://regexr.com/6ualn
|
|
47
|
+
const isPixelUnit = propertyValue.match(/^-?(\d*)(\.?)(\d*)*px$/);
|
|
48
|
+
|
|
49
|
+
if (isPixelUnit) {
|
|
50
|
+
const newValue = parseFloat(propertyValue);
|
|
51
|
+
const normalizedValue = normalizeNumber(newValue);
|
|
52
|
+
if (normalizedValue) {
|
|
53
|
+
return { ...acc, [key]: normalizedValue };
|
|
54
|
+
} else {
|
|
55
|
+
return acc;
|
|
56
|
+
}
|
|
57
|
+
} else if (propertyValue === '0') {
|
|
58
|
+
// 0 edge case needs to be handled
|
|
59
|
+
return { ...acc, [key]: 0 };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return { ...acc, [key]: propertyValue };
|
|
64
|
+
}, {});
|
|
65
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { isBrowser } from './is-browser.js';
|
|
2
|
+
|
|
3
|
+
const settings: Settings = {};
|
|
4
|
+
|
|
5
|
+
export type Settings = { customInsertMenu?: boolean };
|
|
6
|
+
|
|
7
|
+
export function setEditorSettings(newSettings: Settings) {
|
|
8
|
+
if (isBrowser()) {
|
|
9
|
+
Object.assign(settings, newSettings);
|
|
10
|
+
const message = {
|
|
11
|
+
type: 'builder.settingsChange',
|
|
12
|
+
data: settings,
|
|
13
|
+
};
|
|
14
|
+
parent.postMessage(message, '*');
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { set } from './set.js';
|
|
2
|
+
|
|
3
|
+
test('can shallow set a property', () => {
|
|
4
|
+
const obj = { foo: 'bar' };
|
|
5
|
+
set(obj, 'foo', 'baz');
|
|
6
|
+
expect(obj.foo).toBe('baz');
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
test('can deeply set a property', () => {
|
|
10
|
+
const obj = { foo: 'bar' };
|
|
11
|
+
set(obj, 'foo.bar', 'baz');
|
|
12
|
+
expect((obj.foo as any).bar).toBe('baz');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test('can deeply create arrays', () => {
|
|
16
|
+
const obj = { foo: 'bar' };
|
|
17
|
+
set(obj, 'foo.bar.0', 'hi');
|
|
18
|
+
expect((obj.foo as any).bar).toEqual(['hi']);
|
|
19
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal implementation of lodash's _.set
|
|
3
|
+
* https://lodash.com/docs/4.17.15#set
|
|
4
|
+
*
|
|
5
|
+
* See ./set.test.ts for usage examples
|
|
6
|
+
*/
|
|
7
|
+
export const set = (obj: any, _path: string | string[], value: any) => {
|
|
8
|
+
if (Object(obj) !== obj) {
|
|
9
|
+
return obj;
|
|
10
|
+
}
|
|
11
|
+
const path: string[] = Array.isArray(_path)
|
|
12
|
+
? _path
|
|
13
|
+
: (_path.toString().match(/[^.[\]]+/g) as string[]);
|
|
14
|
+
|
|
15
|
+
path
|
|
16
|
+
.slice(0, -1)
|
|
17
|
+
.reduce(
|
|
18
|
+
(a, c, i) =>
|
|
19
|
+
Object(a[c]) === a[c]
|
|
20
|
+
? a[c]
|
|
21
|
+
: (a[c] =
|
|
22
|
+
Math.abs(Number(path[i + 1])) >> 0 === +path[i + 1] ? [] : {}),
|
|
23
|
+
obj
|
|
24
|
+
)[path[path.length - 1]] = value;
|
|
25
|
+
return obj;
|
|
26
|
+
};
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { TARGET } from '../constants/target.js';
|
|
2
|
+
import { getSessionId } from '../helpers/sessionId.js';
|
|
3
|
+
import { getVisitorId } from '../helpers/visitorId.js';
|
|
4
|
+
import type { CanTrack } from '../types/can-track.js';
|
|
5
|
+
import { isBrowser } from './is-browser.js';
|
|
6
|
+
import { isEditing } from './is-editing.js';
|
|
7
|
+
|
|
8
|
+
interface Event {
|
|
9
|
+
type: 'click' | 'impression';
|
|
10
|
+
data: {
|
|
11
|
+
/**
|
|
12
|
+
* (Optional) The content's ID. Useful if this event pertains to a specific piece of content.
|
|
13
|
+
*/
|
|
14
|
+
contentId?: string;
|
|
15
|
+
/**
|
|
16
|
+
* This is the ID of the space that the content belongs to.
|
|
17
|
+
*/
|
|
18
|
+
ownerId: string;
|
|
19
|
+
/**
|
|
20
|
+
* Stringified JSON object containing any additional metadata you want to track.
|
|
21
|
+
*/
|
|
22
|
+
metadata?: string;
|
|
23
|
+
/**
|
|
24
|
+
* Session ID of the user. This is provided by the SDK by checking session storage.
|
|
25
|
+
*/
|
|
26
|
+
sessionId: string | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* Visitor ID of the user. This is provided by the SDK by checking cookies.
|
|
29
|
+
*/
|
|
30
|
+
visitorId: string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* (Optional) If running an A/B test, the ID of the variation that the user is in.
|
|
33
|
+
*/
|
|
34
|
+
variationId?: string;
|
|
35
|
+
[index: string]: any;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type TrackingData = {
|
|
40
|
+
visitorId: string | undefined;
|
|
41
|
+
sessionId: string | undefined;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const getTrackingEventData = async ({
|
|
45
|
+
canTrack,
|
|
46
|
+
}: CanTrack): Promise<TrackingData> => {
|
|
47
|
+
if (!canTrack) {
|
|
48
|
+
return { visitorId: undefined, sessionId: undefined };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const sessionId = await getSessionId({ canTrack });
|
|
52
|
+
const visitorId = getVisitorId({ canTrack });
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
sessionId,
|
|
56
|
+
visitorId,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type EventProperties = Pick<Event, 'type'> &
|
|
61
|
+
Pick<Event['data'], 'contentId' | 'variationId'> & {
|
|
62
|
+
/**
|
|
63
|
+
* Your organization's API key.
|
|
64
|
+
*/
|
|
65
|
+
apiKey: Event['data']['ownerId'];
|
|
66
|
+
/**
|
|
67
|
+
* (Optional) metadata that you want to provide with your event.
|
|
68
|
+
*/
|
|
69
|
+
metadata?: {
|
|
70
|
+
[index: string]: any;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* (Optional) Any additional (non-metadata) properties to add to the event.
|
|
75
|
+
*/
|
|
76
|
+
[index: string]: any;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type EventProps = EventProperties & CanTrack;
|
|
80
|
+
|
|
81
|
+
const createEvent = async ({
|
|
82
|
+
type: eventType,
|
|
83
|
+
canTrack,
|
|
84
|
+
apiKey,
|
|
85
|
+
metadata,
|
|
86
|
+
...properties
|
|
87
|
+
}: EventProps): Promise<Event> => ({
|
|
88
|
+
type: eventType,
|
|
89
|
+
data: {
|
|
90
|
+
...properties,
|
|
91
|
+
metadata: JSON.stringify(metadata),
|
|
92
|
+
...(await getTrackingEventData({ canTrack })),
|
|
93
|
+
ownerId: apiKey,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
export async function _track(eventProps: EventProps) {
|
|
98
|
+
if (!eventProps.canTrack) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (isEditing()) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!(isBrowser() || TARGET === 'reactNative')) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return fetch(`https://builder.io/api/v1/track`, {
|
|
110
|
+
method: 'POST',
|
|
111
|
+
body: JSON.stringify({
|
|
112
|
+
events: [await createEvent(eventProps)],
|
|
113
|
+
}),
|
|
114
|
+
headers: {
|
|
115
|
+
'content-type': 'application/json',
|
|
116
|
+
},
|
|
117
|
+
mode: 'cors',
|
|
118
|
+
}).catch((err) => {
|
|
119
|
+
console.error('Failed to track: ', err);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export const track = (args: EventProperties) =>
|
|
124
|
+
_track({ ...args, canTrack: true });
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CanTrack } from '../types/can-track.js';
|
|
2
|
+
import { getCookie, setCookie } from './cookie.js';
|
|
3
|
+
|
|
4
|
+
const BUILDER_STORE_PREFIX = 'builderio.variations';
|
|
5
|
+
|
|
6
|
+
const getContentTestKey = (id: string) => `${BUILDER_STORE_PREFIX}.${id}`;
|
|
7
|
+
|
|
8
|
+
export const getContentVariationCookie = ({
|
|
9
|
+
contentId,
|
|
10
|
+
canTrack,
|
|
11
|
+
}: { contentId: string } & CanTrack) =>
|
|
12
|
+
getCookie({ name: getContentTestKey(contentId), canTrack });
|
|
13
|
+
|
|
14
|
+
export const setContentVariationCookie = ({
|
|
15
|
+
contentId,
|
|
16
|
+
canTrack,
|
|
17
|
+
value,
|
|
18
|
+
}: {
|
|
19
|
+
contentId: string;
|
|
20
|
+
value: string;
|
|
21
|
+
} & CanTrack) =>
|
|
22
|
+
setCookie({ name: getContentTestKey(contentId), value, canTrack });
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { isBrowser } from '../functions/is-browser.js';
|
|
2
|
+
import type { CanTrack } from '../types/can-track.js';
|
|
3
|
+
import { getTopLevelDomain } from './url.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* NOTE: This function is `async` because its react-native override is async. Do not remove the `async` keyword!
|
|
7
|
+
*/
|
|
8
|
+
export const getCookie = async ({
|
|
9
|
+
name,
|
|
10
|
+
canTrack,
|
|
11
|
+
}: {
|
|
12
|
+
name: string;
|
|
13
|
+
} & CanTrack) => {
|
|
14
|
+
try {
|
|
15
|
+
if (!canTrack) {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extracted from MDN docs
|
|
21
|
+
* https://developer.mozilla.org/en-US/docs/Web/API/Document/cookie#example_2_get_a_sample_cookie_named_test2
|
|
22
|
+
*/
|
|
23
|
+
return document.cookie
|
|
24
|
+
.split('; ')
|
|
25
|
+
.find((row) => row.startsWith(`${name}=`))
|
|
26
|
+
?.split('=')[1];
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.debug('[COOKIE] GET error: ', err);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
type CookieConfiguration = Array<
|
|
33
|
+
| ['expires', string]
|
|
34
|
+
| ['path', '/']
|
|
35
|
+
| ['domain', string]
|
|
36
|
+
| ['secure', '']
|
|
37
|
+
| ['SameSite', 'None']
|
|
38
|
+
| [string, string]
|
|
39
|
+
| []
|
|
40
|
+
>;
|
|
41
|
+
|
|
42
|
+
const stringifyCookie = (cookie: CookieConfiguration): string =>
|
|
43
|
+
cookie.map(([key, value]) => (value ? `${key}=${value}` : key)).join('; ');
|
|
44
|
+
|
|
45
|
+
const SECURE_CONFIG: CookieConfiguration = [
|
|
46
|
+
['secure', ''],
|
|
47
|
+
['SameSite', 'None'],
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const createCookieString = ({
|
|
51
|
+
name,
|
|
52
|
+
value,
|
|
53
|
+
expires,
|
|
54
|
+
}: {
|
|
55
|
+
name: string;
|
|
56
|
+
value: string;
|
|
57
|
+
expires?: Date;
|
|
58
|
+
}) => {
|
|
59
|
+
const secure = isBrowser() ? location.protocol === 'https:' : true;
|
|
60
|
+
|
|
61
|
+
const secureObj: CookieConfiguration = secure ? SECURE_CONFIG : [[]];
|
|
62
|
+
|
|
63
|
+
// TODO: need to know if secure server side
|
|
64
|
+
const expiresObj: CookieConfiguration = expires
|
|
65
|
+
? [['expires', expires.toUTCString()]]
|
|
66
|
+
: [[]];
|
|
67
|
+
|
|
68
|
+
const cookieValue: CookieConfiguration = [
|
|
69
|
+
[name, value],
|
|
70
|
+
...expiresObj,
|
|
71
|
+
['path', '/'],
|
|
72
|
+
['domain', getTopLevelDomain(window.location.hostname)],
|
|
73
|
+
...secureObj,
|
|
74
|
+
];
|
|
75
|
+
const cookie = stringifyCookie(cookieValue);
|
|
76
|
+
|
|
77
|
+
return cookie;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* NOTE: This function is `async` because its react-native override is async. Do not remove the `async` keyword!
|
|
82
|
+
*/
|
|
83
|
+
export const setCookie = async ({
|
|
84
|
+
name,
|
|
85
|
+
value,
|
|
86
|
+
expires,
|
|
87
|
+
canTrack,
|
|
88
|
+
}: {
|
|
89
|
+
name: string;
|
|
90
|
+
value: string;
|
|
91
|
+
expires?: Date;
|
|
92
|
+
} & CanTrack) => {
|
|
93
|
+
try {
|
|
94
|
+
if (!canTrack) {
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
const cookie = createCookieString({ name, value, expires });
|
|
98
|
+
document.cookie = cookie;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
console.warn('[COOKIE] SET error: ', err);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { camelToKebabCase } from '../functions/camel-to-kebab-case.js';
|
|
2
|
+
|
|
3
|
+
const convertStyleMaptoCSS = (style: Partial<CSSStyleDeclaration>): string => {
|
|
4
|
+
const cssProps = Object.entries(style).map(([key, value]) => {
|
|
5
|
+
if (typeof value === 'string') {
|
|
6
|
+
return `${camelToKebabCase(key)}: ${value};`;
|
|
7
|
+
}
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
return cssProps.join('\n');
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export const createCssClass = ({
|
|
14
|
+
mediaQuery,
|
|
15
|
+
className,
|
|
16
|
+
styles,
|
|
17
|
+
}: {
|
|
18
|
+
mediaQuery?: string;
|
|
19
|
+
className: string;
|
|
20
|
+
styles: Partial<CSSStyleDeclaration>;
|
|
21
|
+
}) => {
|
|
22
|
+
const cssClass = `.${className} {
|
|
23
|
+
${convertStyleMaptoCSS(styles)}
|
|
24
|
+
}`;
|
|
25
|
+
|
|
26
|
+
if (mediaQuery) {
|
|
27
|
+
return `${mediaQuery} {
|
|
28
|
+
${cssClass}
|
|
29
|
+
}`;
|
|
30
|
+
} else {
|
|
31
|
+
return cssClass;
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert deep object to a flat object with dots
|
|
3
|
+
*
|
|
4
|
+
* { foo: { bar: 'baz' }} -> { 'foo.bar': 'baz' }
|
|
5
|
+
*/
|
|
6
|
+
export function flatten<T extends Record<string, any>>(
|
|
7
|
+
object: T,
|
|
8
|
+
path: string | null = null,
|
|
9
|
+
separator = '.'
|
|
10
|
+
): T {
|
|
11
|
+
return Object.keys(object).reduce((acc: T, key: string): T => {
|
|
12
|
+
const value = object[key];
|
|
13
|
+
const newPath = [path, key].filter(Boolean).join(separator);
|
|
14
|
+
const isObject = [
|
|
15
|
+
typeof value === 'object',
|
|
16
|
+
value !== null,
|
|
17
|
+
!(Array.isArray(value) && value.length === 0),
|
|
18
|
+
].every(Boolean);
|
|
19
|
+
|
|
20
|
+
return isObject
|
|
21
|
+
? { ...acc, ...flatten(value, newPath, separator) }
|
|
22
|
+
: { ...acc, [newPath]: value };
|
|
23
|
+
}, {} as T);
|
|
24
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { isBrowser } from '../functions/is-browser.js';
|
|
2
|
+
import type { CanTrack } from '../types/can-track.js';
|
|
3
|
+
|
|
4
|
+
const getLocalStorage = () =>
|
|
5
|
+
isBrowser() && typeof localStorage !== 'undefined' ? localStorage : undefined;
|
|
6
|
+
|
|
7
|
+
export const getLocalStorageItem = ({
|
|
8
|
+
key,
|
|
9
|
+
canTrack,
|
|
10
|
+
}: {
|
|
11
|
+
key: string;
|
|
12
|
+
} & CanTrack) => {
|
|
13
|
+
try {
|
|
14
|
+
if (canTrack) {
|
|
15
|
+
return getLocalStorage()?.getItem(key);
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
} catch (err) {
|
|
19
|
+
console.debug('[LocalStorage] GET error: ', err);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const setLocalStorageItem = ({
|
|
24
|
+
key,
|
|
25
|
+
canTrack,
|
|
26
|
+
value,
|
|
27
|
+
}: {
|
|
28
|
+
key: string;
|
|
29
|
+
value: string;
|
|
30
|
+
} & CanTrack) => {
|
|
31
|
+
try {
|
|
32
|
+
if (canTrack) {
|
|
33
|
+
getLocalStorage()?.setItem(key, value);
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
console.debug('[LocalStorage] SET error: ', err);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CanTrack } from '../types/can-track.js';
|
|
2
|
+
import { getCookie, setCookie } from './cookie.js';
|
|
3
|
+
import { checkIsDefined } from './nullable.js';
|
|
4
|
+
import { uuid } from './uuid.js';
|
|
5
|
+
|
|
6
|
+
const SESSION_LOCAL_STORAGE_KEY = 'builderSessionId';
|
|
7
|
+
|
|
8
|
+
export const getSessionId = async ({ canTrack }: CanTrack) => {
|
|
9
|
+
if (!canTrack) {
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const sessionId = await getCookie({
|
|
14
|
+
name: SESSION_LOCAL_STORAGE_KEY,
|
|
15
|
+
canTrack,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
if (checkIsDefined(sessionId)) {
|
|
19
|
+
return sessionId;
|
|
20
|
+
} else {
|
|
21
|
+
const newSessionId = createSessionId();
|
|
22
|
+
setSessionId({ id: newSessionId, canTrack });
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const createSessionId = () => uuid();
|
|
27
|
+
|
|
28
|
+
export const setSessionId = ({
|
|
29
|
+
id,
|
|
30
|
+
canTrack,
|
|
31
|
+
}: {
|
|
32
|
+
id: string;
|
|
33
|
+
} & CanTrack) =>
|
|
34
|
+
setCookie({ name: SESSION_LOCAL_STORAGE_KEY, value: id, canTrack });
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getTopLevelDomain } from './url.js';
|
|
2
|
+
|
|
3
|
+
describe('getTopLevelDomain', () => {
|
|
4
|
+
test('handles root domain', () => {
|
|
5
|
+
const output = getTopLevelDomain('example.com');
|
|
6
|
+
expect(output).toBe('example.com');
|
|
7
|
+
});
|
|
8
|
+
test('handles subdomain', () => {
|
|
9
|
+
const output = getTopLevelDomain('wwww.example.com');
|
|
10
|
+
expect(output).toBe('example.com');
|
|
11
|
+
});
|
|
12
|
+
test('handles subdomain with long suffix', () => {
|
|
13
|
+
const output = getTopLevelDomain('www.example.co.uk');
|
|
14
|
+
expect(output).toBe('example.co.uk');
|
|
15
|
+
});
|
|
16
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Only gets one level up from hostname
|
|
3
|
+
* wwww.example.com -> example.com
|
|
4
|
+
* www.example.co.uk -> example.co.uk
|
|
5
|
+
*/
|
|
6
|
+
export const getTopLevelDomain = (host: string) => {
|
|
7
|
+
const parts = host.split('.');
|
|
8
|
+
if (parts.length > 2) {
|
|
9
|
+
return parts.slice(1).join('.');
|
|
10
|
+
}
|
|
11
|
+
return host;
|
|
12
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @credit https://stackoverflow.com/a/2117523
|
|
3
|
+
*/
|
|
4
|
+
export function uuidv4() {
|
|
5
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
|
6
|
+
const r = (Math.random() * 16) | 0,
|
|
7
|
+
v = c == 'x' ? r : (r & 0x3) | 0x8;
|
|
8
|
+
return v.toString(16);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Slightly cleaner and smaller UUIDs
|
|
14
|
+
*/
|
|
15
|
+
export function uuid() {
|
|
16
|
+
return uuidv4().replace(/-/g, '');
|
|
17
|
+
}
|