@elementor/editor-canvas 3.35.0-359 → 3.35.0-361

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.
@@ -1,553 +0,0 @@
1
- import { type ExtendedWindow, type V1Element } from '@elementor/editor-elements';
2
- import { getElementType } from '@elementor/editor-elements';
3
- import { isExperimentActive } from '@elementor/editor-v1-adapters';
4
-
5
- import {
6
- getBlockedValue,
7
- getHtmlPropertyName,
8
- getHtmlPropType,
9
- shouldRenderInlineEditingView,
10
- } from '../inline-editing-utils';
11
-
12
- type MockContainer = Pick< V1Element, 'id' | 'model' >;
13
-
14
- jest.mock( '@elementor/editor-v1-adapters', () => ( {
15
- ...jest.requireActual( '@elementor/editor-v1-adapters' ),
16
- isExperimentActive: jest.fn(),
17
- } ) );
18
-
19
- jest.mock( '@elementor/editor-elements', () => ( {
20
- ...jest.requireActual( '@elementor/editor-elements' ),
21
- getElementType: jest.fn(),
22
- } ) );
23
-
24
- const TEST_ELEMENT_ID = '1-heading';
25
-
26
- describe( 'inline-editing-utils', () => {
27
- describe( 'shouldRenderInlineEditingView', () => {
28
- it( 'should return true if the element type is in the widget property map and the experiment is active', () => {
29
- // Arrange.
30
- jest.mocked( isExperimentActive ).mockReturnValue( true );
31
-
32
- // Act & Assert.
33
- expect( shouldRenderInlineEditingView( 'e-heading' ) ).toBe( true );
34
- } );
35
-
36
- it( 'should return false if the element type is not in the widget property map', () => {
37
- // Arrange.
38
- jest.mocked( isExperimentActive ).mockReturnValue( true );
39
-
40
- // Act & Assert.
41
- expect( shouldRenderInlineEditingView( 'e-not-supported' ) ).toBe( false );
42
- } );
43
-
44
- it( 'should return false if the experiment is not active', () => {
45
- // Arrange.
46
- jest.mocked( isExperimentActive ).mockReturnValue( false );
47
-
48
- // Act & Assert.
49
- expect( shouldRenderInlineEditingView( 'e-heading' ) ).toBe( false );
50
- } );
51
- } );
52
-
53
- describe( 'getHtmlPropertyName', () => {
54
- let elementContainer: MockContainer | null = null;
55
- const mockGetContainer = jest.fn().mockImplementation( () => {
56
- return elementContainer;
57
- } );
58
-
59
- beforeEach( () => {
60
- const extendedWindow = window as unknown as ExtendedWindow;
61
-
62
- extendedWindow.elementor = {
63
- getContainer: mockGetContainer,
64
- };
65
- } );
66
-
67
- it( 'should return the html property name if the element type is in the widget property map', () => {
68
- // Arrange.
69
- elementContainer = createMockContainer( 'widget', {
70
- widgetType: 'e-heading',
71
- } );
72
-
73
- // Act.
74
- const htmlPropertyName = getHtmlPropertyName( elementContainer as V1Element );
75
-
76
- // Assert.
77
- expect( htmlPropertyName ).toBe( 'title' );
78
- } );
79
-
80
- it( 'should return the html property name if the element type is not in the widget property map: plain propType', () => {
81
- // Arrange.
82
- elementContainer = createMockContainer( 'widget', {
83
- widgetType: 'e-not-supported',
84
- } );
85
- jest.mocked( getElementType ).mockReturnValue( {
86
- key: 'e-not-supported',
87
- controls: [],
88
- propsSchema: {
89
- editMe: {
90
- kind: 'plain',
91
- key: 'html',
92
- settings: {},
93
- meta: {},
94
- },
95
- },
96
- title: 'Not Supported',
97
- } );
98
-
99
- // Act.
100
- const htmlPropertyName = getHtmlPropertyName( elementContainer as V1Element );
101
-
102
- // Assert.
103
- expect( htmlPropertyName ).toBe( 'editMe' );
104
- } );
105
-
106
- it( 'should return the html property name if the element type is not in the widget property map: object propType', () => {
107
- // Arrange.
108
- elementContainer = createMockContainer( 'widget', {
109
- widgetType: 'e-not-supported',
110
- } );
111
- jest.mocked( getElementType ).mockReturnValue( {
112
- key: 'e-not-supported',
113
- controls: [],
114
- propsSchema: {
115
- editMe: {
116
- kind: 'object',
117
- key: 'Prop Key',
118
- settings: {},
119
- meta: {},
120
- shape: {
121
- nonHtml: {
122
- kind: 'plain',
123
- key: 'nonHtml',
124
- settings: {},
125
- meta: {},
126
- },
127
- html: {
128
- kind: 'plain',
129
- key: 'html',
130
- settings: {},
131
- meta: {},
132
- },
133
- },
134
- },
135
- },
136
- title: 'Not Supported',
137
- } );
138
-
139
- // Act.
140
- const htmlPropertyName = getHtmlPropertyName( elementContainer as V1Element );
141
-
142
- // Assert.
143
- expect( htmlPropertyName ).toBe( 'editMe' );
144
- } );
145
-
146
- it( 'should return the html property name if the element type is not in the widget property map: array propType', () => {
147
- // Arrange.
148
- elementContainer = createMockContainer( 'widget', {
149
- widgetType: 'e-not-supported',
150
- } );
151
- jest.mocked( getElementType ).mockReturnValue( {
152
- key: 'e-not-supported',
153
- controls: [],
154
- propsSchema: {
155
- editMe: {
156
- kind: 'array',
157
- key: 'Prop Key',
158
- settings: {},
159
- meta: {},
160
- item_prop_type: {
161
- kind: 'plain',
162
- key: 'html',
163
- settings: {},
164
- meta: {},
165
- },
166
- },
167
- },
168
- title: 'Not Supported',
169
- } );
170
-
171
- // Act.
172
- const htmlPropertyName = getHtmlPropertyName( elementContainer as V1Element );
173
-
174
- // Assert.
175
- expect( htmlPropertyName ).toBe( 'editMe' );
176
- } );
177
-
178
- it( 'should return the html property name if the element type is not in the widget property map: union propType', () => {
179
- // Arrange.
180
- elementContainer = createMockContainer( 'widget', {
181
- widgetType: 'e-not-supported',
182
- } );
183
- jest.mocked( getElementType ).mockReturnValue( {
184
- key: 'e-not-supported',
185
- controls: [],
186
- propsSchema: {
187
- editMe: {
188
- kind: 'union',
189
- key: 'Prop Key',
190
- settings: {},
191
- meta: {},
192
- prop_types: {
193
- nonHtml: {
194
- kind: 'plain',
195
- key: 'nonHtml',
196
- settings: {},
197
- meta: {},
198
- },
199
- html: {
200
- kind: 'plain',
201
- key: 'html',
202
- settings: {},
203
- meta: {},
204
- },
205
- },
206
- },
207
- },
208
- title: 'Not Supported',
209
- } );
210
-
211
- // Act.
212
- const htmlPropertyName = getHtmlPropertyName( elementContainer as V1Element );
213
-
214
- // Assert.
215
- expect( htmlPropertyName ).toBe( 'editMe' );
216
- } );
217
- } );
218
-
219
- describe( 'getHtmlPropType', () => {
220
- let elementContainer: MockContainer | null = null;
221
- const mockGetContainer = jest.fn().mockImplementation( () => {
222
- return elementContainer;
223
- } );
224
-
225
- beforeEach( () => {
226
- const extendedWindow = window as unknown as ExtendedWindow;
227
-
228
- extendedWindow.elementor = {
229
- getContainer: mockGetContainer,
230
- };
231
- } );
232
-
233
- it( 'should return the html propType if the element type is in the widget property map', () => {
234
- // Arrange.
235
- elementContainer = createMockContainer( 'widget', {
236
- widgetType: 'e-paragraph',
237
- } );
238
- jest.mocked( getElementType ).mockReturnValue( {
239
- key: 'e-paragraph',
240
- controls: [],
241
- propsSchema: {
242
- paragraph: {
243
- kind: 'plain',
244
- key: 'html',
245
- settings: {},
246
- meta: {},
247
- },
248
- },
249
- title: 'Not Supported',
250
- } );
251
-
252
- // Act.
253
- const htmlPropType = getHtmlPropType( elementContainer as V1Element );
254
-
255
- // Assert.
256
- expect( htmlPropType ).toBeTruthy();
257
- } );
258
-
259
- it( 'should return the html propType if the element type is not in the widget property map', () => {
260
- // Arrange.
261
- elementContainer = createMockContainer( 'widget', {
262
- widgetType: 'e-not-supported',
263
- } );
264
- jest.mocked( getElementType ).mockReturnValue( {
265
- key: 'e-not-supported',
266
- controls: [],
267
- propsSchema: {
268
- editMe: {
269
- kind: 'plain',
270
- key: 'html',
271
- settings: {},
272
- meta: {},
273
- },
274
- },
275
- title: 'Not Supported',
276
- } );
277
-
278
- // Act.
279
- const htmlPropType = getHtmlPropType( elementContainer as V1Element );
280
-
281
- // Assert.
282
- expect( htmlPropType ).toBeTruthy();
283
- } );
284
-
285
- it( 'should return null if the element type is not in the widget property map', () => {
286
- // Arrange.
287
- elementContainer = createMockContainer( 'widget', {
288
- widgetType: 'e-not-supported',
289
- } );
290
- jest.mocked( getElementType ).mockReturnValue( {
291
- key: 'e-not-supported',
292
- controls: [],
293
- propsSchema: {
294
- editMe: {
295
- kind: 'plain',
296
- key: 'notHtml',
297
- settings: {},
298
- meta: {},
299
- },
300
- },
301
- title: 'Not Supported',
302
- } );
303
-
304
- // Act.
305
- const htmlPropType = getHtmlPropType( elementContainer as V1Element );
306
-
307
- // Assert.
308
- expect( htmlPropType ).toBeNull();
309
- } );
310
- } );
311
-
312
- describe( 'getBlockedValue', () => {
313
- it( 'should return empty string when value is null', () => {
314
- // Arrange.
315
- const value = null;
316
- const expectedTag = 'div';
317
-
318
- // Act.
319
- const result = getBlockedValue( value, expectedTag );
320
-
321
- // Assert.
322
- expect( result ).toBe( '' );
323
- } );
324
-
325
- it( 'should return empty string when value is empty string', () => {
326
- // Arrange.
327
- const value = '';
328
- const expectedTag = 'div';
329
-
330
- // Act.
331
- const result = getBlockedValue( value, expectedTag );
332
-
333
- // Assert.
334
- expect( result ).toBe( '' );
335
- } );
336
-
337
- it( 'should return value as-is when expectedTag is null', () => {
338
- // Arrange.
339
- const value = '<p>Some content</p>';
340
- const expectedTag = null;
341
-
342
- // Act.
343
- const result = getBlockedValue( value, expectedTag );
344
-
345
- // Assert.
346
- expect( result ).toBe( '<p>Some content</p>' );
347
- } );
348
-
349
- it( 'should return value as-is when expectedTag is empty string', () => {
350
- // Arrange.
351
- const value = '<p>Some content</p>';
352
- const expectedTag = '';
353
-
354
- // Act.
355
- const result = getBlockedValue( value, expectedTag );
356
-
357
- // Assert.
358
- expect( result ).toBe( '<p>Some content</p>' );
359
- } );
360
-
361
- it( 'should wrap plain text with expectedTag when no HTML elements exist', () => {
362
- // Arrange.
363
- const value = 'Plain text content';
364
- const expectedTag = 'h2';
365
-
366
- // Act.
367
- const result = getBlockedValue( value, expectedTag );
368
-
369
- // Assert.
370
- expect( result ).toBe( '<h2>Plain text content</h2>' );
371
- } );
372
-
373
- it( 'should return value as-is when single child matches expectedTag', () => {
374
- // Arrange.
375
- const value = '<h2>Heading text</h2>';
376
- const expectedTag = 'h2';
377
-
378
- // Act.
379
- const result = getBlockedValue( value, expectedTag );
380
-
381
- // Assert.
382
- expect( result ).toBe( '<h2>Heading text</h2>' );
383
- } );
384
-
385
- it( 'should replace when single child has different tag', () => {
386
- // Arrange.
387
- const value = '<p>Some content</p>';
388
- const expectedTag = 'h2';
389
-
390
- // Act.
391
- const result = getBlockedValue( value, expectedTag );
392
-
393
- // Assert.
394
- expect( result ).toBe( '<h2>Some content</h2>' );
395
- } );
396
-
397
- it( 'should wrap entire content when multiple children exist', () => {
398
- // Arrange.
399
- const value = '<p>First paragraph</p><p>Second paragraph</p>';
400
- const expectedTag = 'div';
401
-
402
- // Act.
403
- const result = getBlockedValue( value, expectedTag );
404
-
405
- // Assert.
406
- expect( result ).toBe( '<div><p>First paragraph</p><p>Second paragraph</p></div>' );
407
- } );
408
-
409
- it( 'should wrap content when it does not start with expectedTag', () => {
410
- // Arrange.
411
- const value = '<span>Content</span><h2>More content</h2>';
412
- const expectedTag = 'h2';
413
-
414
- // Act.
415
- const result = getBlockedValue( value, expectedTag );
416
-
417
- // Assert.
418
- expect( result ).toBe( '<h2><span>Content</span><h2>More content</h2></h2>' );
419
- } );
420
-
421
- it( 'should wrap content when it does not end with expectedTag', () => {
422
- // Arrange.
423
- const value = '<h2>Content</h2><span>More content</span>';
424
- const expectedTag = 'h2';
425
-
426
- // Act.
427
- const result = getBlockedValue( value, expectedTag );
428
-
429
- // Assert.
430
- expect( result ).toBe( '<h2><h2>Content</h2><span>More content</span></h2>' );
431
- } );
432
-
433
- it( 'should handle case-insensitive tag comparison', () => {
434
- // Arrange.
435
- const value = '<H2>Heading text</H2>';
436
- const expectedTag = 'h2';
437
-
438
- // Act.
439
- const result = getBlockedValue( value, expectedTag );
440
-
441
- // Assert.
442
- expect( result ).toBe( '<H2>Heading text</H2>' );
443
- } );
444
-
445
- it( 'should preserve inner HTML when unwrapping and rewrapping, with expected tag', () => {
446
- // Arrange.
447
- const value = '<p><strong>Bold</strong> and <em>italic</em></p>';
448
- const expectedTag = 'h2';
449
-
450
- // Act.
451
- const result = getBlockedValue( value, expectedTag );
452
-
453
- // Assert.
454
- expect( result ).toBe( '<h2><strong>Bold</strong> and <em>italic</em></h2>' );
455
- } );
456
-
457
- it( 'should handle nested elements correctly when single child matches expectedTag', () => {
458
- // Arrange.
459
- const value = '<h2><span>Nested content</span></h2>';
460
- const expectedTag = 'h2';
461
-
462
- // Act.
463
- const result = getBlockedValue( value, expectedTag );
464
-
465
- // Assert.
466
- expect( result ).toBe( '<h2><span>Nested content</span></h2>' );
467
- } );
468
-
469
- it( 'should handle self-closing tags in content', () => {
470
- // Arrange.
471
- const value = 'Text with <br> break';
472
- const expectedTag = 'p';
473
-
474
- // Act.
475
- const result = getBlockedValue( value, expectedTag );
476
-
477
- // Assert.
478
- expect( result ).toBe( '<p>Text with <br> break</p>' );
479
- } );
480
-
481
- it( 'should wrap content when first child matches but last child does not', () => {
482
- // Arrange.
483
- const value = '<h2>First</h2><p>Second</p>';
484
- const expectedTag = 'h2';
485
-
486
- // Act.
487
- const result = getBlockedValue( value, expectedTag );
488
-
489
- // Assert.
490
- expect( result ).toBe( '<h2><h2>First</h2><p>Second</p></h2>' );
491
- } );
492
-
493
- it( 'should handle whitespace-only content', () => {
494
- // Arrange.
495
- const value = ' ';
496
- const expectedTag = 'p';
497
-
498
- // Act.
499
- const result = getBlockedValue( value, expectedTag );
500
-
501
- // Assert.
502
- expect( result ).toBe( '<p> </p>' );
503
- } );
504
-
505
- it( 'should handle special characters in content', () => {
506
- // Arrange.
507
- const value = 'Content with & < > " \' characters';
508
- const expectedTag = 'p';
509
-
510
- // Act.
511
- const result = getBlockedValue( value, expectedTag );
512
-
513
- // Assert.
514
- expect( result ).toBe( '<p>Content with & < > " \' characters</p>' );
515
- } );
516
-
517
- it( 'should wrap content when edges are wrapped correctly but the rest is not', () => {
518
- // Arrange.
519
- const value = '<h2>this</h2> is <h2>cool</h2>';
520
- const expectedTag = 'h2';
521
-
522
- // Act.
523
- const result = getBlockedValue( value, expectedTag );
524
-
525
- // Assert.
526
- expect( result ).toBe( '<h2><h2>this</h2> is <h2>cool</h2></h2>' );
527
- } );
528
- } );
529
- } );
530
-
531
- function createMockContainer( elType: string, elementOverrides: Record< string, unknown > = {} ): MockContainer {
532
- const elementData = {
533
- id: TEST_ELEMENT_ID,
534
- elType,
535
- settings: {},
536
- ...elementOverrides,
537
- };
538
-
539
- return {
540
- id: TEST_ELEMENT_ID,
541
- model: {
542
- get: ( key: string ) => {
543
- if ( key === 'elType' ) {
544
- return elType;
545
- }
546
-
547
- return elementData[ key as keyof typeof elementData ];
548
- },
549
- set: jest.fn(),
550
- toJSON: () => elementData,
551
- },
552
- } as MockContainer;
553
- }
@@ -1,138 +0,0 @@
1
- import { getContainer, getElementType, type V1Element } from '@elementor/editor-elements';
2
- import { isExperimentActive } from '@elementor/editor-v1-adapters';
3
-
4
- import { type LegacyWindow } from '../legacy/types';
5
-
6
- const WIDGET_PROPERTY_MAP: Record< string, string > = {
7
- 'e-heading': 'title',
8
- 'e-paragraph': 'paragraph',
9
- };
10
-
11
- const EXPERIMENT_KEY = 'v4-inline-text-editing';
12
-
13
- export const legacyWindow = window as unknown as LegacyWindow;
14
-
15
- export const shouldRenderInlineEditingView = ( elementType: string ): boolean => {
16
- return elementType in WIDGET_PROPERTY_MAP && isExperimentActive( EXPERIMENT_KEY );
17
- };
18
-
19
- export const getWidgetType = ( container: V1Element | null ) => {
20
- return container?.model?.get( 'widgetType' ) ?? container?.model?.get( 'elType' ) ?? null;
21
- };
22
-
23
- export const getHtmlPropertyName = ( container: V1Element | null ) => {
24
- const widgetType = getWidgetType( container );
25
-
26
- if ( ! widgetType ) {
27
- return '';
28
- }
29
-
30
- const propsSchema = getElementType( widgetType )?.propsSchema;
31
-
32
- if ( WIDGET_PROPERTY_MAP[ widgetType ] ) {
33
- return WIDGET_PROPERTY_MAP[ widgetType ];
34
- }
35
-
36
- if ( ! propsSchema ) {
37
- return '';
38
- }
39
-
40
- const entry = Object.entries( propsSchema ).find( ( [ , propType ] ) => {
41
- switch ( propType.kind ) {
42
- case 'union':
43
- return propType.prop_types.html;
44
- case 'object':
45
- return propType.shape.html;
46
- case 'array':
47
- return 'key' in propType.item_prop_type && propType.item_prop_type.key === 'html';
48
- }
49
-
50
- return propType.key === 'html';
51
- } );
52
-
53
- return entry?.[ 0 ] ?? '';
54
- };
55
-
56
- export const getHtmlPropType = ( container: V1Element | null ) => {
57
- const widgetType = getWidgetType( container );
58
-
59
- if ( ! widgetType ) {
60
- return null;
61
- }
62
-
63
- const propsSchema = getElementType( widgetType )?.propsSchema;
64
- const propertyName = getHtmlPropertyName( container ) ?? null;
65
-
66
- return propsSchema?.[ propertyName ] ?? null;
67
- };
68
-
69
- export const hasInlineEditableProperty = ( containerId: string ): boolean => {
70
- const container = getContainer( containerId );
71
- const widgetType = container?.model?.get( 'widgetType' ) ?? container?.model?.get( 'elType' );
72
-
73
- if ( ! widgetType ) {
74
- return false;
75
- }
76
-
77
- return widgetType in WIDGET_PROPERTY_MAP;
78
- };
79
-
80
- export const getInlineEditablePropertyName = ( container: V1Element | null ): string => {
81
- return getHtmlPropertyName( container ) ?? '';
82
- };
83
-
84
- export const getBlockedValue = ( value: string | null, tag: string | null ) => {
85
- if ( ! value ) {
86
- return '';
87
- }
88
-
89
- if ( ! tag ) {
90
- return value;
91
- }
92
-
93
- const pseudoElement = document.createElement( 'div' );
94
-
95
- pseudoElement.innerHTML = value;
96
-
97
- if ( ! pseudoElement?.children.length ) {
98
- return `<${ tag }>${ value }</${ tag }>`;
99
- }
100
-
101
- const firstChild = pseudoElement.children[ 0 ];
102
- const lastChild = Array.from( pseudoElement.children ).slice( -1 )[ 0 ];
103
-
104
- if ( firstChild === lastChild && pseudoElement.textContent === firstChild.textContent ) {
105
- return compareTag( firstChild, tag ) ? value : `<${ tag }>${ firstChild.innerHTML }</${ tag }>`;
106
- }
107
-
108
- if ( ! value.startsWith( `<${ tag }` ) || ! value.endsWith( `</${ tag }>` ) ) {
109
- return `<${ tag }>${ value }</${ tag }>`;
110
- }
111
-
112
- if ( firstChild !== lastChild || ! compareTag( firstChild, tag ) ) {
113
- return `<${ tag }>${ value }</${ tag }>`;
114
- }
115
-
116
- return value;
117
- };
118
-
119
- export const compareTag = ( el: Element, tag: string ) => {
120
- return el.tagName.toUpperCase() === tag.toUpperCase();
121
- };
122
-
123
- export const getInitialPopoverPosition = () => {
124
- const positionFallback = { left: 0, top: 0 };
125
-
126
- const iFrameElement = legacyWindow?.elementor?.$preview?.get( 0 );
127
- const iFramePosition = iFrameElement?.getBoundingClientRect() ?? positionFallback;
128
-
129
- const previewElement = legacyWindow?.elementor?.$previewWrapper?.get( 0 );
130
- const previewPosition = previewElement
131
- ? { left: previewElement.scrollLeft, top: previewElement.scrollTop }
132
- : positionFallback;
133
-
134
- return {
135
- left: iFramePosition.left + previewPosition.left,
136
- top: iFramePosition.top + previewPosition.top,
137
- };
138
- };