@atlaskit/editor-plugin-card
Version:
Card plugin for @atlaskit/editor-core
284 lines (283 loc) • 16.1 kB
JavaScript
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { ACTION_SUBJECT_ID, EVENT_TYPE, ACTION, ACTION_SUBJECT } from '@atlaskit/editor-common/analytics/types/enums';
import HoverLinkOverlay from '@atlaskit/editor-common/HoverLinkOverlay';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/useSharedPluginStateWithSelector';
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
import { extractSmartLinkEmbed } from '@atlaskit/link-extractors/extract-smart-link-embed';
import { isWithinPreviewPanelIFrame } from '@atlaskit/linking-common/utils/is-within-preview-panel-iframe';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { getObjectAri, getObjectName, getObjectIconUrl } from '@atlaskit/smart-card';
import { useSmartLinkDestinationUrl } from '@atlaskit/smart-card/hook/use-smart-link-destination-url';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { registerRemoveOverlay } from '../pm-plugins/actions';
import { pluginKey } from '../pm-plugins/plugin-key';
import { AwarenessWrapper } from '../ui/AwarenessWrapper';
import { PreviewInvoker } from '../ui/preview/PreviewInvoker';
import { InlineCard } from './inlineCardBase';
// Module scope so the stored callback's closure chain cannot reach the
// component scope (getPos, node, view) via V8 context sharing.
const createRemoveOverlay = setIsInsertedRef => () => {
var _setIsInsertedRef$cur;
return (_setIsInsertedRef$cur = setIsInsertedRef.current) === null || _setIsInsertedRef$cur === void 0 ? void 0 : _setIsInsertedRef$cur.call(setIsInsertedRef, false);
};
const selector = states => {
var _states$editorViewMod, _states$selectionStat;
return {
mode: (_states$editorViewMod = states.editorViewModeState) === null || _states$editorViewMod === void 0 ? void 0 : _states$editorViewMod.mode,
selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection
};
};
export const InlineCardWithAwareness = /*#__PURE__*/memo(({
node,
cardContext,
actionOptions,
useAlternativePreloader,
view,
getPos,
pluginInjectionApi,
onClick,
isPulseEnabled,
isOverlayEnabled,
isSelected,
isPageSSRed,
provider,
appearance
}) => {
var _pluginInjectionApi$c;
const [isHovered, setIsHovered] = useState(false);
const [isInserted, setIsInserted] = useState(false);
const setIsInsertedRef = useRef(setIsInserted);
useEffect(() => {
return () => {
if (isExperimentEnabled('platform_editor_reduce_memory_leaks')) {
setIsInsertedRef.current = undefined;
}
};
}, []);
const [isResolvedViewRendered, setIsResolvedViewRendered] = useState(false);
const editorAppearance = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c = pluginInjectionApi.card.sharedState.currentState()) === null || _pluginInjectionApi$c === void 0 ? void 0 : _pluginInjectionApi$c.editorAppearance;
const destinationUrl = useSmartLinkDestinationUrl(node.attrs.url);
// Remove tr: Transaction | undefined parameter with platform_editor_reduce_forced_layout_2 cleanup.
const onResolve = useCallback((tr, title) => {
if (tr) {
const metadata = tr.getMeta(pluginKey);
const removeOverlay = isExperimentEnabled('platform_editor_reduce_memory_leaks') ? createRemoveOverlay(setIsInsertedRef) : () => setIsInserted(false);
if (metadata && metadata.type === 'REGISTER') {
registerRemoveOverlay(removeOverlay, metadata.info)(tr);
} else {
registerRemoveOverlay(removeOverlay)(tr);
}
}
if (title) {
setIsResolvedViewRendered(true);
}
}, []);
const markMostRecentlyInsertedLink = useCallback(isLinkMostRecentlyInserted => {
if (isOverlayEnabled && !isExperimentEnabled('platform_editor_reduce_forced_layout_2')) {
setIsInserted(isLinkMostRecentlyInserted);
}
}, [isExperimentEnabled('platform_editor_reduce_forced_layout_2') ? undefined : isOverlayEnabled]);
const setOverlayHoveredStyles = useCallback(isHovered => {
if (isOverlayEnabled) {
setIsHovered(isHovered);
}
}, [isOverlayEnabled]);
const {
mode,
selection
} = useSharedPluginStateWithSelector(pluginInjectionApi, ['selection', 'editorViewMode'], selector);
const floatingToolbarNode = selection instanceof NodeSelection && selection.node;
// This is a prop to show Hover card, Hover card should be shown only in Live View and Classic Renderer (note when only Editor controls enabled we don't show in Live view)
const showHoverPreview = floatingToolbarNode !== node;
const innerCardWithOpenButtonOverlay = useMemo(() => {
var _pluginInjectionApi$a;
return /*#__PURE__*/React.createElement(HoverLinkOverlay, {
isVisible: isResolvedViewRendered,
url: node.attrs.url,
destinationUrl: destinationUrl,
compactPadding: editorAppearance === 'comment' || editorAppearance === 'chromeless',
editorAnalyticsApi: pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions,
view: view
}, /*#__PURE__*/React.createElement(InlineCard, {
node: node,
view: view,
getPos: getPos,
useAlternativePreloader: useAlternativePreloader,
actionOptions: actionOptions,
onResolve: onResolve,
onClick: onClick,
cardContext: cardContext,
isHovered: isHovered,
isPageSSRed: isPageSSRed,
provider: provider,
pluginInjectionApi: pluginInjectionApi,
disablePreviewPanel: true
}));
}, [isResolvedViewRendered, node, editorAppearance, view, getPos, useAlternativePreloader, actionOptions, onResolve, onClick, cardContext, isHovered, isPageSSRed, provider, pluginInjectionApi, destinationUrl]);
const innerCardOriginal = useMemo(() => /*#__PURE__*/React.createElement(InlineCard, {
node: node,
view: view,
getPos: getPos,
useAlternativePreloader: useAlternativePreloader,
actionOptions: actionOptions,
onResolve: onResolve,
onClick: onClick,
cardContext: cardContext,
isHovered: isHovered,
isPageSSRed: isPageSSRed,
provider: provider,
pluginInjectionApi: pluginInjectionApi,
showHoverPreview: false
}), [actionOptions, cardContext, getPos, isHovered, node, onClick, onResolve, useAlternativePreloader, view, isPageSSRed, provider, pluginInjectionApi]);
const shouldShowOpenButtonOverlay = useMemo(() => {
const shouldShowOpenButtonOverlayInChomeless = editorAppearance === 'chromeless';
return mode === 'edit' || editorAppearance === 'comment' || shouldShowOpenButtonOverlayInChomeless;
}, [mode, editorAppearance]);
let innerCard = shouldShowOpenButtonOverlay ? innerCardWithOpenButtonOverlay : innerCardOriginal;
if (mode === 'view') {
var _cardContext$value, _cardContext$value$st;
const url = node.attrs.url;
const cardState = cardContext === null || cardContext === void 0 ? void 0 : (_cardContext$value = cardContext.value) === null || _cardContext$value === void 0 ? void 0 : (_cardContext$value$st = _cardContext$value.store) === null || _cardContext$value$st === void 0 ? void 0 : _cardContext$value$st.getState()[url];
if (cardState) {
var _cardContext$value2, _cardContext$value2$i, _cardContext$value3;
const ari = getObjectAri(cardState.details);
const name = getObjectName(cardState.details);
const iconUrl = getObjectIconUrl(cardState.details);
const isPanelAvailable = ari && (cardContext === null || cardContext === void 0 ? void 0 : (_cardContext$value2 = cardContext.value) === null || _cardContext$value2 === void 0 ? void 0 : (_cardContext$value2$i = _cardContext$value2.isPreviewPanelAvailable) === null || _cardContext$value2$i === void 0 ? void 0 : _cardContext$value2$i.call(_cardContext$value2, {
ari
}));
const openPreviewPanel = cardContext === null || cardContext === void 0 ? void 0 : (_cardContext$value3 = cardContext.value) === null || _cardContext$value3 === void 0 ? void 0 : _cardContext$value3.openPreviewPanel;
const isPreviewPanelAvailable = Boolean(openPreviewPanel && isPanelAvailable);
const firePreviewPanelClickEvent = ({
previewType
}) => {
var _cardContext$value4, _pluginInjectionApi$a2;
const store = cardContext === null || cardContext === void 0 ? void 0 : (_cardContext$value4 = cardContext.value) === null || _cardContext$value4 === void 0 ? void 0 : _cardContext$value4.store;
const urlState = store === null || store === void 0 ? void 0 : store.getState()[url || ''];
if (pluginInjectionApi !== null && pluginInjectionApi !== void 0 && (_pluginInjectionApi$a2 = pluginInjectionApi.analytics) !== null && _pluginInjectionApi$a2 !== void 0 && _pluginInjectionApi$a2.actions) {
var _pluginInjectionApi$a3, _pluginInjectionApi$a4, _urlState$details$met, _urlState$details, _urlState$details$met2, _urlState$details$met3, _urlState$details2, _urlState$details2$me;
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a3 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a3 === void 0 ? void 0 : (_pluginInjectionApi$a4 = _pluginInjectionApi$a3.actions) === null || _pluginInjectionApi$a4 === void 0 ? void 0 : _pluginInjectionApi$a4.fireAnalyticsEvent({
action: ACTION.CLICKED,
actionSubject: ACTION_SUBJECT.SMART_LINK,
actionSubjectId: ACTION_SUBJECT_ID.HOVER_LABEL,
eventType: EVENT_TYPE.UI,
attributes: {
previewType,
destinationProduct: (_urlState$details$met = urlState === null || urlState === void 0 ? void 0 : (_urlState$details = urlState.details) === null || _urlState$details === void 0 ? void 0 : (_urlState$details$met2 = _urlState$details.meta) === null || _urlState$details$met2 === void 0 ? void 0 : _urlState$details$met2.product) !== null && _urlState$details$met !== void 0 ? _urlState$details$met : null,
destinationSubproduct: (_urlState$details$met3 = urlState === null || urlState === void 0 ? void 0 : (_urlState$details2 = urlState.details) === null || _urlState$details2 === void 0 ? void 0 : (_urlState$details2$me = _urlState$details2.meta) === null || _urlState$details2$me === void 0 ? void 0 : _urlState$details2$me.subproduct) !== null && _urlState$details$met3 !== void 0 ? _urlState$details$met3 : null
}
});
}
};
const innerCardWithPanelButtonOverlay = /*#__PURE__*/React.createElement(PreviewInvoker, {
url: url,
appearance: "inline"
}, ({
canPreview,
invokePreview
}) => {
const isPreviewModalAvailable = Boolean(canPreview && invokePreview);
// In view mode we show HoverLinkOverlay only with if preview mode or panel is available
// otherwise a use can click on smartlink itself to open the link in a new tab.
const isPreviewAvailable = isPreviewPanelAvailable || isPreviewModalAvailable;
// When inside preview panel iframe, hide the overlay button
const isInPreviewPanel = isWithinPreviewPanelIFrame();
const showPanelButton = isInPreviewPanel ? isPreviewPanelAvailable : isPreviewAvailable;
if (isPreviewAvailable) {
var _pluginInjectionApi$a5;
return /*#__PURE__*/React.createElement(HoverLinkOverlay, {
isVisible: isResolvedViewRendered,
url: url,
destinationUrl: destinationUrl,
compactPadding: editorAppearance === 'comment' || editorAppearance === 'chromeless',
editorAnalyticsApi: pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a5 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a5 === void 0 ? void 0 : _pluginInjectionApi$a5.actions,
view: view,
showPanelButton: showPanelButton,
showPanelButtonIcon: isPreviewAvailable && isPreviewPanelAvailable ? 'panel' : 'modal'
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
onClick: event => {
if (isPreviewPanelAvailable) {
var _extractSmartLinkEmbe;
event.preventDefault();
openPreviewPanel === null || openPreviewPanel === void 0 ? void 0 : openPreviewPanel({
url,
ari: ari || '',
name: name || '',
iconUrl,
panelData: {
embedUrl: expValEquals('platform_hover_card_preview_panel', 'cohort', 'test') ? (_extractSmartLinkEmbe = extractSmartLinkEmbed(cardState === null || cardState === void 0 ? void 0 : cardState.details)) === null || _extractSmartLinkEmbe === void 0 ? void 0 : _extractSmartLinkEmbe.src : undefined
}
});
firePreviewPanelClickEvent({
previewType: 'panel'
});
} else if (isPreviewModalAvailable) {
event.preventDefault();
invokePreview === null || invokePreview === void 0 ? void 0 : invokePreview();
firePreviewPanelClickEvent({
previewType: 'modal'
});
}
}
}, /*#__PURE__*/React.createElement(InlineCard, {
node: node,
view: view,
getPos: getPos,
useAlternativePreloader: useAlternativePreloader,
actionOptions: actionOptions,
onResolve: onResolve,
onClick: onClick,
cardContext: cardContext,
isHovered: isHovered,
isPageSSRed: isPageSSRed,
provider: provider,
pluginInjectionApi: pluginInjectionApi,
showHoverPreview: mode === 'view' && showHoverPreview,
disablePreviewPanel: true
}));
} else {
return /*#__PURE__*/React.createElement(InlineCard, {
node: node,
view: view,
getPos: getPos,
useAlternativePreloader: useAlternativePreloader,
actionOptions: actionOptions,
onResolve: onResolve,
onClick: onClick,
cardContext: cardContext,
isHovered: isHovered,
isPageSSRed: isPageSSRed,
provider: provider,
pluginInjectionApi: pluginInjectionApi,
showHoverPreview: mode === 'view' && showHoverPreview
});
}
});
innerCard = innerCardWithPanelButtonOverlay;
}
}
const getPosFunction = typeof getPos === 'function' ? getPos : undefined;
const placeholderUniqId = (getPosFunction === null || getPosFunction === void 0 ? void 0 : getPosFunction()) || 0;
return isOverlayEnabled || isPulseEnabled ? /*#__PURE__*/React.createElement(AwarenessWrapper, {
isOverlayEnabled: isOverlayEnabled,
isPulseEnabled: isPulseEnabled,
cardContext: cardContext,
getPos: getPos,
isHovered: isHovered,
isInserted: isInserted,
url: node.attrs.url,
isSelected: isSelected,
isResolvedViewRendered: isResolvedViewRendered,
markMostRecentlyInsertedLink: markMostRecentlyInsertedLink,
pluginInjectionApi: pluginInjectionApi,
setOverlayHoveredStyles: setOverlayHoveredStyles,
appearance: appearance
}, innerCard) : /*#__PURE__*/React.createElement("span", {
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
className: "card",
"data-vc": "editor-plugin-inline-card",
"data-ssr-placeholder": `editor-plugin-inline-card-${placeholderUniqId}`,
"data-ssr-placeholder-replace": `editor-plugin-inline-card-${placeholderUniqId}`
}, innerCard);
});