UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

256 lines (254 loc) 8.88 kB
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { injectIntl } from 'react-intl'; import { usePreviousState } from '@atlaskit/editor-common/hooks'; import { nodeViewsMessages as messages } from '@atlaskit/editor-common/media'; import { isNodeSelectedOrInRange, SelectedState, setNodeSelection } from '@atlaskit/editor-common/utils'; import EditorCloseIcon from '@atlaskit/icon/core/cross'; import { getMediaFeatureFlag } from '@atlaskit/media-common'; import { Filmstrip } from '@atlaskit/media-filmstrip'; import { fg } from '@atlaskit/platform-feature-flags'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { stateKey as mediaStateKey } from '../pm-plugins/plugin-key'; import { createMediaNodeUpdater } from './mediaNodeUpdater'; const getIdentifier = item => { if (item.attrs.type === 'external') { return { mediaItemType: 'external-image', dataURI: item.attrs.url }; } return { id: item.attrs.id, mediaItemType: 'file', collectionName: item.attrs.collection }; }; const isNodeSelected = props => (mediaItemPos, mediaGroupPos) => { const selected = isNodeSelectedOrInRange(props.anchorPos, props.headPos, mediaGroupPos, props.nodeSize); if (selected === SelectedState.selectedInRange) { return true; } if (selected === SelectedState.selectedInside && props.anchorPos === mediaItemPos) { return true; } return false; }; const prepareFilmstripItem = ({ allowLazyLoading, allowMediaInlineImages, enableDownloadButton, handleMediaNodeRemoval, getPos, intl, isMediaItemSelected, setMediaGroupNodeSelection, featureFlags }) => (item, idx) => { // We declared this to get a fresh position every time const getNodePos = () => { const pos = getPos(); if (typeof pos !== 'number') { // That may seems weird, but the previous type wasn't match with the real ProseMirror code. And a lot of Media API was built expecting a number // Because the original code would return NaN on runtime // We are just make it explict now. // We may run a deep investagation on Media code to figure out a better fix. But, for now, we want to keep the current behavior. // TODO: ED-13910 - prosemirror-bump leftovers return NaN; } return pos + idx + 1; }; // Media Inline creates a floating toolbar with the same options, excludes these options if enabled const mediaInlineOptions = (allowMediaInline = false) => { if (!allowMediaInline) { return { shouldEnableDownloadButton: enableDownloadButton, actions: [{ handler: handleMediaNodeRemoval.bind(null, undefined, getNodePos), icon: /*#__PURE__*/React.createElement(EditorCloseIcon, { label: intl.formatMessage(messages.mediaGroupDeleteLabel) }) }] }; } }; const mediaGroupPos = getPos(); return { identifier: getIdentifier(item), isLazy: allowLazyLoading, selected: isMediaItemSelected(getNodePos(), typeof mediaGroupPos === 'number' ? mediaGroupPos : NaN), onClick: () => { setMediaGroupNodeSelection(getNodePos()); }, ...mediaInlineOptions(fg('platform_editor_remove_media_inline_feature_flag') ? allowMediaInlineImages : getMediaFeatureFlag('mediaInline', featureFlags)) }; }; /** * Keep returning the same ProseMirror Node, unless the node content changed. * * React uses shallow comparation with `Object.is`, * but that can cause multiple re-renders when the same node is given in a different instance. * * To avoid unnecessary re-renders, this hook uses the `Node.eq` from ProseMirror API to compare * previous and new values. */ const useLatestMediaGroupNode = nextMediaNode => { const previousMediaNode = usePreviousState(nextMediaNode); const [mediaNode, setMediaNode] = React.useState(nextMediaNode); React.useEffect(() => { if (!previousMediaNode) { return; } if (!previousMediaNode.eq(nextMediaNode)) { setMediaNode(nextMediaNode); } }, [previousMediaNode, nextMediaNode]); return mediaNode; }; const runMediaNodeUpdate = async ({ mediaNodeUpdater, getPos, node, updateAttrs }) => { if (updateAttrs) { await mediaNodeUpdater.updateNodeAttrs(getPos); } const contextId = mediaNodeUpdater.getNodeContextId(); if (!contextId) { await mediaNodeUpdater.updateNodeContextId(getPos); } const shouldNodeBeDeepCopied = await mediaNodeUpdater.shouldNodeBeDeepCopied(); if (shouldNodeBeDeepCopied) { await mediaNodeUpdater.copyNodeFromPos(getPos, { traceId: node.attrs.__mediaTraceId }); } }; const noop = () => {}; // eslint-disable-next-line @typescript-eslint/ban-types export const MediaGroupNext = injectIntl( /*#__PURE__*/React.memo(props => { const { mediaOptions: { allowLazyLoading, allowMediaInlineImages, enableDownloadButton, featureFlags }, intl, getPos, anchorPos, headPos, view, disabled, editorViewMode, mediaProvider, contextIdentifierProvider, isCopyPasteEnabled } = props; const mediaGroupNode = useLatestMediaGroupNode(props.node); const mediaPluginState = useMemo(() => { return mediaStateKey.getState(view.state); }, [view.state]); const mediaClientConfig = mediaPluginState === null || mediaPluginState === void 0 ? void 0 : mediaPluginState.mediaClientConfig; const handleMediaGroupUpdate = mediaPluginState === null || mediaPluginState === void 0 ? void 0 : mediaPluginState.handleMediaGroupUpdate; const [viewMediaClientConfig, setViewMediaClientConfig] = useState(undefined); const nodeSize = mediaGroupNode.nodeSize; const mediaNodesWithOffsets = useMemo(() => { const result = []; mediaGroupNode.forEach((item, childOffset) => { result.push({ node: item, offset: childOffset }); }); return result; }, [mediaGroupNode]); const previousMediaNodesWithOffsets = usePreviousState(mediaNodesWithOffsets); const handleMediaNodeRemoval = useMemo(() => { return disabled || !mediaPluginState ? noop : mediaPluginState.handleMediaNodeRemoval; }, [disabled, mediaPluginState]); const setMediaGroupNodeSelection = useCallback(pos => { setNodeSelection(view, pos); }, [view]); const isMediaItemSelected = useMemo(() => { return isNodeSelected({ anchorPos, headPos, nodeSize }); }, [anchorPos, headPos, nodeSize]); const filmstripItem = useMemo(() => { return prepareFilmstripItem({ allowLazyLoading, allowMediaInlineImages, enableDownloadButton, handleMediaNodeRemoval, getPos, intl, isMediaItemSelected, setMediaGroupNodeSelection, featureFlags }); }, [allowLazyLoading, allowMediaInlineImages, enableDownloadButton, handleMediaNodeRemoval, getPos, intl, isMediaItemSelected, setMediaGroupNodeSelection, featureFlags]); const items = useMemo(() => { return mediaNodesWithOffsets.map(({ node, offset }) => { return filmstripItem(node, offset); }); }, [mediaNodesWithOffsets, filmstripItem]); useEffect(() => { setViewMediaClientConfig(mediaClientConfig); }, [mediaClientConfig]); useEffect(() => { // eslint-disable-next-line @atlassian/perf-linting/no-chain-state-updates -- Ignored via go/ees017 (to be fixed) mediaNodesWithOffsets.forEach(({ node, offset }) => { const mediaNodeUpdater = createMediaNodeUpdater({ view, mediaProvider, contextIdentifierProvider, node, isMediaSingle: false }); const updateAttrs = isCopyPasteEnabled || isCopyPasteEnabled === undefined; runMediaNodeUpdate({ mediaNodeUpdater, node, updateAttrs, getPos: () => { const pos = getPos(); if (typeof pos !== 'number') { return undefined; } return pos + offset + 1; } }); }); }, [view, contextIdentifierProvider, getPos, mediaProvider, mediaNodesWithOffsets, isCopyPasteEnabled]); useEffect(() => { if (!handleMediaGroupUpdate || !previousMediaNodesWithOffsets) { return; } const old = previousMediaNodesWithOffsets.map(({ node }) => node); const next = mediaNodesWithOffsets.map(({ node }) => node); handleMediaGroupUpdate(old, next); return () => { handleMediaGroupUpdate(next, []); }; }, [handleMediaGroupUpdate, mediaNodesWithOffsets, previousMediaNodesWithOffsets]); return /*#__PURE__*/React.createElement(Filmstrip, { items: items, mediaClientConfig: viewMediaClientConfig, featureFlags: featureFlags, shouldOpenMediaViewer: editorViewMode && editorExperiment('platform_editor_controls', 'control') }); })); MediaGroupNext.displayName = 'MediaGroup';