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:
|
||||
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
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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 ? (
|
||||
|
@ -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',
|
||||
);
|
||||
|
@ -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';
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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">
|
||||
|
@ -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) {
|
||||
|
@ -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} />;
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
|
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 = (
|
||||
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;
|
||||
|
@ -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),
|
||||
});
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user