Bugfix/image widget starting value (#57)

* Fix image asset loading
* Fix scroll sync for frame
This commit is contained in:
Daniel Lautzenheiser 2022-11-01 14:07:30 -04:00 committed by GitHub
parent e62563e4a3
commit 6135a6c8d8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 354 additions and 162 deletions

View File

Before

Width:  |  Height:  |  Size: 808 KiB

After

Width:  |  Height:  |  Size: 808 KiB

View File

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 310 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -1,10 +1,12 @@
backend:
name: gitlab
branch: master
repo: owner/repo
branch: main
repo: static-cms/static-cms-gitlab
auth_type: pkce
app_id: 91cc479ec663625098d456850c4dc4943fd8462064ebd9693b330e66f9d8f11a
media_folder: static/media
public_folder: /media
media_folder: assets/upload
public_folder: /assets/upload
collections:
- name: posts
label: Posts
@ -92,7 +94,8 @@ collections:
label: Settings
delete: false
editor:
preview: false
preview: true
frame: false
files:
- name: general
label: Site Settings

File diff suppressed because one or more lines are too long

View File

@ -19,6 +19,7 @@ const PostPreview = createClass({
},
});
// TODO Hook this back up, getAsset returns a promise now
const GeneralPreview = createClass({
render: function () {
const entry = this.props.entry;
@ -45,6 +46,7 @@ const GeneralPreview = createClass({
);
},
});
const AuthorsPreview = createClass({
render: function () {
return h(
@ -90,7 +92,7 @@ const RelationKitchenSinkPostPreview = createClass({
CMS.registerPreviewStyle('.toastui-editor-contents h1 { color: blue }', { raw: true });
CMS.registerPreviewTemplate('posts', PostPreview);
CMS.registerPreviewTemplate('general', GeneralPreview);
// CMS.registerPreviewTemplate('general', GeneralPreview);
CMS.registerPreviewTemplate('authors', AuthorsPreview);
// Pass the name of a registered control to reuse with a new widget preview.
CMS.registerWidget('relationKitchenSinkPost', 'relation', RelationKitchenSinkPostPreview);

View File

@ -6,7 +6,7 @@ import { getMediaDisplayURL, getMediaFile, waitForMediaLibraryToLoad } from './m
import type { AnyAction } from 'redux';
import type { ThunkDispatch } from 'redux-thunk';
import type { Field, Collection, Entry } from '../interface';
import type { Collection, Entry, Field } from '../interface';
import type { RootState } from '../store';
import type AssetProxy from '../valueObjects/AssetProxy';
@ -42,33 +42,6 @@ export function loadAssetFailure(path: string, error: Error) {
return { type: LOAD_ASSET_FAILURE, payload: { path, error } } as const;
}
export function loadAsset(resolvedPath: string) {
return async (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
try {
dispatch(loadAssetRequest(resolvedPath));
// load asset url from backend
await waitForMediaLibraryToLoad(dispatch, getState());
const file = selectMediaFileByPath(getState(), resolvedPath);
if (file) {
const url = await getMediaDisplayURL(dispatch, getState(), file);
const asset = createAssetProxy({ path: resolvedPath, url: url || resolvedPath });
dispatch(addAsset(asset));
} else {
const { url } = await getMediaFile(getState(), resolvedPath);
const asset = createAssetProxy({ path: resolvedPath, url });
dispatch(addAsset(asset));
}
dispatch(loadAssetSuccess(resolvedPath));
} catch (error: unknown) {
console.error(error);
if (error instanceof Error) {
dispatch(loadAssetFailure(resolvedPath, error));
}
}
};
}
const emptyAsset = createAssetProxy({
path: 'empty.svg',
file: new File([`<svg xmlns="http://www.w3.org/2000/svg"></svg>`], 'empty.svg', {
@ -76,50 +49,93 @@ const emptyAsset = createAssetProxy({
}),
});
async function loadAsset(
resolvedPath: string,
dispatch: ThunkDispatch<RootState, {}, AnyAction>,
getState: () => RootState,
): Promise<AssetProxy> {
try {
dispatch(loadAssetRequest(resolvedPath));
// load asset url from backend
await waitForMediaLibraryToLoad(dispatch, getState());
const file = selectMediaFileByPath(getState(), resolvedPath);
let asset: AssetProxy;
if (file) {
const url = await getMediaDisplayURL(dispatch, getState(), file);
asset = createAssetProxy({ path: resolvedPath, url: url || resolvedPath });
dispatch(addAsset(asset));
} else {
const { url } = await getMediaFile(getState(), resolvedPath);
asset = createAssetProxy({ path: resolvedPath, url });
dispatch(addAsset(asset));
}
dispatch(loadAssetSuccess(resolvedPath));
return asset;
} catch (error: unknown) {
console.error(error);
if (error instanceof Error) {
dispatch(loadAssetFailure(resolvedPath, error));
}
return emptyAsset;
}
}
const promiseCache: Record<string, Promise<AssetProxy>> = {};
export function getAsset(
collection: Collection | null | undefined,
entry: Entry | null | undefined,
path: string,
field?: Field,
) {
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
return (
dispatch: ThunkDispatch<RootState, {}, AnyAction>,
getState: () => RootState,
): Promise<AssetProxy> => {
if (!collection || !entry || !path) {
return emptyAsset;
return Promise.resolve(emptyAsset);
}
const state = getState();
if (!state.config.config) {
return emptyAsset;
return Promise.resolve(emptyAsset);
}
const resolvedPath = selectMediaFilePath(state.config.config, collection, entry, path, field);
let { asset, isLoading, error } = state.medias[resolvedPath] || {};
if (isLoading) {
return emptyAsset;
return promiseCache[resolvedPath];
}
if (asset) {
// There is already an AssetProxy in memory for this path. Use it.
return asset;
return Promise.resolve(asset);
}
if (isAbsolutePath(resolvedPath)) {
// asset path is a public url so we can just use it as is
asset = createAssetProxy({ path: resolvedPath, url: path });
dispatch(addAsset(asset));
} else {
if (error) {
// on load error default back to original path
const p = new Promise<AssetProxy>(resolve => {
if (isAbsolutePath(resolvedPath)) {
// asset path is a public url so we can just use it as is
asset = createAssetProxy({ path: resolvedPath, url: path });
dispatch(addAsset(asset));
resolve(asset);
} else {
dispatch(loadAsset(resolvedPath));
asset = emptyAsset;
if (error) {
// on load error default back to original path
asset = createAssetProxy({ path: resolvedPath, url: path });
dispatch(addAsset(asset));
resolve(asset);
} else {
loadAsset(resolvedPath, dispatch, getState).then(asset => {
resolve(asset);
});
}
}
}
});
return asset;
promiseCache[resolvedPath] = p;
return p;
};
}

View File

@ -3,7 +3,7 @@ import CardActionArea from '@mui/material/CardActionArea';
import CardContent from '@mui/material/CardContent';
import CardMedia from '@mui/material/CardMedia';
import Typography from '@mui/material/Typography';
import React, { useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
@ -29,15 +29,24 @@ const EntryCard = ({
}: NestedCollectionProps) => {
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
const [imageUrl, setImageUrl] = useState<string>();
useEffect(() => {
if (!image) {
return;
}
const getImage = async () => {
setImageUrl((await getAsset(collection, entry, image, imageField)).toString());
};
getImage();
}, [collection, entry, getAsset, image, imageField]);
return (
<Card>
<CardActionArea component={Link} to={path}>
{viewStyle === VIEW_STYLE_GRID && image && imageField ? (
<CardMedia
component="img"
height="140"
image={getAsset(collection, entry, image, imageField).toString()}
/>
<CardMedia component="img" height="140" image={imageUrl} />
) : null}
<CardContent>
{collectionLabel ? (

View File

@ -23,6 +23,7 @@ import type { ConnectedProps } from 'react-redux';
import type { InferredField } from '../../../constants/fieldInference';
import type {
Collection,
Config,
Entry,
EntryData,
Field,
@ -84,6 +85,7 @@ const StyledPreviewContent = styled('div')`
* exposed for use in custom preview templates.
*/
function getWidgetFor(
config: Config,
collection: Collection,
name: string,
fields: Field[],
@ -112,6 +114,7 @@ function getWidgetFor(
fieldWithWidgets = {
...fieldWithWidgets,
fields: getNestedWidgets(
config,
collection,
fields,
entry,
@ -125,6 +128,7 @@ function getWidgetFor(
fieldWithWidgets = {
...fieldWithWidgets,
fields: getTypedNestedWidgets(
config,
collection,
field,
entry,
@ -161,7 +165,7 @@ function getWidgetFor(
);
}
return renderedValue
? getWidget(fieldWithWidgets, collection, renderedValue, entry, getAsset)
? getWidget(config, fieldWithWidgets, collection, renderedValue, entry, getAsset)
: null;
}
@ -180,6 +184,7 @@ function isReactFragment(value: any): value is ReactFragment {
}
function getWidget(
config: Config,
field: RenderedField,
collection: Collection,
value: ValueOrNestedValue | ReactNode,
@ -203,6 +208,7 @@ function getWidget(
key={key}
field={field}
getAsset={getAsset}
config={config}
collection={collection}
value={
value &&
@ -223,6 +229,7 @@ function getWidget(
* Use getWidgetFor as a mapping function for recursive widget retrieval
*/
function widgetsForNestedFields(
config: Config,
collection: Collection,
fields: Field[],
entry: Entry,
@ -234,6 +241,7 @@ function widgetsForNestedFields(
return widgetFields
.map(field =>
getWidgetFor(
config,
collection,
field.name,
fields,
@ -251,6 +259,7 @@ function widgetsForNestedFields(
* Retrieves widgets for nested fields (children of object/list fields)
*/
function getTypedNestedWidgets(
config: Config,
collection: Collection,
field: ListField,
entry: Entry,
@ -259,13 +268,14 @@ function getTypedNestedWidgets(
values: EntryData[],
) {
return values
.flatMap((value, index) => {
?.flatMap((value, index) => {
const itemType = getTypedFieldForValue(field, value ?? {}, index);
if (!itemType) {
return null;
}
return widgetsForNestedFields(
config,
collection,
itemType.fields,
entry,
@ -282,6 +292,7 @@ function getTypedNestedWidgets(
* Retrieves widgets for nested fields (children of object/list fields)
*/
function getNestedWidgets(
config: Config,
collection: Collection,
fields: Field[],
entry: Entry,
@ -294,6 +305,7 @@ function getNestedWidgets(
if (Array.isArray(values)) {
return values.flatMap(value =>
widgetsForNestedFields(
config,
collection,
fields,
entry,
@ -307,6 +319,7 @@ function getNestedWidgets(
// Fields nested within an object field will be paired with a single Record of values.
return widgetsForNestedFields(
config,
collection,
fields,
entry,
@ -332,9 +345,20 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
const widgetFor = useCallback(
(name: string) => {
return getWidgetFor(collection, name, fields, entry, inferedFields, handleGetAsset);
if (!config.config) {
return null;
}
return getWidgetFor(
config.config,
collection,
name,
fields,
entry,
inferedFields,
handleGetAsset,
);
},
[collection, entry, fields, handleGetAsset, inferedFields],
[collection, config, entry, fields, handleGetAsset, inferedFields],
);
/**
@ -422,34 +446,41 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
}
return ReactDOM.createPortal(
<ScrollSyncPane>
<StyledPreviewContent className="preview-content">
{!entry || !entry.data ? null : (
<ErrorBoundary config={config}>
{previewInFrame ? (
<PreviewPaneFrame
key="preview-frame"
id="preview-pane"
head={previewStyles}
initialContent={initialFrameContent}
>
{!collection ? (
t('collection.notFound')
) : (
<FrameContextConsumer>
{({ document, window }) => {
return (
<StyledPreviewContent className="preview-content">
{!entry || !entry.data ? null : (
<ErrorBoundary config={config}>
{previewInFrame ? (
<PreviewPaneFrame
key="preview-frame"
id="preview-pane"
head={previewStyles}
initialContent={initialFrameContent}
>
{!collection ? (
t('collection.notFound')
) : (
<FrameContextConsumer>
{({ document, window }) => {
return (
<ScrollSyncPane
key="preview-frame-scroll-sync"
attachTo={
(document?.scrollingElement ?? undefined) as HTMLElement | undefined
}
>
<EditorPreviewContent
key="preview-frame-content"
previewComponent={previewComponent}
previewProps={{ ...previewProps, document, window }}
/>
);
}}
</FrameContextConsumer>
)}
</PreviewPaneFrame>
) : (
</ScrollSyncPane>
);
}}
</FrameContextConsumer>
)}
</PreviewPaneFrame>
) : (
<ScrollSyncPane key="preview-wrapper-scroll-sync">
<PreviewPaneWrapper key="preview-wrapper" id="preview-pane">
{!collection ? (
t('collection.notFound')
@ -464,11 +495,11 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
</>
)}
</PreviewPaneWrapper>
)}
</ErrorBoundary>
)}
</StyledPreviewContent>
</ScrollSyncPane>,
</ScrollSyncPane>
)}
</ErrorBoundary>
)}
</StyledPreviewContent>,
element,
'preview-content',
);

View File

@ -13,6 +13,7 @@ import type { I18N_STRUCTURE } from './lib/i18n';
import type { AllowedEvent } from './lib/registry';
import type Cursor from './lib/util/Cursor';
import type AssetProxy from './valueObjects/AssetProxy';
import type { MediaHolder } from './widgets/markdown/hooks/useMedia';
export interface SlugConfig {
encoding: string;
@ -227,7 +228,7 @@ export type Hook = string | boolean;
export type TranslatedProps<T> = T & ReactPolyglotTranslateProps;
export type GetAssetFunction = (path: string, field?: Field) => AssetProxy;
export type GetAssetFunction = (path: string, field?: Field) => Promise<AssetProxy>;
export interface WidgetControlProps<T, F extends Field = Field> {
clearFieldErrors: EditorControlProps['clearFieldErrors'];
@ -262,6 +263,7 @@ export interface WidgetControlProps<T, F extends Field = Field> {
}
export interface WidgetPreviewProps<T = unknown, F extends Field = Field> {
config: Config;
collection: Collection;
entry: Entry;
field: RenderedField<F>;
@ -322,28 +324,6 @@ export interface WidgetParam<T = unknown, F extends Field = Field> {
options?: WidgetOptions<T, F>;
}
export interface PreviewTemplateComponentProps {
entry: Entry;
collection: Collection;
widgetFor: (name: string) => ReactNode;
widgetsFor: (name: string) =>
| {
data: EntryData | null;
widgets: Record<string, React.ReactNode>;
}
| {
data: EntryData | null;
widgets: Record<string, React.ReactNode>;
}[];
getAsset: GetAssetFunction;
boundGetAsset: (collection: Collection, path: string) => GetAssetFunction;
config: Config;
fields: Field[];
isLoadingAsset: boolean;
window: Window;
document: Document;
}
export interface PersistOptions {
newEntry?: boolean;
commitMessage: string;
@ -907,8 +887,9 @@ export interface PreviewStyle {
}
export interface MarkdownPluginFactoryProps {
getAsset: GetAssetFunction;
config: Config;
field: MarkdownField;
media: MediaHolder;
mode: 'editor' | 'preview';
}

View File

@ -57,7 +57,7 @@ function fromFetchArguments(wholeURL: string, options?: RequestInit): ApiRequest
function encodeParams(params: Required<ApiRequestURL>['params']): string {
return Object.entries(params)
.map(([v, k]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
}
@ -70,7 +70,6 @@ function toFetchArguments(req: ApiRequestObject): {
init?: RequestInit | undefined;
} {
const { url, params, ...rest } = req;
return { input: toURL({ url, params }), init: rest };
}
@ -110,9 +109,17 @@ const withWrapper =
return fromFetchArguments(req, { [key]: value });
}
let finalValue = value;
if (key === 'headers') {
finalValue = {
...(req.headers ?? {}),
...(value as HeadersInit),
} as ApiRequestObject[K];
}
return {
...req,
[key]: value,
[key]: finalValue,
};
};

View File

@ -18,8 +18,14 @@ const FileLink = ({ value, getAsset, field }: FileLinkProps) => {
return;
}
setAssetSource(getAsset(value, field)?.toString() ?? '');
}, [field, getAsset, value]);
const getImage = async() => {
const asset = (await getAsset(value, field))?.toString() ?? '';
setAssetSource(asset);
};
getImage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return (
<a href={assetSource} rel="noopener noreferrer" target="_blank">

View File

@ -136,8 +136,14 @@ const SortableImage = SortableElement<SortableImageProps>(
({ itemValue, getAsset, field, onRemove, onReplace }: SortableImageProps) => {
const [assetSource, setAssetSource] = useState('');
useEffect(() => {
setAssetSource(getAsset(itemValue, field)?.toString() ?? '');
}, [field, getAsset, itemValue]);
const getImage = async() => {
const asset = (await getAsset(itemValue, field))?.toString() ?? '';
setAssetSource(asset);
};
getImage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [itemValue]);
return (
<div>
@ -389,11 +395,16 @@ export default function withFileControl({ forImage = false }: WithImageOptions =
return;
}
const newValue = getAsset(internalValue, field)?.toString() ?? '';
if (newValue !== internalValue) {
setAssetSource(newValue);
}
}, [field, getAsset, internalValue]);
const getImage = async() => {
const newValue = (await getAsset(internalValue, field))?.toString() ?? '';
if (newValue !== internalValue) {
setAssetSource(newValue);
}
};
getImage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [internalValue]);
const renderedImagesLinks = useMemo(() => {
if (forImage) {

View File

@ -26,8 +26,14 @@ interface ImageAssetProps {
function ImageAsset({ getAsset, value, field }: ImageAssetProps) {
const [assetSource, setAssetSource] = useState('');
useEffect(() => {
setAssetSource(getAsset(value, field)?.toString() ?? '');
}, [field, getAsset, value]);
const getImage = async() => {
const asset = (await getAsset(value, field))?.toString() ?? '';
setAssetSource(asset);
};
getImage();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return <StyledImage src={assetSource} />;
}

View File

@ -11,6 +11,7 @@ import { doesUrlFileExist } from '../../lib/util/fetch.util';
import { isNotNullish } from '../../lib/util/null.util';
import { isNotEmpty } from '../../lib/util/string.util';
import useEditorOptions from './hooks/useEditorOptions';
import useMedia, { MediaHolder } from './hooks/useMedia';
import usePlugins from './hooks/usePlugins';
import useToolbarItems from './hooks/useToolbarItems';
@ -49,6 +50,7 @@ const MarkdownControl = ({
openMediaLibrary,
mediaPaths,
getAsset,
config,
}: WidgetControlProps<string, MarkdownField>) => {
const [internalValue, setInternalValue] = useState(value ?? '');
const editorRef = useMemo(() => React.createRef(), []) as RefObject<Editor>;
@ -95,7 +97,7 @@ const MarkdownControl = ({
async (path: string) => {
const { type, exists } = await doesUrlFileExist(path);
if (!exists) {
const asset = getAsset(path, field);
const asset = await getAsset(path, field);
if (isNotNullish(asset)) {
return {
type: IMAGE_EXTENSION_REGEX.test(path) ? 'image' : 'file',
@ -152,7 +154,22 @@ const MarkdownControl = ({
}, [field, mediaPath]);
const { initialEditType, height, ...markdownEditorOptions } = useEditorOptions();
const plugins = usePlugins(markdownEditorOptions.plugins, { getAsset, field, mode: 'editor' });
const media = useMedia({ value, getAsset, field });
const mediaHolder = useMemo(() => new MediaHolder(), []);
useEffect(() => {
mediaHolder.setBulkMedia(media);
editorRef.current?.getInstance().setMarkdown(internalValue);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [media]);
const plugins = usePlugins(markdownEditorOptions.plugins, {
media: mediaHolder,
config,
field,
mode: 'editor',
});
const toolbarItems = useToolbarItems(markdownEditorOptions.toolbarItems, handleOpenMedialLibrary);
return useMemo(

View File

@ -1,36 +1,59 @@
import { Viewer } from '@toast-ui/react-editor';
import React, { useEffect, useRef } from 'react';
import React, { useEffect, useMemo, useRef } from 'react';
import WidgetPreviewContainer from '../../components/UI/WidgetPreviewContainer';
import useEditorOptions from './hooks/useEditorOptions';
import useMedia, { MediaHolder } from './hooks/useMedia';
import usePlugins from './hooks/usePlugins';
import type { MarkdownField, WidgetPreviewProps } from '../../interface';
const MarkdownPreview = ({ value, getAsset, field }: WidgetPreviewProps<string, MarkdownField>) => {
const MarkdownPreview = ({
value,
getAsset,
config,
field,
}: WidgetPreviewProps<string, MarkdownField>) => {
const options = useEditorOptions();
const plugins = usePlugins(options.plugins, { getAsset, field, mode: 'preview' });
const mediaHolder = useMemo(() => new MediaHolder(), []);
const media = useMedia({ value, getAsset, field });
useEffect(() => {
mediaHolder.setBulkMedia(media);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [media]);
const plugins = usePlugins(options.plugins, {
config,
media: mediaHolder,
field,
mode: 'preview',
});
const viewer = useRef<Viewer | null>(null);
useEffect(() => {
viewer.current?.getInstance().setMarkdown(value ?? '');
}, [value]);
}, [value, media]);
if (!value) {
return null;
}
return useMemo(() => {
if (!value) {
return null;
}
return (
<WidgetPreviewContainer>
<Viewer
ref={viewer}
initialValue={value}
customHTMLSanitizer={(content: string) => content}
plugins={plugins}
/>
</WidgetPreviewContainer>
);
return (
<WidgetPreviewContainer>
<Viewer
ref={viewer}
initialValue={value}
customHTMLSanitizer={(content: string) => content}
plugins={plugins}
/>
</WidgetPreviewContainer>
);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [plugins]);
};
export default MarkdownPreview;

View File

@ -0,0 +1,78 @@
import { useEffect, useState } from 'react';
import type { GetAssetFunction, MarkdownField } from '../../../interface';
import type AssetProxy from '../../../valueObjects/AssetProxy';
interface UseMediaProps {
value: string | undefined | null;
getAsset: GetAssetFunction;
field: MarkdownField;
}
export class MediaHolder {
private media: Record<string, AssetProxy> = {};
public setBulkMedia(otherMedia: Record<string, AssetProxy>) {
for (const [url, asset] of Object.entries(otherMedia)) {
this.setMedia(url, asset);
}
}
public setMedia(url: string, asset: AssetProxy) {
this.media[url] = asset;
}
public getMedia(url: string) {
return this.media[url];
}
}
const useMedia = ({ value, getAsset, field }: UseMediaProps) => {
const [media, setMedia] = useState<Record<string, AssetProxy>>({});
useEffect(() => {
if (!value) {
return;
}
let alive = true;
const getMedia = async () => {
const regExp = /!\[[^\]()]*\]\(([^)]+)\)/g;
let matches = regExp.exec(value);
const mediaToLoad: string[] = [];
while (matches && matches.length === 2) {
mediaToLoad.push(matches[1]);
matches = regExp.exec(value);
}
const uniqueMediaToLoad = mediaToLoad.filter(
(value, index, self) => self.indexOf(value) === index,
);
for (const url of uniqueMediaToLoad) {
const asset = await getAsset(url, field);
if (!alive) {
break;
}
setMedia(oldValue => ({
...oldValue,
[url]: asset,
}));
}
};
getMedia();
return () => {
alive = false;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [value]);
return media;
};
export default useMedia;

View File

@ -6,17 +6,19 @@ import type { MarkdownEditorOptions, MarkdownPluginFactoryProps } from '../../..
const usePlugins = (
editorPlugins: MarkdownEditorOptions['plugins'] = [],
{ getAsset, field, mode }: MarkdownPluginFactoryProps,
{ config, media, field, mode }: MarkdownPluginFactoryProps,
) => {
return useMemo(() => {
const plugins = [imagePlugin({ getAsset, field, mode })];
const plugins = [imagePlugin({ config, media, field, mode })];
if (plugins) {
plugins.push(...editorPlugins.map(editorPlugin => editorPlugin({ getAsset, field, mode })));
plugins.push(
...editorPlugins.map(editorPlugin => editorPlugin({ config, media, field, mode })),
);
}
return plugins;
}, [editorPlugins, field, getAsset, mode]);
}, [config, editorPlugins, field, media, mode]);
};
export default usePlugins;

View File

@ -6,25 +6,23 @@ function isLinkNode(node: MdNode): node is LinkMdNode {
return 'destination' in node;
}
const toHTMLRenderers: (props: MarkdownPluginFactoryProps) => CustomHTMLRenderer = ({
getAsset,
field,
}) => ({
const toHTMLRenderer: (props: MarkdownPluginFactoryProps) => CustomHTMLRenderer = ({ media }) => ({
image: (node: MdNode, { entering, skipChildren }) => {
if (entering && isLinkNode(node)) {
skipChildren();
let imageUrl = node.destination ?? '';
if (node.destination) {
imageUrl = media.getMedia(node.destination)?.toString() ?? node.destination;
}
return {
type: 'openTag',
tagName: 'img',
outerNewLine: true,
attributes: {
src: node.destination,
onerror: `this.onerror=null; this.src='${
node.destination
? getAsset(node.destination, field)?.toString() ?? node.destination
: ''
}'`,
onerror: `this.onerror=null; this.src='${imageUrl}'`,
},
selfClose: true,
};
@ -36,7 +34,7 @@ const toHTMLRenderers: (props: MarkdownPluginFactoryProps) => CustomHTMLRenderer
const imagePlugin: MarkdownPluginFactory = props => {
return () => ({
toHTMLRenderers: toHTMLRenderers(props),
toHTMLRenderers: toHTMLRenderer(props),
});
};