UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

221 lines (218 loc) • 9.26 kB
/** * @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 });