Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type {StoryObj} from '@storybook/react';

import {ImgResolveSrcDemo as component} from './ImgResolveSrc';

export const Story: StoryObj<typeof component> = {
args: {},
};
Story.storyName = 'Resolve Image Src';

export default {
title: 'Examples / Resolve Image Src',
component,
};
48 changes: 48 additions & 0 deletions demo/src/stories/examples/img-resolve-src/ImgResolveSrc.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {memo} from 'react';

import {MarkdownEditorView, useMarkdownEditor} from '@gravity-ui/markdown-editor';

import {PlaygroundLayout} from '../../../components/PlaygroundLayout';

const INITIAL_MARKUP = `![Avatar](./avatar.jpg =320x200)`;

const resolveImageSrc = (src: string): string => {
const imageMap: Record<string, string> = {
'./avatar.jpg':
'https://avatars.mds.yandex.net/get-shedevrum/14441318/img_1c3b6b42eee211efad66ea120268400c/orig',
};
return imageMap[src] ?? src;
};

export const ImgResolveSrcDemo = memo(() => {
const editor = useMarkdownEditor({
initial: {
mode: 'wysiwyg',
markup: INITIAL_MARKUP,
},
wysiwygConfig: {
extensionOptions: {
imgSize: {
resolveImageSrc,
},
},
},
});

return (
<PlaygroundLayout
editor={editor}
view={({className}) => (
<MarkdownEditorView
autofocus
stickyToolbar
settingsVisible
editor={editor}
className={className}
/>
)}
/>
);
});

ImgResolveSrcDemo.displayName = 'ImgResolveSrcDemo';
7 changes: 6 additions & 1 deletion packages/editor/src/extensions/yfm/ImgSize/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ export type ImgSizeOptions = ImgSizeSpecsOptions & {
* @default false
*/
needToSetDimensionsForUploadedImages?: boolean;
/**
* Transforms the image src before rendering, e.g. to resolve a relative path.
* Affects only the displayed src; the node attrs and markup are left intact.
*/
resolveImageSrc?: (src: string) => string;
} & Pick<
ImagePasteOptions,
'imageUploadHandler' | 'parseInsertedUrlAsImage' | 'enableNewImageSizeCalculation'
Expand Down Expand Up @@ -40,7 +45,7 @@ export const ImgSize: ExtensionAuto<ImgSizeOptions> = (builder, opts) => {

builder.addAction(addImageAction, ({schema}) => addImage(schema));

builder.addPlugin(imgSizeNodeViewPlugin);
builder.addPlugin(imgSizeNodeViewPlugin({resolveImageSrc: opts.resolveImageSrc}));
};

declare global {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,16 @@ import './ImgNodeView.scss';

export const cnImgSizeNodeView = cn('img-size-node-view');

export const ImageNodeView: React.FC<ReactNodeViewProps> = ({
export type ImgSizeNodeViewOpts = {
resolveImageSrc?: (src: string) => string;
};

export const ImageNodeView: React.FC<ReactNodeViewProps<ImgSizeNodeViewOpts>> = ({
node,
view,
getPos,
updateAttributes,
extensionOptions,
}) => {
const imageContainerRef = useRef<HTMLDivElement>(null);
const imageRef = useRef<HTMLImageElement>(null);
Expand All @@ -34,7 +39,8 @@ export const ImageNodeView: React.FC<ReactNodeViewProps> = ({
const alt = node.attrs[ImgSizeAttr.Alt] || '';
const initialHeight = node.attrs[ImgSizeAttr.Height];
const initialWidth = node.attrs[ImgSizeAttr.Width];
const src = node.attrs[ImgSizeAttr.Src] || '';
const rawSrc = node.attrs[ImgSizeAttr.Src] || '';
const src = extensionOptions?.resolveImageSrc?.(rawSrc) ?? rawSrc;
const title = node.attrs[ImgSizeAttr.Title] || '';

const isNodeHovered = useNodeHovered(imageContainerRef);
Expand Down Expand Up @@ -89,6 +95,22 @@ export const ImageNodeView: React.FC<ReactNodeViewProps> = ({
view.focus();
}, [getPos, node, view]);

const handleImgError = useCallback(() => {
const pos = getPos();

if (pos === undefined) {
return;
}

const text = view.state.schema.text(`![${alt}](${rawSrc})`);

view.dispatch(
view.state.tr
.replaceWith(pos, pos + node.nodeSize, text)
.setMeta('image-load-failed', rawSrc),
);
}, [alt, rawSrc, getPos, node, view]);

const createHandleResize =
(direction: ResizeDirection) => (event: React.MouseEvent<HTMLElement>) => {
startResizing(event, direction);
Expand Down Expand Up @@ -128,7 +150,7 @@ export const ImageNodeView: React.FC<ReactNodeViewProps> = ({
onDelete={handleDelete}
unsetEdit={unsetEdit}
/>
<img ref={refFn} src={src} alt={alt} style={style} />
<img ref={refFn} src={src} alt={alt} style={style} onError={handleImgError} />
</Resizable>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@ import type {ExtensionDeps} from '../../../../../core';
import {reactNodeViewFactory} from '../../../../../react-utils';
import {imageNodeName, imageRendererKey} from '../../const';

import {ImageNodeView, cnImgSizeNodeView} from './NodeView';
import {ImageNodeView, type ImgSizeNodeViewOpts, cnImgSizeNodeView} from './NodeView';

export const imgSizeNodeViewPlugin = (deps: ExtensionDeps) =>
new Plugin({
key: imageRendererKey,
state: {
init: () => undefined,
apply: (tr) => {
return tr.getMeta(imageRendererKey);
export const imgSizeNodeViewPlugin =
(opts: ImgSizeNodeViewOpts = {}) =>
(deps: ExtensionDeps) =>
new Plugin({
key: imageRendererKey,
state: {
init: () => undefined,
apply: (tr) => {
return tr.getMeta(imageRendererKey);
},
},
},
props: {
nodeViews: {
[imageNodeName]: reactNodeViewFactory(ImageNodeView, {
isInline: true,
reactNodeWrapperCn: cnImgSizeNodeView('wrapper'),
})(deps),
props: {
nodeViews: {
[imageNodeName]: reactNodeViewFactory<ImgSizeNodeViewOpts>(ImageNodeView, {
isInline: true,
reactNodeWrapperCn: cnImgSizeNodeView('wrapper'),
extensionOptions: opts,
})(deps),
},
},
},
});
});
Loading