@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
277 lines (272 loc) • 12.4 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
/**
* @jsxRuntime classic
* @jsx jsx
* @jsxFrag
*/
import { useCallback, useMemo } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
import { jsx } from '@emotion/react';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { WithProviders } from '@atlaskit/editor-common/provider-factory';
import ReactNodeView from '@atlaskit/editor-common/react-node-view';
import { useSharedPluginStateSelector } from '@atlaskit/editor-common/use-shared-plugin-state-selector';
// eslint-disable-next-line @typescript-eslint/consistent-type-imports
import { isNodeSelectedOrInRange } from '@atlaskit/editor-common/utils';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { MEDIA_CONTENT_WRAP_CLASS_NAME } from '../pm-plugins/main';
import { MediaSingleNodeNext } from './mediaSingleNext';
const selector = states => {
var _states$mediaState, _states$mediaState2, _states$annotationSta, _states$annotationSta2, _states$widthState, _states$widthState2, _states$editorDisable, _states$editorViewMod;
return {
mediaProviderPromise: (_states$mediaState = states.mediaState) === null || _states$mediaState === void 0 ? void 0 : _states$mediaState.mediaProvider,
addPendingTask: (_states$mediaState2 = states.mediaState) === null || _states$mediaState2 === void 0 ? void 0 : _states$mediaState2.addPendingTask,
isDrafting: (_states$annotationSta = states.annotationState) === null || _states$annotationSta === void 0 ? void 0 : _states$annotationSta.isDrafting,
targetNodeId: (_states$annotationSta2 = states.annotationState) === null || _states$annotationSta2 === void 0 ? void 0 : _states$annotationSta2.targetNodeId,
width: (_states$widthState = states.widthState) === null || _states$widthState === void 0 ? void 0 : _states$widthState.width,
lineLength: (_states$widthState2 = states.widthState) === null || _states$widthState2 === void 0 ? void 0 : _states$widthState2.lineLength,
editorDisabled: (_states$editorDisable = states.editorDisabledState) === null || _states$editorDisable === void 0 ? void 0 : _states$editorDisable.editorDisabled,
viewMode: (_states$editorViewMod = states.editorViewModeState) === null || _states$editorViewMod === void 0 ? void 0 : _states$editorViewMod.mode
};
};
const MediaSingleNodeWrapper = ({
pluginInjectionApi,
contextIdentifierProvider,
node,
getPos,
mediaOptions,
view,
fullWidthMode,
selected,
eventDispatcher,
dispatchAnalyticsEvent,
forwardRef,
editorAppearance
}) => {
const {
mediaProviderPromise,
addPendingTask,
isDrafting,
targetNodeId,
width,
lineLength,
editorDisabled,
viewMode
} = useSharedPluginStateWithSelector(pluginInjectionApi, ['width', 'media', 'annotation', 'editorDisabled', 'editorViewMode'], selector);
const interactionState = useSharedPluginStateSelector(pluginInjectionApi, 'interaction.interactionState');
const mediaProvider = useMemo(() => mediaProviderPromise ? Promise.resolve(mediaProviderPromise) : undefined, [mediaProviderPromise]);
const isSelectedAndInteracted = useCallback(() => Boolean(selected() && interactionState !== 'hasNotHadInteraction'), [interactionState, selected]);
return jsx(MediaSingleNodeNext, {
width: width || 0,
lineLength: lineLength || 0,
node: node,
getPos: getPos,
mediaProvider: mediaProvider,
contextIdentifierProvider: contextIdentifierProvider,
mediaOptions: mediaOptions,
view: view,
fullWidthMode: fullWidthMode,
selected: isSelectedAndInteracted,
eventDispatcher: eventDispatcher,
addPendingTask: addPendingTask,
isDrafting: isDrafting,
targetNodeId: targetNodeId,
dispatchAnalyticsEvent: dispatchAnalyticsEvent,
forwardRef: forwardRef,
pluginInjectionApi: pluginInjectionApi,
editorDisabled: editorDisabled,
editorViewMode: viewMode === 'view',
editorAppearance: editorAppearance
});
};
class MediaSingleNodeView extends ReactNodeView {
constructor(...args) {
super(...args);
_defineProperty(this, "lastOffsetLeft", 0);
_defineProperty(this, "forceViewUpdate", false);
_defineProperty(this, "selectionType", null);
_defineProperty(this, "hasResized", false);
_defineProperty(this, "checkAndUpdateSelectionType", () => {
const getPos = this.getPos;
const {
selection
} = this.view.state;
/**
* 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 = getPos ? getPos() : undefined;
} catch {
pos = undefined;
}
const isNodeSelected = isNodeSelectedOrInRange(selection.$anchor.pos, selection.$head.pos, pos, this.node.nodeSize);
this.selectionType = isNodeSelected;
return isNodeSelected;
});
_defineProperty(this, "isNodeSelected", () => {
this.checkAndUpdateSelectionType();
return this.selectionType !== null;
});
}
createDomRef() {
var _this$reactComponentP, _this$reactComponentP2, _this$reactComponentP3, _this$reactComponentP4;
const domRef = document.createElement('div');
// control the domRef contentEditable attribute based on the editor view mode
this.unsubscribeToViewModeChange = this.subscribeToViewModeChange(domRef);
const initialViewMode = (_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$reactComponentP3 = _this$reactComponentP2.sharedState.currentState()) === null || _this$reactComponentP3 === void 0 ? void 0 : _this$reactComponentP3.mode;
this.updateDomRefContentEditable(domRef, initialViewMode);
if ((_this$reactComponentP4 = this.reactComponentProps.mediaOptions) !== null && _this$reactComponentP4 !== void 0 && _this$reactComponentP4.allowPixelResizing) {
domRef.classList.add('media-extended-resize-experience');
}
domRef.setAttribute('data-media-vc-wrapper', 'true');
return domRef;
}
getContentDOM() {
const dom = document.createElement('div');
dom.classList.add(MEDIA_CONTENT_WRAP_CLASS_NAME);
return {
dom
};
}
viewShouldUpdate(nextNode) {
if (this.forceViewUpdate) {
this.forceViewUpdate = false;
return true;
}
if (this.node.attrs !== nextNode.attrs) {
return true;
}
if (this.selectionType !== this.checkAndUpdateSelectionType()) {
return true;
}
if (this.node.childCount !== nextNode.childCount) {
return true;
}
return super.viewShouldUpdate(nextNode);
}
subscribeToViewModeChange(domRef) {
var _this$reactComponentP5, _this$reactComponentP6;
return (_this$reactComponentP5 = this.reactComponentProps.pluginInjectionApi) === null || _this$reactComponentP5 === void 0 ? void 0 : (_this$reactComponentP6 = _this$reactComponentP5.editorViewMode) === null || _this$reactComponentP6 === void 0 ? void 0 : _this$reactComponentP6.sharedState.onChange(viewModeState => {
var _viewModeState$nextSh;
this.updateDomRefContentEditable(domRef, (_viewModeState$nextSh = viewModeState.nextSharedState) === null || _viewModeState$nextSh === void 0 ? void 0 : _viewModeState$nextSh.mode);
});
}
updateDomRefContentEditable(domRef, editorViewMode) {
var _this$reactComponentP7;
// if the editor is in view mode, we should not allow editing
if (editorViewMode === 'view') {
domRef.contentEditable = 'false';
return;
}
// if the editor is in edit mode, we should allow editing if the media options allow it
if ((_this$reactComponentP7 = this.reactComponentProps.mediaOptions) !== null && _this$reactComponentP7 !== void 0 && _this$reactComponentP7.allowMediaSingleEditable) {
// workaround Chrome bug in https://product-fabric.atlassian.net/browse/ED-5379
// see also: https://github.com/ProseMirror/prosemirror/issues/884
domRef.contentEditable = 'true';
}
}
getNodeMediaId(node) {
if (node.firstChild) {
return node.firstChild.attrs.id;
}
return undefined;
}
stopEvent(event) {
if (this.isNodeSelected() && event instanceof KeyboardEvent && (event === null || event === void 0 ? void 0 : event.target) instanceof HTMLElement) {
const targetType = event.target.type;
if (event.key === 'Enter' && targetType === 'button') {
return true;
}
}
return false;
}
update(node, decorations, _innerDecorations, isValidUpdate) {
if (!isValidUpdate) {
isValidUpdate = (currentNode, newNode) => this.getNodeMediaId(currentNode) === this.getNodeMediaId(newNode);
}
// Detect mediaSingle width attribute changes and signal child media node to update
if (!this.hasResized && this.node.attrs.width !== node.attrs.width && expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true)) {
const target = this.dom.querySelector('div[data-prosemirror-node-name="media"]');
target === null || target === void 0 ? void 0 : target.dispatchEvent(new CustomEvent('resized'));
}
return super.update(node, decorations, _innerDecorations, isValidUpdate);
}
render(props, forwardRef) {
const {
eventDispatcher,
fullWidthMode,
providerFactory,
mediaOptions,
dispatchAnalyticsEvent,
pluginInjectionApi,
editorAppearance
} = this.reactComponentProps;
// getPos is a boolean for marks, since this is a node we know it must be a function
const getPos = this.getPos;
return jsx(WithProviders
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
, {
providers: ['contextIdentifierProvider'],
providerFactory: providerFactory
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
renderNode: ({
contextIdentifierProvider
}) => {
return jsx(MediaSingleNodeWrapper, {
pluginInjectionApi: pluginInjectionApi,
contextIdentifierProvider: contextIdentifierProvider,
node: this.node,
getPos: getPos,
mediaOptions: mediaOptions,
view: this.view,
fullWidthMode: fullWidthMode,
selected: this.isNodeSelected,
eventDispatcher: eventDispatcher
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
,
dispatchAnalyticsEvent: dispatchAnalyticsEvent
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
,
forwardRef: forwardRef,
editorAppearance: editorAppearance
});
}
});
}
ignoreMutation() {
// DOM has changed; recalculate if we need to re-render
if (this.dom) {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
const offsetLeft = this.dom.offsetLeft;
if (offsetLeft !== this.lastOffsetLeft) {
this.lastOffsetLeft = offsetLeft;
this.forceViewUpdate = true;
this.update(this.node, [], undefined, () => true);
}
}
return true;
}
destroy() {
var _this$unsubscribeToVi;
(_this$unsubscribeToVi = this.unsubscribeToViewModeChange) === null || _this$unsubscribeToVi === void 0 ? void 0 : _this$unsubscribeToVi.call(this);
}
}
export const ReactMediaSingleNode = (portalProviderAPI, eventDispatcher, providerFactory, pluginInjectionApi, dispatchAnalyticsEvent, mediaOptions = {}) => (node, view, getPos) => {
return new MediaSingleNodeView(node, view, getPos, portalProviderAPI, eventDispatcher, {
eventDispatcher,
fullWidthMode: mediaOptions.fullWidthEnabled,
providerFactory,
mediaOptions,
dispatchAnalyticsEvent,
isCopyPasteEnabled: mediaOptions.isCopyPasteEnabled,
pluginInjectionApi,
editorAppearance: mediaOptions.editorAppearance
}).init();
};