Skip to content

Commit 594c3f4

Browse files
authored
Rich Text Editor: The media picker skips the "edit media" dialog when editing an image (closes #20066) (#20740)
* fix: Tiptap Media Picker: Skip media picker modal when editing existing images Fixes the media picker workflow to match v13 behavior where clicking an existing image directly opens the alt text/caption editor instead of forcing users to re-select the same image from the media library. Also fixes caption text extraction to properly read from the figcaption node using Tiptap's NodeSelection API instead of unreliable attribute-based approach. Changes: - Skip media picker when currentMediaUdi exists (lines 77-92) - Extract caption from NodeSelection.node using descendants() (lines 55-73) - Add NodeSelection export to tiptap externals for proper typing * Refactor: Extract nested logic from media picker execute method Reduces cyclomatic complexity from 15 to 1 by extracting conditional logic into focused private helper methods. Addresses CodeScene warnings for complex method and nested conditionals (bumpy road smell). Created helper methods: - #extractMediaUdi, #extractCaption, #findFigcaptionText - #getMediaGuid, #updateImageWithMetadata No functional changes - improves maintainability and testability.
1 parent 72d7ed4 commit 594c3f4

File tree

2 files changed

+58
-22
lines changed

2 files changed

+58
-22
lines changed

src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/media-picker/media-picker.tiptap-toolbar-api.ts

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import type { Editor } from '../../externals.js';
1+
import type { Editor, ProseMirrorNode } from '../../externals.js';
2+
import { NodeSelection } from '../../externals.js';
23
import { UmbTiptapToolbarElementApiBase } from '../tiptap-toolbar-element-api-base.js';
34
import { getGuidFromUdi, imageSize } from '@umbraco-cms/backoffice/utils';
45
import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api';
@@ -41,36 +42,67 @@ export default class UmbTiptapToolbarMediaPickerToolbarExtensionApi extends UmbT
4142

4243
override async execute(editor: Editor) {
4344
const currentTarget = editor.getAttributes('image');
44-
const figure = editor.getAttributes('figure');
45+
const currentMediaUdi = this.#extractMediaUdi(currentTarget);
46+
const currentAltText = currentTarget?.alt;
47+
const currentCaption = this.#extractCaption(editor.state.selection);
4548

46-
let currentMediaUdi: string | undefined = undefined;
47-
if (currentTarget?.['data-udi']) {
48-
currentMediaUdi = getGuidFromUdi(currentTarget['data-udi']);
49-
}
49+
await this.#updateImageWithMetadata(editor, currentMediaUdi, currentAltText, currentCaption);
50+
}
5051

51-
let currentAltText: string | undefined = undefined;
52-
if (currentTarget?.alt) {
53-
currentAltText = currentTarget.alt;
54-
}
52+
async #updateImageWithMetadata(
53+
editor: Editor,
54+
currentMediaUdi: string | undefined,
55+
currentAltText: string | undefined,
56+
currentCaption: string | undefined,
57+
) {
58+
const mediaGuid = await this.#getMediaGuid(currentMediaUdi);
59+
if (!mediaGuid) return;
5560

56-
let currentCaption: string | undefined = undefined;
57-
if (figure?.figcaption) {
58-
currentCaption = figure.figcaption;
59-
}
61+
const media = await this.#showMediaCaptionAltText(mediaGuid, currentAltText, currentCaption);
62+
if (!media) return;
6063

61-
const selection = await this.#openMediaPicker(currentMediaUdi);
62-
if (!selection?.length) return;
64+
this.#insertInEditor(editor, mediaGuid, media);
65+
}
6366

64-
const mediaGuid = selection[0];
67+
#extractMediaUdi(imageAttributes: Record<string, unknown>): string | undefined {
68+
return imageAttributes?.['data-udi'] ? getGuidFromUdi(imageAttributes['data-udi'] as string) : undefined;
69+
}
6570

66-
if (!mediaGuid) {
67-
throw new Error('No media selected');
71+
#extractCaption(selection: unknown): string | undefined {
72+
if (!(selection instanceof NodeSelection)) return undefined;
73+
if (selection.node.type.name !== 'figure') return undefined;
74+
75+
return this.#findFigcaptionText(selection.node);
76+
}
77+
78+
#findFigcaptionText(figureNode: ProseMirrorNode): string | undefined {
79+
let caption: string | undefined;
80+
figureNode.descendants((child) => {
81+
if (child.type.name === 'figcaption') {
82+
caption = child.textContent || undefined;
83+
return false; // Stop searching
84+
}
85+
return true; // Continue searching
86+
});
87+
return caption;
88+
}
89+
90+
async #getMediaGuid(currentMediaUdi?: string): Promise<string | undefined> {
91+
if (currentMediaUdi) {
92+
// Image already exists, go directly to edit alt text/caption
93+
return currentMediaUdi;
6894
}
6995

70-
const media = await this.#showMediaCaptionAltText(mediaGuid, currentAltText, currentCaption);
71-
if (!media) return;
96+
// No image selected, open media picker
97+
const selection = await this.#openMediaPicker();
98+
if (!selection?.length) return undefined;
7299

73-
this.#insertInEditor(editor, mediaGuid, media);
100+
const selectedGuid = selection[0];
101+
if (!selectedGuid) {
102+
throw new Error('No media selected');
103+
}
104+
105+
return selectedGuid;
74106
}
75107

76108
async #openMediaPicker(currentMediaUdi?: string) {

src/Umbraco.Web.UI.Client/src/packages/tiptap/externals.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ export { HardBreak } from '@tiptap/extension-hard-break';
1414
export { Paragraph } from '@tiptap/extension-paragraph';
1515
export { Text } from '@tiptap/extension-text';
1616

17+
// PROSEMIRROR TYPES
18+
export { NodeSelection } from '@tiptap/pm/state';
19+
export type { Node as ProseMirrorNode } from '@tiptap/pm/model';
20+
1721
// OPTIONAL EXTENSIONS
1822
export { Blockquote } from '@tiptap/extension-blockquote';
1923
export { Bold } from '@tiptap/extension-bold';

0 commit comments

Comments
 (0)