Skip to content

Commit 0be9db4

Browse files
authored
fix(inline-edit): disable TinyMCE URL conversion to preserve absolute asset paths on blur (#35172)
Fixes #34807 ## Problem When a user clicks on an image inside the Rich Text inline editor and then clicks outside (triggering `blur`), TinyMCE's default `convert_urls: true` behavior rewrites root-relative asset paths: - **Before blur:** `/dA/9bf3ad75d5/dog 2.webp?language_id=1` - **After blur:** `../dA/9bf3ad75d5/dog 2.webp?language_id=1` This happens because the editor runs inside an iframe whose base URL differs from the site root, so TinyMCE incorrectly calculates a relative path on `getContent()`. The result is broken images whose severity depends on the page's URL depth. ## Root Cause `convert_urls` defaults to `true` in TinyMCE. When the editor serializes content on blur, it resolves all URLs relative to the iframe's base — not the site root — silently corrupting absolute paths. ## Solution Set `convert_urls: false` in the shared TinyMCE base options. This tells TinyMCE to leave all URLs exactly as authored, which is the correct behavior for a CMS where asset paths are always root-relative. The base options are extracted into an exported constant (`INLINE_EDIT_TINYMCE_BASE_OPTIONS`) so the URL-handling behavior is covered by unit tests independently of the editor lifecycle. ## Testing - Open a Rich Text contentlet with an image using an absolute path (`/dA/...`) - Click the image to focus the inline editor - Click outside to trigger blur - Verify the `src` attribute is unchanged in both the DOM and after saving ### Video
1 parent de786d6 commit 0be9db4

2 files changed

Lines changed: 40 additions & 9 deletions

File tree

core-web/libs/portlets/edit-ema/portlet/src/lib/services/inline-edit/inline-edit.service.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,30 @@ describe('InlineEditService', () => {
1212

1313
beforeEach(() => (spectator = createService()));
1414

15+
it.each([
16+
{ mode: 'minimal', label: 'minimal' },
17+
{ mode: 'full', label: 'full' },
18+
{ mode: '', label: 'default (minimal)' }
19+
])('should pass convert_urls: false to tinymce.init ($label)', ({ mode }) => {
20+
const initMock = jest.fn().mockResolvedValue([]);
21+
const iframeWindow = {
22+
tinymce: { init: initMock }
23+
} as unknown as Window;
24+
25+
spectator.service.setIframeWindow(iframeWindow);
26+
spectator.service.setTargetInlineMCEDataset({
27+
inode: '1',
28+
fieldName: 'body',
29+
language: 'en',
30+
mode
31+
});
32+
33+
spectator.service.initEditor();
34+
35+
expect(initMock).toHaveBeenCalledTimes(1);
36+
expect(initMock.mock.calls[0][0].convert_urls).toBe(false);
37+
});
38+
1539
it('should inject inline edit', () => {
1640
const iframe = document.createElement('iframe');
1741
document.body.appendChild(iframe);

core-web/libs/portlets/edit-ema/portlet/src/lib/services/inline-edit/inline-edit.service.ts

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,19 @@ export const INLINE_CONTENT_STYLES = `
3030
}
3131
`;
3232

33+
/** Shared TinyMCE options for EMA inline editing (excluding per-instance `setup`). */
34+
export const INLINE_EDIT_TINYMCE_BASE_OPTIONS = {
35+
menubar: false,
36+
inline: true,
37+
// Avoid rewriting root-relative URLs (e.g. /dA/...) to ../... on serialize; iframe base differs from site root.
38+
convert_urls: false,
39+
valid_styles: {
40+
'*': 'font-size,font-family,color,text-decoration,text-align'
41+
},
42+
powerpaste_word_import: 'clean',
43+
powerpaste_html_import: 'clean'
44+
} as const;
45+
3346
@Injectable({
3447
providedIn: 'root'
3548
})
@@ -39,17 +52,11 @@ export class InlineEditService {
3952
private $inlineEditingTargetDataset = signal<InlineEditingContentletDataset | null>(null);
4053

4154
private readonly DEFAULT_TINYMCE_CONFIG = {
42-
menubar: false,
43-
inline: true,
44-
valid_styles: {
45-
'*': 'font-size,font-family,color,text-decoration,text-align'
46-
},
47-
powerpaste_word_import: 'clean',
48-
powerpaste_html_import: 'clean',
55+
...INLINE_EDIT_TINYMCE_BASE_OPTIONS,
4956
setup: this.#handleInlineEditEvents
5057
};
5158

52-
private readonly TINYCME_CONFIG = {
59+
private readonly TINYMCE_CONFIG = {
5360
minimal: {
5461
plugins: ['link', 'autolink'],
5562
toolbar: 'bold italic underline | link',
@@ -162,7 +169,7 @@ export class InlineEditService {
162169

163170
this.$iframeWindow()
164171
.tinymce.init({
165-
...this.TINYCME_CONFIG[dataset.mode || 'minimal'],
172+
...this.TINYMCE_CONFIG[dataset.mode || 'minimal'],
166173
selector: dataSelector
167174
})
168175
.then(([ed]) => {

0 commit comments

Comments
 (0)