UNPKG

@atlaskit/editor-plugin-card

Version:

Card plugin for @atlaskit/editor-core

298 lines (293 loc) 14.6 kB
import rafSchedule from 'raf-schd'; import { getInlineNodeViewProducer } from '@atlaskit/editor-common/react-node-view'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { DATASOURCE_INNER_CONTAINER_CLASSNAME } from '@atlaskit/editor-common/styles'; import { NodeSelection } from '@atlaskit/editor-prosemirror/state'; import { findDomRefAtPos } from '@atlaskit/editor-prosemirror/utils'; import { DATASOURCE_DEFAULT_LAYOUT } from '@atlaskit/linking-common'; import { fg } from '@atlaskit/platform-feature-flags'; import { InlineCardNodeView } from '../nodeviews/inlineCard'; import { lazyBlockCardView } from '../nodeviews/lazy-block-card'; import { lazyEmbedCardView } from '../nodeviews/lazy-embed-card'; import { lazyInlineCardView } from '../nodeviews/lazy-inline-card'; import { eventsFromTransaction } from '../ui/analytics/events-from-tr'; import { isDatasourceTableLayout } from '../ui/LayoutButton/utils'; import { isLocalStorageKeyDiscovered } from '../ui/local-storage'; import { clearOverlayCandidate, setCardLayoutAndDatasourceTableRef, setDatasourceTableRef } from './actions'; import { pluginKey } from './plugin-key'; import reducer from './reducers'; import { handleProvider, resolveWithProvider } from './util/resolve'; import { getNewRequests, getPluginState, getPluginStateWithUpdatedPos } from './util/state'; import { isBlockSupportedAtPosition, isEmbedSupportedAtPosition } from './utils'; const LOCAL_STORAGE_DISCOVERY_KEY_SMART_LINK = 'smart-link-upgrade-pulse'; // Only the first resolved inline smart link is needed for PO spotlight targeting const MAX_RESOLVED_INLINE_SMART_LINKS = 1; const handleAwarenessOverlay = view => { const currentState = getPluginState(view.state); const overlayCandidatePos = currentState === null || currentState === void 0 ? void 0 : currentState.overlayCandidatePosition; if (overlayCandidatePos) { var _currentState$removeO; (_currentState$removeO = currentState.removeOverlay) === null || _currentState$removeO === void 0 ? void 0 : _currentState$removeO.call(currentState); const tr = view.state.tr; clearOverlayCandidate(tr); view.dispatch(tr); } }; export const createPlugin = (options, pluginInjectionApi) => pmPluginFactoryParams => { const { editorAppearance, allowResizing, useAlternativePreloader, fullWidthMode, actionOptions, cardPluginEvents, showUpgradeDiscoverability, allowEmbeds, allowBlockCards, onClickCallback, isPageSSRed, provider, CompetitorPrompt, embedCardTransformers } = options; const enableInlineUpgradeFeatures = !!showUpgradeDiscoverability; const inlineCardViewProducer = getInlineNodeViewProducer({ pmPluginFactoryParams, Component: InlineCardNodeView, extraComponentProps: { useAlternativePreloader, actionOptions, enableInlineUpgradeFeatures, allowEmbeds, allowBlockCards, pluginInjectionApi, onClickCallback, isPageSSRed, provider, CompetitorPrompt } }); return new SafePlugin({ state: { init() { return { requests: [], provider: null, cards: [], datasourceStash: {}, resolvedToolbarAttributesByUrl: {}, showLinkingToolbar: false, smartLinkEvents: undefined, editorAppearance, embedCardTransformers, showDatasourceModal: false, datasourceModalType: undefined, datasourceTableRef: undefined, layout: undefined }; }, apply(tr, pluginState, prevEditorState) { var _pluginState$requests, _pluginState$requests2; // Update all the positions of outstanding requests and // cards in the plugin state. const pluginStateWithUpdatedPos = getPluginStateWithUpdatedPos(pluginState, tr); // apply any actions const meta = tr.getMeta(pluginKey); if (cardPluginEvents) { const events = eventsFromTransaction(tr, prevEditorState); cardPluginEvents.push(...events); } if (!meta) { if (pluginState.datasourceTableRef) { if (!(tr.selection instanceof NodeSelection) || !tr.selection.node.attrs.datasource) { // disable resize button when switching from datasource to block card return { ...pluginStateWithUpdatedPos, datasourceTableRef: undefined }; } } } if (!meta) { return pluginStateWithUpdatedPos; } const newState = reducer(pluginStateWithUpdatedPos, meta); // Track the first resolved inline smart link for PO spotlight DOM targeting if (meta.type === 'RESOLVE' && pluginState !== null && pluginState !== void 0 && (_pluginState$requests = pluginState.requests) !== null && _pluginState$requests !== void 0 && _pluginState$requests.length && fg('cc_dnd_smart_link_changeboard_po_template_gate')) { const resolvedRequest = pluginState.requests.find(req => req.url === meta.url); if ((resolvedRequest === null || resolvedRequest === void 0 ? void 0 : resolvedRequest.appearance) === 'inline') { var _newState$resolvedInl, _newState$resolvedInl2; if (((_newState$resolvedInl = (_newState$resolvedInl2 = newState.resolvedInlineSmartLinks) === null || _newState$resolvedInl2 === void 0 ? void 0 : _newState$resolvedInl2.length) !== null && _newState$resolvedInl !== void 0 ? _newState$resolvedInl : 0) < MAX_RESOLVED_INLINE_SMART_LINKS) { var _newState$resolvedInl3; newState.resolvedInlineSmartLinks = [...((_newState$resolvedInl3 = newState.resolvedInlineSmartLinks) !== null && _newState$resolvedInl3 !== void 0 ? _newState$resolvedInl3 : []), { pos: resolvedRequest.pos, url: resolvedRequest.url, source: resolvedRequest.source }]; } } } if (!enableInlineUpgradeFeatures) { return newState; } // the code below is related to the "Inline Switcher" project, for more information pls see EDM-7984 const isSingleInlineLink = (pluginState === null || pluginState === void 0 ? void 0 : (_pluginState$requests2 = pluginState.requests) === null || _pluginState$requests2 === void 0 ? void 0 : _pluginState$requests2.length) === 1 && pluginState.requests[0].appearance === 'inline'; const isSmartLinkPulseDiscovered = isLocalStorageKeyDiscovered(LOCAL_STORAGE_DISCOVERY_KEY_SMART_LINK); if (meta.type !== 'RESOLVE' || !isSingleInlineLink) { return newState; } const linkPosition = pluginState.requests[0].pos; const canBeUpgradedToBlock = allowBlockCards && isBlockSupportedAtPosition(linkPosition, prevEditorState, 'inline'); const canBeUpgradedToEmbed = allowEmbeds && isEmbedSupportedAtPosition(linkPosition, prevEditorState, 'inline'); if (canBeUpgradedToBlock || canBeUpgradedToEmbed) { newState.overlayCandidatePosition = linkPosition; } if (!isSmartLinkPulseDiscovered && canBeUpgradedToEmbed) { newState.inlineCardAwarenessCandidatePosition = linkPosition; } return newState; } }, filterTransaction(tr) { const isOutsideClicked = tr.getMeta('outsideProsemirrorEditorClicked'); if (isOutsideClicked) { const isInlineEditingActive = document.getElementById('sllv-active-inline-edit'); if (isInlineEditingActive) { return false; } } return true; }, view(view) { const domAtPos = view.domAtPos.bind(view); const rafCancellationCallbacks = []; if (options.provider) { handleProvider('cardProvider', options.provider, view); } return { update(view, prevState) { var _selection$node; const currentState = getPluginState(view.state); const oldState = getPluginState(prevState); const { state, dispatch } = view; const { selection, tr, schema } = state; const isBlockCardSelected = selection instanceof NodeSelection && ((_selection$node = selection.node) === null || _selection$node === void 0 ? void 0 : _selection$node.type) === schema.nodes.blockCard; if (isBlockCardSelected) { var _findDomRefAtPos, _node$attrs; // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting const datasourceTableRef = // Ignored via go/ees005 // eslint-disable-next-line @atlaskit/editor/no-as-casting (_findDomRefAtPos = findDomRefAtPos(selection.from, domAtPos)) === null || _findDomRefAtPos === void 0 ? void 0 : _findDomRefAtPos.querySelector(`.${DATASOURCE_INNER_CONTAINER_CLASSNAME}`); const { node } = selection; const isDatasource = !!(node !== null && node !== void 0 && (_node$attrs = node.attrs) !== null && _node$attrs !== void 0 && _node$attrs.datasource); const shouldUpdateTableRef = datasourceTableRef && (currentState === null || currentState === void 0 ? void 0 : currentState.datasourceTableRef) !== datasourceTableRef; if (isDatasource && shouldUpdateTableRef) { // since we use the plugin state, which is a shared state, we need to update the datasourceTableRef, layout on each selection const layout = isDatasourceTableLayout(node.attrs.layout) ? node.attrs.layout : DATASOURCE_DEFAULT_LAYOUT; const isNested = selection.$anchor.depth > 0; // we want to disable resize button when datasource table is nested by not setting then datasourceTableRef on selection if (!isNested) { // we use cardAction to set the same meta, hence, we will need to combine both layout+datasourceTableRef in one transaction dispatch(setCardLayoutAndDatasourceTableRef({ datasourceTableRef, layout })(tr)); } } } else { if (currentState !== null && currentState !== void 0 && currentState.datasourceTableRef) { dispatch(setDatasourceTableRef(undefined)(tr)); } } if (currentState && currentState.provider) { // Find requests in this state that weren't in the old one. const newRequests = getNewRequests(oldState, currentState); // Ask the CardProvider to resolve all new requests. const { provider } = currentState; newRequests.forEach(request => { /** * Queue each asynchronous resolve request on separate frames. * --- * NB: The promise for each request is queued to take place on separate animation frames. This avoids * the scenario debugged and discovered in EDM-668, wherein the queuing of too many promises in quick succession * leads to the browser's macrotask queue being overwhelmed, locking interactivity of the browser tab. * By using this approach, the browser is free to schedule the resolution of the promises below in between rendering/network/ * other tasks as per common implementations of the JavaScript event loop in browsers. */ const invoke = rafSchedule(() => { var _pluginInjectionApi$a, _pluginInjectionApi$a2, _pluginInjectionApi$a3, _pluginInjectionApi$a4, _currentState$embedCa; return resolveWithProvider(view, provider, request, options, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions, (_pluginInjectionApi$a2 = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a3 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a3 === void 0 ? void 0 : (_pluginInjectionApi$a4 = _pluginInjectionApi$a3.sharedState.currentState()) === null || _pluginInjectionApi$a4 === void 0 ? void 0 : _pluginInjectionApi$a4.createAnalyticsEvent) !== null && _pluginInjectionApi$a2 !== void 0 ? _pluginInjectionApi$a2 : undefined, currentState === null || currentState === void 0 ? void 0 : (_currentState$embedCa = currentState.embedCardTransformers) === null || _currentState$embedCa === void 0 ? void 0 : _currentState$embedCa.embedCardNodeTransformer); }); rafCancellationCallbacks.push(invoke.cancel); invoke(); }); } /** * If there have been any events queued, flush them * so subscribers can now be notified and dispatch * analytics events */ cardPluginEvents === null || cardPluginEvents === void 0 ? void 0 : cardPluginEvents.flush(); }, destroy() { // Cancel any outstanding raf callbacks. rafCancellationCallbacks.forEach(cancellationCallback => cancellationCallback()); } }; }, props: { nodeViews: { inlineCard: lazyInlineCardView({ inlineCardViewProducer, isPageSSRed // no need provider here, it's in the inlineCardViewProducer.extraComponentProps }), blockCard: lazyBlockCardView({ pmPluginFactoryParams, actionOptions, pluginInjectionApi, onClickCallback, allowDatasource: options.allowDatasource, inlineCardViewProducer, isPageSSRed, provider, CompetitorPrompt: options.CompetitorPrompt }), embedCard: lazyEmbedCardView({ allowResizing, fullWidthMode, pmPluginFactoryParams, pluginInjectionApi, actionOptions, onClickCallback: options.onClickCallback, isPageSSRed, provider, CompetitorPrompt: options.CompetitorPrompt }) }, ...(enableInlineUpgradeFeatures && { handleKeyDown: view => { handleAwarenessOverlay(view); return false; }, handleClick: view => { handleAwarenessOverlay(view); return false; } }) }, key: pluginKey }); };