UNPKG

@atlaskit/editor-plugin-card

Version:

Card plugin for @atlaskit/editor-core

418 lines (413 loc) 20.4 kB
/* PasteDisplayAsMenu.tsx generated by @compiled/babel-plugin v0.40.0 */ import "./PasteDisplayAsMenu.compiled.css"; import { ax, ix } from "@compiled/react/runtime"; import React, { useCallback, useEffect, useRef } from 'react'; import { useIntl } from 'react-intl'; import { cx } from '@atlaskit/css'; import { appearancePropsMap } from '@atlaskit/editor-common/card'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { getActiveLinkMark } from '@atlaskit/editor-common/link'; import { PASTE_MENU, useEditorToolbar } from '@atlaskit/editor-common/toolbar'; import { isSupportedInParent } from '@atlaskit/editor-common/utils'; import { Fragment } from '@atlaskit/editor-prosemirror/model'; import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state'; import { ToolbarDropdownItemSection, ToolbarTooltip, useToolbarDropdownMenu } from '@atlaskit/editor-toolbar'; import MinusIcon from '@atlaskit/icon/core/minus'; import SmartLinkCardIcon from '@atlaskit/icon/core/smart-link-card'; import SmartLinkEmbedIcon from '@atlaskit/icon/core/smart-link-embed'; import SmartLinkInlineIcon from '@atlaskit/icon/core/smart-link-inline'; import { useSmartCardContext } from '@atlaskit/link-provider/context'; import { Box, Flex, Pressable } from '@atlaskit/primitives/compiled'; import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure'; import { changeSelectedCardToLink, setSelectedCardAppearance } from '../pm-plugins/doc'; import { getSingleSmartLinkUrlFromSlice } from './currentPastedSmartLink'; import { getCardAtPasteRange } from './pasteDisplayAsUtils'; export const SMART_LINK_DISPLAY_AS_PASTE_MENU_SECTION_KEY = 'smart-link-display-as-paste-menu-section'; // Subset of `PasteMenuRuleFactories` we structurally consume here — kept local // to avoid importing the toolbar package (see cycle note below). // `pasteOptionsToolbarPlugin` is intentionally NOT declared in `CardPluginDependencies` // because doing so introduces a runtime package cycle // (editor-plugin-card -> editor-plugin-paste-options-toolbar -> editor-plugin-paste // -> editor-plugin-card) AND a TS2719 "two different types with this name exist" error // at downstream consumers (e.g. Jira's `EditorAfterBanner.tsx`). The `api` shape is augmented locally instead. const styles = { appearanceBox: "_2rko1qi0 _bfhkhfxm _19pku2gc _2hwxu2gc _otyru2gc _18u0u2gc _ca0q1b66 _u5f31b66 _n3td1b66 _19bv1b66", iconWrapper: "_1e0c11p5 _yv0ehkll _1bsb1osq", iconButton: "_2rko1qi0 _1h6d1l7x _1dqonqa1 _189ee4h9 _4cvr1h6o _syazi7uo _1e0c1txw _4t3izwfg _1bah1h6o _1bsb1osq _bfhksm61 _irr31dpa _1di6fcek", iconButtonSelected: "_2rko1qi0 _1h6dq98m _1dqonqa1 _189ee4h9 _4cvr1h6o _bfhk15s3 _syazi7uo _1e0c1txw _4t3izwfg _1bah1h6o _1bsb1osq _irr3ufnl _1di6nozp", iconButtonDisabled: "_2rko1qi0 _1h6dbk0g _1dqonqa1 _189ee4h9 _4cvr1h6o _bfhk187o _syaz1gmx _1e0c1txw _4t3izwfg _1bah1h6o _1bsb1osq" }; const AppearanceOptionIconButton = ({ appearance, currentAppearance, isDisabled, label, Icon, onClick }) => { return /*#__PURE__*/React.createElement(Box, { xcss: styles.iconWrapper }, /*#__PURE__*/React.createElement(ToolbarTooltip, { content: label, position: "bottom" }, /*#__PURE__*/React.createElement(Pressable, { xcss: cx(styles.iconButton, isDisabled && styles.iconButtonDisabled, !isDisabled && currentAppearance === appearance && styles.iconButtonSelected), "aria-label": label, "aria-pressed": currentAppearance === appearance, isDisabled: isDisabled, onClick: onClick }, /*#__PURE__*/React.createElement(Icon, { label: label })))); }; const InlineAppearanceIconButton = ({ currentAppearance, isDisabled, label, onClick }) => /*#__PURE__*/React.createElement(AppearanceOptionIconButton, { appearance: "inline", currentAppearance: currentAppearance, isDisabled: isDisabled, label: label, Icon: SmartLinkInlineIcon, onClick: onClick }); const BlockAppearanceIconButton = ({ currentAppearance, isDisabled, label, onClick }) => /*#__PURE__*/React.createElement(AppearanceOptionIconButton, { appearance: "block", currentAppearance: currentAppearance, isDisabled: isDisabled, label: label, Icon: SmartLinkCardIcon, onClick: onClick }); const EmbedAppearanceIconButton = ({ currentAppearance, isDisabled, label, onClick }) => /*#__PURE__*/React.createElement(AppearanceOptionIconButton, { appearance: "embed", currentAppearance: currentAppearance, isDisabled: isDisabled, label: label, Icon: SmartLinkEmbedIcon, onClick: onClick }); const getCurrentPastedSlice = api => { var _apiWithPaste$paste, _pasteState$lastConte; const apiWithPaste = api; const pasteState = apiWithPaste === null || apiWithPaste === void 0 ? void 0 : (_apiWithPaste$paste = apiWithPaste.paste) === null || _apiWithPaste$paste === void 0 ? void 0 : _apiWithPaste$paste.sharedState.currentState(); const slice = pasteState === null || pasteState === void 0 ? void 0 : (_pasteState$lastConte = pasteState.lastContentPasted) === null || _pasteState$lastConte === void 0 ? void 0 : _pasteState$lastConte.pastedSlice; return slice; }; const getCardUrlAtPasteRange = ({ editorView, pasteStartPos, pasteEndPos }) => { var _editorView$state$doc, _maybeAttrs$url, _maybeAttrs$data; const cardAtPasteRange = getCardAtPasteRange(editorView.state, pasteStartPos, pasteEndPos); const maybeAttrs = cardAtPasteRange ? (_editorView$state$doc = editorView.state.doc.nodeAt(cardAtPasteRange.pos)) === null || _editorView$state$doc === void 0 ? void 0 : _editorView$state$doc.attrs : undefined; const maybeUrl = (_maybeAttrs$url = maybeAttrs === null || maybeAttrs === void 0 ? void 0 : maybeAttrs.url) !== null && _maybeAttrs$url !== void 0 ? _maybeAttrs$url : maybeAttrs === null || maybeAttrs === void 0 ? void 0 : (_maybeAttrs$data = maybeAttrs.data) === null || _maybeAttrs$data === void 0 ? void 0 : _maybeAttrs$data.url; return typeof maybeUrl === 'string' ? maybeUrl : undefined; }; export const setAppearanceSelection = ({ editorView, pasteStartPos, pasteEndPos, targetPos }) => { const { state, dispatch } = editorView; const maxPos = state.doc.content.size; const clampedStart = Math.max(0, Math.min(pasteStartPos, maxPos)); const clampedEnd = Math.max(0, Math.min(pasteEndPos, maxPos)); const from = Math.min(clampedStart, clampedEnd); const to = Math.max(clampedStart, clampedEnd); const isTargetPosInBounds = targetPos !== undefined && targetPos >= 0 && targetPos <= state.doc.content.size; try { const nextSelection = isTargetPosInBounds && targetPos !== undefined ? NodeSelection.create(state.doc, targetPos) : TextSelection.create(state.doc, from, to); const tr = state.tr.setSelection(nextSelection); dispatch(tr); return true; } catch { return false; } }; export const getFirstLinkRangeInSelection = editorView => { const { state } = editorView; const linkMarkType = state.schema.marks.link; if (!linkMarkType) { return; } let firstLinkRange; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos) => { if (firstLinkRange || !node.isText) { return; } if (linkMarkType.isInSet(node.marks)) { firstLinkRange = { from: pos, to: pos + node.nodeSize }; } }); return firstLinkRange; }; export const normalizeSelectionToLinkRangeForUrlAppearance = ({ editorView, targetPos }) => { if (targetPos !== undefined || getActiveLinkMark(editorView.state)) { return; } const firstLinkRange = getFirstLinkRangeInSelection(editorView); if (!firstLinkRange) { return; } const linkRangeSelectionTr = editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, firstLinkRange.from, firstLinkRange.to)); editorView.dispatch(linkRangeSelectionTr); }; const PasteDisplayAsMenuHorizontalView = ({ api, allowBlockCards, allowEmbeds }) => { var _smartCardContext$val, _smartCardContext$val2, _smartCardContext$val3, _getCardAtPasteRange$, _getCardAtPasteRange, _smartCardContext$val4; const intl = useIntl(); const { editorView } = useEditorToolbar(); const toolbarDropdownMenu = useToolbarDropdownMenu(); const smartCardContext = useSmartCardContext(); const frameRef = useRef(null); const isApplyingRef = useRef(false); const apiWithPasteOptionsToolbar = api; const pasteRange = useSharedPluginStateWithSelector(apiWithPasteOptionsToolbar, ['pasteOptionsToolbarPlugin'], // @ts-ignore TS7006 - isolated Fast Typecheck can't infer the selector states generic states => { const pluginState = states.pasteOptionsToolbarPluginState; return pluginState ? { pasteEndPos: pluginState.pasteEndPos, pasteStartPos: pluginState.pasteStartPos } : undefined; }); // Subscribe to card state so the menu re-renders when resolved card metadata updates. // @ts-ignore TS7006 - isolated Fast Typecheck can't infer the selector states generic useSharedPluginStateWithSelector(api, ['card'], states => { return states.cardState; }); const pastedLinkUrlFromSlice = getSingleSmartLinkUrlFromSlice(getCurrentPastedSlice(api)); const pastedLinkUrlFromCard = editorView && pasteRange ? getCardUrlAtPasteRange({ editorView, pasteStartPos: pasteRange.pasteStartPos, pasteEndPos: pasteRange.pasteEndPos }) : undefined; const pastedLinkUrl = pastedLinkUrlFromSlice !== null && pastedLinkUrlFromSlice !== void 0 ? pastedLinkUrlFromSlice : pastedLinkUrlFromCard; const pastedLinkUrlState = pastedLinkUrl ? (_smartCardContext$val = smartCardContext.value) === null || _smartCardContext$val === void 0 ? void 0 : (_smartCardContext$val2 = _smartCardContext$val.store) === null || _smartCardContext$val2 === void 0 ? void 0 : (_smartCardContext$val3 = _smartCardContext$val2.getState()) === null || _smartCardContext$val3 === void 0 ? void 0 : _smartCardContext$val3[pastedLinkUrl] : undefined; const hasResolvedSmartLinkData = Boolean(pastedLinkUrlState === null || pastedLinkUrlState === void 0 ? void 0 : pastedLinkUrlState.details); const currentAppearance = editorView && pasteRange ? (_getCardAtPasteRange$ = (_getCardAtPasteRange = getCardAtPasteRange(editorView.state, pasteRange.pasteStartPos, pasteRange.pasteEndPos)) === null || _getCardAtPasteRange === void 0 ? void 0 : _getCardAtPasteRange.appearance) !== null && _getCardAtPasteRange$ !== void 0 ? _getCardAtPasteRange$ : 'url' : undefined; const handleClick = useCallback(appearance => () => { var _state$doc$nodeAt2, _targetNodeAttrs$url, _targetNodeAttrs$data; if (!editorView || !pasteRange || !pastedLinkUrl || !currentAppearance || isApplyingRef.current) { return; } isApplyingRef.current = true; const { state, dispatch } = editorView; const { pasteStartPos, pasteEndPos } = pasteRange; const cardAtPasteRange = getCardAtPasteRange(state, pasteStartPos, pasteEndPos); if (appearance === 'url') { if (cardAtPasteRange) { var _state$doc$nodeAt, _api$analytics; changeSelectedCardToLink(pastedLinkUrl, pastedLinkUrl, true, (_state$doc$nodeAt = state.doc.nodeAt(cardAtPasteRange.pos)) !== null && _state$doc$nodeAt !== void 0 ? _state$doc$nodeAt : undefined, cardAtPasteRange.pos, api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions)(state, dispatch, editorView); } toolbarDropdownMenu === null || toolbarDropdownMenu === void 0 ? void 0 : toolbarDropdownMenu.closeMenu(null); isApplyingRef.current = false; return; } const targetPos = cardAtPasteRange === null || cardAtPasteRange === void 0 ? void 0 : cardAtPasteRange.pos; const targetNodeAttrs = targetPos === undefined ? undefined : (_state$doc$nodeAt2 = state.doc.nodeAt(targetPos)) === null || _state$doc$nodeAt2 === void 0 ? void 0 : _state$doc$nodeAt2.attrs; const targetNodeUrl = (_targetNodeAttrs$url = targetNodeAttrs === null || targetNodeAttrs === void 0 ? void 0 : targetNodeAttrs.url) !== null && _targetNodeAttrs$url !== void 0 ? _targetNodeAttrs$url : targetNodeAttrs === null || targetNodeAttrs === void 0 ? void 0 : (_targetNodeAttrs$data = targetNodeAttrs.data) === null || _targetNodeAttrs$data === void 0 ? void 0 : _targetNodeAttrs$data.url; const isRecoveredAdjacentPastedCard = expValEqualsNoExposure('confluence_editor_paste_3p_link_actions_menu', 'isEnabled', true) && targetPos === pasteStartPos - 1 && pasteStartPos === pasteEndPos && targetNodeUrl === pastedLinkUrl; if (targetPos !== undefined && targetPos < pasteStartPos && !isRecoveredAdjacentPastedCard) { isApplyingRef.current = false; return; } const didApplySelection = setAppearanceSelection({ editorView, pasteStartPos, pasteEndPos, targetPos }); if (!didApplySelection) { toolbarDropdownMenu === null || toolbarDropdownMenu === void 0 ? void 0 : toolbarDropdownMenu.closeMenu(null); isApplyingRef.current = false; return; } normalizeSelectionToLinkRangeForUrlAppearance({ editorView, targetPos }); frameRef.current = requestAnimationFrame(() => { var _api$analytics2; frameRef.current = null; setSelectedCardAppearance(appearance, api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions)(editorView.state, editorView.dispatch, editorView); toolbarDropdownMenu === null || toolbarDropdownMenu === void 0 ? void 0 : toolbarDropdownMenu.closeMenu(null); isApplyingRef.current = false; }); }, [api, currentAppearance, editorView, pasteRange, pastedLinkUrl, toolbarDropdownMenu]); useEffect(() => { return () => { if (frameRef.current !== null) { cancelAnimationFrame(frameRef.current); } isApplyingRef.current = false; }; }, []); if (!editorView || !pasteRange || !pastedLinkUrl || !currentAppearance) { return null; } const preview = allowEmbeds && pastedLinkUrl && ((_smartCardContext$val4 = smartCardContext.value) === null || _smartCardContext$val4 === void 0 ? void 0 : _smartCardContext$val4.extractors.getPreview(pastedLinkUrl, 'web')); const blockCardNodeType = editorView.state.schema.nodes.blockCard; const embedCardNodeType = editorView.state.schema.nodes.embedCard; const isBlockSupportedFromAppearanceContext = allowBlockCards && blockCardNodeType && isSupportedInParent(editorView.state, Fragment.from(blockCardNodeType.createChecked({})), currentAppearance === 'url' || currentAppearance === 'inline' ? undefined : currentAppearance); const isEmbedSupportedFromAppearanceContext = allowEmbeds && preview && embedCardNodeType && isSupportedInParent(editorView.state, Fragment.from(embedCardNodeType.createChecked({})), currentAppearance === 'url' || currentAppearance === 'inline' ? undefined : currentAppearance); const isSmartLinkConvertible = hasResolvedSmartLinkData; const isBlockSupportedFromSelection = allowBlockCards && blockCardNodeType && isSupportedInParent(editorView.state, Fragment.from(blockCardNodeType.createChecked({})), undefined); const isEmbedSupportedFromSelection = allowEmbeds && preview && embedCardNodeType && isSupportedInParent(editorView.state, Fragment.from(embedCardNodeType.createChecked({})), undefined); const isBlockSupported = Boolean(isBlockSupportedFromAppearanceContext || isBlockSupportedFromSelection); const isEmbedSupported = Boolean(isEmbedSupportedFromAppearanceContext || isEmbedSupportedFromSelection); return /*#__PURE__*/React.createElement(Flex, { xcss: styles.appearanceBox, gap: "space.050" }, /*#__PURE__*/React.createElement(AppearanceOptionIconButton, { appearance: "url", currentAppearance: currentAppearance, isDisabled: false, label: intl.formatMessage(appearancePropsMap.url.title), Icon: MinusIcon, onClick: handleClick('url') }), /*#__PURE__*/React.createElement(InlineAppearanceIconButton, { currentAppearance: currentAppearance, isDisabled: !isSmartLinkConvertible, label: intl.formatMessage(appearancePropsMap.inline.title), onClick: handleClick('inline') }), /*#__PURE__*/React.createElement(BlockAppearanceIconButton, { currentAppearance: currentAppearance, isDisabled: !isSmartLinkConvertible || !isBlockSupported, label: intl.formatMessage(appearancePropsMap.block.title), onClick: handleClick('block') }), /*#__PURE__*/React.createElement(EmbedAppearanceIconButton, { currentAppearance: currentAppearance, isDisabled: !isEmbedSupported, label: intl.formatMessage(appearancePropsMap.embed.title), onClick: handleClick('embed') })); }; const PasteDisplayAsMenuSection = ({ api, allowBlockCards, allowEmbeds }) => { const intl = useIntl(); return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, { title: intl.formatMessage({ defaultMessage: 'Display as', description: 'Section title for Smart Link display options in the paste actions menu.', id: 'fabric.editor.pasteDisplayAsMenu.displayAs' }) }, /*#__PURE__*/React.createElement(PasteDisplayAsMenuHorizontalView, { api: api, allowBlockCards: allowBlockCards, allowEmbeds: allowEmbeds })); }; export const getPasteDisplayAsMenuComponents = ({ api, allowBlockCards, allowEmbeds, getEditorView, smartCardClientRef }) => { const smartlinkAsyncHidden = async context => { const { pastedUrl } = context; if (!pastedUrl) { return false; } const isEnabled = expValEqualsNoExposure('confluence_editor_paste_3p_link_actions_menu', 'isEnabled', true); const smartCardClient = smartCardClientRef === null || smartCardClientRef === void 0 ? void 0 : smartCardClientRef.current; if (!isEnabled || !smartCardClient) { return true; } try { var _response$meta; const response = await smartCardClient.fetchData(pastedUrl); return ((_response$meta = response.meta) === null || _response$meta === void 0 ? void 0 : _response$meta.access) !== 'granted'; } catch { // Network / auth errors → hide return true; } }; return [{ type: 'menu-section', key: SMART_LINK_DISPLAY_AS_PASTE_MENU_SECTION_KEY, parents: [{ type: PASTE_MENU.type, key: PASTE_MENU.key, rank: 60 }], isAsyncHidden: smartlinkAsyncHidden, isHidden: () => { var _apiWithPasteOptionsT, _apiWithPasteOptionsT2; if (!expValEqualsNoExposure('confluence_editor_paste_3p_link_actions_menu', 'isEnabled', true)) { return true; } const apiWithPasteOptionsToolbar = api; const pasteRange = apiWithPasteOptionsToolbar === null || apiWithPasteOptionsToolbar === void 0 ? void 0 : (_apiWithPasteOptionsT = apiWithPasteOptionsToolbar.pasteOptionsToolbarPlugin) === null || _apiWithPasteOptionsT === void 0 ? void 0 : _apiWithPasteOptionsT.sharedState.currentState(); const editorView = getEditorView(); if (!editorView || !pasteRange) { return true; } // Slice-shape check — delegated to the toolbar plugin's shared // `notSingleLinkRule` via the structural cast (see note above). const rules = apiWithPasteOptionsToolbar === null || apiWithPasteOptionsToolbar === void 0 ? void 0 : (_apiWithPasteOptionsT2 = apiWithPasteOptionsToolbar.pasteOptionsToolbarPlugin) === null || _apiWithPasteOptionsT2 === void 0 ? void 0 : _apiWithPasteOptionsT2.actions.getPasteMenuRules(); if (!rules || rules.notSingleLinkRule()) { return true; } const urlFromSlice = getSingleSmartLinkUrlFromSlice(getCurrentPastedSlice(api)); if (urlFromSlice) { return false; } // Fallback: plain-text URL paste that the card plugin already resolved into a card node in the doc. const urlFromCard = getCardUrlAtPasteRange({ editorView, pasteStartPos: pasteRange.pasteStartPos, pasteEndPos: pasteRange.pasteEndPos }); return !urlFromCard; }, component: () => /*#__PURE__*/React.createElement(PasteDisplayAsMenuSection, { api: api, allowBlockCards: allowBlockCards, allowEmbeds: allowEmbeds }) }]; };