@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
221 lines (218 loc) • 9.26 kB
JavaScript
/**
* @jsxRuntime classic
* @jsx jsx
*/
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports -- Ignored via go/DSP-18766; jsx required at runtime for @jsxRuntime classic
import { jsx } from '@emotion/react';
import ReactDOM from 'react-dom';
import { createIntl, injectIntl } from 'react-intl';
import { useAnalyticsEvents } from '@atlaskit/analytics-next/useAnalyticsEvents';
import { fireFailedMediaInlineEvent, fireSucceededMediaInlineEvent, MediaCardError } from '@atlaskit/media-card';
import { FileFetcherError } from '@atlaskit/media-client';
import { MediaClientContext } from '@atlaskit/media-client-react';
import { MediaViewer } from '@atlaskit/media-viewer';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { messages } from '../messages/media-inline-card';
import { referenceHeights } from './constants';
import { InlineImageCard } from './inline-image-card';
import { InlineImageWrapper } from './inline-image-wrapper';
import { InlineImageCardErrorView } from './views/error-view';
import { InlineImageCardLoadingView } from './views/loading-view';
export const MediaInlineImageCardInternal = ({
mediaClient,
identifier,
isSelected,
intl,
alt,
isLazy,
width,
height,
border,
ssr,
serializeDataAttrs,
shouldOpenMediaViewer,
isViewOnly
}) => {
const [fileState, setFileState] = useState();
const [subscribeError, setSubscribeError] = useState();
const [isFailedEventSent, setIsFailedEventSent] = useState(false);
const [isSucceededEventSent, setIsSucceededEventSent] = useState(false);
const [isMediaViewerVisible, setMediaViewerVisible] = useState(false);
const {
formatMessage
} = intl || createIntl({
locale: 'en'
});
const {
createAnalyticsEvent
} = useAnalyticsEvents();
const fireFailedOperationalEvent = (error = new MediaCardError('missing-error-data'), failReason) => {
if (!isFailedEventSent && fileState) {
setIsFailedEventSent(true);
fireFailedMediaInlineEvent(fileState, error, failReason, createAnalyticsEvent);
}
};
const fireSucceededOperationalEvent = () => {
if (!isSucceededEventSent && fileState) {
setIsSucceededEventSent(true);
fireSucceededMediaInlineEvent(fileState, createAnalyticsEvent);
}
};
useEffect(() => {
if (mediaClient) {
const subscription = mediaClient.file.getFileState(identifier.id, {
collectionName: identifier.collectionName
}).subscribe({
next: fileState => {
setFileState(fileState);
setSubscribeError(undefined);
},
error: e => {
setSubscribeError(e);
}
});
return () => {
subscription.unsubscribe();
};
}
}, [identifier, mediaClient]);
const memoizedRenderError = useCallback(() => jsx(InlineImageCardErrorView, {
message: formatMessage(messages.unableToLoadContent)
}), [formatMessage]);
const content = dimensions => {
if (!mediaClient) {
return jsx(InlineImageCardLoadingView, null);
}
if (!ssr) {
if (subscribeError) {
const isUploading = (fileState === null || fileState === void 0 ? void 0 : fileState.status) === 'uploading';
const errorMessage = isUploading ? messages.failedToUpload : messages.unableToLoadContent;
const errorReason = (fileState === null || fileState === void 0 ? void 0 : fileState.status) === 'uploading' ? 'upload' : 'metadata-fetch';
fireFailedOperationalEvent(new MediaCardError(errorReason, subscribeError));
return jsx(InlineImageCardErrorView, {
message: formatMessage(errorMessage)
});
}
if (!fileState || (fileState === null || fileState === void 0 ? void 0 : fileState.status) === 'uploading') {
return jsx(InlineImageCardLoadingView, null);
}
if (fileState.status === 'error') {
fireFailedOperationalEvent(new MediaCardError('error-file-state', new Error(fileState.message)));
return jsx(InlineImageCardErrorView, {
message: formatMessage(messages.unableToLoadContent)
});
} else if (fileState.status === 'failed-processing') {
fireFailedOperationalEvent(undefined, 'failed-processing');
return jsx(InlineImageCardErrorView, {
message: formatMessage(messages.unableToLoadContent)
});
} else if (!fileState.name) {
fireFailedOperationalEvent(new MediaCardError('metadata-fetch', new FileFetcherError('emptyFileName', {
id: fileState.id
})));
return jsx(InlineImageCardErrorView, {
message: formatMessage(messages.unableToLoadContent)
});
}
if (fileState.status === 'processed') {
fireSucceededOperationalEvent();
}
}
return jsx(MediaClientContext.Provider, {
value: mediaClient
}, jsx(InlineImageCard, {
dimensions: dimensions,
identifier: identifier,
renderError: expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? memoizedRenderError :
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- intentional fallback for experiment off path
() => jsx(InlineImageCardErrorView, {
message: formatMessage(messages.unableToLoadContent)
}),
alt: alt,
ssr: ssr === null || ssr === void 0 ? void 0 : ssr.mode,
isLazy: isLazy,
crop: true,
stretch: false
}));
};
const aspectRatio = useMemo(() => width && height ? width / height : undefined, [width, height]);
/**
* scaledDimensions is used to define the correct media size fetched from media service
* inline images will only ever be rendered at a maximum height of H1 and so scaled dimensions
* will only ever return a width and height where the height has a maximum height of H1
*/
const scaledDimension = useMemo(() => {
if (!width || !height || !aspectRatio) {
return {
width,
height
};
}
return {
width: Math.round(aspectRatio * referenceHeights['h1']),
height: referenceHeights['h1']
};
}, [width, height, aspectRatio]);
const htmlAttributes = useMemo(() => {
if (serializeDataAttrs) {
const resolvedAttrs = fileState && fileState.status !== 'error' ? {
'data-file-size': fileState.size,
'data-file-mime-type': fileState.mimeType,
'data-file-name': fileState.name
} : {};
return {
'data-type': 'image',
'data-node-type': 'mediaInline',
'data-id': identifier.id,
'data-collection': identifier.collectionName,
'data-width': width,
'data-height': height,
'data-alt': alt,
...resolvedAttrs
};
}
return {};
}, [alt, fileState, height, identifier, width, serializeDataAttrs]);
const onMediaInlineImageClick = useCallback(e => {
if (shouldOpenMediaViewer) {
setMediaViewerVisible(true);
}
// In edit mode (node content wrapper has contenteditable set to true), link redirection is disabled by default
// We need to call "stopPropagation" here in order to prevent in editor view mode, the browser from navigating to
// another URL if the media node is wrapped in a link mark.
if (isViewOnly && editorExperiment('platform_editor_controls', 'variant1')) {
e.preventDefault();
}
}, [shouldOpenMediaViewer, isViewOnly]);
const onMediaInlinePreviewClose = useCallback(() => {
setMediaViewerVisible(false);
}, []);
const memoizedMediaViewerItems = useMemo(() => [identifier], [identifier]);
const mediaViewer = useMemo(() => {
if (isMediaViewerVisible && mediaClient !== null && mediaClient !== void 0 && mediaClient.mediaClientConfig) {
return /*#__PURE__*/ReactDOM.createPortal(jsx(MediaViewer, {
collectionName: identifier.collectionName || '',
items: expValEquals('platform_editor_perf_lint_cleanup', 'isEnabled', true) ? memoizedMediaViewerItems :
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- intentional fallback for experiment off path
[identifier],
mediaClientConfig: mediaClient === null || mediaClient === void 0 ? void 0 : mediaClient.mediaClientConfig,
selectedItem: identifier,
onClose: onMediaInlinePreviewClose
}), document.body);
}
return null;
}, [identifier, isMediaViewerVisible, mediaClient === null || mediaClient === void 0 ? void 0 : mediaClient.mediaClientConfig, onMediaInlinePreviewClose, memoizedMediaViewerItems]);
return jsx(Fragment, null, jsx(InlineImageWrapper, {
isSelected: isSelected,
aspectRatio: aspectRatio,
borderColor: border === null || border === void 0 ? void 0 : border.borderColor,
borderSize: border === null || border === void 0 ? void 0 : border.borderSize,
htmlAttrs: htmlAttributes,
onClick: onMediaInlineImageClick
}, content(scaledDimension)), mediaViewer);
};
export const MediaInlineImageCard = injectIntl(MediaInlineImageCardInternal, {
enforceContext: false
});