@atlaskit/editor-plugin-card
Version:
Card plugin for @atlaskit/editor-core
298 lines (293 loc) • 14.6 kB
JavaScript
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
});
};