Bugfix/image widget starting value (#57)
* Fix image asset loading * Fix scroll sync for frame
This commit is contained in:
parent
e62563e4a3
commit
6135a6c8d8
Before Width: | Height: | Size: 808 KiB After Width: | Height: | Size: 808 KiB |
Before Width: | Height: | Size: 310 KiB After Width: | Height: | Size: 310 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
@ -1,10 +1,12 @@
|
|||||||
backend:
|
backend:
|
||||||
name: gitlab
|
name: gitlab
|
||||||
branch: master
|
branch: main
|
||||||
repo: owner/repo
|
repo: static-cms/static-cms-gitlab
|
||||||
|
auth_type: pkce
|
||||||
|
app_id: 91cc479ec663625098d456850c4dc4943fd8462064ebd9693b330e66f9d8f11a
|
||||||
|
|
||||||
media_folder: static/media
|
media_folder: assets/upload
|
||||||
public_folder: /media
|
public_folder: /assets/upload
|
||||||
collections:
|
collections:
|
||||||
- name: posts
|
- name: posts
|
||||||
label: Posts
|
label: Posts
|
||||||
@ -92,7 +94,8 @@ collections:
|
|||||||
label: Settings
|
label: Settings
|
||||||
delete: false
|
delete: false
|
||||||
editor:
|
editor:
|
||||||
preview: false
|
preview: true
|
||||||
|
frame: false
|
||||||
files:
|
files:
|
||||||
- name: general
|
- name: general
|
||||||
label: Site Settings
|
label: Site Settings
|
||||||
|
File diff suppressed because one or more lines are too long
@ -19,6 +19,7 @@ const PostPreview = createClass({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO Hook this back up, getAsset returns a promise now
|
||||||
const GeneralPreview = createClass({
|
const GeneralPreview = createClass({
|
||||||
render: function () {
|
render: function () {
|
||||||
const entry = this.props.entry;
|
const entry = this.props.entry;
|
||||||
@ -45,6 +46,7 @@ const GeneralPreview = createClass({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const AuthorsPreview = createClass({
|
const AuthorsPreview = createClass({
|
||||||
render: function () {
|
render: function () {
|
||||||
return h(
|
return h(
|
||||||
@ -90,7 +92,7 @@ const RelationKitchenSinkPostPreview = createClass({
|
|||||||
|
|
||||||
CMS.registerPreviewStyle('.toastui-editor-contents h1 { color: blue }', { raw: true });
|
CMS.registerPreviewStyle('.toastui-editor-contents h1 { color: blue }', { raw: true });
|
||||||
CMS.registerPreviewTemplate('posts', PostPreview);
|
CMS.registerPreviewTemplate('posts', PostPreview);
|
||||||
CMS.registerPreviewTemplate('general', GeneralPreview);
|
// CMS.registerPreviewTemplate('general', GeneralPreview);
|
||||||
CMS.registerPreviewTemplate('authors', AuthorsPreview);
|
CMS.registerPreviewTemplate('authors', AuthorsPreview);
|
||||||
// Pass the name of a registered control to reuse with a new widget preview.
|
// Pass the name of a registered control to reuse with a new widget preview.
|
||||||
CMS.registerWidget('relationKitchenSinkPost', 'relation', RelationKitchenSinkPostPreview);
|
CMS.registerWidget('relationKitchenSinkPost', 'relation', RelationKitchenSinkPostPreview);
|
||||||
|
@ -6,7 +6,7 @@ import { getMediaDisplayURL, getMediaFile, waitForMediaLibraryToLoad } from './m
|
|||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
import type { ThunkDispatch } from 'redux-thunk';
|
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 { RootState } from '../store';
|
||||||
import type AssetProxy from '../valueObjects/AssetProxy';
|
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;
|
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({
|
const emptyAsset = createAssetProxy({
|
||||||
path: 'empty.svg',
|
path: 'empty.svg',
|
||||||
file: new File([`<svg xmlns="http://www.w3.org/2000/svg"></svg>`], '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(
|
export function getAsset(
|
||||||
collection: Collection | null | undefined,
|
collection: Collection | null | undefined,
|
||||||
entry: Entry | null | undefined,
|
entry: Entry | null | undefined,
|
||||||
path: string,
|
path: string,
|
||||||
field?: Field,
|
field?: Field,
|
||||||
) {
|
) {
|
||||||
return (dispatch: ThunkDispatch<RootState, {}, AnyAction>, getState: () => RootState) => {
|
return (
|
||||||
|
dispatch: ThunkDispatch<RootState, {}, AnyAction>,
|
||||||
|
getState: () => RootState,
|
||||||
|
): Promise<AssetProxy> => {
|
||||||
if (!collection || !entry || !path) {
|
if (!collection || !entry || !path) {
|
||||||
return emptyAsset;
|
return Promise.resolve(emptyAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
const state = getState();
|
const state = getState();
|
||||||
if (!state.config.config) {
|
if (!state.config.config) {
|
||||||
return emptyAsset;
|
return Promise.resolve(emptyAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = selectMediaFilePath(state.config.config, collection, entry, path, field);
|
const resolvedPath = selectMediaFilePath(state.config.config, collection, entry, path, field);
|
||||||
|
|
||||||
let { asset, isLoading, error } = state.medias[resolvedPath] || {};
|
let { asset, isLoading, error } = state.medias[resolvedPath] || {};
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return emptyAsset;
|
return promiseCache[resolvedPath];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (asset) {
|
if (asset) {
|
||||||
// There is already an AssetProxy in memory for this path. Use it.
|
// There is already an AssetProxy in memory for this path. Use it.
|
||||||
return asset;
|
return Promise.resolve(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isAbsolutePath(resolvedPath)) {
|
const p = new Promise<AssetProxy>(resolve => {
|
||||||
// asset path is a public url so we can just use it as is
|
if (isAbsolutePath(resolvedPath)) {
|
||||||
asset = createAssetProxy({ path: resolvedPath, url: path });
|
// asset path is a public url so we can just use it as is
|
||||||
dispatch(addAsset(asset));
|
|
||||||
} else {
|
|
||||||
if (error) {
|
|
||||||
// on load error default back to original path
|
|
||||||
asset = createAssetProxy({ path: resolvedPath, url: path });
|
asset = createAssetProxy({ path: resolvedPath, url: path });
|
||||||
dispatch(addAsset(asset));
|
dispatch(addAsset(asset));
|
||||||
|
resolve(asset);
|
||||||
} else {
|
} else {
|
||||||
dispatch(loadAsset(resolvedPath));
|
if (error) {
|
||||||
asset = emptyAsset;
|
// 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;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@ import CardActionArea from '@mui/material/CardActionArea';
|
|||||||
import CardContent from '@mui/material/CardContent';
|
import CardContent from '@mui/material/CardContent';
|
||||||
import CardMedia from '@mui/material/CardMedia';
|
import CardMedia from '@mui/material/CardMedia';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import React, { useMemo } from 'react';
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
import { connect } from 'react-redux';
|
import { connect } from 'react-redux';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
@ -29,15 +29,24 @@ const EntryCard = ({
|
|||||||
}: NestedCollectionProps) => {
|
}: NestedCollectionProps) => {
|
||||||
const summary = useMemo(() => selectEntryCollectionTitle(collection, entry), [collection, entry]);
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<CardActionArea component={Link} to={path}>
|
<CardActionArea component={Link} to={path}>
|
||||||
{viewStyle === VIEW_STYLE_GRID && image && imageField ? (
|
{viewStyle === VIEW_STYLE_GRID && image && imageField ? (
|
||||||
<CardMedia
|
<CardMedia component="img" height="140" image={imageUrl} />
|
||||||
component="img"
|
|
||||||
height="140"
|
|
||||||
image={getAsset(collection, entry, image, imageField).toString()}
|
|
||||||
/>
|
|
||||||
) : null}
|
) : null}
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{collectionLabel ? (
|
{collectionLabel ? (
|
||||||
|
@ -23,6 +23,7 @@ import type { ConnectedProps } from 'react-redux';
|
|||||||
import type { InferredField } from '../../../constants/fieldInference';
|
import type { InferredField } from '../../../constants/fieldInference';
|
||||||
import type {
|
import type {
|
||||||
Collection,
|
Collection,
|
||||||
|
Config,
|
||||||
Entry,
|
Entry,
|
||||||
EntryData,
|
EntryData,
|
||||||
Field,
|
Field,
|
||||||
@ -84,6 +85,7 @@ const StyledPreviewContent = styled('div')`
|
|||||||
* exposed for use in custom preview templates.
|
* exposed for use in custom preview templates.
|
||||||
*/
|
*/
|
||||||
function getWidgetFor(
|
function getWidgetFor(
|
||||||
|
config: Config,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
name: string,
|
name: string,
|
||||||
fields: Field[],
|
fields: Field[],
|
||||||
@ -112,6 +114,7 @@ function getWidgetFor(
|
|||||||
fieldWithWidgets = {
|
fieldWithWidgets = {
|
||||||
...fieldWithWidgets,
|
...fieldWithWidgets,
|
||||||
fields: getNestedWidgets(
|
fields: getNestedWidgets(
|
||||||
|
config,
|
||||||
collection,
|
collection,
|
||||||
fields,
|
fields,
|
||||||
entry,
|
entry,
|
||||||
@ -125,6 +128,7 @@ function getWidgetFor(
|
|||||||
fieldWithWidgets = {
|
fieldWithWidgets = {
|
||||||
...fieldWithWidgets,
|
...fieldWithWidgets,
|
||||||
fields: getTypedNestedWidgets(
|
fields: getTypedNestedWidgets(
|
||||||
|
config,
|
||||||
collection,
|
collection,
|
||||||
field,
|
field,
|
||||||
entry,
|
entry,
|
||||||
@ -161,7 +165,7 @@ function getWidgetFor(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
return renderedValue
|
return renderedValue
|
||||||
? getWidget(fieldWithWidgets, collection, renderedValue, entry, getAsset)
|
? getWidget(config, fieldWithWidgets, collection, renderedValue, entry, getAsset)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -180,6 +184,7 @@ function isReactFragment(value: any): value is ReactFragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getWidget(
|
function getWidget(
|
||||||
|
config: Config,
|
||||||
field: RenderedField,
|
field: RenderedField,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
value: ValueOrNestedValue | ReactNode,
|
value: ValueOrNestedValue | ReactNode,
|
||||||
@ -203,6 +208,7 @@ function getWidget(
|
|||||||
key={key}
|
key={key}
|
||||||
field={field}
|
field={field}
|
||||||
getAsset={getAsset}
|
getAsset={getAsset}
|
||||||
|
config={config}
|
||||||
collection={collection}
|
collection={collection}
|
||||||
value={
|
value={
|
||||||
value &&
|
value &&
|
||||||
@ -223,6 +229,7 @@ function getWidget(
|
|||||||
* Use getWidgetFor as a mapping function for recursive widget retrieval
|
* Use getWidgetFor as a mapping function for recursive widget retrieval
|
||||||
*/
|
*/
|
||||||
function widgetsForNestedFields(
|
function widgetsForNestedFields(
|
||||||
|
config: Config,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
fields: Field[],
|
fields: Field[],
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
@ -234,6 +241,7 @@ function widgetsForNestedFields(
|
|||||||
return widgetFields
|
return widgetFields
|
||||||
.map(field =>
|
.map(field =>
|
||||||
getWidgetFor(
|
getWidgetFor(
|
||||||
|
config,
|
||||||
collection,
|
collection,
|
||||||
field.name,
|
field.name,
|
||||||
fields,
|
fields,
|
||||||
@ -251,6 +259,7 @@ function widgetsForNestedFields(
|
|||||||
* Retrieves widgets for nested fields (children of object/list fields)
|
* Retrieves widgets for nested fields (children of object/list fields)
|
||||||
*/
|
*/
|
||||||
function getTypedNestedWidgets(
|
function getTypedNestedWidgets(
|
||||||
|
config: Config,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
field: ListField,
|
field: ListField,
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
@ -259,13 +268,14 @@ function getTypedNestedWidgets(
|
|||||||
values: EntryData[],
|
values: EntryData[],
|
||||||
) {
|
) {
|
||||||
return values
|
return values
|
||||||
.flatMap((value, index) => {
|
?.flatMap((value, index) => {
|
||||||
const itemType = getTypedFieldForValue(field, value ?? {}, index);
|
const itemType = getTypedFieldForValue(field, value ?? {}, index);
|
||||||
if (!itemType) {
|
if (!itemType) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return widgetsForNestedFields(
|
return widgetsForNestedFields(
|
||||||
|
config,
|
||||||
collection,
|
collection,
|
||||||
itemType.fields,
|
itemType.fields,
|
||||||
entry,
|
entry,
|
||||||
@ -282,6 +292,7 @@ function getTypedNestedWidgets(
|
|||||||
* Retrieves widgets for nested fields (children of object/list fields)
|
* Retrieves widgets for nested fields (children of object/list fields)
|
||||||
*/
|
*/
|
||||||
function getNestedWidgets(
|
function getNestedWidgets(
|
||||||
|
config: Config,
|
||||||
collection: Collection,
|
collection: Collection,
|
||||||
fields: Field[],
|
fields: Field[],
|
||||||
entry: Entry,
|
entry: Entry,
|
||||||
@ -294,6 +305,7 @@ function getNestedWidgets(
|
|||||||
if (Array.isArray(values)) {
|
if (Array.isArray(values)) {
|
||||||
return values.flatMap(value =>
|
return values.flatMap(value =>
|
||||||
widgetsForNestedFields(
|
widgetsForNestedFields(
|
||||||
|
config,
|
||||||
collection,
|
collection,
|
||||||
fields,
|
fields,
|
||||||
entry,
|
entry,
|
||||||
@ -307,6 +319,7 @@ function getNestedWidgets(
|
|||||||
|
|
||||||
// Fields nested within an object field will be paired with a single Record of values.
|
// Fields nested within an object field will be paired with a single Record of values.
|
||||||
return widgetsForNestedFields(
|
return widgetsForNestedFields(
|
||||||
|
config,
|
||||||
collection,
|
collection,
|
||||||
fields,
|
fields,
|
||||||
entry,
|
entry,
|
||||||
@ -332,9 +345,20 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
|||||||
|
|
||||||
const widgetFor = useCallback(
|
const widgetFor = useCallback(
|
||||||
(name: string) => {
|
(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(
|
return ReactDOM.createPortal(
|
||||||
<ScrollSyncPane>
|
<StyledPreviewContent className="preview-content">
|
||||||
<StyledPreviewContent className="preview-content">
|
{!entry || !entry.data ? null : (
|
||||||
{!entry || !entry.data ? null : (
|
<ErrorBoundary config={config}>
|
||||||
<ErrorBoundary config={config}>
|
{previewInFrame ? (
|
||||||
{previewInFrame ? (
|
<PreviewPaneFrame
|
||||||
<PreviewPaneFrame
|
key="preview-frame"
|
||||||
key="preview-frame"
|
id="preview-pane"
|
||||||
id="preview-pane"
|
head={previewStyles}
|
||||||
head={previewStyles}
|
initialContent={initialFrameContent}
|
||||||
initialContent={initialFrameContent}
|
>
|
||||||
>
|
{!collection ? (
|
||||||
{!collection ? (
|
t('collection.notFound')
|
||||||
t('collection.notFound')
|
) : (
|
||||||
) : (
|
<FrameContextConsumer>
|
||||||
<FrameContextConsumer>
|
{({ document, window }) => {
|
||||||
{({ document, window }) => {
|
return (
|
||||||
return (
|
<ScrollSyncPane
|
||||||
|
key="preview-frame-scroll-sync"
|
||||||
|
attachTo={
|
||||||
|
(document?.scrollingElement ?? undefined) as HTMLElement | undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
<EditorPreviewContent
|
<EditorPreviewContent
|
||||||
key="preview-frame-content"
|
key="preview-frame-content"
|
||||||
previewComponent={previewComponent}
|
previewComponent={previewComponent}
|
||||||
previewProps={{ ...previewProps, document, window }}
|
previewProps={{ ...previewProps, document, window }}
|
||||||
/>
|
/>
|
||||||
);
|
</ScrollSyncPane>
|
||||||
}}
|
);
|
||||||
</FrameContextConsumer>
|
}}
|
||||||
)}
|
</FrameContextConsumer>
|
||||||
</PreviewPaneFrame>
|
)}
|
||||||
) : (
|
</PreviewPaneFrame>
|
||||||
|
) : (
|
||||||
|
<ScrollSyncPane key="preview-wrapper-scroll-sync">
|
||||||
<PreviewPaneWrapper key="preview-wrapper" id="preview-pane">
|
<PreviewPaneWrapper key="preview-wrapper" id="preview-pane">
|
||||||
{!collection ? (
|
{!collection ? (
|
||||||
t('collection.notFound')
|
t('collection.notFound')
|
||||||
@ -464,11 +495,11 @@ const PreviewPane = (props: TranslatedProps<EditorPreviewPaneProps>) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</PreviewPaneWrapper>
|
</PreviewPaneWrapper>
|
||||||
)}
|
</ScrollSyncPane>
|
||||||
</ErrorBoundary>
|
)}
|
||||||
)}
|
</ErrorBoundary>
|
||||||
</StyledPreviewContent>
|
)}
|
||||||
</ScrollSyncPane>,
|
</StyledPreviewContent>,
|
||||||
element,
|
element,
|
||||||
'preview-content',
|
'preview-content',
|
||||||
);
|
);
|
||||||
|
@ -13,6 +13,7 @@ import type { I18N_STRUCTURE } from './lib/i18n';
|
|||||||
import type { AllowedEvent } from './lib/registry';
|
import type { AllowedEvent } from './lib/registry';
|
||||||
import type Cursor from './lib/util/Cursor';
|
import type Cursor from './lib/util/Cursor';
|
||||||
import type AssetProxy from './valueObjects/AssetProxy';
|
import type AssetProxy from './valueObjects/AssetProxy';
|
||||||
|
import type { MediaHolder } from './widgets/markdown/hooks/useMedia';
|
||||||
|
|
||||||
export interface SlugConfig {
|
export interface SlugConfig {
|
||||||
encoding: string;
|
encoding: string;
|
||||||
@ -227,7 +228,7 @@ export type Hook = string | boolean;
|
|||||||
|
|
||||||
export type TranslatedProps<T> = T & ReactPolyglotTranslateProps;
|
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> {
|
export interface WidgetControlProps<T, F extends Field = Field> {
|
||||||
clearFieldErrors: EditorControlProps['clearFieldErrors'];
|
clearFieldErrors: EditorControlProps['clearFieldErrors'];
|
||||||
@ -262,6 +263,7 @@ export interface WidgetControlProps<T, F extends Field = Field> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface WidgetPreviewProps<T = unknown, F extends Field = Field> {
|
export interface WidgetPreviewProps<T = unknown, F extends Field = Field> {
|
||||||
|
config: Config;
|
||||||
collection: Collection;
|
collection: Collection;
|
||||||
entry: Entry;
|
entry: Entry;
|
||||||
field: RenderedField<F>;
|
field: RenderedField<F>;
|
||||||
@ -322,28 +324,6 @@ export interface WidgetParam<T = unknown, F extends Field = Field> {
|
|||||||
options?: WidgetOptions<T, F>;
|
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 {
|
export interface PersistOptions {
|
||||||
newEntry?: boolean;
|
newEntry?: boolean;
|
||||||
commitMessage: string;
|
commitMessage: string;
|
||||||
@ -907,8 +887,9 @@ export interface PreviewStyle {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MarkdownPluginFactoryProps {
|
export interface MarkdownPluginFactoryProps {
|
||||||
getAsset: GetAssetFunction;
|
config: Config;
|
||||||
field: MarkdownField;
|
field: MarkdownField;
|
||||||
|
media: MediaHolder;
|
||||||
mode: 'editor' | 'preview';
|
mode: 'editor' | 'preview';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,7 +57,7 @@ function fromFetchArguments(wholeURL: string, options?: RequestInit): ApiRequest
|
|||||||
|
|
||||||
function encodeParams(params: Required<ApiRequestURL>['params']): string {
|
function encodeParams(params: Required<ApiRequestURL>['params']): string {
|
||||||
return Object.entries(params)
|
return Object.entries(params)
|
||||||
.map(([v, k]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
.map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
|
||||||
.join('&');
|
.join('&');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +70,6 @@ function toFetchArguments(req: ApiRequestObject): {
|
|||||||
init?: RequestInit | undefined;
|
init?: RequestInit | undefined;
|
||||||
} {
|
} {
|
||||||
const { url, params, ...rest } = req;
|
const { url, params, ...rest } = req;
|
||||||
|
|
||||||
return { input: toURL({ url, params }), init: rest };
|
return { input: toURL({ url, params }), init: rest };
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,9 +109,17 @@ const withWrapper =
|
|||||||
return fromFetchArguments(req, { [key]: value });
|
return fromFetchArguments(req, { [key]: value });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let finalValue = value;
|
||||||
|
if (key === 'headers') {
|
||||||
|
finalValue = {
|
||||||
|
...(req.headers ?? {}),
|
||||||
|
...(value as HeadersInit),
|
||||||
|
} as ApiRequestObject[K];
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...req,
|
...req,
|
||||||
[key]: value,
|
[key]: finalValue,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -18,8 +18,14 @@ const FileLink = ({ value, getAsset, field }: FileLinkProps) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAssetSource(getAsset(value, field)?.toString() ?? '');
|
const getImage = async() => {
|
||||||
}, [field, getAsset, value]);
|
const asset = (await getAsset(value, field))?.toString() ?? '';
|
||||||
|
setAssetSource(asset);
|
||||||
|
};
|
||||||
|
|
||||||
|
getImage();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={assetSource} rel="noopener noreferrer" target="_blank">
|
<a href={assetSource} rel="noopener noreferrer" target="_blank">
|
||||||
|
@ -136,8 +136,14 @@ const SortableImage = SortableElement<SortableImageProps>(
|
|||||||
({ itemValue, getAsset, field, onRemove, onReplace }: SortableImageProps) => {
|
({ itemValue, getAsset, field, onRemove, onReplace }: SortableImageProps) => {
|
||||||
const [assetSource, setAssetSource] = useState('');
|
const [assetSource, setAssetSource] = useState('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAssetSource(getAsset(itemValue, field)?.toString() ?? '');
|
const getImage = async() => {
|
||||||
}, [field, getAsset, itemValue]);
|
const asset = (await getAsset(itemValue, field))?.toString() ?? '';
|
||||||
|
setAssetSource(asset);
|
||||||
|
};
|
||||||
|
|
||||||
|
getImage();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [itemValue]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -389,11 +395,16 @@ export default function withFileControl({ forImage = false }: WithImageOptions =
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newValue = getAsset(internalValue, field)?.toString() ?? '';
|
const getImage = async() => {
|
||||||
if (newValue !== internalValue) {
|
const newValue = (await getAsset(internalValue, field))?.toString() ?? '';
|
||||||
setAssetSource(newValue);
|
if (newValue !== internalValue) {
|
||||||
}
|
setAssetSource(newValue);
|
||||||
}, [field, getAsset, internalValue]);
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
getImage();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [internalValue]);
|
||||||
|
|
||||||
const renderedImagesLinks = useMemo(() => {
|
const renderedImagesLinks = useMemo(() => {
|
||||||
if (forImage) {
|
if (forImage) {
|
||||||
|
@ -26,8 +26,14 @@ interface ImageAssetProps {
|
|||||||
function ImageAsset({ getAsset, value, field }: ImageAssetProps) {
|
function ImageAsset({ getAsset, value, field }: ImageAssetProps) {
|
||||||
const [assetSource, setAssetSource] = useState('');
|
const [assetSource, setAssetSource] = useState('');
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setAssetSource(getAsset(value, field)?.toString() ?? '');
|
const getImage = async() => {
|
||||||
}, [field, getAsset, value]);
|
const asset = (await getAsset(value, field))?.toString() ?? '';
|
||||||
|
setAssetSource(asset);
|
||||||
|
};
|
||||||
|
|
||||||
|
getImage();
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [value]);
|
||||||
|
|
||||||
return <StyledImage src={assetSource} />;
|
return <StyledImage src={assetSource} />;
|
||||||
}
|
}
|
||||||
|
@ -11,6 +11,7 @@ import { doesUrlFileExist } from '../../lib/util/fetch.util';
|
|||||||
import { isNotNullish } from '../../lib/util/null.util';
|
import { isNotNullish } from '../../lib/util/null.util';
|
||||||
import { isNotEmpty } from '../../lib/util/string.util';
|
import { isNotEmpty } from '../../lib/util/string.util';
|
||||||
import useEditorOptions from './hooks/useEditorOptions';
|
import useEditorOptions from './hooks/useEditorOptions';
|
||||||
|
import useMedia, { MediaHolder } from './hooks/useMedia';
|
||||||
import usePlugins from './hooks/usePlugins';
|
import usePlugins from './hooks/usePlugins';
|
||||||
import useToolbarItems from './hooks/useToolbarItems';
|
import useToolbarItems from './hooks/useToolbarItems';
|
||||||
|
|
||||||
@ -49,6 +50,7 @@ const MarkdownControl = ({
|
|||||||
openMediaLibrary,
|
openMediaLibrary,
|
||||||
mediaPaths,
|
mediaPaths,
|
||||||
getAsset,
|
getAsset,
|
||||||
|
config,
|
||||||
}: WidgetControlProps<string, MarkdownField>) => {
|
}: WidgetControlProps<string, MarkdownField>) => {
|
||||||
const [internalValue, setInternalValue] = useState(value ?? '');
|
const [internalValue, setInternalValue] = useState(value ?? '');
|
||||||
const editorRef = useMemo(() => React.createRef(), []) as RefObject<Editor>;
|
const editorRef = useMemo(() => React.createRef(), []) as RefObject<Editor>;
|
||||||
@ -95,7 +97,7 @@ const MarkdownControl = ({
|
|||||||
async (path: string) => {
|
async (path: string) => {
|
||||||
const { type, exists } = await doesUrlFileExist(path);
|
const { type, exists } = await doesUrlFileExist(path);
|
||||||
if (!exists) {
|
if (!exists) {
|
||||||
const asset = getAsset(path, field);
|
const asset = await getAsset(path, field);
|
||||||
if (isNotNullish(asset)) {
|
if (isNotNullish(asset)) {
|
||||||
return {
|
return {
|
||||||
type: IMAGE_EXTENSION_REGEX.test(path) ? 'image' : 'file',
|
type: IMAGE_EXTENSION_REGEX.test(path) ? 'image' : 'file',
|
||||||
@ -152,7 +154,22 @@ const MarkdownControl = ({
|
|||||||
}, [field, mediaPath]);
|
}, [field, mediaPath]);
|
||||||
|
|
||||||
const { initialEditType, height, ...markdownEditorOptions } = useEditorOptions();
|
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);
|
const toolbarItems = useToolbarItems(markdownEditorOptions.toolbarItems, handleOpenMedialLibrary);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
|
@ -1,36 +1,59 @@
|
|||||||
import { Viewer } from '@toast-ui/react-editor';
|
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 WidgetPreviewContainer from '../../components/UI/WidgetPreviewContainer';
|
||||||
import useEditorOptions from './hooks/useEditorOptions';
|
import useEditorOptions from './hooks/useEditorOptions';
|
||||||
|
import useMedia, { MediaHolder } from './hooks/useMedia';
|
||||||
import usePlugins from './hooks/usePlugins';
|
import usePlugins from './hooks/usePlugins';
|
||||||
|
|
||||||
import type { MarkdownField, WidgetPreviewProps } from '../../interface';
|
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 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);
|
const viewer = useRef<Viewer | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
viewer.current?.getInstance().setMarkdown(value ?? '');
|
viewer.current?.getInstance().setMarkdown(value ?? '');
|
||||||
}, [value]);
|
}, [value, media]);
|
||||||
|
|
||||||
if (!value) {
|
return useMemo(() => {
|
||||||
return null;
|
if (!value) {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WidgetPreviewContainer>
|
<WidgetPreviewContainer>
|
||||||
<Viewer
|
<Viewer
|
||||||
ref={viewer}
|
ref={viewer}
|
||||||
initialValue={value}
|
initialValue={value}
|
||||||
customHTMLSanitizer={(content: string) => content}
|
customHTMLSanitizer={(content: string) => content}
|
||||||
plugins={plugins}
|
plugins={plugins}
|
||||||
/>
|
/>
|
||||||
</WidgetPreviewContainer>
|
</WidgetPreviewContainer>
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [plugins]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MarkdownPreview;
|
export default MarkdownPreview;
|
||||||
|
78
core/src/widgets/markdown/hooks/useMedia.ts
Normal file
78
core/src/widgets/markdown/hooks/useMedia.ts
Normal 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;
|
@ -6,17 +6,19 @@ import type { MarkdownEditorOptions, MarkdownPluginFactoryProps } from '../../..
|
|||||||
|
|
||||||
const usePlugins = (
|
const usePlugins = (
|
||||||
editorPlugins: MarkdownEditorOptions['plugins'] = [],
|
editorPlugins: MarkdownEditorOptions['plugins'] = [],
|
||||||
{ getAsset, field, mode }: MarkdownPluginFactoryProps,
|
{ config, media, field, mode }: MarkdownPluginFactoryProps,
|
||||||
) => {
|
) => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const plugins = [imagePlugin({ getAsset, field, mode })];
|
const plugins = [imagePlugin({ config, media, field, mode })];
|
||||||
|
|
||||||
if (plugins) {
|
if (plugins) {
|
||||||
plugins.push(...editorPlugins.map(editorPlugin => editorPlugin({ getAsset, field, mode })));
|
plugins.push(
|
||||||
|
...editorPlugins.map(editorPlugin => editorPlugin({ config, media, field, mode })),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return plugins;
|
return plugins;
|
||||||
}, [editorPlugins, field, getAsset, mode]);
|
}, [config, editorPlugins, field, media, mode]);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default usePlugins;
|
export default usePlugins;
|
||||||
|
@ -6,25 +6,23 @@ function isLinkNode(node: MdNode): node is LinkMdNode {
|
|||||||
return 'destination' in node;
|
return 'destination' in node;
|
||||||
}
|
}
|
||||||
|
|
||||||
const toHTMLRenderers: (props: MarkdownPluginFactoryProps) => CustomHTMLRenderer = ({
|
const toHTMLRenderer: (props: MarkdownPluginFactoryProps) => CustomHTMLRenderer = ({ media }) => ({
|
||||||
getAsset,
|
|
||||||
field,
|
|
||||||
}) => ({
|
|
||||||
image: (node: MdNode, { entering, skipChildren }) => {
|
image: (node: MdNode, { entering, skipChildren }) => {
|
||||||
if (entering && isLinkNode(node)) {
|
if (entering && isLinkNode(node)) {
|
||||||
skipChildren();
|
skipChildren();
|
||||||
|
|
||||||
|
let imageUrl = node.destination ?? '';
|
||||||
|
if (node.destination) {
|
||||||
|
imageUrl = media.getMedia(node.destination)?.toString() ?? node.destination;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'openTag',
|
type: 'openTag',
|
||||||
tagName: 'img',
|
tagName: 'img',
|
||||||
outerNewLine: true,
|
outerNewLine: true,
|
||||||
attributes: {
|
attributes: {
|
||||||
src: node.destination,
|
src: node.destination,
|
||||||
onerror: `this.onerror=null; this.src='${
|
onerror: `this.onerror=null; this.src='${imageUrl}'`,
|
||||||
node.destination
|
|
||||||
? getAsset(node.destination, field)?.toString() ?? node.destination
|
|
||||||
: ''
|
|
||||||
}'`,
|
|
||||||
},
|
},
|
||||||
selfClose: true,
|
selfClose: true,
|
||||||
};
|
};
|
||||||
@ -36,7 +34,7 @@ const toHTMLRenderers: (props: MarkdownPluginFactoryProps) => CustomHTMLRenderer
|
|||||||
|
|
||||||
const imagePlugin: MarkdownPluginFactory = props => {
|
const imagePlugin: MarkdownPluginFactory = props => {
|
||||||
return () => ({
|
return () => ({
|
||||||
toHTMLRenderers: toHTMLRenderers(props),
|
toHTMLRenderers: toHTMLRenderer(props),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user