From 594c3f4eacae7ff0ff13e02f4a237ee215cc0aaa Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:51:16 +0100 Subject: [PATCH] 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. --- .../media-picker.tiptap-toolbar-api.ts | 80 +++++++++++++------ .../src/packages/tiptap/externals.ts | 4 + 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/media-picker/media-picker.tiptap-toolbar-api.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/media-picker/media-picker.tiptap-toolbar-api.ts index f07fd64530..2de61bc239 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/media-picker/media-picker.tiptap-toolbar-api.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/extensions/media-picker/media-picker.tiptap-toolbar-api.ts @@ -1,4 +1,5 @@ -import type { Editor } from '../../externals.js'; +import type { Editor, ProseMirrorNode } from '../../externals.js'; +import { NodeSelection } from '../../externals.js'; import { UmbTiptapToolbarElementApiBase } from '../tiptap-toolbar-element-api-base.js'; import { getGuidFromUdi, imageSize } from '@umbraco-cms/backoffice/utils'; import { ImageCropModeModel } from '@umbraco-cms/backoffice/external/backend-api'; @@ -41,31 +42,21 @@ export default class UmbTiptapToolbarMediaPickerToolbarExtensionApi extends UmbT override async execute(editor: Editor) { const currentTarget = editor.getAttributes('image'); - const figure = editor.getAttributes('figure'); + const currentMediaUdi = this.#extractMediaUdi(currentTarget); + const currentAltText = currentTarget?.alt; + const currentCaption = this.#extractCaption(editor.state.selection); - let currentMediaUdi: string | undefined = undefined; - if (currentTarget?.['data-udi']) { - currentMediaUdi = getGuidFromUdi(currentTarget['data-udi']); - } + await this.#updateImageWithMetadata(editor, currentMediaUdi, currentAltText, currentCaption); + } - let currentAltText: string | undefined = undefined; - if (currentTarget?.alt) { - currentAltText = currentTarget.alt; - } - - let currentCaption: string | undefined = undefined; - if (figure?.figcaption) { - currentCaption = figure.figcaption; - } - - const selection = await this.#openMediaPicker(currentMediaUdi); - if (!selection?.length) return; - - const mediaGuid = selection[0]; - - if (!mediaGuid) { - throw new Error('No media selected'); - } + async #updateImageWithMetadata( + editor: Editor, + currentMediaUdi: string | undefined, + currentAltText: string | undefined, + currentCaption: string | undefined, + ) { + const mediaGuid = await this.#getMediaGuid(currentMediaUdi); + if (!mediaGuid) return; const media = await this.#showMediaCaptionAltText(mediaGuid, currentAltText, currentCaption); if (!media) return; @@ -73,6 +64,47 @@ export default class UmbTiptapToolbarMediaPickerToolbarExtensionApi extends UmbT this.#insertInEditor(editor, mediaGuid, media); } + #extractMediaUdi(imageAttributes: Record): string | undefined { + return imageAttributes?.['data-udi'] ? getGuidFromUdi(imageAttributes['data-udi'] as string) : undefined; + } + + #extractCaption(selection: unknown): string | undefined { + if (!(selection instanceof NodeSelection)) return undefined; + if (selection.node.type.name !== 'figure') return undefined; + + return this.#findFigcaptionText(selection.node); + } + + #findFigcaptionText(figureNode: ProseMirrorNode): string | undefined { + let caption: string | undefined; + figureNode.descendants((child) => { + if (child.type.name === 'figcaption') { + caption = child.textContent || undefined; + return false; // Stop searching + } + return true; // Continue searching + }); + return caption; + } + + async #getMediaGuid(currentMediaUdi?: string): Promise { + if (currentMediaUdi) { + // Image already exists, go directly to edit alt text/caption + return currentMediaUdi; + } + + // No image selected, open media picker + const selection = await this.#openMediaPicker(); + if (!selection?.length) return undefined; + + const selectedGuid = selection[0]; + if (!selectedGuid) { + throw new Error('No media selected'); + } + + return selectedGuid; + } + async #openMediaPicker(currentMediaUdi?: string) { const modalHandler = this.#modalManager?.open(this, UMB_MEDIA_PICKER_MODAL, { data: { diff --git a/src/Umbraco.Web.UI.Client/src/packages/tiptap/externals.ts b/src/Umbraco.Web.UI.Client/src/packages/tiptap/externals.ts index a34560b3f8..e848d669d1 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/tiptap/externals.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/tiptap/externals.ts @@ -14,6 +14,10 @@ export { HardBreak } from '@tiptap/extension-hard-break'; export { Paragraph } from '@tiptap/extension-paragraph'; export { Text } from '@tiptap/extension-text'; +// PROSEMIRROR TYPES +export { NodeSelection } from '@tiptap/pm/state'; +export type { Node as ProseMirrorNode } from '@tiptap/pm/model'; + // OPTIONAL EXTENSIONS export { Blockquote } from '@tiptap/extension-blockquote'; export { Bold } from '@tiptap/extension-bold';