@elementor/editor-canvas 4.0.0 → 4.0.1

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.
@@ -9,6 +9,7 @@ import { queryTransformer } from './transformers/settings/query-transformer';
9
9
  import { imageSrcTransformer } from './transformers/shared/image-src-transformer';
10
10
  import { imageTransformer } from './transformers/shared/image-transformer';
11
11
  import { plainTransformer } from './transformers/shared/plain-transformer';
12
+ import { svgSrcTransformer } from './transformers/shared/svg-src-transformer';
12
13
  import { videoSrcTransformer } from './transformers/shared/video-src-transformer';
13
14
 
14
15
  export function initSettingsTransformers() {
@@ -18,6 +19,7 @@ export function initSettingsTransformers() {
18
19
  .register( 'query', queryTransformer )
19
20
  .register( 'image', imageTransformer )
20
21
  .register( 'image-src', imageSrcTransformer )
22
+ .register( 'svg-src', svgSrcTransformer )
21
23
  .register( 'video-src', videoSrcTransformer )
22
24
  .register( 'attributes', attributesTransformer )
23
25
  .register( 'date-time', dateTimeTransformer )
@@ -26,6 +26,8 @@ export class ReplacementBase implements ReplacementBaseInterface {
26
26
  protected type: ReplacementSettings[ 'type' ];
27
27
  protected id: ReplacementSettings[ 'id' ];
28
28
  protected refreshView: ReplacementSettings[ 'refreshView' ];
29
+ protected reactRoot: ReplacementSettings[ 'reactRoot' ];
30
+ protected reactContainer: ReplacementSettings[ 'reactContainer' ];
29
31
 
30
32
  constructor( settings: ReplacementSettings ) {
31
33
  this.getSetting = settings.getSetting;
@@ -34,6 +36,8 @@ export class ReplacementBase implements ReplacementBaseInterface {
34
36
  this.type = settings.type;
35
37
  this.id = settings.id;
36
38
  this.refreshView = settings.refreshView;
39
+ this.reactRoot = settings.reactRoot;
40
+ this.reactContainer = settings.reactContainer;
37
41
  }
38
42
 
39
43
  static getTypes(): string[] | null {
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react';
2
- import { useEffect, useLayoutEffect, useState } from 'react';
2
+ import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
3
3
  import { InlineEditor, InlineEditorToolbar } from '@elementor/editor-controls';
4
4
  import { Box, ThemeProvider } from '@elementor/ui';
5
5
  import { autoUpdate, flip, FloatingPortal, useFloating } from '@floating-ui/react';
@@ -9,68 +9,83 @@ import {
9
9
  type Editor,
10
10
  getInlineEditorElement,
11
11
  horizontalShifterMiddleware as horizontalShifter,
12
- removeToolbarAnchor,
13
12
  useOnClickOutsideIframe,
14
13
  useRenderToolbar,
15
14
  } from './inline-editing-utils';
16
15
 
17
- const EDITOR_WRAPPER_SELECTOR = 'inline-editor-wrapper';
18
-
19
16
  export const CanvasInlineEditor = ( {
20
17
  elementClasses,
21
18
  initialValue,
22
19
  expectedTag,
23
20
  rootElement,
21
+ contentElement,
24
22
  id,
25
23
  setValue,
26
- ...props
24
+ requestDestroy,
27
25
  }: {
28
26
  elementClasses: string;
29
27
  initialValue: string | null;
30
28
  expectedTag: string | null;
31
29
  rootElement: HTMLElement;
30
+ contentElement: HTMLElement;
32
31
  id: string;
33
32
  setValue: ( value: string | null ) => void;
34
- onBlur: () => void;
33
+ requestDestroy: () => void;
35
34
  } ) => {
35
+ const [ active, setActive ] = useState( true );
36
36
  const [ editor, setEditor ] = useState< Editor | null >( null );
37
- const { onSelectionEnd, anchor: toolbarAnchor } = useRenderToolbar( rootElement.ownerDocument, id );
37
+ const { onSelectionEnd, anchor: toolbarAnchor, clearAnchor } = useRenderToolbar( rootElement.ownerDocument, id );
38
+
39
+ useEffect( () => {
40
+ if ( ! active ) {
41
+ clearAnchor();
42
+ requestDestroy();
43
+ }
44
+ }, [ active, clearAnchor, requestDestroy ] );
45
+
46
+ const dismiss = useCallback( () => {
47
+ setEditor( null );
48
+ setActive( false );
49
+ }, [] );
38
50
 
39
- const onBlur = () => {
40
- removeToolbarAnchor( rootElement.ownerDocument, id );
51
+ useOnClickOutsideIframe( dismiss );
41
52
 
42
- props.onBlur();
43
- };
53
+ useEffect( () => {
54
+ const ownerDocument = contentElement.ownerDocument;
55
+
56
+ const handleClickAway = ( event: MouseEvent ) => {
57
+ if ( contentElement.contains( event.target as Node ) ) {
58
+ return;
59
+ }
44
60
 
45
- useOnClickOutsideIframe( onBlur );
61
+ dismiss();
62
+ };
63
+
64
+ ownerDocument.addEventListener( 'mousedown', handleClickAway );
65
+
66
+ return () => ownerDocument.removeEventListener( 'mousedown', handleClickAway );
67
+ }, [ contentElement, dismiss ] );
68
+
69
+ if ( ! active ) {
70
+ return null;
71
+ }
46
72
 
47
73
  return (
48
74
  <ThemeProvider>
49
75
  <InlineEditingOverlay expectedTag={ expectedTag } rootElement={ rootElement } id={ id } />
50
- <style>
51
- { `
52
- .ProseMirror > * {
53
- height: 100%;
54
- }
55
- .${ EDITOR_WRAPPER_SELECTOR } .ProseMirror > button[contenteditable="true"] {
56
- height: auto;
57
- cursor: text;
58
- }
59
- ` }
60
- </style>
61
76
  <InlineEditor
62
77
  onEditorCreate={ setEditor }
78
+ mountElement={ contentElement }
63
79
  editorProps={ {
64
80
  attributes: {
65
- style: 'outline: none;overflow-wrap: normal;height:100%',
81
+ style: 'outline: none; display: inherit; justify-content: inherit; align-items: inherit; flex-direction: inherit; text-align: inherit;',
66
82
  },
67
83
  } }
68
84
  elementClasses={ elementClasses }
69
85
  value={ initialValue }
70
86
  setValue={ setValue }
71
- onBlur={ onBlur }
87
+ onBlur={ dismiss }
72
88
  autofocus
73
- expectedTag={ expectedTag }
74
89
  onSelectionEnd={ onSelectionEnd }
75
90
  />
76
91
  { toolbarAnchor && editor && <InlineEditingToolbar anchor={ toolbarAnchor } editor={ editor } id={ id } /> }
@@ -114,7 +129,14 @@ const InlineEditingToolbar = ( { anchor, editor, id }: { anchor: HTMLElement; ed
114
129
 
115
130
  return (
116
131
  <FloatingPortal id={ CANVAS_WRAPPER_ID }>
117
- <Box ref={ refs.setFloating } role="presentation" style={ { ...floatingStyles, pointerEvents: 'none' } }>
132
+ <Box
133
+ ref={ refs.setFloating }
134
+ role="presentation"
135
+ style={ {
136
+ ...floatingStyles,
137
+ pointerEvents: 'none',
138
+ } }
139
+ >
118
140
  <InlineEditorToolbar editor={ editor } elementId={ id } />
119
141
  </Box>
120
142
  </FloatingPortal>
@@ -1,5 +1,4 @@
1
1
  import * as React from 'react';
2
- import { createRoot, type Root } from 'react-dom/client';
3
2
  import { getContainer, getElementLabel, getElementType } from '@elementor/editor-elements';
4
3
  import {
5
4
  htmlV3PropTypeUtil,
@@ -25,8 +24,8 @@ type TagPropType = PropType< 'tag' > & {
25
24
  const HISTORY_DEBOUNCE_WAIT = 800;
26
25
 
27
26
  export default class InlineEditingReplacement extends ReplacementBase {
28
- private inlineEditorRoot: Root | null = null;
29
27
  private handlerAttached = false;
28
+ private editing = false;
30
29
 
31
30
  getReplacementKey() {
32
31
  return 'inline-editing';
@@ -37,7 +36,7 @@ export default class InlineEditingReplacement extends ReplacementBase {
37
36
  }
38
37
 
39
38
  isEditingModeActive() {
40
- return !! this.inlineEditorRoot;
39
+ return this.editing;
41
40
  }
42
41
 
43
42
  shouldRenderReplacement() {
@@ -91,8 +90,8 @@ export default class InlineEditingReplacement extends ReplacementBase {
91
90
  resetInlineEditorRoot() {
92
91
  this.element.removeEventListener( 'click', this.handleRenderInlineEditor );
93
92
  this.handlerAttached = false;
94
- this.inlineEditorRoot?.unmount?.();
95
- this.inlineEditorRoot = null;
93
+ this.reactRoot.render( null );
94
+ this.editing = false;
96
95
  }
97
96
 
98
97
  unmountInlineEditor() {
@@ -239,22 +238,29 @@ export default class InlineEditingReplacement extends ReplacementBase {
239
238
  this.resetInlineEditorRoot();
240
239
  }
241
240
 
242
- const elementClasses = this.element.children?.[ 0 ]?.classList.toString() ?? '';
241
+ const contentElement = this.element.children?.[ 0 ] as HTMLElement | undefined;
242
+
243
+ if ( ! contentElement ) {
244
+ return;
245
+ }
246
+
247
+ const elementClasses = contentElement.classList.toString();
243
248
  const propValue = this.getExtractedContentValue();
244
249
  const expectedTag = this.getExpectedTag();
245
250
 
246
- this.element.innerHTML = '';
251
+ contentElement.innerHTML = '';
252
+ this.editing = true;
247
253
 
248
- this.inlineEditorRoot = createRoot( this.element );
249
- this.inlineEditorRoot.render(
254
+ this.reactRoot.render(
250
255
  <CanvasInlineEditor
251
256
  elementClasses={ elementClasses }
252
257
  initialValue={ propValue }
253
258
  expectedTag={ expectedTag }
254
259
  rootElement={ this.element }
260
+ contentElement={ contentElement }
255
261
  id={ this.id }
256
262
  setValue={ this.setContentValue.bind( this ) }
257
- onBlur={ this.unmountInlineEditor.bind( this ) }
263
+ requestDestroy={ this.unmountInlineEditor.bind( this ) }
258
264
  />
259
265
  );
260
266
  }
@@ -43,6 +43,7 @@ export const INLINE_EDITING_PROPERTY_PER_TYPE: Record< string, string > = {
43
43
  'e-form-label': 'text',
44
44
  'e-heading': 'title',
45
45
  'e-paragraph': 'paragraph',
46
+ 'e-form-submit-button': 'text',
46
47
  };
47
48
 
48
49
  export const legacyWindow = window as unknown as LegacyWindow;
@@ -79,6 +80,12 @@ export const useOnClickOutsideIframe = ( handleUnmount: () => void ) => {
79
80
  export const useRenderToolbar = ( ownerDocument: Document, id: string ) => {
80
81
  const [ anchor, setAnchor ] = useState< HTMLElement | null >( null );
81
82
 
83
+ useEffect( () => {
84
+ if ( ! anchor ) {
85
+ removeToolbarAnchor( ownerDocument, id );
86
+ }
87
+ }, [ anchor, ownerDocument, id ] );
88
+
82
89
  const onSelectionEnd = ( view: EditorView ) => {
83
90
  const hasSelection = ! view.state.selection.empty;
84
91
 
@@ -91,7 +98,11 @@ export const useRenderToolbar = ( ownerDocument: Document, id: string ) => {
91
98
  }
92
99
  };
93
100
 
94
- return { onSelectionEnd, anchor };
101
+ const clearAnchor = useCallback( () => {
102
+ setAnchor( null );
103
+ }, [] );
104
+
105
+ return { onSelectionEnd, anchor, clearAnchor };
95
106
  };
96
107
 
97
108
  const createAnchorBasedOnSelection = ( ownerDocument: Document, id: string ): HTMLElement | null => {
@@ -1,3 +1,5 @@
1
+ import { createRoot, type Root } from 'react-dom/client';
2
+
1
3
  import type { CreateTemplatedElementTypeOptions } from '../create-templated-element-type';
2
4
  import { createTemplatedElementView } from '../create-templated-element-type';
3
5
  import type { ElementType, ElementView, LegacyWindow, ReplacementSettings } from '../types';
@@ -34,11 +36,18 @@ export const createViewWithReplacements = ( options: CreateTemplatedElementTypeO
34
36
  return class extends TemplatedView {
35
37
  #replacement: ReplacementBaseInterface | null = null;
36
38
  #config: ReplacementSettings;
39
+ #reactContainer: HTMLElement;
40
+ #reactRoot: Root;
37
41
 
38
42
  constructor( ...args: unknown[] ) {
39
43
  super( ...args );
40
44
  const settings = this.model.get( 'settings' );
41
45
 
46
+ this.#reactContainer = this.el.ownerDocument.createElement( 'div' );
47
+ this.#reactContainer.style.display = 'none';
48
+ this.el.ownerDocument.body.appendChild( this.#reactContainer );
49
+ this.#reactRoot = createRoot( this.#reactContainer );
50
+
42
51
  this.#config = {
43
52
  getSetting: settings.get.bind( settings ),
44
53
  setSetting: settings.set.bind( settings ),
@@ -46,6 +55,8 @@ export const createViewWithReplacements = ( options: CreateTemplatedElementTypeO
46
55
  type: this?.model?.get( 'widgetType' ) ?? this.container?.model?.get( 'elType' ) ?? null,
47
56
  id: this?.model?.get( 'id' ) ?? null,
48
57
  refreshView: this.refreshView.bind( this ),
58
+ reactRoot: this.#reactRoot,
59
+ reactContainer: this.#reactContainer,
49
60
  };
50
61
  }
51
62
 
@@ -72,6 +83,8 @@ export const createViewWithReplacements = ( options: CreateTemplatedElementTypeO
72
83
 
73
84
  onDestroy() {
74
85
  this.#triggerAltMethod( 'onDestroy' );
86
+ this.#reactRoot.unmount();
87
+ this.#reactContainer.remove();
75
88
  }
76
89
 
77
90
  _afterRender() {
@@ -1,3 +1,4 @@
1
+ import { type Root } from 'react-dom/client';
1
2
  import { type V1Element } from '@elementor/editor-elements';
2
3
  import { type Props, type PropValue } from '@elementor/editor-props';
3
4
 
@@ -241,4 +242,6 @@ export type ReplacementSettings = {
241
242
  id: string;
242
243
  element: HTMLElement;
243
244
  refreshView: () => void;
245
+ reactRoot: Root;
246
+ reactContainer: HTMLElement;
244
247
  };
@@ -0,0 +1,184 @@
1
+ import { getMediaAttachment } from '@elementor/wp-media';
2
+
3
+ import { svgSrcTransformer } from '../svg-src-transformer';
4
+
5
+ jest.mock( '@elementor/wp-media' );
6
+
7
+ const mockedGetMediaAttachment = jest.mocked( getMediaAttachment );
8
+
9
+ const SAMPLE_SVG = '<svg xmlns="http://www.w3.org/2000/svg" width="100" height="100"><path d="M0 0h100v100H0z"/></svg>';
10
+
11
+ const mockFetch = ( body: string, contentType = 'image/svg+xml', ok = true ) => {
12
+ global.fetch = jest.fn().mockResolvedValue( {
13
+ ok,
14
+ headers: new Headers( { 'content-type': contentType } ),
15
+ text: () => Promise.resolve( body ),
16
+ } );
17
+ };
18
+
19
+ describe( 'svgSrcTransformer', () => {
20
+ beforeEach( () => {
21
+ jest.clearAllMocks();
22
+ } );
23
+
24
+ afterEach( () => {
25
+ jest.restoreAllMocks();
26
+ } );
27
+
28
+ it( 'fetches SVG and returns processed html with currentColor fill', async () => {
29
+ // Arrange.
30
+ mockFetch( SAMPLE_SVG );
31
+
32
+ // Act.
33
+ const result = await svgSrcTransformer( { id: null, url: 'https://example.com/icon.svg' }, { key: 'svg' } );
34
+
35
+ // Assert.
36
+ expect( result ).toEqual( {
37
+ html: expect.stringContaining( 'fill="currentColor"' ),
38
+ url: 'https://example.com/icon.svg',
39
+ } );
40
+ } );
41
+
42
+ it( 'adds inline styles to the svg element', async () => {
43
+ // Arrange.
44
+ mockFetch( SAMPLE_SVG );
45
+
46
+ // Act.
47
+ const result = ( await svgSrcTransformer(
48
+ { id: null, url: 'https://example.com/icon.svg' },
49
+ { key: 'svg' }
50
+ ) ) as { html: string; url: string };
51
+
52
+ // Assert.
53
+ expect( result.html ).toContain( 'width: 100%' );
54
+ expect( result.html ).toContain( 'height: 100%' );
55
+ expect( result.html ).toContain( 'overflow: unset' );
56
+ } );
57
+
58
+ it( 'returns null html when url is null', async () => {
59
+ // Arrange & Act.
60
+ const result = await svgSrcTransformer( { id: null, url: null }, { key: 'svg' } );
61
+
62
+ // Assert.
63
+ expect( result ).toEqual( { html: null, url: null } );
64
+ expect( mockedGetMediaAttachment ).not.toHaveBeenCalled();
65
+ } );
66
+
67
+ it( 'resolves url from attachment id then fetches SVG', async () => {
68
+ // Arrange.
69
+ mockedGetMediaAttachment.mockResolvedValue( {
70
+ url: 'https://example.com/resolved.svg',
71
+ } as never );
72
+ mockFetch( SAMPLE_SVG );
73
+
74
+ // Act.
75
+ const result = await svgSrcTransformer( { id: 42, url: null }, { key: 'svg' } );
76
+
77
+ // Assert.
78
+ expect( mockedGetMediaAttachment ).toHaveBeenCalledWith( { id: 42 } );
79
+ expect( result ).toEqual( {
80
+ html: expect.stringContaining( 'fill="currentColor"' ),
81
+ url: 'https://example.com/resolved.svg',
82
+ } );
83
+ } );
84
+
85
+ it( 'falls back to provided url when attachment lookup fails', async () => {
86
+ // Arrange.
87
+ mockedGetMediaAttachment.mockResolvedValue( null as never );
88
+ mockFetch( SAMPLE_SVG );
89
+
90
+ // Act.
91
+ const result = ( await svgSrcTransformer(
92
+ { id: 99, url: 'https://example.com/fallback.svg' },
93
+ { key: 'svg' }
94
+ ) ) as { html: string; url: string };
95
+
96
+ // Assert.
97
+ expect( result.url ).toBe( 'https://example.com/fallback.svg' );
98
+ expect( result.html ).toContain( 'fill="currentColor"' );
99
+ } );
100
+
101
+ it( 'returns null html when id is set but attachment and url are missing', async () => {
102
+ // Arrange.
103
+ mockedGetMediaAttachment.mockResolvedValue( null as never );
104
+
105
+ // Act.
106
+ const result = await svgSrcTransformer( { id: 7, url: null }, { key: 'svg' } );
107
+
108
+ // Assert.
109
+ expect( result ).toEqual( { html: null, url: null } );
110
+ } );
111
+
112
+ it( 'returns null html when fetch fails', async () => {
113
+ // Arrange.
114
+ mockFetch( '', 'image/svg+xml', false );
115
+
116
+ // Act.
117
+ const result = await svgSrcTransformer( { id: null, url: 'https://example.com/missing.svg' }, { key: 'svg' } );
118
+
119
+ // Assert.
120
+ expect( result ).toEqual( {
121
+ html: null,
122
+ url: 'https://example.com/missing.svg',
123
+ } );
124
+ } );
125
+
126
+ it( 'returns null html when content is not SVG', async () => {
127
+ // Arrange.
128
+ mockFetch( '<html><body>Not SVG</body></html>', 'text/html' );
129
+
130
+ // Act.
131
+ const result = await svgSrcTransformer( { id: null, url: 'https://example.com/page.html' }, { key: 'svg' } );
132
+
133
+ // Assert.
134
+ expect( result ).toEqual( {
135
+ html: null,
136
+ url: 'https://example.com/page.html',
137
+ } );
138
+ } );
139
+
140
+ it( 'merges with existing style attribute', async () => {
141
+ // Arrange.
142
+ const svgWithStyle = '<svg xmlns="http://www.w3.org/2000/svg" style="display: block"><path d="M0 0"/></svg>';
143
+ mockFetch( svgWithStyle );
144
+
145
+ // Act.
146
+ const result = ( await svgSrcTransformer(
147
+ { id: null, url: 'https://example.com/styled.svg' },
148
+ { key: 'svg' }
149
+ ) ) as { html: string; url: string };
150
+
151
+ // Assert.
152
+ expect( result.html ).toContain( 'display: block' );
153
+ expect( result.html ).toContain( 'width: 100%' );
154
+ } );
155
+
156
+ it( 'returns null html when response has no svg element', async () => {
157
+ // Arrange.
158
+ mockFetch( '<div>Not an SVG</div>' );
159
+
160
+ // Act.
161
+ const result = await svgSrcTransformer( { id: null, url: 'https://example.com/not-svg.svg' }, { key: 'svg' } );
162
+
163
+ // Assert.
164
+ expect( result ).toEqual( {
165
+ html: null,
166
+ url: 'https://example.com/not-svg.svg',
167
+ } );
168
+ } );
169
+
170
+ it( 'passes abort signal to fetch', async () => {
171
+ // Arrange.
172
+ mockFetch( SAMPLE_SVG );
173
+ const controller = new AbortController();
174
+
175
+ // Act.
176
+ await svgSrcTransformer(
177
+ { id: null, url: 'https://example.com/icon.svg' },
178
+ { key: 'svg', signal: controller.signal }
179
+ );
180
+
181
+ // Assert.
182
+ expect( global.fetch ).toHaveBeenCalledWith( 'https://example.com/icon.svg', { signal: controller.signal } );
183
+ } );
184
+ } );
@@ -0,0 +1,87 @@
1
+ import DOMPurify from 'dompurify';
2
+ import { getMediaAttachment } from '@elementor/wp-media';
3
+
4
+ import { createTransformer } from '../create-transformer';
5
+ import type { TransformerOptions } from '../types';
6
+
7
+ type SvgSrc = {
8
+ id?: unknown;
9
+ url?: unknown;
10
+ };
11
+
12
+ const SVG_INLINE_STYLES = 'width: 100%; height: 100%; overflow: unset;';
13
+
14
+ function processSvgContent( svgText: string ): string | null {
15
+ const sanitized = DOMPurify.sanitize( svgText, {
16
+ USE_PROFILES: { svg: true, svgFilters: true },
17
+ } );
18
+
19
+ const parser = new DOMParser();
20
+ const doc = parser.parseFromString( sanitized, 'image/svg+xml' );
21
+ const svgElement = doc.querySelector( 'svg' );
22
+
23
+ if ( ! svgElement ) {
24
+ return null;
25
+ }
26
+
27
+ svgElement.setAttribute( 'fill', 'currentColor' );
28
+
29
+ const existingStyle = svgElement.getAttribute( 'style' ) ?? '';
30
+ const trimmed = existingStyle.trim();
31
+ const merged = trimmed ? `${ trimmed.replace( /;$/, '' ) }; ${ SVG_INLINE_STYLES }` : SVG_INLINE_STYLES;
32
+ svgElement.setAttribute( 'style', merged );
33
+
34
+ return svgElement.outerHTML;
35
+ }
36
+
37
+ async function fetchSvgContent( url: string, signal?: AbortSignal ): Promise< string | null > {
38
+ try {
39
+ const response = await fetch( url, { signal } );
40
+
41
+ if ( ! response.ok ) {
42
+ return null;
43
+ }
44
+
45
+ const contentType = response.headers.get( 'content-type' ) ?? '';
46
+ const isSvg = contentType.includes( 'svg' ) || contentType.includes( 'xml' ) || url.endsWith( '.svg' );
47
+
48
+ if ( ! isSvg ) {
49
+ return null;
50
+ }
51
+
52
+ return await response.text();
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ function resolveSvgSrcId( id: unknown ): number | null {
59
+ if ( typeof id !== 'number' || id <= 0 ) {
60
+ return null;
61
+ }
62
+
63
+ return id;
64
+ }
65
+
66
+ export const svgSrcTransformer = createTransformer( async ( value: SvgSrc, { signal }: TransformerOptions ) => {
67
+ const id = resolveSvgSrcId( value.id );
68
+ const urlFromValue = typeof value.url === 'string' ? value.url : null;
69
+
70
+ let url: string | null | undefined = urlFromValue;
71
+
72
+ if ( id && ! urlFromValue ) {
73
+ const attachment = await getMediaAttachment( { id } );
74
+ url = attachment?.url ?? null;
75
+ }
76
+
77
+ const resolvedUrl = typeof url === 'string' ? url : null;
78
+
79
+ if ( ! resolvedUrl ) {
80
+ return { html: null, url: null };
81
+ }
82
+
83
+ const svgText = await fetchSvgContent( resolvedUrl, signal );
84
+ const html = svgText ? processSvgContent( svgText ) : null;
85
+
86
+ return { html, url: resolvedUrl };
87
+ } );