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.
This commit is contained in:
@@ -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, unknown>): 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<string | undefined> {
|
||||
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: {
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user