@atlaskit/editor-plugin-card
Version:
Card plugin for @atlaskit/editor-core
263 lines (258 loc) • 10.2 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import React from 'react';
import rafSchedule from 'raf-schd';
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
import uuid from 'uuid/v4';
import { getBrowserInfo } from '@atlaskit/editor-common/browser';
import ReactNodeView from '@atlaskit/editor-common/react-node-view';
import { findOverflowScrollParent, UnsupportedBlock } from '@atlaskit/editor-common/ui';
import { canRenderDatasource } from '@atlaskit/editor-common/utils';
import { SmartLinkDraggable, SMART_LINK_DRAG_TYPES, SMART_LINK_APPEARANCE } from '@atlaskit/editor-smart-link-draggable';
import { fg } from '@atlaskit/platform-feature-flags';
import { Card as SmartCard } from '@atlaskit/smart-card';
import { CardSSR } from '@atlaskit/smart-card/ssr';
import { Datasource } from '../nodeviews/datasource';
import { registerCard, removeCard } from '../pm-plugins/actions';
import { isDatasourceNode } from '../pm-plugins/utils';
import { Card } from './genericCard';
// eslint-disable-next-line @repo/internal/react/no-class-components
export class BlockCardComponent extends React.PureComponent {
constructor(props) {
super(props);
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
_defineProperty(this, "onResolve", data => {
const {
getPos,
view
} = this.props;
if (!getPos || typeof getPos === 'boolean') {
return;
}
const {
title,
url
} = data;
// don't dispatch immediately since we might be in the middle of
// rendering a nodeview
rafSchedule(() => {
const pos = getPos();
if (typeof pos !== 'number') {
return;
}
view.dispatch(registerCard({
title,
url,
pos,
id: this.props.id
})(view.state.tr));
})();
});
_defineProperty(this, "removeCardDispatched", false);
_defineProperty(this, "gapCursorSpan", () => {
const browser = getBrowserInfo();
// Don't render in EdgeHTMl version <= 18 (Edge version 44)
// as it forces the edit popup to render 24px lower than it should
if (browser.ie && browser.ie_version < 79) {
return;
}
// render an empty span afterwards to get around Webkit bug
// that puts caret in next editable text element
return /*#__PURE__*/React.createElement("span", {
contentEditable: true
});
});
_defineProperty(this, "onError", ({
err
}) => {
if (err) {
throw err;
}
});
this.scrollContainer = findOverflowScrollParent(props.view.dom) || undefined;
}
componentWillUnmount() {
this.removeCard();
}
removeCard() {
if (this.removeCardDispatched) {
return;
}
this.removeCardDispatched = true;
const {
tr
} = this.props.view.state;
removeCard({
id: this.props.id
})(tr);
this.props.view.dispatch(tr);
}
render() {
const {
node,
cardContext,
actionOptions,
onClick,
CompetitorPrompt,
isPageSSRed
} = this.props;
const {
url,
data
} = node.attrs;
const cardInner = isPageSSRed ? /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(CardSSR, {
key: url,
url: url !== null && url !== void 0 ? url : data.url,
container: this.scrollContainer,
appearance: "block",
onClick: onClick,
onResolve: this.onResolve,
onError: this.onError,
platform: 'web',
actionOptions: actionOptions,
CompetitorPrompt: CompetitorPrompt,
hideIconLoadingSkeleton: true
}), this.gapCursorSpan()) : /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(SmartCard, {
key: url,
url: url !== null && url !== void 0 ? url : data.url,
container: this.scrollContainer,
appearance: "block",
onClick: onClick,
onResolve: this.onResolve,
onError: this.onError,
platform: 'web',
actionOptions: actionOptions,
CompetitorPrompt: CompetitorPrompt
}), this.gapCursorSpan());
// [WS-2307]: we only render card wrapped into a Provider when the value is ready,
// otherwise if we got data, we can render the card directly since it doesn't need the Provider
return /*#__PURE__*/React.createElement(SmartLinkDraggable, {
url: url,
appearance: SMART_LINK_APPEARANCE.BLOCK,
source: SMART_LINK_DRAG_TYPES.EDITOR
}, /*#__PURE__*/React.createElement("div", null, cardContext && cardContext.value ? /*#__PURE__*/React.createElement(cardContext.Provider, {
value: cardContext.value
}, cardInner) : data ? cardInner : null));
}
}
const WrappedBlockCard = Card(BlockCardComponent, UnsupportedBlock);
export class BlockCard extends ReactNodeView {
constructor(...args) {
super(...args);
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
_defineProperty(this, "id", uuid());
_defineProperty(this, "updateContentEditable", (editorViewModeState, divElement) => {
divElement.contentEditable = (editorViewModeState === null || editorViewModeState === void 0 ? void 0 : editorViewModeState.mode) === 'view' ? 'false' : 'true';
});
}
createDomRef() {
var _this$reactComponentP, _this$reactComponentP2, _this$reactComponentP3, _this$reactComponentP4;
const domRef = document.createElement('div');
// workaround Chrome bug in https://product-fabric.atlassian.net/browse/ED-5379
// see also: https://github.com/ProseMirror/prosemirror/issues/884
this.unsubscribe = (_this$reactComponentP = this.reactComponentProps.pluginInjectionApi) === null || _this$reactComponentP === void 0 ? void 0 : (_this$reactComponentP2 = _this$reactComponentP.editorViewMode) === null || _this$reactComponentP2 === void 0 ? void 0 : _this$reactComponentP2.sharedState.onChange(({
nextSharedState
}) => this.updateContentEditable(nextSharedState, domRef));
this.updateContentEditable((_this$reactComponentP3 = this.reactComponentProps.pluginInjectionApi) === null || _this$reactComponentP3 === void 0 ? void 0 : (_this$reactComponentP4 = _this$reactComponentP3.editorViewMode) === null || _this$reactComponentP4 === void 0 ? void 0 : _this$reactComponentP4.sharedState.currentState(), domRef);
domRef.setAttribute('spellcheck', 'false');
return domRef;
}
// Need this function to check if the datasource attribute was added or not to a blockCard.
// If so, we return false so we can get the node to re-render properly as a datasource node instead.
// Otherwise, the node view will still consider the node as a blockCard and render a regular blockCard.
validUpdate(currentNode, newNode) {
const isCurrentNodeBlockCard = !isDatasourceNode(currentNode);
const isNewNodeDatasource = isDatasourceNode(newNode);
// need to return falsy to update node
return !(isCurrentNodeBlockCard && isNewNodeDatasource);
}
update(node, decorations, _innerDecorations) {
return super.update(node, decorations, _innerDecorations, this.validUpdate);
}
render() {
const {
actionOptions,
pluginInjectionApi,
onClickCallback,
CompetitorPrompt,
isPageSSRed,
provider
} = this.reactComponentProps;
return /*#__PURE__*/React.createElement(WrappedBlockCard, {
node: this.node,
view: this.view,
getPos: this.getPos,
actionOptions: actionOptions,
pluginInjectionApi: pluginInjectionApi,
onClickCallback: onClickCallback,
id: this.id,
CompetitorPrompt: CompetitorPrompt,
isPageSSRed: isPageSSRed,
provider: provider
});
}
/**
* Prevent ProseMirror from handling drag events on the smart-element-link,
* allowing native drag to work so SmartLinkDraggable can intercept it.
* @see {@link https://prosemirror.net/docs/ref/#view.NodeView.stopEvent}
*/
stopEvent(event) {
if (event.type === 'dragstart') {
const target = event.target;
if (target instanceof HTMLElement && target.closest('[data-smart-element-link]') && fg('cc_drag_and_drop_smart_link_from_content_to_tree')) {
return true;
}
}
return false;
}
destroy() {
var _this$unsubscribe;
(_this$unsubscribe = this.unsubscribe) === null || _this$unsubscribe === void 0 ? void 0 : _this$unsubscribe.call(this);
super.destroy();
}
}
export const blockCardNodeView = ({
pmPluginFactoryParams,
actionOptions,
pluginInjectionApi,
onClickCallback,
allowDatasource,
inlineCardViewProducer,
CompetitorPrompt,
isPageSSRed,
provider
}) => (node, view, getPos, decorations) => {
const {
portalProviderAPI,
eventDispatcher
} = pmPluginFactoryParams;
const reactComponentProps = {
actionOptions,
pluginInjectionApi,
onClickCallback: onClickCallback,
CompetitorPrompt,
isPageSSRed,
provider
};
const isDatasource = isDatasourceNode(node);
if (isDatasource) {
var _node$attrs, _node$attrs$datasourc;
if (allowDatasource && canRenderDatasource(node === null || node === void 0 ? void 0 : (_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : (_node$attrs$datasourc = _node$attrs.datasource) === null || _node$attrs$datasourc === void 0 ? void 0 : _node$attrs$datasourc.id)) {
const datasourcePosition = typeof getPos === 'function' && getPos();
const datasourceResolvedPosition = datasourcePosition && view.state.doc.resolve(datasourcePosition);
const isNodeNested = !!(datasourceResolvedPosition && datasourceResolvedPosition.depth > 0);
return new Datasource({
node,
view,
getPos,
portalProviderAPI,
eventDispatcher,
pluginInjectionApi,
isNodeNested
}).init();
} else {
return inlineCardViewProducer(node, view, getPos, decorations);
}
}
return new BlockCard(node, view, getPos, portalProviderAPI, eventDispatcher, reactComponentProps, undefined).init();
};