@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
314 lines (312 loc) • 11.7 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import React from 'react';
import { injectIntl } from 'react-intl-next';
import { useSharedPluginState } from '@atlaskit/editor-common/hooks';
import { WithProviders } from '@atlaskit/editor-common/provider-factory';
import ReactNodeView from '@atlaskit/editor-common/react-node-view';
import { isNodeSelectedOrInRange, SelectedState, setNodeSelection } from '@atlaskit/editor-common/utils';
import EditorCloseIcon from '@atlaskit/icon/glyph/editor/close';
import { getMediaFeatureFlag } from '@atlaskit/media-common';
import { Filmstrip } from '@atlaskit/media-filmstrip';
import { stateKey as mediaStateKey } from '../pm-plugins/plugin-key';
import { MediaNodeUpdater } from './mediaNodeUpdater';
import { messages } from './messages';
const isMediaGroupSelectedFromProps = props => {
/**
* ED-19831
* There is a getPos issue coming from this code. We need to apply this workaround for now and apply a patch
* directly to confluence since this bug is now in production.
*/
let pos;
try {
pos = props.getPos ? props.getPos() : undefined;
} catch (e) {
pos = undefined;
}
if (typeof pos !== 'number') {
return false;
}
return isNodeSelectedOrInRange(props.anchorPos, props.headPos, pos, props.node.nodeSize);
};
const hasSelectionChanged = (oldProps, newProps) => {
if (isMediaGroupSelectedFromProps(oldProps) !== isMediaGroupSelectedFromProps(newProps)) {
return true;
}
if (isMediaGroupSelectedFromProps(newProps) === SelectedState.selectedInside) {
return oldProps.anchorPos !== newProps.anchorPos;
}
return false;
};
// eslint-disable-next-line @repo/internal/react/no-class-components
class MediaGroup extends React.Component {
constructor(_props) {
super(_props);
_defineProperty(this, "state", {
viewMediaClientConfig: undefined
});
_defineProperty(this, "updateNodeAttrs", (props, node, getPos) => {
const {
view,
mediaProvider,
contextIdentifierProvider
} = props;
const mediaNodeUpdater = new MediaNodeUpdater({
view,
mediaProvider,
contextIdentifierProvider,
node,
isMediaSingle: false
});
mediaNodeUpdater.updateNodeAttrs(getPos);
});
_defineProperty(this, "setMediaItems", (props, updatedAttrs = false) => {
var _this$mediaPluginStat;
const {
node
} = props;
const oldMediaNodes = this.mediaNodes;
this.mediaNodes = [];
node.forEach((item, childOffset) => {
const getPos = () => {
const pos = props.getPos();
if (typeof pos !== 'number') {
// That may seems weird, but the previous type wasn't match with the real ProseMirror code. And a lot of Media API was built expecting a number
// Because the original code would return NaN on runtime
// We are just make it explict now.
// We may run a deep investagation on Media code to figure out a better fix. But, for now, we want to keep the current behavior.
// TODO: ED-13910 prosemirror-bump leftovers
return NaN;
}
return pos + childOffset + 1;
};
this.mediaNodes.push(item);
if (updatedAttrs) {
this.updateNodeAttrs(props, item, getPos);
}
});
(_this$mediaPluginStat = this.mediaPluginState) === null || _this$mediaPluginStat === void 0 ? void 0 : _this$mediaPluginStat.handleMediaGroupUpdate(oldMediaNodes, this.mediaNodes);
});
_defineProperty(this, "getIdentifier", item => {
if (item.attrs.type === 'external') {
return {
mediaItemType: 'external-image',
dataURI: item.attrs.url
};
}
return {
id: item.attrs.id,
mediaItemType: 'file',
collectionName: item.attrs.collection
};
});
_defineProperty(this, "isNodeSelected", nodePos => {
const selected = isMediaGroupSelectedFromProps(this.props);
if (selected === SelectedState.selectedInRange) {
return true;
}
if (selected === SelectedState.selectedInside && this.props.anchorPos === nodePos) {
return true;
}
return false;
});
_defineProperty(this, "renderChildNodes", () => {
const {
viewMediaClientConfig
} = this.state;
const {
getPos,
allowLazyLoading,
disabled,
mediaOptions
} = this.props;
const items = this.mediaNodes.map((item, idx) => {
// We declared this to get a fresh position every time
const getNodePos = () => {
const pos = getPos();
if (typeof pos !== 'number') {
// That may seems weird, but the previous type wasn't match with the real ProseMirror code. And a lot of Media API was built expecting a number
// Because the original code would return NaN on runtime
// We are just make it explict now.
// We may run a deep investagation on Media code to figure out a better fix. But, for now, we want to keep the current behavior.
// TODO: ED-13910 prosemirror-bump leftovers
return NaN;
}
return pos + idx + 1;
};
// Media Inline creates a floating toolbar with the same options, excludes these options if enabled
const mediaInlineOptions = (allowMediaInline = false) => {
if (!allowMediaInline) {
return {
shouldEnableDownloadButton: mediaOptions.enableDownloadButton,
actions: [{
handler: disabled || !this.mediaPluginState ? () => {} : this.mediaPluginState.handleMediaNodeRemoval.bind(null, undefined, getNodePos),
icon: /*#__PURE__*/React.createElement(EditorCloseIcon, {
label: this.props.intl.formatMessage(messages.mediaGroupDeleteLabel)
})
}]
};
}
};
return {
identifier: this.getIdentifier(item),
isLazy: allowLazyLoading,
selected: this.isNodeSelected(getNodePos()),
onClick: () => {
setNodeSelection(this.props.view, getNodePos());
},
...mediaInlineOptions(getMediaFeatureFlag('mediaInline', mediaOptions.featureFlags))
};
});
return /*#__PURE__*/React.createElement(Filmstrip, {
items: items,
mediaClientConfig: viewMediaClientConfig,
featureFlags: mediaOptions.featureFlags
});
});
this.mediaNodes = [];
this.mediaPluginState = mediaStateKey.getState(_props.view.state);
this.setMediaItems(_props);
this.state = {
viewMediaClientConfig: undefined
};
}
componentDidMount() {
this.updateMediaClientConfig();
this.mediaNodes.forEach(async node => {
if (node.attrs.type === 'external') {
return;
}
const {
view,
mediaProvider,
contextIdentifierProvider
} = this.props;
const mediaNodeUpdater = new MediaNodeUpdater({
view,
mediaProvider,
contextIdentifierProvider,
node,
isMediaSingle: false
});
const getPos = () => {
const pos = this.props.getPos();
if (typeof pos !== 'number') {
// That may seems weird, but the previous type wasn't match with the real ProseMirror code. And a lot of Media API was built expecting a number
// Because the original code would return NaN on runtime
// We are just make it explict now.
// We may run a deep investagation on Media code to figure out a better fix. But, for now, we want to keep the current behavior.
// TODO: ED-13910 prosemirror-bump leftovers
return NaN;
}
return pos + 1;
};
const contextId = mediaNodeUpdater.getNodeContextId();
if (!contextId) {
await mediaNodeUpdater.updateNodeContextId(getPos);
}
const hasDifferentContextId = await mediaNodeUpdater.hasDifferentContextId();
if (hasDifferentContextId) {
await mediaNodeUpdater.copyNodeFromPos(getPos, {
traceId: node.attrs.__mediaTraceId
});
}
});
}
componentWillUnmount() {
var _this$mediaPluginStat2;
(_this$mediaPluginStat2 = this.mediaPluginState) === null || _this$mediaPluginStat2 === void 0 ? void 0 : _this$mediaPluginStat2.handleMediaGroupUpdate(this.mediaNodes, []);
}
UNSAFE_componentWillReceiveProps(props) {
this.updateMediaClientConfig();
this.setMediaItems(props, props.isCopyPasteEnabled || props.isCopyPasteEnabled === undefined);
}
shouldComponentUpdate(nextProps) {
var _this$mediaPluginStat3;
if (hasSelectionChanged(this.props, nextProps) || this.props.node !== nextProps.node || this.state.viewMediaClientConfig !== ((_this$mediaPluginStat3 = this.mediaPluginState) === null || _this$mediaPluginStat3 === void 0 ? void 0 : _this$mediaPluginStat3.mediaClientConfig)) {
return true;
}
return false;
}
updateMediaClientConfig() {
const {
viewMediaClientConfig
} = this.state;
const {
mediaClientConfig
} = this.mediaPluginState || {};
if (!viewMediaClientConfig && mediaClientConfig) {
this.setState({
viewMediaClientConfig: mediaClientConfig
});
}
}
render() {
return this.renderChildNodes();
}
}
_defineProperty(MediaGroup, "displayName", 'MediaGroup');
const IntlMediaGroup = injectIntl(MediaGroup);
export default IntlMediaGroup;
function MediaGroupNodeViewInternal({
renderFn,
pluginInjectionApi
}) {
const {
editorDisabledState: editorDisabledPlugin
} = useSharedPluginState(pluginInjectionApi, ['editorDisabled']);
return renderFn({
editorDisabledPlugin
});
}
class MediaGroupNodeView extends ReactNodeView {
render(props, forwardRef) {
const {
providerFactory,
mediaOptions,
pluginInjectionApi
} = props;
const getPos = this.getPos;
return /*#__PURE__*/React.createElement(WithProviders, {
providers: ['mediaProvider', 'contextIdentifierProvider'],
providerFactory: providerFactory,
renderNode: ({
mediaProvider,
contextIdentifierProvider
}) => {
const renderFn = ({
editorDisabledPlugin
}) => {
if (!mediaProvider) {
return null;
}
return /*#__PURE__*/React.createElement(IntlMediaGroup, {
node: this.node,
getPos: getPos,
view: this.view,
forwardRef: forwardRef,
disabled: (editorDisabledPlugin || {}).editorDisabled,
allowLazyLoading: mediaOptions.allowLazyLoading,
mediaProvider: mediaProvider,
contextIdentifierProvider: contextIdentifierProvider,
isCopyPasteEnabled: mediaOptions.isCopyPasteEnabled,
anchorPos: this.view.state.selection.$anchor.pos,
headPos: this.view.state.selection.$head.pos,
mediaOptions: mediaOptions
});
};
return /*#__PURE__*/React.createElement(MediaGroupNodeViewInternal, {
renderFn: renderFn,
pluginInjectionApi: pluginInjectionApi
});
}
});
}
}
export const ReactMediaGroupNode = (portalProviderAPI, eventDispatcher, providerFactory, mediaOptions = {}, pluginInjectionApi) => (node, view, getPos) => {
const hasIntlContext = true;
return new MediaGroupNodeView(node, view, getPos, portalProviderAPI, eventDispatcher, {
providerFactory,
mediaOptions,
pluginInjectionApi
}, undefined, undefined, undefined, hasIntlContext).init();
};