@builder.io/sdk-svelte 0.0.4 → 0.0.5
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.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 +354 -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 +90 -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 +12 -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,90 @@
|
|
|
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
|
+
contentId?: string;
|
|
12
|
+
ownerId: string;
|
|
13
|
+
metadata?: string;
|
|
14
|
+
sessionId: string | undefined;
|
|
15
|
+
visitorId: string | undefined;
|
|
16
|
+
[index: string]: any;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type TrackingData = {
|
|
21
|
+
visitorId: string | undefined;
|
|
22
|
+
sessionId: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const getTrackingEventData = async ({
|
|
26
|
+
canTrack,
|
|
27
|
+
}: CanTrack): Promise<TrackingData> => {
|
|
28
|
+
if (!canTrack) {
|
|
29
|
+
return { visitorId: undefined, sessionId: undefined };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const sessionId = await getSessionId({ canTrack });
|
|
33
|
+
const visitorId = getVisitorId({ canTrack });
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
sessionId,
|
|
37
|
+
visitorId,
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type EventProperties = {
|
|
42
|
+
type: Event['type'];
|
|
43
|
+
orgId: Event['data']['ownerId'];
|
|
44
|
+
contentId: Event['data']['contentId'];
|
|
45
|
+
[index: string]: any;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type EventProps = EventProperties & CanTrack;
|
|
49
|
+
|
|
50
|
+
const createEvent = async ({
|
|
51
|
+
type: eventType,
|
|
52
|
+
canTrack,
|
|
53
|
+
orgId,
|
|
54
|
+
contentId,
|
|
55
|
+
...properties
|
|
56
|
+
}: EventProps): Promise<Event> => ({
|
|
57
|
+
type: eventType,
|
|
58
|
+
data: {
|
|
59
|
+
...properties,
|
|
60
|
+
...(await getTrackingEventData({ canTrack })),
|
|
61
|
+
ownerId: orgId,
|
|
62
|
+
contentId,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export async function track(eventProps: EventProps) {
|
|
67
|
+
if (!eventProps.canTrack) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (isEditing()) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!(isBrowser() || TARGET === 'reactNative')) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return fetch(`https://builder.io/api/v1/track`, {
|
|
79
|
+
method: 'POST',
|
|
80
|
+
body: JSON.stringify({
|
|
81
|
+
events: [await createEvent(eventProps)],
|
|
82
|
+
}),
|
|
83
|
+
headers: {
|
|
84
|
+
'content-type': 'application/json',
|
|
85
|
+
},
|
|
86
|
+
mode: 'cors',
|
|
87
|
+
}).catch((err) => {
|
|
88
|
+
console.error('Failed to track: ', err);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
@@ -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
|
+
}
|