@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 CHANGED
@@ -1,6 +1,14 @@
1
- type Handler = (params: {
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 Handler = (params: {
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 unmount = handler({ element, signal: controller.signal });
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 unmount = handler({ element, signal: controller.signal });
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
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@elementor/frontend-handlers",
3
3
  "description": "Elementor Frontend Handlers",
4
- "version": "3.33.0-99",
4
+ "version": "3.35.0-324",
5
5
  "private": false,
6
6
  "author": "Elementor Team",
7
7
  "homepage": "https://elementor.com/",
@@ -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
+ } );
@@ -1,4 +1,19 @@
1
- type Handler = ( params: { element: Element; signal: AbortSignal } ) => ( () => void ) | undefined;
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
 
@@ -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 unmount = handler( { element, signal: controller.signal } );
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 );