@elementor/editor-canvas 0.13.1 → 0.15.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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +33 -0
- package/dist/index.d.mts +9 -8
- package/dist/index.d.ts +9 -8
- package/dist/index.js +184 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +192 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -8
- package/src/__tests__/__mocks__/styles-schema.ts +3 -3
- package/src/__tests__/settings-props-resolver.test.ts +1 -1
- package/src/__tests__/styles-prop-resolver.test.ts +7 -7
- package/src/components/__tests__/elements-overlays.test.tsx +40 -35
- package/src/components/element-overlay.tsx +3 -2
- package/src/components/elements-overlays.tsx +26 -9
- package/src/hooks/use-floating-on-element.ts +9 -6
- package/src/init-settings-transformers.ts +1 -5
- package/src/init-style-transformers.ts +2 -6
- package/src/init-styles-renderer.ts +1 -1
- package/src/legacy/__tests__/signalized-process.test.ts +80 -0
- package/src/legacy/create-element-type.ts +2 -2
- package/src/legacy/create-templated-element-type.ts +131 -0
- package/src/legacy/init-legacy-views.ts +7 -1
- package/src/legacy/signalized-process.ts +35 -0
- package/src/legacy/types.ts +27 -3
- package/src/renderers/__tests__/create-dom-renderer.test.ts +66 -0
- package/src/renderers/__tests__/create-props-resolver.test.ts +123 -15
- package/src/renderers/create-dom-renderer.ts +56 -0
- package/src/renderers/create-props-resolver.ts +10 -8
- package/src/renderers/render-styles.ts +4 -0
- package/src/transformers/create-transformers-registry.ts +16 -5
- package/src/transformers/styles/background-image-position-offset-transformer.ts +1 -1
- package/src/transformers/styles/background-image-size-scale-transformer.ts +1 -1
- package/src/transformers/types.ts +1 -5
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { getElements, useSelectedElement } from '@elementor/editor-elements';
|
|
3
|
+
import {
|
|
4
|
+
__privateUseIsRouteActive as useIsRouteActive,
|
|
5
|
+
__privateUseListenTo as useListenTo,
|
|
6
|
+
useEditMode,
|
|
7
|
+
windowEvent,
|
|
8
|
+
} from '@elementor/editor-v1-adapters';
|
|
4
9
|
|
|
5
10
|
import { ElementOverlay } from './element-overlay';
|
|
6
11
|
|
|
7
12
|
export function ElementsOverlays() {
|
|
8
13
|
const selected = useSelectedElement();
|
|
9
|
-
const
|
|
14
|
+
const elements = useElementsDom();
|
|
10
15
|
const currentEditMode = useEditMode();
|
|
11
16
|
|
|
12
17
|
const isEditMode = currentEditMode === 'edit';
|
|
@@ -16,12 +21,24 @@ export function ElementsOverlays() {
|
|
|
16
21
|
|
|
17
22
|
return (
|
|
18
23
|
isActive &&
|
|
19
|
-
|
|
20
|
-
<ElementOverlay
|
|
21
|
-
element={ el }
|
|
22
|
-
key={ el.dataset.id }
|
|
23
|
-
isSelected={ selected.element?.id === el.dataset.id }
|
|
24
|
-
/>
|
|
24
|
+
elements.map( ( [ id, element ] ) => (
|
|
25
|
+
<ElementOverlay key={ id } id={ id } element={ element } isSelected={ selected.element?.id === id } />
|
|
25
26
|
) )
|
|
26
27
|
);
|
|
27
28
|
}
|
|
29
|
+
|
|
30
|
+
const ELEMENTS_DATA_ATTR = 'atomic';
|
|
31
|
+
|
|
32
|
+
type IdElementTuple = [ string, HTMLElement ];
|
|
33
|
+
|
|
34
|
+
function useElementsDom() {
|
|
35
|
+
return useListenTo(
|
|
36
|
+
[ windowEvent( 'elementor/editor/element-rendered' ), windowEvent( 'elementor/editor/element-destroyed' ) ],
|
|
37
|
+
() => {
|
|
38
|
+
return getElements()
|
|
39
|
+
.filter( ( el ) => ELEMENTS_DATA_ATTR in ( el.view?.el?.dataset ?? {} ) )
|
|
40
|
+
.map( ( element ) => [ element.id, element.view?.getDomElement?.()?.get?.( 0 ) ] )
|
|
41
|
+
.filter( ( item ): item is IdElementTuple => !! item[ 1 ] );
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState } from 'react';
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
2
|
import { autoUpdate, offset, size, useFloating } from '@floating-ui/react';
|
|
3
3
|
|
|
4
4
|
type Options = {
|
|
@@ -14,11 +14,7 @@ export function useFloatingOnElement( { element, isSelected }: Options ) {
|
|
|
14
14
|
open: isOpen || isSelected,
|
|
15
15
|
onOpenChange: setIsOpen,
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
whileElementsMounted: ( ...args ) => autoUpdate( ...args, { animationFrame: true } ),
|
|
19
|
-
|
|
20
|
-
// The first element in the canvas is `display: contents` so we need to use the first child.
|
|
21
|
-
elements: { reference: element.firstElementChild },
|
|
17
|
+
whileElementsMounted: autoUpdate,
|
|
22
18
|
|
|
23
19
|
middleware: [
|
|
24
20
|
// Match the floating element's size to the reference element.
|
|
@@ -36,6 +32,13 @@ export function useFloatingOnElement( { element, isSelected }: Options ) {
|
|
|
36
32
|
],
|
|
37
33
|
} );
|
|
38
34
|
|
|
35
|
+
useEffect( () => {
|
|
36
|
+
// Update the reference manually because Floating UI does not recalculate
|
|
37
|
+
// the reference element when it is being used in `option.elements.reference`.
|
|
38
|
+
// @link https://github.com/floating-ui/floating-ui/blob/master/packages/react/src/hooks/useFloatingRootContext.ts
|
|
39
|
+
refs.setReference( element );
|
|
40
|
+
}, [ element, refs ] );
|
|
41
|
+
|
|
39
42
|
return {
|
|
40
43
|
isVisible: isOpen || isSelected,
|
|
41
44
|
context,
|
|
@@ -7,13 +7,9 @@ import { plainTransformer } from './transformers/shared/plain-transformer';
|
|
|
7
7
|
|
|
8
8
|
export function initSettingsTransformers() {
|
|
9
9
|
settingsTransformersRegistry
|
|
10
|
-
.register( 'string', plainTransformer )
|
|
11
|
-
.register( 'url', plainTransformer )
|
|
12
|
-
.register( 'number', plainTransformer )
|
|
13
|
-
.register( 'boolean', plainTransformer )
|
|
14
10
|
.register( 'classes', arrayTransformer )
|
|
15
11
|
.register( 'link', linkTransformer )
|
|
16
12
|
.register( 'image', imageTransformer )
|
|
17
13
|
.register( 'image-src', imageSrcTransformer )
|
|
18
|
-
.
|
|
14
|
+
.registerFallback( plainTransformer );
|
|
19
15
|
}
|
|
@@ -27,10 +27,6 @@ export function initStyleTransformers() {
|
|
|
27
27
|
( { propKey, key } ) => `${ propKey }-${ key }`
|
|
28
28
|
)
|
|
29
29
|
)
|
|
30
|
-
.register( 'color', plainTransformer )
|
|
31
|
-
.register( 'number', plainTransformer )
|
|
32
|
-
.register( 'string', plainTransformer )
|
|
33
|
-
.register( 'url', plainTransformer )
|
|
34
30
|
.register( 'box-shadow', createCombineArrayTransformer( ',' ) )
|
|
35
31
|
.register( 'background', backgroundTransformer )
|
|
36
32
|
.register( 'background-overlay', createCombineArrayTransformer( ',' ) )
|
|
@@ -41,7 +37,6 @@ export function initStyleTransformers() {
|
|
|
41
37
|
.register( 'color-stop', colorStopTransformer )
|
|
42
38
|
.register( 'background-image-position-offset', backgroundImagePositionOffsetTransformer )
|
|
43
39
|
.register( 'background-image-size-scale', backgroundImageSizeScaleTransformer )
|
|
44
|
-
.register( 'image-attachment-id', plainTransformer )
|
|
45
40
|
.register( 'image-src', imageSrcTransformer )
|
|
46
41
|
.register( 'image', imageTransformer )
|
|
47
42
|
.register(
|
|
@@ -61,5 +56,6 @@ export function initStyleTransformers() {
|
|
|
61
56
|
[ 'start-start', 'start-end', 'end-start', 'end-end' ],
|
|
62
57
|
( { key } ) => `border-${ key }-radius`
|
|
63
58
|
)
|
|
64
|
-
)
|
|
59
|
+
)
|
|
60
|
+
.registerFallback( plainTransformer );
|
|
65
61
|
}
|
|
@@ -17,7 +17,7 @@ export function initStylesRenderer() {
|
|
|
17
17
|
let abortController: AbortController | null = null;
|
|
18
18
|
|
|
19
19
|
const resolve = createPropsResolver( {
|
|
20
|
-
transformers: styleTransformersRegistry
|
|
20
|
+
transformers: styleTransformersRegistry,
|
|
21
21
|
schema: getStylesSchema(),
|
|
22
22
|
onPropResolve: enqueueUsedFonts,
|
|
23
23
|
} );
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { signalizedProcess } from '../signalized-process';
|
|
2
|
+
|
|
3
|
+
describe( 'signalizedProcess', () => {
|
|
4
|
+
it( 'should run the steps in order', async () => {
|
|
5
|
+
// Arrange.
|
|
6
|
+
const abortController = new AbortController();
|
|
7
|
+
|
|
8
|
+
let value = '';
|
|
9
|
+
|
|
10
|
+
const process = signalizedProcess( abortController.signal )
|
|
11
|
+
.then( () => {
|
|
12
|
+
value += 'a';
|
|
13
|
+
|
|
14
|
+
return Promise.resolve( 'b' );
|
|
15
|
+
} )
|
|
16
|
+
.then( ( v ) => {
|
|
17
|
+
value += v;
|
|
18
|
+
|
|
19
|
+
return Promise.resolve( 'c' );
|
|
20
|
+
} )
|
|
21
|
+
.then( ( v ) => {
|
|
22
|
+
value += v;
|
|
23
|
+
} );
|
|
24
|
+
|
|
25
|
+
// Act.
|
|
26
|
+
await process.execute();
|
|
27
|
+
|
|
28
|
+
// Assert.
|
|
29
|
+
expect( value ).toBe( 'abc' );
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
it( 'should not execute anything when the signal is aborted before the process has started', async () => {
|
|
33
|
+
// Arrange.
|
|
34
|
+
const abortController = new AbortController();
|
|
35
|
+
|
|
36
|
+
let value = 'initial';
|
|
37
|
+
|
|
38
|
+
const process = signalizedProcess( abortController.signal ).then( () => {
|
|
39
|
+
value = 'updated';
|
|
40
|
+
} );
|
|
41
|
+
|
|
42
|
+
// Act.
|
|
43
|
+
abortController.abort();
|
|
44
|
+
|
|
45
|
+
await process.execute();
|
|
46
|
+
|
|
47
|
+
// Assert.
|
|
48
|
+
expect( value ).toBe( 'initial' );
|
|
49
|
+
} );
|
|
50
|
+
|
|
51
|
+
it( 'should abort all queued steps when the signal is aborted', async () => {
|
|
52
|
+
// Arrange.
|
|
53
|
+
const abortController = new AbortController();
|
|
54
|
+
|
|
55
|
+
let value = '';
|
|
56
|
+
|
|
57
|
+
const process = signalizedProcess( abortController.signal )
|
|
58
|
+
.then( () => {
|
|
59
|
+
value += 'a';
|
|
60
|
+
|
|
61
|
+
return Promise.resolve( 'b' );
|
|
62
|
+
} )
|
|
63
|
+
.then( ( v ) => {
|
|
64
|
+
value += v;
|
|
65
|
+
|
|
66
|
+
abortController.abort();
|
|
67
|
+
|
|
68
|
+
return Promise.resolve( 'c' );
|
|
69
|
+
} )
|
|
70
|
+
.then( ( v ) => {
|
|
71
|
+
value += v;
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
// Act.
|
|
75
|
+
await process.execute();
|
|
76
|
+
|
|
77
|
+
// Assert.
|
|
78
|
+
expect( value ).toBe( 'ab' );
|
|
79
|
+
} );
|
|
80
|
+
} );
|
|
@@ -13,12 +13,12 @@ export function createElementType( type: string ): typeof ElementType {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
getView() {
|
|
16
|
-
return
|
|
16
|
+
return createElementViewClassDeclaration();
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
function
|
|
21
|
+
export function createElementViewClassDeclaration(): typeof ElementView {
|
|
22
22
|
const legacyWindow = window as unknown as LegacyWindow;
|
|
23
23
|
|
|
24
24
|
return class extends legacyWindow.elementor.modules.elements.views.Widget {
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import type { V1ElementConfig } from '@elementor/editor-elements';
|
|
2
|
+
|
|
3
|
+
import { type DomRenderer } from '../renderers/create-dom-renderer';
|
|
4
|
+
import { createPropsResolver, type PropsResolver } from '../renderers/create-props-resolver';
|
|
5
|
+
import { settingsTransformersRegistry } from '../settings-transformers-registry';
|
|
6
|
+
import { createElementViewClassDeclaration } from './create-element-type';
|
|
7
|
+
import { signalizedProcess } from './signalized-process';
|
|
8
|
+
import { type ElementType, type ElementView, type LegacyWindow } from './types';
|
|
9
|
+
|
|
10
|
+
type CreateTypeOptions = {
|
|
11
|
+
type: string;
|
|
12
|
+
renderer: DomRenderer;
|
|
13
|
+
element: TemplatedElementConfig;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type TemplatedElementConfig = Required<
|
|
17
|
+
Pick< V1ElementConfig, 'twig_templates' | 'twig_main_template' | 'atomic_props_schema' | 'base_styles_dictionary' >
|
|
18
|
+
>;
|
|
19
|
+
|
|
20
|
+
export function createTemplatedElementType( { type, renderer, element }: CreateTypeOptions ): typeof ElementType {
|
|
21
|
+
const legacyWindow = window as unknown as LegacyWindow;
|
|
22
|
+
|
|
23
|
+
Object.entries( element.twig_templates ).forEach( ( [ key, template ] ) => {
|
|
24
|
+
renderer.register( key, template );
|
|
25
|
+
} );
|
|
26
|
+
|
|
27
|
+
const propsResolver = createPropsResolver( {
|
|
28
|
+
transformers: settingsTransformersRegistry,
|
|
29
|
+
schema: element.atomic_props_schema,
|
|
30
|
+
} );
|
|
31
|
+
|
|
32
|
+
return class extends legacyWindow.elementor.modules.elements.types.Widget {
|
|
33
|
+
getType() {
|
|
34
|
+
return type;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
getView() {
|
|
38
|
+
return createTemplatedElementViewClassDeclaration( {
|
|
39
|
+
type,
|
|
40
|
+
renderer,
|
|
41
|
+
propsResolver,
|
|
42
|
+
baseStylesDictionary: element.base_styles_dictionary,
|
|
43
|
+
templateKey: element.twig_main_template,
|
|
44
|
+
} );
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function canBeTemplated( element: Partial< TemplatedElementConfig > ): element is TemplatedElementConfig {
|
|
50
|
+
return !! (
|
|
51
|
+
element.atomic_props_schema &&
|
|
52
|
+
element.twig_templates &&
|
|
53
|
+
element.twig_main_template &&
|
|
54
|
+
element.base_styles_dictionary
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
type CreateViewOptions = {
|
|
59
|
+
type: string;
|
|
60
|
+
renderer: DomRenderer;
|
|
61
|
+
propsResolver: PropsResolver;
|
|
62
|
+
templateKey: string;
|
|
63
|
+
baseStylesDictionary: Record< string, string >;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
function createTemplatedElementViewClassDeclaration( {
|
|
67
|
+
type,
|
|
68
|
+
renderer,
|
|
69
|
+
propsResolver: resolveProps,
|
|
70
|
+
templateKey,
|
|
71
|
+
baseStylesDictionary,
|
|
72
|
+
}: CreateViewOptions ): typeof ElementView {
|
|
73
|
+
const BaseView = createElementViewClassDeclaration();
|
|
74
|
+
|
|
75
|
+
return class extends BaseView {
|
|
76
|
+
#abortController: AbortController | null = null;
|
|
77
|
+
|
|
78
|
+
getTemplateType() {
|
|
79
|
+
return 'twig';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
renderOnChange() {
|
|
83
|
+
this.render();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Overriding Marionette original render method to inject our renderer.
|
|
87
|
+
async _renderTemplate() {
|
|
88
|
+
this.#beforeRenderTemplate();
|
|
89
|
+
|
|
90
|
+
this.#abortController?.abort();
|
|
91
|
+
this.#abortController = new AbortController();
|
|
92
|
+
|
|
93
|
+
const process = signalizedProcess( this.#abortController.signal )
|
|
94
|
+
.then( ( _, signal ) => {
|
|
95
|
+
const settings = this.model.get( 'settings' ).toJSON();
|
|
96
|
+
|
|
97
|
+
return resolveProps( {
|
|
98
|
+
props: settings,
|
|
99
|
+
signal,
|
|
100
|
+
} );
|
|
101
|
+
} )
|
|
102
|
+
.then( ( resolvedSettings ) => {
|
|
103
|
+
// Same as the Backend.
|
|
104
|
+
const context = {
|
|
105
|
+
id: this.model.get( 'id' ),
|
|
106
|
+
type,
|
|
107
|
+
settings: resolvedSettings,
|
|
108
|
+
base_styles: baseStylesDictionary,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
return renderer.render( templateKey, context );
|
|
112
|
+
} )
|
|
113
|
+
.then( ( html ) => this.$el.html( html ) );
|
|
114
|
+
|
|
115
|
+
await process.execute();
|
|
116
|
+
|
|
117
|
+
this.#afterRenderTemplate();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Emulating the original Marionette behavior.
|
|
121
|
+
#beforeRenderTemplate() {
|
|
122
|
+
this.triggerMethod( 'before:render:template' );
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
#afterRenderTemplate() {
|
|
126
|
+
this.bindUIElements();
|
|
127
|
+
|
|
128
|
+
this.triggerMethod( 'render:template' );
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { getWidgetsCache } from '@elementor/editor-elements';
|
|
2
2
|
import { __privateListenTo, v1ReadyEvent } from '@elementor/editor-v1-adapters';
|
|
3
3
|
|
|
4
|
+
import { createDomRenderer } from '../renderers/create-dom-renderer';
|
|
4
5
|
import { createElementType } from './create-element-type';
|
|
6
|
+
import { canBeTemplated, createTemplatedElementType } from './create-templated-element-type';
|
|
5
7
|
import type { LegacyWindow } from './types';
|
|
6
8
|
|
|
7
9
|
export function initLegacyViews() {
|
|
@@ -9,12 +11,16 @@ export function initLegacyViews() {
|
|
|
9
11
|
const config = getWidgetsCache() ?? {};
|
|
10
12
|
const legacyWindow = window as unknown as LegacyWindow;
|
|
11
13
|
|
|
14
|
+
const renderer = createDomRenderer();
|
|
15
|
+
|
|
12
16
|
Object.entries( config ).forEach( ( [ type, element ] ) => {
|
|
13
17
|
if ( ! element.atomic ) {
|
|
14
18
|
return;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
const ElementType =
|
|
21
|
+
const ElementType = canBeTemplated( element )
|
|
22
|
+
? createTemplatedElementType( { type, renderer, element } )
|
|
23
|
+
: createElementType( type );
|
|
18
24
|
|
|
19
25
|
legacyWindow.elementor.elementsManager.registerElementType( new ElementType() );
|
|
20
26
|
} );
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2
|
+
type AnyFn = ( ...args: any[] ) => any;
|
|
3
|
+
|
|
4
|
+
type SignalizedProcess< TNextArg = never > = {
|
|
5
|
+
then: < TReturn >(
|
|
6
|
+
cb: ( arg: TNextArg, signal: AbortSignal ) => TReturn
|
|
7
|
+
) => SignalizedProcess< Awaited< TReturn > >;
|
|
8
|
+
|
|
9
|
+
execute: () => Promise< void >;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function signalizedProcess< TNextArg = never >(
|
|
13
|
+
signal: AbortSignal,
|
|
14
|
+
steps: AnyFn[] = []
|
|
15
|
+
): SignalizedProcess< TNextArg > {
|
|
16
|
+
return {
|
|
17
|
+
then: ( cb ) => {
|
|
18
|
+
steps.push( cb );
|
|
19
|
+
|
|
20
|
+
return signalizedProcess( signal, steps );
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
execute: async () => {
|
|
24
|
+
let lastResult: TNextArg | undefined;
|
|
25
|
+
|
|
26
|
+
for ( const step of steps ) {
|
|
27
|
+
if ( signal.aborted ) {
|
|
28
|
+
break;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
lastResult = await step( lastResult, signal );
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
package/src/legacy/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { type Props } from '@elementor/editor-props';
|
|
2
|
+
|
|
1
3
|
export type LegacyWindow = Window & {
|
|
2
4
|
elementor: {
|
|
3
5
|
modules: {
|
|
@@ -25,7 +27,7 @@ export declare class ElementType {
|
|
|
25
27
|
export declare class ElementView {
|
|
26
28
|
$el: JQueryElement;
|
|
27
29
|
|
|
28
|
-
model: BackboneModel
|
|
30
|
+
model: BackboneModel< ElementModel >;
|
|
29
31
|
|
|
30
32
|
onRender( ...args: unknown[] ): void;
|
|
31
33
|
|
|
@@ -40,18 +42,40 @@ export declare class ElementView {
|
|
|
40
42
|
getHandlesOverlay(): JQueryElement | null;
|
|
41
43
|
|
|
42
44
|
getContextMenuGroups(): ContextMenuGroup[];
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Templated view methods:
|
|
48
|
+
*/
|
|
49
|
+
getTemplateType(): string;
|
|
50
|
+
|
|
51
|
+
renderOnChange(): void;
|
|
52
|
+
|
|
53
|
+
render(): void;
|
|
54
|
+
|
|
55
|
+
_renderTemplate(): void;
|
|
56
|
+
|
|
57
|
+
triggerMethod( method: string ): void;
|
|
58
|
+
|
|
59
|
+
bindUIElements(): void;
|
|
43
60
|
}
|
|
44
61
|
|
|
45
62
|
type JQueryElement = {
|
|
46
63
|
find: ( selector: string ) => JQueryElement;
|
|
64
|
+
html: ( html: string ) => void;
|
|
47
65
|
};
|
|
48
66
|
|
|
49
|
-
type BackboneModel = {
|
|
67
|
+
type BackboneModel< Model extends object > = {
|
|
50
68
|
get: < T extends keyof Model >( key: T ) => Model[ T ];
|
|
69
|
+
toJSON: () => ToJSON< Model >;
|
|
51
70
|
};
|
|
52
71
|
|
|
53
|
-
type
|
|
72
|
+
type ElementModel = {
|
|
54
73
|
id: string;
|
|
74
|
+
settings: BackboneModel< Props >;
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
type ToJSON< T > = {
|
|
78
|
+
[ K in keyof T ]: T[ K ] extends BackboneModel< infer M > ? ToJSON< M > : T[ K ];
|
|
55
79
|
};
|
|
56
80
|
|
|
57
81
|
type ContextMenuGroup = {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/* eslint-disable testing-library/render-result-naming-convention */
|
|
2
|
+
import { createDomRenderer } from '../create-dom-renderer';
|
|
3
|
+
|
|
4
|
+
describe( 'createDomRenderer', () => {
|
|
5
|
+
it.each( [
|
|
6
|
+
{
|
|
7
|
+
title: 'basic string',
|
|
8
|
+
template: 'Hello {{ name }}',
|
|
9
|
+
context: { name: 'StyleShit' },
|
|
10
|
+
expected: 'Hello StyleShit',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
title: 'allowed html tags',
|
|
14
|
+
template: `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`,
|
|
15
|
+
context: { tag: 'a' },
|
|
16
|
+
expected: '<a></a>',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
title: 'disallowed html tags',
|
|
20
|
+
template: `<{{ tag | e( 'html_tag' ) }}></{{ tag | e( 'html_tag' ) }}>`,
|
|
21
|
+
context: { tag: 'script' },
|
|
22
|
+
expected: '<div></div>',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
title: 'allowed url (http)',
|
|
26
|
+
template: `{{ url | e( 'full_url' ) }}`,
|
|
27
|
+
context: { url: 'http://localhost/test-page' },
|
|
28
|
+
expected: 'http://localhost/test-page',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
title: 'allowed url (https)',
|
|
32
|
+
template: `{{ url | e( 'full_url' ) }}`,
|
|
33
|
+
context: { url: 'https://localhost/test-page' },
|
|
34
|
+
expected: 'https://localhost/test-page',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
title: 'allowed url (tel)',
|
|
38
|
+
template: `{{ url | e( 'full_url' ) }}`,
|
|
39
|
+
context: { url: 'tel:050-1234567' },
|
|
40
|
+
expected: 'tel:050-1234567',
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
title: 'allowed url (mailto)',
|
|
44
|
+
template: `{{ url | e( 'full_url' ) }}`,
|
|
45
|
+
context: { url: 'mailto:user@example.com' },
|
|
46
|
+
expected: 'mailto:user@example.com',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
title: 'disallowed url',
|
|
50
|
+
template: `{{ url | e( 'full_url' ) }}`,
|
|
51
|
+
context: { url: 'javascript:alert(123)' },
|
|
52
|
+
expected: '',
|
|
53
|
+
},
|
|
54
|
+
] )( 'should render template with $title', async ( { template, context, expected } ) => {
|
|
55
|
+
// Arrange.
|
|
56
|
+
const domRenderer = createDomRenderer();
|
|
57
|
+
|
|
58
|
+
domRenderer.register( 'test-template', template );
|
|
59
|
+
|
|
60
|
+
// Act.
|
|
61
|
+
const result = await domRenderer.render( 'test-template', context );
|
|
62
|
+
|
|
63
|
+
// Assert.
|
|
64
|
+
expect( result ).toBe( expected );
|
|
65
|
+
} );
|
|
66
|
+
} );
|