@servicetitan/dte-unlayer 0.152.0 → 0.153.0
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/dist/editor.d.ts.map +1 -1
- package/dist/editor.js +25 -10
- package/dist/editor.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/loadScript.d.ts.map +1 -1
- package/dist/loadScript.js +8 -1
- package/dist/loadScript.js.map +1 -1
- package/dist/log.d.ts +3 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +21 -0
- package/dist/log.js.map +1 -0
- package/dist/merge-tags.d.ts +18 -1
- package/dist/merge-tags.d.ts.map +1 -1
- package/dist/merge-tags.js +36 -9
- package/dist/merge-tags.js.map +1 -1
- package/dist/store.d.ts +5 -0
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +59 -9
- package/dist/store.js.map +1 -1
- package/dist/unlayer.d.ts.map +1 -1
- package/dist/unlayer.js +89 -74
- package/dist/unlayer.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/load-script.test.ts +41 -0
- package/src/__tests__/log.test.ts +44 -0
- package/src/__tests__/merge-tags.test.ts +111 -18
- package/src/editor.tsx +20 -9
- package/src/index.ts +8 -1
- package/src/loadScript.ts +9 -1
- package/src/log.ts +24 -0
- package/src/merge-tags.ts +62 -9
- package/src/store.ts +66 -8
- package/src/unlayer.tsx +79 -61
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { toUnlayerMergeTags } from '../merge-tags';
|
|
1
|
+
import { scheduleUnlayerMergeTags, toUnlayerMergeTags } from '../merge-tags';
|
|
2
2
|
|
|
3
3
|
const customerNameTag = {
|
|
4
4
|
id: '10',
|
|
@@ -20,6 +20,22 @@ const renewalDateTag = {
|
|
|
20
20
|
group: 'Custom Fields',
|
|
21
21
|
};
|
|
22
22
|
|
|
23
|
+
const groupedCustomFields = {
|
|
24
|
+
custom_fields: {
|
|
25
|
+
name: 'Custom Fields',
|
|
26
|
+
mergeTags: {
|
|
27
|
+
custom_field_12345: {
|
|
28
|
+
name: 'Contract Term',
|
|
29
|
+
value: '{{ __custom_fields["Contract Term"] | safe | nl2br }}',
|
|
30
|
+
},
|
|
31
|
+
custom_field_12346: {
|
|
32
|
+
name: 'Renewal Date',
|
|
33
|
+
value: '{{ __custom_fields["Renewal Date"] | safe | nl2br }}',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
23
39
|
describe('toUnlayerMergeTags', () => {
|
|
24
40
|
describe('when tags are missing', () => {
|
|
25
41
|
test.each([undefined, []])('returns undefined for %p', tags => {
|
|
@@ -46,21 +62,7 @@ describe('toUnlayerMergeTags', () => {
|
|
|
46
62
|
const subject = () => toUnlayerMergeTags([contractTermTag, renewalDateTag]);
|
|
47
63
|
|
|
48
64
|
test('nests tags under a submenu named by the host group', () => {
|
|
49
|
-
expect(subject()).toEqual(
|
|
50
|
-
custom_fields: {
|
|
51
|
-
name: 'Custom Fields',
|
|
52
|
-
mergeTags: {
|
|
53
|
-
'custom-field-12345': {
|
|
54
|
-
name: 'Contract Term',
|
|
55
|
-
value: '{{ __custom_fields["Contract Term"] | safe | nl2br }}',
|
|
56
|
-
},
|
|
57
|
-
'custom-field-12346': {
|
|
58
|
-
name: 'Renewal Date',
|
|
59
|
-
value: '{{ __custom_fields["Renewal Date"] | safe | nl2br }}',
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
});
|
|
65
|
+
expect(subject()).toEqual(groupedCustomFields);
|
|
64
66
|
});
|
|
65
67
|
});
|
|
66
68
|
|
|
@@ -76,7 +78,7 @@ describe('toUnlayerMergeTags', () => {
|
|
|
76
78
|
custom_fields: {
|
|
77
79
|
name: 'Custom Fields',
|
|
78
80
|
mergeTags: {
|
|
79
|
-
|
|
81
|
+
custom_field_12345: {
|
|
80
82
|
name: 'Contract Term',
|
|
81
83
|
value: '{{ __custom_fields["Contract Term"] | safe | nl2br }}',
|
|
82
84
|
},
|
|
@@ -103,7 +105,7 @@ describe('toUnlayerMergeTags', () => {
|
|
|
103
105
|
custom_fields: {
|
|
104
106
|
name: 'Custom Fields',
|
|
105
107
|
mergeTags: {
|
|
106
|
-
|
|
108
|
+
custom_field_12345: {
|
|
107
109
|
name: 'Contract Term',
|
|
108
110
|
value: '{{ __custom_fields["Contract Term"] | safe | nl2br }}',
|
|
109
111
|
},
|
|
@@ -140,4 +142,95 @@ describe('toUnlayerMergeTags', () => {
|
|
|
140
142
|
});
|
|
141
143
|
});
|
|
142
144
|
});
|
|
145
|
+
|
|
146
|
+
describe('when groups are disabled', () => {
|
|
147
|
+
const subject = () =>
|
|
148
|
+
toUnlayerMergeTags([customerNameTag, contractTermTag], { groups: false });
|
|
149
|
+
|
|
150
|
+
test('keeps every tag flat like Unlayer 0.151', () => {
|
|
151
|
+
expect(subject()).toEqual({
|
|
152
|
+
'10': {
|
|
153
|
+
name: 'Customer Name',
|
|
154
|
+
value: '{{Customer.Name | safe | nl2br }}',
|
|
155
|
+
},
|
|
156
|
+
custom_field_12345: {
|
|
157
|
+
name: 'Contract Term',
|
|
158
|
+
value: '{{ __custom_fields["Contract Term"] | safe | nl2br }}',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
describe('scheduleUnlayerMergeTags', () => {
|
|
166
|
+
describe('when grouped tags exist', () => {
|
|
167
|
+
const setMergeTags = jest.fn();
|
|
168
|
+
const addEventListener = jest.fn();
|
|
169
|
+
|
|
170
|
+
beforeEach(() => {
|
|
171
|
+
setMergeTags.mockReset();
|
|
172
|
+
addEventListener.mockReset();
|
|
173
|
+
scheduleUnlayerMergeTags(
|
|
174
|
+
{ addEventListener, setMergeTags },
|
|
175
|
+
[customerNameTag, contractTermTag, renewalDateTag],
|
|
176
|
+
);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('does not apply grouped merge tags until the editor is ready', () => {
|
|
180
|
+
expect(setMergeTags).not.toHaveBeenCalled();
|
|
181
|
+
expect(addEventListener).toHaveBeenCalledWith('editor:ready', expect.any(Function));
|
|
182
|
+
expect(addEventListener).toHaveBeenCalledWith('design:loaded', expect.any(Function));
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test('applies grouped merge tags when the editor is ready', () => {
|
|
186
|
+
addEventListener.mock.calls[0][1]();
|
|
187
|
+
|
|
188
|
+
expect(setMergeTags).toHaveBeenCalledWith({
|
|
189
|
+
'10': {
|
|
190
|
+
name: 'Customer Name',
|
|
191
|
+
value: '{{Customer.Name | safe | nl2br }}',
|
|
192
|
+
},
|
|
193
|
+
...groupedCustomFields,
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test('applies grouped merge tags only once', () => {
|
|
198
|
+
addEventListener.mock.calls[0][1]();
|
|
199
|
+
addEventListener.mock.calls[1][1]();
|
|
200
|
+
|
|
201
|
+
expect(setMergeTags).toHaveBeenCalledTimes(1);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test('retries on design:loaded when setMergeTags throws', () => {
|
|
205
|
+
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
206
|
+
setMergeTags.mockImplementationOnce(() => {
|
|
207
|
+
throw new Error('setMergeTags failed');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
addEventListener.mock.calls[0][1]();
|
|
211
|
+
addEventListener.mock.calls[1][1]();
|
|
212
|
+
|
|
213
|
+
expect(setMergeTags).toHaveBeenCalledTimes(2);
|
|
214
|
+
expect(consoleError).toHaveBeenCalledWith(
|
|
215
|
+
'[dte-unlayer]',
|
|
216
|
+
'Failed to apply grouped merge tags',
|
|
217
|
+
expect.any(Error),
|
|
218
|
+
);
|
|
219
|
+
consoleError.mockRestore();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe('when tags are missing', () => {
|
|
224
|
+
const setMergeTags = jest.fn();
|
|
225
|
+
const addEventListener = jest.fn();
|
|
226
|
+
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
scheduleUnlayerMergeTags({ addEventListener, setMergeTags }, undefined);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('does not touch the editor', () => {
|
|
232
|
+
expect(setMergeTags).not.toHaveBeenCalled();
|
|
233
|
+
expect(addEventListener).not.toHaveBeenCalled();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
143
236
|
});
|
package/src/editor.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
useState,
|
|
9
9
|
} from 'react';
|
|
10
10
|
import { DisplayConditionModal } from './display-conditions/DisplayConditionModal';
|
|
11
|
+
import { getErrorMessage } from './log';
|
|
11
12
|
import { UnlayerEditorTwin } from './shared/const';
|
|
12
13
|
import { UnlayerStore, UnlayerDesignChangeInfo } from './store';
|
|
13
14
|
import { UnlayerRef, CreateUnlayerEditorProps } from './unlayer-interface';
|
|
@@ -36,6 +37,7 @@ export const useUnlayerRef = () => useRef<UnlayerRef | null>(null);
|
|
|
36
37
|
|
|
37
38
|
export const UnlayerEditor = forwardRef<UnlayerRef, UnlayerEditorProps>((props, ref) => {
|
|
38
39
|
const [isReady, setIsReady] = useState(false);
|
|
40
|
+
const [loadError, setLoadError] = useState<string>();
|
|
39
41
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
40
42
|
const store = useMemo(
|
|
41
43
|
() => new UnlayerStore(props.opts),
|
|
@@ -43,15 +45,30 @@ export const UnlayerEditor = forwardRef<UnlayerRef, UnlayerEditorProps>((props,
|
|
|
43
45
|
[],
|
|
44
46
|
);
|
|
45
47
|
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
store.setOnError(props.onError);
|
|
50
|
+
|
|
51
|
+
return () => store.setOnError();
|
|
52
|
+
}, [props.onError, store]);
|
|
53
|
+
|
|
46
54
|
useEffect(() => {
|
|
47
55
|
if (containerRef.current) {
|
|
48
56
|
store
|
|
49
57
|
.init(containerRef.current)
|
|
50
|
-
.then(() =>
|
|
51
|
-
|
|
58
|
+
.then(() => {
|
|
59
|
+
setLoadError(undefined);
|
|
60
|
+
setIsReady(true);
|
|
61
|
+
})
|
|
62
|
+
.catch((error: unknown) => {
|
|
63
|
+
const description = getErrorMessage(error);
|
|
64
|
+
setIsReady(false);
|
|
65
|
+
setLoadError(description);
|
|
66
|
+
props.onError?.('Editor failed to load', description);
|
|
67
|
+
});
|
|
52
68
|
}
|
|
53
69
|
|
|
54
70
|
return () => store.destroy();
|
|
71
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
55
72
|
}, [store]);
|
|
56
73
|
useImperativeHandle(ref, () => store.unlayerRef, [store]);
|
|
57
74
|
|
|
@@ -91,12 +108,6 @@ export const UnlayerEditor = forwardRef<UnlayerRef, UnlayerEditorProps>((props,
|
|
|
91
108
|
return () => store.setOnImage();
|
|
92
109
|
}, [props.onImage, store]);
|
|
93
110
|
|
|
94
|
-
useEffect(() => {
|
|
95
|
-
store.setOnError(props.onError);
|
|
96
|
-
|
|
97
|
-
return () => store.setOnError();
|
|
98
|
-
}, [props.onError, store]);
|
|
99
|
-
|
|
100
111
|
useEffect(() => {
|
|
101
112
|
store.setOnMessage(props.onMessage);
|
|
102
113
|
|
|
@@ -125,7 +136,7 @@ export const UnlayerEditor = forwardRef<UnlayerRef, UnlayerEditorProps>((props,
|
|
|
125
136
|
|
|
126
137
|
return (
|
|
127
138
|
<div style={{ minHeight, display: 'flex' }}>
|
|
128
|
-
{
|
|
139
|
+
{loadError && <p className="c-red-500">error loading editor: {loadError}</p>}
|
|
129
140
|
<div id={props.id ?? 'editor'} style={style} ref={containerRef} />
|
|
130
141
|
{props.opts.displayConditions && (
|
|
131
142
|
<DisplayConditionModal
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,14 @@ export * from './tools';
|
|
|
3
3
|
export * from './api-core';
|
|
4
4
|
export * from './api-custom-tools';
|
|
5
5
|
export * from './unlayer-interface';
|
|
6
|
-
export
|
|
6
|
+
export { isUnlayerMergeTagGroup, scheduleUnlayerMergeTags, toUnlayerMergeTags } from './merge-tags';
|
|
7
|
+
export type {
|
|
8
|
+
ToUnlayerMergeTagsOptions,
|
|
9
|
+
UnlayerMergeTagGroup,
|
|
10
|
+
UnlayerMergeTagLeaf,
|
|
11
|
+
UnlayerMergeTagsConfig,
|
|
12
|
+
UnlayerMergeTagsEditor,
|
|
13
|
+
} from './merge-tags';
|
|
7
14
|
export * from './loadScript';
|
|
8
15
|
export * from './shared/const';
|
|
9
16
|
export * from './shared/forms';
|
package/src/loadScript.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { logUnlayerError } from './log';
|
|
2
|
+
|
|
1
3
|
const isScriptInjected = (scriptUrl: string) => {
|
|
2
4
|
const scripts = document.querySelectorAll('script');
|
|
3
5
|
let injected = false;
|
|
@@ -12,9 +14,10 @@ const isScriptInjected = (scriptUrl: string) => {
|
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
export const loadScript = (scriptUrl: string): Promise<void> => {
|
|
15
|
-
return new Promise<void>(resolve => {
|
|
17
|
+
return new Promise<void>((resolve, reject) => {
|
|
16
18
|
if (isScriptInjected(scriptUrl)) {
|
|
17
19
|
resolve();
|
|
20
|
+
return;
|
|
18
21
|
}
|
|
19
22
|
|
|
20
23
|
const embedScript = document.createElement('script');
|
|
@@ -22,6 +25,11 @@ export const loadScript = (scriptUrl: string): Promise<void> => {
|
|
|
22
25
|
embedScript.onload = () => {
|
|
23
26
|
resolve();
|
|
24
27
|
};
|
|
28
|
+
embedScript.onerror = () => {
|
|
29
|
+
const error = new Error(`Failed to load Unlayer script: ${scriptUrl}`);
|
|
30
|
+
logUnlayerError('Unlayer embed script failed to load', error);
|
|
31
|
+
reject(error);
|
|
32
|
+
};
|
|
25
33
|
document.head.appendChild(embedScript);
|
|
26
34
|
});
|
|
27
35
|
};
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const LOG_PREFIX = '[dte-unlayer]';
|
|
2
|
+
|
|
3
|
+
export const getErrorMessage = (error: unknown) => {
|
|
4
|
+
if (error instanceof Error && error.message) {
|
|
5
|
+
return error.message;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
if (typeof error === 'string' && error) {
|
|
9
|
+
return error;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return 'Unknown Unlayer error';
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const logUnlayerError = (message: string, error?: unknown) => {
|
|
16
|
+
if (error === undefined) {
|
|
17
|
+
// eslint-disable-next-line no-console
|
|
18
|
+
console.error(LOG_PREFIX, message);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.error(LOG_PREFIX, message, error);
|
|
24
|
+
};
|
package/src/merge-tags.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { logUnlayerError } from './log';
|
|
1
2
|
import { UnlayerEditorMergeTagInfo } from './unlayer-interface';
|
|
2
3
|
|
|
3
4
|
export interface UnlayerMergeTagLeaf {
|
|
@@ -12,54 +13,106 @@ export interface UnlayerMergeTagGroup {
|
|
|
12
13
|
|
|
13
14
|
export type UnlayerMergeTagsConfig = Record<string, UnlayerMergeTagLeaf | UnlayerMergeTagGroup>;
|
|
14
15
|
|
|
16
|
+
export interface ToUnlayerMergeTagsOptions {
|
|
17
|
+
/**
|
|
18
|
+
* When false, ignore `group` and emit the flat 0.151 shape. Unlayer custom
|
|
19
|
+
* tools fail to register if nested groups are present during createEditor.
|
|
20
|
+
*/
|
|
21
|
+
groups?: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface UnlayerMergeTagsEditor {
|
|
25
|
+
addEventListener(event: string, cb: (arg: any) => void): void;
|
|
26
|
+
setMergeTags(tags: UnlayerMergeTagsConfig): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
15
29
|
export function isUnlayerMergeTagGroup(
|
|
16
30
|
entry: UnlayerMergeTagLeaf | UnlayerMergeTagGroup,
|
|
17
31
|
): entry is UnlayerMergeTagGroup {
|
|
18
32
|
return 'mergeTags' in entry;
|
|
19
33
|
}
|
|
20
34
|
|
|
21
|
-
const
|
|
22
|
-
const key =
|
|
35
|
+
const toMergeTagKey = (value: string) => {
|
|
36
|
+
const key = value
|
|
23
37
|
.toLowerCase()
|
|
24
38
|
.split(/[^a-z0-9]+/)
|
|
25
39
|
.filter(Boolean)
|
|
26
40
|
.join('_');
|
|
27
41
|
|
|
28
|
-
return key ||
|
|
42
|
+
return key || value;
|
|
29
43
|
};
|
|
30
44
|
|
|
31
|
-
export const toUnlayerMergeTags = (
|
|
45
|
+
export const toUnlayerMergeTags = (
|
|
46
|
+
tags: UnlayerEditorMergeTagInfo[] | undefined,
|
|
47
|
+
options?: ToUnlayerMergeTagsOptions,
|
|
48
|
+
) => {
|
|
32
49
|
if (!tags?.length) {
|
|
33
50
|
return undefined;
|
|
34
51
|
}
|
|
35
52
|
|
|
53
|
+
const groupsEnabled = options?.groups !== false;
|
|
54
|
+
|
|
36
55
|
return tags.reduce<UnlayerMergeTagsConfig>((out, tag) => {
|
|
37
56
|
const mergeTag: UnlayerMergeTagLeaf = {
|
|
38
57
|
name: tag.name,
|
|
39
58
|
value: tag.propertyPath,
|
|
40
59
|
};
|
|
41
|
-
const
|
|
60
|
+
const tagKey = toMergeTagKey(tag.id);
|
|
61
|
+
const groupName = groupsEnabled ? tag.group?.trim() : undefined;
|
|
42
62
|
|
|
43
63
|
if (!groupName) {
|
|
44
|
-
out[
|
|
64
|
+
out[tagKey] = mergeTag;
|
|
45
65
|
return out;
|
|
46
66
|
}
|
|
47
67
|
|
|
48
|
-
const groupKey =
|
|
68
|
+
const groupKey = toMergeTagKey(groupName);
|
|
49
69
|
const existing = out[groupKey];
|
|
50
70
|
|
|
51
71
|
if (existing && isUnlayerMergeTagGroup(existing)) {
|
|
52
|
-
existing.mergeTags[
|
|
72
|
+
existing.mergeTags[tagKey] = mergeTag;
|
|
53
73
|
return out;
|
|
54
74
|
}
|
|
55
75
|
|
|
56
76
|
out[groupKey] = {
|
|
57
77
|
name: groupName,
|
|
58
78
|
mergeTags: {
|
|
59
|
-
[
|
|
79
|
+
[tagKey]: mergeTag,
|
|
60
80
|
},
|
|
61
81
|
};
|
|
62
82
|
|
|
63
83
|
return out;
|
|
64
84
|
}, {});
|
|
65
85
|
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Nested Unlayer groups passed to createEditor prevent DTE custom tools from
|
|
89
|
+
* loading in the host app. Apply the 0.151 flat shape at init, then upgrade to
|
|
90
|
+
* grouped submenus once the editor (and customJS tools) are ready.
|
|
91
|
+
*/
|
|
92
|
+
export const scheduleUnlayerMergeTags = (
|
|
93
|
+
editor: UnlayerMergeTagsEditor,
|
|
94
|
+
tags: UnlayerEditorMergeTagInfo[] | undefined,
|
|
95
|
+
) => {
|
|
96
|
+
const grouped = toUnlayerMergeTags(tags);
|
|
97
|
+
|
|
98
|
+
if (!grouped) {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let isApplied = false;
|
|
103
|
+
const apply = () => {
|
|
104
|
+
if (isApplied) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
editor.setMergeTags(grouped);
|
|
110
|
+
isApplied = true;
|
|
111
|
+
} catch (error: unknown) {
|
|
112
|
+
logUnlayerError('Failed to apply grouped merge tags', error);
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
editor.addEventListener('editor:ready', apply);
|
|
117
|
+
editor.addEventListener('design:loaded', apply);
|
|
118
|
+
};
|
package/src/store.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { loadScript } from './loadScript';
|
|
2
|
+
import { logUnlayerError } from './log';
|
|
2
3
|
import { defaultImageValidation } from './shared/configs';
|
|
3
4
|
import type { UnlayerEditorTwin, UnlayerEventConfig, UnlayerEventRegister } from './shared/const';
|
|
4
5
|
import { unlayerSupportedFonts } from './shared/fonts';
|
|
@@ -20,6 +21,7 @@ import {
|
|
|
20
21
|
import { CreateUnlayerEditorProps, UnlayerDesignFormat, UnlayerRef } from './unlayer-interface';
|
|
21
22
|
|
|
22
23
|
const defaultScriptUrl = 'https://editor.unlayer.com/embed.js?2';
|
|
24
|
+
const UNLAYER_TOOLS_READY_TIMEOUT_MS = 30000;
|
|
23
25
|
|
|
24
26
|
const normalizeFontToken = (font: string) =>
|
|
25
27
|
font.replace(/["']/g, '').split(',')[0].trim().toLowerCase();
|
|
@@ -184,6 +186,9 @@ export class UnlayerStore {
|
|
|
184
186
|
private isInit = false;
|
|
185
187
|
private iframe?: HTMLIFrameElement;
|
|
186
188
|
private hasDesign = false;
|
|
189
|
+
private hasCoreLoaded = false;
|
|
190
|
+
private hasToolsRegistered = false;
|
|
191
|
+
private toolsReadyWatchdogId?: number;
|
|
187
192
|
private formFieldsByFormId: Record<number, FormFieldInfo[]> = {};
|
|
188
193
|
private formFieldSamples: Record<string, unknown> = {};
|
|
189
194
|
|
|
@@ -241,19 +246,37 @@ export class UnlayerStore {
|
|
|
241
246
|
|
|
242
247
|
this.isInit = true;
|
|
243
248
|
|
|
244
|
-
|
|
249
|
+
try {
|
|
250
|
+
setTimeout(() => window.addEventListener('message', this.onPostMessage));
|
|
251
|
+
|
|
252
|
+
await loadScript(defaultScriptUrl);
|
|
253
|
+
|
|
254
|
+
if (!(window as any).unlayer) {
|
|
255
|
+
throw new Error('Unlayer embed script loaded but window.unlayer is missing');
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
this.editor = createUnlayerEditor(container, this.props);
|
|
259
|
+
this.editor.addEventListener('design:loaded', this.onDesignLoaded);
|
|
260
|
+
this.editor.addEventListener('design:updated', this.onDesignUpdated);
|
|
261
|
+
this.editor.registerCallback('image', this.uploadImage);
|
|
245
262
|
|
|
246
|
-
|
|
263
|
+
this.iframe = container.querySelector(`iframe`) ?? undefined;
|
|
247
264
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
this.editor.registerCallback('image', this.uploadImage);
|
|
265
|
+
if (!this.iframe) {
|
|
266
|
+
logUnlayerError('Unlayer iframe was not created after createEditor');
|
|
267
|
+
}
|
|
252
268
|
|
|
253
|
-
|
|
269
|
+
this.startToolsReadyWatchdog();
|
|
270
|
+
} catch (error: unknown) {
|
|
271
|
+
this.isInit = false;
|
|
272
|
+
this.clearToolsReadyWatchdog();
|
|
273
|
+
logUnlayerError('Failed to initialize Unlayer editor', error);
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
254
276
|
};
|
|
255
277
|
|
|
256
278
|
destroy = () => {
|
|
279
|
+
this.clearToolsReadyWatchdog();
|
|
257
280
|
window.removeEventListener('message', this.onPostMessage);
|
|
258
281
|
|
|
259
282
|
if (this.editor) {
|
|
@@ -392,8 +415,11 @@ export class UnlayerStore {
|
|
|
392
415
|
}
|
|
393
416
|
|
|
394
417
|
if (type === '--ready' || type === '--registered') {
|
|
418
|
+
this.hasToolsRegistered = true;
|
|
419
|
+
this.clearToolsReadyWatchdog();
|
|
395
420
|
this.onReadyCB?.();
|
|
396
421
|
} else if (type === '--core-loaded') {
|
|
422
|
+
this.hasCoreLoaded = true;
|
|
397
423
|
const configData: UnlayerEventConfig = {
|
|
398
424
|
dummyData: this.props.dummyData,
|
|
399
425
|
schema: this.props.schema,
|
|
@@ -588,6 +614,7 @@ export class UnlayerStore {
|
|
|
588
614
|
});
|
|
589
615
|
|
|
590
616
|
if (!res.isValid) {
|
|
617
|
+
logUnlayerError(`Image validation failed: ${res.title}`, res.description);
|
|
591
618
|
this.onErrorCB?.(res.title, res.description);
|
|
592
619
|
return;
|
|
593
620
|
}
|
|
@@ -597,7 +624,8 @@ export class UnlayerStore {
|
|
|
597
624
|
try {
|
|
598
625
|
const { url } = await this.onImageCB(file);
|
|
599
626
|
done({ progress: 100, url });
|
|
600
|
-
} catch {
|
|
627
|
+
} catch (error: unknown) {
|
|
628
|
+
logUnlayerError('Image upload failed', error);
|
|
601
629
|
this.onErrorCB?.(
|
|
602
630
|
'Image upload failed',
|
|
603
631
|
'Something went wrong while uploading the image. Please try again or select another image.',
|
|
@@ -606,6 +634,36 @@ export class UnlayerStore {
|
|
|
606
634
|
}
|
|
607
635
|
};
|
|
608
636
|
|
|
637
|
+
private startToolsReadyWatchdog = () => {
|
|
638
|
+
this.clearToolsReadyWatchdog();
|
|
639
|
+
this.toolsReadyWatchdogId = window.setTimeout(() => {
|
|
640
|
+
if (this.hasToolsRegistered) {
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
logUnlayerError(
|
|
645
|
+
'Unlayer custom tools did not finish loading. customJS may not have run, or tool registration stalled.',
|
|
646
|
+
{
|
|
647
|
+
hasCoreLoaded: this.hasCoreLoaded,
|
|
648
|
+
hasToolsRegistered: this.hasToolsRegistered,
|
|
649
|
+
},
|
|
650
|
+
);
|
|
651
|
+
this.onErrorCB?.(
|
|
652
|
+
'Editor failed to load custom tools',
|
|
653
|
+
'Unlayer custom tools did not finish loading. Check the browser console for [dte-unlayer] details.',
|
|
654
|
+
);
|
|
655
|
+
}, UNLAYER_TOOLS_READY_TIMEOUT_MS);
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
private clearToolsReadyWatchdog = () => {
|
|
659
|
+
if (this.toolsReadyWatchdogId === undefined) {
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
window.clearTimeout(this.toolsReadyWatchdogId);
|
|
664
|
+
this.toolsReadyWatchdogId = undefined;
|
|
665
|
+
};
|
|
666
|
+
|
|
609
667
|
private validateImage = async (
|
|
610
668
|
file: File,
|
|
611
669
|
imageValidation: { maxFileSize: number; maxWidth: number; maxHeight: number },
|