@elementor/frontend-handlers 3.33.0-99 → 3.35.0-324
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/index.d.mts +9 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +34 -1
- package/dist/index.mjs +34 -1
- package/package.json +1 -1
- package/src/__tests__/index.test.ts +593 -0
- package/src/handlers-registry.ts +16 -1
- package/src/lifecycle-events.ts +41 -1
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
type
|
|
1
|
+
type Settings = Record<string, unknown>;
|
|
2
|
+
type ChildRenderCallback = () => void;
|
|
3
|
+
interface ListenToChildrenAPI {
|
|
4
|
+
render: (callback: ChildRenderCallback) => void;
|
|
5
|
+
}
|
|
6
|
+
type ListenToChildrenFunction = (elementTypes: string[]) => ListenToChildrenAPI;
|
|
7
|
+
type Handler = <TSettings extends Settings = Settings>(params: {
|
|
2
8
|
element: Element;
|
|
3
9
|
signal: AbortSignal;
|
|
10
|
+
settings: TSettings;
|
|
11
|
+
listenToChildren: ListenToChildrenFunction;
|
|
4
12
|
}) => (() => void) | undefined;
|
|
5
13
|
declare const register: ({ elementType, id, callback }: {
|
|
6
14
|
elementType: string;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
type
|
|
1
|
+
type Settings = Record<string, unknown>;
|
|
2
|
+
type ChildRenderCallback = () => void;
|
|
3
|
+
interface ListenToChildrenAPI {
|
|
4
|
+
render: (callback: ChildRenderCallback) => void;
|
|
5
|
+
}
|
|
6
|
+
type ListenToChildrenFunction = (elementTypes: string[]) => ListenToChildrenAPI;
|
|
7
|
+
type Handler = <TSettings extends Settings = Settings>(params: {
|
|
2
8
|
element: Element;
|
|
3
9
|
signal: AbortSignal;
|
|
10
|
+
settings: TSettings;
|
|
11
|
+
listenToChildren: ListenToChildrenFunction;
|
|
4
12
|
}) => (() => void) | undefined;
|
|
5
13
|
declare const register: ({ elementType, id, callback }: {
|
|
6
14
|
elementType: string;
|
package/dist/index.js
CHANGED
|
@@ -52,6 +52,7 @@ var unregister = ({ elementType, id }) => {
|
|
|
52
52
|
|
|
53
53
|
// src/lifecycle-events.ts
|
|
54
54
|
var unmountCallbacks = /* @__PURE__ */ new Map();
|
|
55
|
+
var ELEMENT_RENDERED_EVENT_NAME = "elementor/element/rendered";
|
|
55
56
|
var onElementRender = ({
|
|
56
57
|
element,
|
|
57
58
|
elementType,
|
|
@@ -59,11 +60,43 @@ var onElementRender = ({
|
|
|
59
60
|
}) => {
|
|
60
61
|
const controller = new AbortController();
|
|
61
62
|
const manualUnmount = [];
|
|
63
|
+
element.dispatchEvent(
|
|
64
|
+
new CustomEvent(ELEMENT_RENDERED_EVENT_NAME, {
|
|
65
|
+
bubbles: true,
|
|
66
|
+
detail: {
|
|
67
|
+
element,
|
|
68
|
+
elementType,
|
|
69
|
+
elementId
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
);
|
|
62
73
|
if (!handlers.has(elementType)) {
|
|
63
74
|
return;
|
|
64
75
|
}
|
|
65
76
|
Array.from(handlers.get(elementType)?.values() ?? []).forEach((handler) => {
|
|
66
|
-
const
|
|
77
|
+
const settings = element.getAttribute("data-e-settings");
|
|
78
|
+
const listenToChildren = (elementTypes) => ({
|
|
79
|
+
render: (callback) => {
|
|
80
|
+
element.addEventListener(
|
|
81
|
+
ELEMENT_RENDERED_EVENT_NAME,
|
|
82
|
+
(event) => {
|
|
83
|
+
const { elementType: childType } = event.detail;
|
|
84
|
+
if (!elementTypes.includes(childType)) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
callback();
|
|
88
|
+
event.stopPropagation();
|
|
89
|
+
},
|
|
90
|
+
{ signal: controller.signal }
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
const unmount = handler({
|
|
95
|
+
element,
|
|
96
|
+
signal: controller.signal,
|
|
97
|
+
settings: settings ? JSON.parse(settings) : {},
|
|
98
|
+
listenToChildren
|
|
99
|
+
});
|
|
67
100
|
if (typeof unmount === "function") {
|
|
68
101
|
manualUnmount.push(unmount);
|
|
69
102
|
}
|
package/dist/index.mjs
CHANGED
|
@@ -24,6 +24,7 @@ var unregister = ({ elementType, id }) => {
|
|
|
24
24
|
|
|
25
25
|
// src/lifecycle-events.ts
|
|
26
26
|
var unmountCallbacks = /* @__PURE__ */ new Map();
|
|
27
|
+
var ELEMENT_RENDERED_EVENT_NAME = "elementor/element/rendered";
|
|
27
28
|
var onElementRender = ({
|
|
28
29
|
element,
|
|
29
30
|
elementType,
|
|
@@ -31,11 +32,43 @@ var onElementRender = ({
|
|
|
31
32
|
}) => {
|
|
32
33
|
const controller = new AbortController();
|
|
33
34
|
const manualUnmount = [];
|
|
35
|
+
element.dispatchEvent(
|
|
36
|
+
new CustomEvent(ELEMENT_RENDERED_EVENT_NAME, {
|
|
37
|
+
bubbles: true,
|
|
38
|
+
detail: {
|
|
39
|
+
element,
|
|
40
|
+
elementType,
|
|
41
|
+
elementId
|
|
42
|
+
}
|
|
43
|
+
})
|
|
44
|
+
);
|
|
34
45
|
if (!handlers.has(elementType)) {
|
|
35
46
|
return;
|
|
36
47
|
}
|
|
37
48
|
Array.from(handlers.get(elementType)?.values() ?? []).forEach((handler) => {
|
|
38
|
-
const
|
|
49
|
+
const settings = element.getAttribute("data-e-settings");
|
|
50
|
+
const listenToChildren = (elementTypes) => ({
|
|
51
|
+
render: (callback) => {
|
|
52
|
+
element.addEventListener(
|
|
53
|
+
ELEMENT_RENDERED_EVENT_NAME,
|
|
54
|
+
(event) => {
|
|
55
|
+
const { elementType: childType } = event.detail;
|
|
56
|
+
if (!elementTypes.includes(childType)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
callback();
|
|
60
|
+
event.stopPropagation();
|
|
61
|
+
},
|
|
62
|
+
{ signal: controller.signal }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
const unmount = handler({
|
|
67
|
+
element,
|
|
68
|
+
signal: controller.signal,
|
|
69
|
+
settings: settings ? JSON.parse(settings) : {},
|
|
70
|
+
listenToChildren
|
|
71
|
+
});
|
|
39
72
|
if (typeof unmount === "function") {
|
|
40
73
|
manualUnmount.push(unmount);
|
|
41
74
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
import { handlers } from '../handlers-registry';
|
|
2
|
+
import { init, register, unregister } from '../index';
|
|
3
|
+
|
|
4
|
+
describe( 'Frontend Handlers', () => {
|
|
5
|
+
const PARENT_ELEMENT_TYPE = 'e-parent-element';
|
|
6
|
+
const CHILD_ELEMENT_TYPE = 'e-child-element';
|
|
7
|
+
const WIDGET_ELEMENT_TYPE = 'e-widget';
|
|
8
|
+
|
|
9
|
+
beforeAll( () => {
|
|
10
|
+
init();
|
|
11
|
+
} );
|
|
12
|
+
|
|
13
|
+
beforeEach( () => {
|
|
14
|
+
handlers.clear();
|
|
15
|
+
document.body.innerHTML = '';
|
|
16
|
+
} );
|
|
17
|
+
|
|
18
|
+
describe( 'Handler Registration', () => {
|
|
19
|
+
it( 'should register and execute handler callback', () => {
|
|
20
|
+
// Arrange
|
|
21
|
+
const ELEMENT_ID = 'element-1';
|
|
22
|
+
const handlerCallback = jest.fn();
|
|
23
|
+
|
|
24
|
+
register( {
|
|
25
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
26
|
+
id: 'widget-handler',
|
|
27
|
+
callback: handlerCallback,
|
|
28
|
+
} );
|
|
29
|
+
|
|
30
|
+
const element = document.createElement( 'div' );
|
|
31
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
32
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
33
|
+
document.body.appendChild( element );
|
|
34
|
+
|
|
35
|
+
// Act
|
|
36
|
+
window.dispatchEvent(
|
|
37
|
+
new CustomEvent( 'elementor/element/render', {
|
|
38
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
39
|
+
} )
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// Assert
|
|
43
|
+
expect( handlerCallback ).toHaveBeenCalledWith(
|
|
44
|
+
expect.objectContaining( {
|
|
45
|
+
element,
|
|
46
|
+
settings: {},
|
|
47
|
+
signal: expect.any( AbortSignal ),
|
|
48
|
+
} )
|
|
49
|
+
);
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
it( 'should register multiple handlers for same element type', () => {
|
|
53
|
+
// Arrange
|
|
54
|
+
const ELEMENT_ID = 'element-1';
|
|
55
|
+
const handler1 = jest.fn();
|
|
56
|
+
const handler2 = jest.fn();
|
|
57
|
+
|
|
58
|
+
register( {
|
|
59
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
60
|
+
id: 'handler-1',
|
|
61
|
+
callback: handler1,
|
|
62
|
+
} );
|
|
63
|
+
|
|
64
|
+
register( {
|
|
65
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
66
|
+
id: 'handler-2',
|
|
67
|
+
callback: handler2,
|
|
68
|
+
} );
|
|
69
|
+
|
|
70
|
+
const element = document.createElement( 'div' );
|
|
71
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
72
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
73
|
+
document.body.appendChild( element );
|
|
74
|
+
|
|
75
|
+
// Act
|
|
76
|
+
window.dispatchEvent(
|
|
77
|
+
new CustomEvent( 'elementor/element/render', {
|
|
78
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
79
|
+
} )
|
|
80
|
+
);
|
|
81
|
+
|
|
82
|
+
// Assert
|
|
83
|
+
expect( handler1 ).toHaveBeenCalledTimes( 1 );
|
|
84
|
+
expect( handler2 ).toHaveBeenCalledTimes( 1 );
|
|
85
|
+
} );
|
|
86
|
+
|
|
87
|
+
it( 'should unregister handlers', () => {
|
|
88
|
+
// Arrange
|
|
89
|
+
const ELEMENT_ID = 'element-1';
|
|
90
|
+
const handlerCallback = jest.fn();
|
|
91
|
+
|
|
92
|
+
register( {
|
|
93
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
94
|
+
id: 'widget-handler',
|
|
95
|
+
callback: handlerCallback,
|
|
96
|
+
} );
|
|
97
|
+
|
|
98
|
+
unregister( { elementType: WIDGET_ELEMENT_TYPE, id: 'widget-handler' } );
|
|
99
|
+
|
|
100
|
+
const element = document.createElement( 'div' );
|
|
101
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
102
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
103
|
+
document.body.appendChild( element );
|
|
104
|
+
|
|
105
|
+
// Act
|
|
106
|
+
window.dispatchEvent(
|
|
107
|
+
new CustomEvent( 'elementor/element/render', {
|
|
108
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
109
|
+
} )
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// Assert
|
|
113
|
+
expect( handlerCallback ).not.toHaveBeenCalled();
|
|
114
|
+
} );
|
|
115
|
+
} );
|
|
116
|
+
|
|
117
|
+
describe( 'Settings Parsing', () => {
|
|
118
|
+
it( 'should parse settings from data-e-settings attribute', () => {
|
|
119
|
+
// Arrange
|
|
120
|
+
const ELEMENT_ID = 'element-1';
|
|
121
|
+
const SETTINGS = { color: 'red', size: 'large', enabled: true };
|
|
122
|
+
const handlerCallback = jest.fn();
|
|
123
|
+
|
|
124
|
+
register( {
|
|
125
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
126
|
+
id: 'widget-handler',
|
|
127
|
+
callback: handlerCallback,
|
|
128
|
+
} );
|
|
129
|
+
|
|
130
|
+
const element = document.createElement( 'div' );
|
|
131
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
132
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
133
|
+
element.setAttribute( 'data-e-settings', JSON.stringify( SETTINGS ) );
|
|
134
|
+
document.body.appendChild( element );
|
|
135
|
+
|
|
136
|
+
// Act
|
|
137
|
+
window.dispatchEvent(
|
|
138
|
+
new CustomEvent( 'elementor/element/render', {
|
|
139
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
140
|
+
} )
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Assert
|
|
144
|
+
expect( handlerCallback ).toHaveBeenCalledWith(
|
|
145
|
+
expect.objectContaining( {
|
|
146
|
+
settings: SETTINGS,
|
|
147
|
+
} )
|
|
148
|
+
);
|
|
149
|
+
} );
|
|
150
|
+
|
|
151
|
+
it( 'should handle settings changes on re-render', () => {
|
|
152
|
+
// Arrange
|
|
153
|
+
const ELEMENT_ID = 'element-1';
|
|
154
|
+
const INITIAL_SETTINGS = { value: 0 };
|
|
155
|
+
const UPDATED_SETTINGS = { value: 1 };
|
|
156
|
+
const settingsHistory: Record< string, unknown >[] = [];
|
|
157
|
+
|
|
158
|
+
register( {
|
|
159
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
160
|
+
id: 'widget-handler',
|
|
161
|
+
callback: ( { settings } ) => {
|
|
162
|
+
settingsHistory.push( { ...settings } );
|
|
163
|
+
return undefined;
|
|
164
|
+
},
|
|
165
|
+
} );
|
|
166
|
+
|
|
167
|
+
const element = document.createElement( 'div' );
|
|
168
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
169
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
170
|
+
element.setAttribute( 'data-e-settings', JSON.stringify( INITIAL_SETTINGS ) );
|
|
171
|
+
document.body.appendChild( element );
|
|
172
|
+
|
|
173
|
+
// Act
|
|
174
|
+
window.dispatchEvent(
|
|
175
|
+
new CustomEvent( 'elementor/element/render', {
|
|
176
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
177
|
+
} )
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
element.setAttribute( 'data-e-settings', JSON.stringify( UPDATED_SETTINGS ) );
|
|
181
|
+
|
|
182
|
+
window.dispatchEvent(
|
|
183
|
+
new CustomEvent( 'elementor/element/render', {
|
|
184
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
185
|
+
} )
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
// Assert
|
|
189
|
+
expect( settingsHistory.length ).toBeGreaterThanOrEqual( 2 );
|
|
190
|
+
expect( settingsHistory[ 0 ] ).toEqual( INITIAL_SETTINGS );
|
|
191
|
+
expect( settingsHistory[ settingsHistory.length - 1 ] ).toEqual( UPDATED_SETTINGS );
|
|
192
|
+
} );
|
|
193
|
+
} );
|
|
194
|
+
|
|
195
|
+
describe( 'Cleanup and Unmount', () => {
|
|
196
|
+
it( 'should call unmount callback when element is destroyed', () => {
|
|
197
|
+
// Arrange
|
|
198
|
+
const ELEMENT_ID = 'element-1';
|
|
199
|
+
const unmountCallback = jest.fn();
|
|
200
|
+
|
|
201
|
+
register( {
|
|
202
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
203
|
+
id: 'widget-handler',
|
|
204
|
+
callback: () => unmountCallback,
|
|
205
|
+
} );
|
|
206
|
+
|
|
207
|
+
const element = document.createElement( 'div' );
|
|
208
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
209
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
210
|
+
document.body.appendChild( element );
|
|
211
|
+
|
|
212
|
+
window.dispatchEvent(
|
|
213
|
+
new CustomEvent( 'elementor/element/render', {
|
|
214
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
215
|
+
} )
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Act
|
|
219
|
+
window.dispatchEvent(
|
|
220
|
+
new CustomEvent( 'elementor/element/destroy', {
|
|
221
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE },
|
|
222
|
+
} )
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
// Assert
|
|
226
|
+
expect( unmountCallback ).toHaveBeenCalledTimes( 1 );
|
|
227
|
+
} );
|
|
228
|
+
|
|
229
|
+
it( 'should cleanup on re-render before new initialization', () => {
|
|
230
|
+
// Arrange
|
|
231
|
+
const ELEMENT_ID = 'element-1';
|
|
232
|
+
const lifecycleEvents: string[] = [];
|
|
233
|
+
|
|
234
|
+
register( {
|
|
235
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
236
|
+
id: 'widget-handler',
|
|
237
|
+
callback: () => {
|
|
238
|
+
lifecycleEvents.push( 'init' );
|
|
239
|
+
return () => {
|
|
240
|
+
lifecycleEvents.push( 'cleanup' );
|
|
241
|
+
};
|
|
242
|
+
},
|
|
243
|
+
} );
|
|
244
|
+
|
|
245
|
+
const element = document.createElement( 'div' );
|
|
246
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
247
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
248
|
+
document.body.appendChild( element );
|
|
249
|
+
|
|
250
|
+
// Act
|
|
251
|
+
window.dispatchEvent(
|
|
252
|
+
new CustomEvent( 'elementor/element/render', {
|
|
253
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
254
|
+
} )
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
window.dispatchEvent(
|
|
258
|
+
new CustomEvent( 'elementor/element/render', {
|
|
259
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
260
|
+
} )
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
// Assert
|
|
264
|
+
const initCount = lifecycleEvents.filter( ( e ) => e === 'init' ).length;
|
|
265
|
+
const cleanupCount = lifecycleEvents.filter( ( e ) => e === 'cleanup' ).length;
|
|
266
|
+
|
|
267
|
+
expect( initCount ).toBe( 2 );
|
|
268
|
+
expect( cleanupCount ).toBeGreaterThanOrEqual( 1 );
|
|
269
|
+
expect( lifecycleEvents[ 0 ] ).toBe( 'init' );
|
|
270
|
+
expect( lifecycleEvents[ lifecycleEvents.length - 1 ] ).toBe( 'init' );
|
|
271
|
+
} );
|
|
272
|
+
} );
|
|
273
|
+
|
|
274
|
+
describe( 'AbortSignal Integration', () => {
|
|
275
|
+
it( 'should provide AbortSignal to handler callback', () => {
|
|
276
|
+
// Arrange
|
|
277
|
+
const ELEMENT_ID = 'element-1';
|
|
278
|
+
let receivedSignal: AbortSignal | undefined;
|
|
279
|
+
|
|
280
|
+
register( {
|
|
281
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
282
|
+
id: 'widget-handler',
|
|
283
|
+
callback: ( { signal } ) => {
|
|
284
|
+
receivedSignal = signal;
|
|
285
|
+
return undefined;
|
|
286
|
+
},
|
|
287
|
+
} );
|
|
288
|
+
|
|
289
|
+
const element = document.createElement( 'div' );
|
|
290
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
291
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
292
|
+
document.body.appendChild( element );
|
|
293
|
+
|
|
294
|
+
// Act
|
|
295
|
+
window.dispatchEvent(
|
|
296
|
+
new CustomEvent( 'elementor/element/render', {
|
|
297
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
298
|
+
} )
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Assert
|
|
302
|
+
expect( receivedSignal ).toBeInstanceOf( AbortSignal );
|
|
303
|
+
expect( receivedSignal?.aborted ).toBe( false );
|
|
304
|
+
} );
|
|
305
|
+
|
|
306
|
+
it( 'should abort signal when element is destroyed', () => {
|
|
307
|
+
// Arrange
|
|
308
|
+
const ELEMENT_ID = 'element-1';
|
|
309
|
+
let signalAborted = false;
|
|
310
|
+
|
|
311
|
+
register( {
|
|
312
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
313
|
+
id: 'widget-handler',
|
|
314
|
+
callback: ( { signal } ) => {
|
|
315
|
+
signal.addEventListener( 'abort', () => {
|
|
316
|
+
signalAborted = true;
|
|
317
|
+
} );
|
|
318
|
+
return undefined;
|
|
319
|
+
},
|
|
320
|
+
} );
|
|
321
|
+
|
|
322
|
+
const element = document.createElement( 'div' );
|
|
323
|
+
element.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
324
|
+
element.setAttribute( 'data-id', ELEMENT_ID );
|
|
325
|
+
document.body.appendChild( element );
|
|
326
|
+
|
|
327
|
+
window.dispatchEvent(
|
|
328
|
+
new CustomEvent( 'elementor/element/render', {
|
|
329
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE, element },
|
|
330
|
+
} )
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
// Act
|
|
334
|
+
window.dispatchEvent(
|
|
335
|
+
new CustomEvent( 'elementor/element/destroy', {
|
|
336
|
+
detail: { id: ELEMENT_ID, type: WIDGET_ELEMENT_TYPE },
|
|
337
|
+
} )
|
|
338
|
+
);
|
|
339
|
+
|
|
340
|
+
// Assert
|
|
341
|
+
expect( signalAborted ).toBe( true );
|
|
342
|
+
} );
|
|
343
|
+
} );
|
|
344
|
+
|
|
345
|
+
describe( 'Child Render Bubbling', () => {
|
|
346
|
+
it( 'should trigger callback when child of specified type renders', () => {
|
|
347
|
+
// Arrange
|
|
348
|
+
const PARENT_ID = 'parent-1';
|
|
349
|
+
const CHILD_ID = 'child-1';
|
|
350
|
+
const childRenderCallback = jest.fn();
|
|
351
|
+
|
|
352
|
+
register( {
|
|
353
|
+
elementType: PARENT_ELEMENT_TYPE,
|
|
354
|
+
id: 'parent-handler',
|
|
355
|
+
callback: ( { listenToChildren } ) => {
|
|
356
|
+
listenToChildren( [ CHILD_ELEMENT_TYPE ] ).render( childRenderCallback );
|
|
357
|
+
return undefined;
|
|
358
|
+
},
|
|
359
|
+
} );
|
|
360
|
+
|
|
361
|
+
const parent = document.createElement( 'div' );
|
|
362
|
+
parent.setAttribute( 'data-e-type', PARENT_ELEMENT_TYPE );
|
|
363
|
+
parent.setAttribute( 'data-id', PARENT_ID );
|
|
364
|
+
document.body.appendChild( parent );
|
|
365
|
+
|
|
366
|
+
const child = document.createElement( 'div' );
|
|
367
|
+
child.setAttribute( 'data-e-type', CHILD_ELEMENT_TYPE );
|
|
368
|
+
child.setAttribute( 'data-id', CHILD_ID );
|
|
369
|
+
parent.appendChild( child );
|
|
370
|
+
|
|
371
|
+
// Act
|
|
372
|
+
window.dispatchEvent(
|
|
373
|
+
new CustomEvent( 'elementor/element/render', {
|
|
374
|
+
detail: { id: PARENT_ID, type: PARENT_ELEMENT_TYPE, element: parent },
|
|
375
|
+
} )
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
// Render child - this should bubble up to parent
|
|
379
|
+
window.dispatchEvent(
|
|
380
|
+
new CustomEvent( 'elementor/element/render', {
|
|
381
|
+
detail: { id: CHILD_ID, type: CHILD_ELEMENT_TYPE, element: child },
|
|
382
|
+
} )
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// Assert
|
|
386
|
+
expect( childRenderCallback ).toHaveBeenCalledTimes( 1 );
|
|
387
|
+
} );
|
|
388
|
+
|
|
389
|
+
it( 'should not trigger callback for non-descendant elements', () => {
|
|
390
|
+
// Arrange
|
|
391
|
+
const PARENT_ID = 'parent-1';
|
|
392
|
+
const SIBLING_ID = 'sibling-1';
|
|
393
|
+
const childRenderCallback = jest.fn();
|
|
394
|
+
|
|
395
|
+
register( {
|
|
396
|
+
elementType: PARENT_ELEMENT_TYPE,
|
|
397
|
+
id: 'parent-handler',
|
|
398
|
+
callback: ( { listenToChildren } ) => {
|
|
399
|
+
listenToChildren( [ CHILD_ELEMENT_TYPE ] ).render( childRenderCallback );
|
|
400
|
+
return undefined;
|
|
401
|
+
},
|
|
402
|
+
} );
|
|
403
|
+
|
|
404
|
+
const parent = document.createElement( 'div' );
|
|
405
|
+
parent.setAttribute( 'data-e-type', PARENT_ELEMENT_TYPE );
|
|
406
|
+
parent.setAttribute( 'data-id', PARENT_ID );
|
|
407
|
+
document.body.appendChild( parent );
|
|
408
|
+
|
|
409
|
+
const sibling = document.createElement( 'div' );
|
|
410
|
+
sibling.setAttribute( 'data-e-type', CHILD_ELEMENT_TYPE );
|
|
411
|
+
sibling.setAttribute( 'data-id', SIBLING_ID );
|
|
412
|
+
document.body.appendChild( sibling );
|
|
413
|
+
|
|
414
|
+
// Act
|
|
415
|
+
window.dispatchEvent(
|
|
416
|
+
new CustomEvent( 'elementor/element/render', {
|
|
417
|
+
detail: { id: PARENT_ID, type: PARENT_ELEMENT_TYPE, element: parent },
|
|
418
|
+
} )
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
window.dispatchEvent(
|
|
422
|
+
new CustomEvent( 'elementor/element/render', {
|
|
423
|
+
detail: { id: SIBLING_ID, type: CHILD_ELEMENT_TYPE, element: sibling },
|
|
424
|
+
} )
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Assert
|
|
428
|
+
expect( childRenderCallback ).not.toHaveBeenCalled();
|
|
429
|
+
} );
|
|
430
|
+
|
|
431
|
+
it( 'should cleanup listener on signal abort', () => {
|
|
432
|
+
// Arrange
|
|
433
|
+
const PARENT_ID = 'parent-1';
|
|
434
|
+
const CHILD_ID = 'child-1';
|
|
435
|
+
const childRenderCallback = jest.fn();
|
|
436
|
+
|
|
437
|
+
register( {
|
|
438
|
+
elementType: PARENT_ELEMENT_TYPE,
|
|
439
|
+
id: 'parent-handler',
|
|
440
|
+
callback: ( { listenToChildren } ) => {
|
|
441
|
+
listenToChildren( [ CHILD_ELEMENT_TYPE ] ).render( childRenderCallback );
|
|
442
|
+
return undefined;
|
|
443
|
+
},
|
|
444
|
+
} );
|
|
445
|
+
|
|
446
|
+
const parent = document.createElement( 'div' );
|
|
447
|
+
parent.setAttribute( 'data-e-type', PARENT_ELEMENT_TYPE );
|
|
448
|
+
parent.setAttribute( 'data-id', PARENT_ID );
|
|
449
|
+
document.body.appendChild( parent );
|
|
450
|
+
|
|
451
|
+
const child = document.createElement( 'div' );
|
|
452
|
+
child.setAttribute( 'data-e-type', CHILD_ELEMENT_TYPE );
|
|
453
|
+
child.setAttribute( 'data-id', CHILD_ID );
|
|
454
|
+
parent.appendChild( child );
|
|
455
|
+
|
|
456
|
+
// Render Parent
|
|
457
|
+
window.dispatchEvent(
|
|
458
|
+
new CustomEvent( 'elementor/element/render', {
|
|
459
|
+
detail: { id: PARENT_ID, type: PARENT_ELEMENT_TYPE, element: parent },
|
|
460
|
+
} )
|
|
461
|
+
);
|
|
462
|
+
|
|
463
|
+
// Destroy Parent (should remove listener)
|
|
464
|
+
window.dispatchEvent(
|
|
465
|
+
new CustomEvent( 'elementor/element/destroy', {
|
|
466
|
+
detail: { id: PARENT_ID, type: PARENT_ELEMENT_TYPE },
|
|
467
|
+
} )
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
// Render Child
|
|
471
|
+
window.dispatchEvent(
|
|
472
|
+
new CustomEvent( 'elementor/element/render', {
|
|
473
|
+
detail: { id: CHILD_ID, type: CHILD_ELEMENT_TYPE, element: child },
|
|
474
|
+
} )
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
// Assert
|
|
478
|
+
expect( childRenderCallback ).not.toHaveBeenCalled();
|
|
479
|
+
} );
|
|
480
|
+
} );
|
|
481
|
+
|
|
482
|
+
describe( 'Multiple Element Instances', () => {
|
|
483
|
+
it( 'should handle multiple instances of same element type independently', () => {
|
|
484
|
+
// Arrange
|
|
485
|
+
const PARENT_1_ID = 'parent-1';
|
|
486
|
+
const PARENT_2_ID = 'parent-2';
|
|
487
|
+
const CHILD_1_ID = 'child-1';
|
|
488
|
+
const CHILD_2_ID = 'child-2';
|
|
489
|
+
const callbackCounts = new Map< string, number >();
|
|
490
|
+
|
|
491
|
+
register( {
|
|
492
|
+
elementType: PARENT_ELEMENT_TYPE,
|
|
493
|
+
id: 'parent-handler',
|
|
494
|
+
callback: ( { element, listenToChildren } ) => {
|
|
495
|
+
const parentId = element.getAttribute( 'data-id' ) || 'unknown';
|
|
496
|
+
callbackCounts.set( parentId, 0 );
|
|
497
|
+
|
|
498
|
+
listenToChildren( [ CHILD_ELEMENT_TYPE ] ).render( () => {
|
|
499
|
+
const currentCount = callbackCounts.get( parentId ) || 0;
|
|
500
|
+
callbackCounts.set( parentId, currentCount + 1 );
|
|
501
|
+
} );
|
|
502
|
+
return undefined;
|
|
503
|
+
},
|
|
504
|
+
} );
|
|
505
|
+
|
|
506
|
+
const parent1 = document.createElement( 'div' );
|
|
507
|
+
parent1.setAttribute( 'data-e-type', PARENT_ELEMENT_TYPE );
|
|
508
|
+
parent1.setAttribute( 'data-id', PARENT_1_ID );
|
|
509
|
+
document.body.appendChild( parent1 );
|
|
510
|
+
|
|
511
|
+
const child1 = document.createElement( 'div' );
|
|
512
|
+
child1.setAttribute( 'data-e-type', CHILD_ELEMENT_TYPE );
|
|
513
|
+
child1.setAttribute( 'data-id', CHILD_1_ID );
|
|
514
|
+
parent1.appendChild( child1 );
|
|
515
|
+
|
|
516
|
+
const parent2 = document.createElement( 'div' );
|
|
517
|
+
parent2.setAttribute( 'data-e-type', PARENT_ELEMENT_TYPE );
|
|
518
|
+
parent2.setAttribute( 'data-id', PARENT_2_ID );
|
|
519
|
+
document.body.appendChild( parent2 );
|
|
520
|
+
|
|
521
|
+
const child2 = document.createElement( 'div' );
|
|
522
|
+
child2.setAttribute( 'data-e-type', CHILD_ELEMENT_TYPE );
|
|
523
|
+
child2.setAttribute( 'data-id', CHILD_2_ID );
|
|
524
|
+
parent2.appendChild( child2 );
|
|
525
|
+
|
|
526
|
+
// Act
|
|
527
|
+
[ parent1, child1, parent2, child2 ].forEach( ( element ) => {
|
|
528
|
+
window.dispatchEvent(
|
|
529
|
+
new CustomEvent( 'elementor/element/render', {
|
|
530
|
+
detail: {
|
|
531
|
+
id: element.getAttribute( 'data-id' ),
|
|
532
|
+
type: element.getAttribute( 'data-e-type' ),
|
|
533
|
+
element,
|
|
534
|
+
},
|
|
535
|
+
} )
|
|
536
|
+
);
|
|
537
|
+
} );
|
|
538
|
+
|
|
539
|
+
// Assert
|
|
540
|
+
expect( callbackCounts.get( PARENT_1_ID ) ).toBe( 1 );
|
|
541
|
+
expect( callbackCounts.get( PARENT_2_ID ) ).toBe( 1 );
|
|
542
|
+
} );
|
|
543
|
+
} );
|
|
544
|
+
|
|
545
|
+
describe( 'DOMContentLoaded Initialization', () => {
|
|
546
|
+
it( 'should initialize all existing elements on page load', () => {
|
|
547
|
+
// Arrange
|
|
548
|
+
handlers.clear();
|
|
549
|
+
document.body.innerHTML = '';
|
|
550
|
+
|
|
551
|
+
const initializedElements: string[] = [];
|
|
552
|
+
|
|
553
|
+
register( {
|
|
554
|
+
elementType: PARENT_ELEMENT_TYPE,
|
|
555
|
+
id: 'parent-handler',
|
|
556
|
+
callback: ( { element } ) => {
|
|
557
|
+
const id = element.getAttribute( 'data-id' ) || 'unknown';
|
|
558
|
+
initializedElements.push( id );
|
|
559
|
+
return undefined;
|
|
560
|
+
},
|
|
561
|
+
} );
|
|
562
|
+
|
|
563
|
+
register( {
|
|
564
|
+
elementType: WIDGET_ELEMENT_TYPE,
|
|
565
|
+
id: 'widget-handler',
|
|
566
|
+
callback: ( { element } ) => {
|
|
567
|
+
const id = element.getAttribute( 'data-id' ) || 'unknown';
|
|
568
|
+
initializedElements.push( id );
|
|
569
|
+
return undefined;
|
|
570
|
+
},
|
|
571
|
+
} );
|
|
572
|
+
|
|
573
|
+
const parent = document.createElement( 'div' );
|
|
574
|
+
parent.setAttribute( 'data-e-type', PARENT_ELEMENT_TYPE );
|
|
575
|
+
parent.setAttribute( 'data-id', 'parent-1' );
|
|
576
|
+
document.body.appendChild( parent );
|
|
577
|
+
|
|
578
|
+
const widget = document.createElement( 'div' );
|
|
579
|
+
widget.setAttribute( 'data-e-type', WIDGET_ELEMENT_TYPE );
|
|
580
|
+
widget.setAttribute( 'data-id', 'widget-1' );
|
|
581
|
+
document.body.appendChild( widget );
|
|
582
|
+
|
|
583
|
+
init();
|
|
584
|
+
|
|
585
|
+
// Act
|
|
586
|
+
document.dispatchEvent( new Event( 'DOMContentLoaded' ) );
|
|
587
|
+
|
|
588
|
+
// Assert
|
|
589
|
+
expect( initializedElements ).toContain( 'parent-1' );
|
|
590
|
+
expect( initializedElements ).toContain( 'widget-1' );
|
|
591
|
+
} );
|
|
592
|
+
} );
|
|
593
|
+
} );
|
package/src/handlers-registry.ts
CHANGED
|
@@ -1,4 +1,19 @@
|
|
|
1
|
-
type
|
|
1
|
+
type Settings = Record< string, unknown >;
|
|
2
|
+
|
|
3
|
+
type ChildRenderCallback = () => void;
|
|
4
|
+
|
|
5
|
+
interface ListenToChildrenAPI {
|
|
6
|
+
render: ( callback: ChildRenderCallback ) => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
type ListenToChildrenFunction = ( elementTypes: string[] ) => ListenToChildrenAPI;
|
|
10
|
+
|
|
11
|
+
type Handler = < TSettings extends Settings = Settings >( params: {
|
|
12
|
+
element: Element;
|
|
13
|
+
signal: AbortSignal;
|
|
14
|
+
settings: TSettings;
|
|
15
|
+
listenToChildren: ListenToChildrenFunction;
|
|
16
|
+
} ) => ( () => void ) | undefined;
|
|
2
17
|
|
|
3
18
|
export const handlers: Map< string, Map< string, Handler > > = new Map();
|
|
4
19
|
|
package/src/lifecycle-events.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { handlers } from './handlers-registry';
|
|
|
2
2
|
|
|
3
3
|
const unmountCallbacks: Map< string, Map< string, () => void > > = new Map();
|
|
4
4
|
|
|
5
|
+
const ELEMENT_RENDERED_EVENT_NAME = 'elementor/element/rendered';
|
|
6
|
+
|
|
5
7
|
export const onElementRender = ( {
|
|
6
8
|
element,
|
|
7
9
|
elementType,
|
|
@@ -14,12 +16,50 @@ export const onElementRender = ( {
|
|
|
14
16
|
const controller = new AbortController();
|
|
15
17
|
const manualUnmount: ( () => void )[] = [];
|
|
16
18
|
|
|
19
|
+
element.dispatchEvent(
|
|
20
|
+
new CustomEvent( ELEMENT_RENDERED_EVENT_NAME, {
|
|
21
|
+
bubbles: true,
|
|
22
|
+
detail: {
|
|
23
|
+
element,
|
|
24
|
+
elementType,
|
|
25
|
+
elementId,
|
|
26
|
+
},
|
|
27
|
+
} )
|
|
28
|
+
);
|
|
29
|
+
|
|
17
30
|
if ( ! handlers.has( elementType ) ) {
|
|
18
31
|
return;
|
|
19
32
|
}
|
|
20
33
|
|
|
21
34
|
Array.from( handlers.get( elementType )?.values() ?? [] ).forEach( ( handler ) => {
|
|
22
|
-
const
|
|
35
|
+
const settings = element.getAttribute( 'data-e-settings' );
|
|
36
|
+
|
|
37
|
+
const listenToChildren = ( elementTypes: string[] ) => ( {
|
|
38
|
+
render: ( callback: () => void ) => {
|
|
39
|
+
element.addEventListener(
|
|
40
|
+
ELEMENT_RENDERED_EVENT_NAME,
|
|
41
|
+
( event ) => {
|
|
42
|
+
const { elementType: childType } = ( event as CustomEvent ).detail;
|
|
43
|
+
|
|
44
|
+
if ( ! elementTypes.includes( childType ) ) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
callback();
|
|
49
|
+
|
|
50
|
+
event.stopPropagation();
|
|
51
|
+
},
|
|
52
|
+
{ signal: controller.signal }
|
|
53
|
+
);
|
|
54
|
+
},
|
|
55
|
+
} );
|
|
56
|
+
|
|
57
|
+
const unmount = handler( {
|
|
58
|
+
element,
|
|
59
|
+
signal: controller.signal,
|
|
60
|
+
settings: settings ? JSON.parse( settings ) : {},
|
|
61
|
+
listenToChildren,
|
|
62
|
+
} );
|
|
23
63
|
|
|
24
64
|
if ( typeof unmount === 'function' ) {
|
|
25
65
|
manualUnmount.push( unmount );
|