UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

310 lines (306 loc) 11.5 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; import React from 'react'; import { bind } from 'bind-event-listener'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH } from '@atlaskit/editor-common/media-single'; import { WithProviders } from '@atlaskit/editor-common/provider-factory'; import { SelectionBasedNodeView } from '@atlaskit/editor-common/selection-based-node-view'; import { useSharedPluginStateSelector } from '@atlaskit/editor-common/use-shared-plugin-state-selector'; import { akEditorFullWidthLayoutWidth, akEditorDefaultLayoutWidth, akEditorCalculatedWideLayoutWidth } from '@atlaskit/editor-shared-styles'; import { getAttrsFromUrl } from '@atlaskit/media-client'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { hasAIGeneratingDecoration } from '../../pm-plugins/ai-generating-decoration'; import { updateCurrentMediaNodeAttrs } from '../../pm-plugins/commands/helpers'; import { isMediaBlobUrlFromAttrs } from '../../pm-plugins/utils/media-common'; // Ignored via go/ees005 // eslint-disable-next-line import/no-named-as-default import MediaNode from './media'; const selector = states => { var _states$mediaState; return { mediaProvider: (_states$mediaState = states.mediaState) === null || _states$mediaState === void 0 ? void 0 : _states$mediaState.mediaProvider }; }; const MediaNodeWithProviders = ({ pluginInjectionApi, innerComponent }) => { const { mediaProvider } = useSharedPluginStateWithSelector(pluginInjectionApi, ['media'], selector); const interactionState = useSharedPluginStateSelector(pluginInjectionApi, 'interaction.interactionState'); return innerComponent({ mediaProvider: mediaProvider ? Promise.resolve(mediaProvider) : undefined, interactionState }); }; function isMediaDecorationSpec(decoration) { return decoration.spec.type !== undefined && decoration.spec.selected !== undefined; } class MediaNodeView extends SelectionBasedNodeView { constructor(...args) { super(...args); _defineProperty(this, "isSelected", false); _defineProperty(this, "isAIGenerating", false); _defineProperty(this, "hasBeenResized", false); _defineProperty(this, "hasResizedListener", () => { if (!this.hasBeenResized) { this.hasBeenResized = true; this.update(this.node, this.decorations); } }); _defineProperty(this, "onExternalImageLoaded", dimensions => { const getPos = this.getPos; const { width, height, ...rest } = this.getAttrs(); if (!width || !height) { updateCurrentMediaNodeAttrs({ ...rest, width: width || dimensions.width, height: height || dimensions.height }, { node: this.node, getPos }, true)(this.view.state, this.view.dispatch); } }); _defineProperty(this, "getMaxCardDimensions", () => { const flexibleDimensions = { width: '100%', height: '100%' }; if (expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true)) { const pos = this.getPos(); if (typeof pos !== 'number') { return flexibleDimensions; } if (this.hasBeenResized) { return flexibleDimensions; } const mediaSingleNodeParent = this.getMediaSingleNode(this.getPos); // If media parent not found, return default if (!mediaSingleNodeParent) { return flexibleDimensions; } if (expValEquals('platform_editor_media_vc_fixes_patch1', 'isEnabled', true)) { if (this.hasPxWidthType(mediaSingleNodeParent)) { return { width: `${mediaSingleNodeParent.attrs.width}px`, height: '100%' }; } return flexibleDimensions; } // Compute normal dimensions const maxWidth = this.getMaxWidthFromMediaSingleNode(mediaSingleNodeParent); return { width: `${maxWidth}px`, height: '100%' }; } return flexibleDimensions; }); _defineProperty(this, "getMediaProviderToUse", (mediaOptions, mediaProvider) => { if (mediaProvider) { return mediaProvider; } if (expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true)) { return mediaOptions.provider; } return mediaOptions.syncProvider ? Promise.resolve(mediaOptions.syncProvider) : mediaOptions.provider; }); _defineProperty(this, "renderMediaNodeWithState", contextIdentifierProvider => { return ({ mediaProvider, interactionState }) => { var _this$reactComponentP, _this$reactComponentP2, _this$reactComponentP3; const getPos = this.getPos; const { mediaOptions } = this.reactComponentProps; const attrs = this.getAttrs(); const url = attrs.type === 'external' ? attrs.url : ''; let { width, height } = attrs; if (this.isMediaBlobUrl()) { const urlAttrs = getAttrsFromUrl(url); if (urlAttrs) { const { width: urlWidth, height: urlHeight } = urlAttrs; width = width || urlWidth; height = height || urlHeight; } } width = width || DEFAULT_IMAGE_WIDTH; height = height || DEFAULT_IMAGE_HEIGHT; const { pluginInjectionApi } = this.reactComponentProps; // mediaSingle defines the max dimensions, so we don't need to constrain twice. const maxDimensions = this.getMaxCardDimensions(); const originalDimensions = { width, height }; const isSelectedAndInteracted = this.nodeInsideSelection() && interactionState !== 'hasNotHadInteraction'; return /*#__PURE__*/React.createElement(MediaNode, { api: pluginInjectionApi, view: this.view, node: this.node, getPos: getPos, selected: isSelectedAndInteracted, originalDimensions: originalDimensions, maxDimensions: maxDimensions, url: url, mediaProvider: this.getMediaProviderToUse(mediaOptions, mediaProvider), syncProvider: mediaOptions.syncProvider, contextIdentifierProvider: contextIdentifierProvider, mediaOptions: mediaOptions, onExternalImageLoaded: this.onExternalImageLoaded, isViewOnly: ((_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) === 'view', pluginInjectionApi: this.reactComponentProps.pluginInjectionApi, isAIGenerating: this.isAIGenerating }); }; }); _defineProperty(this, "renderMediaNodeWithProviders", ({ contextIdentifierProvider }) => { const { pluginInjectionApi } = this.reactComponentProps; return /*#__PURE__*/React.createElement(MediaNodeWithProviders, { pluginInjectionApi: pluginInjectionApi, innerComponent: this.renderMediaNodeWithState(contextIdentifierProvider) }); }); } getMediaSingleNode(getPos) { const pos = getPos(); if (typeof pos !== 'number') { return null; } const $pos = this.view.state.doc.resolve(pos); // The parent of the media node should be mediaSingle if ($pos.parent && $pos.parent.type.name === 'mediaSingle') { return $pos.parent; } return null; } getMaxWidthFromMediaSingleNode(mediaSingleNode) { const { width: widthAttr, widthType: widthTypeAttr, layout: layoutAttr } = mediaSingleNode.attrs; // for extended mediaSingle nodes with width and widthType attributes ( default behaviour ) if (widthAttr && widthTypeAttr === 'pixel') { return widthAttr; } // for legacy mediaSingle nodes without widthType attribute switch (layoutAttr) { case 'full-width': return akEditorFullWidthLayoutWidth; case 'wide': return akEditorCalculatedWideLayoutWidth; default: return akEditorDefaultLayoutWidth; } } hasPxWidthType(mediaSingleNode) { const { width: widthAttr, widthType: widthTypeAttr } = mediaSingleNode.attrs; // for extended mediaSingle nodes with width and widthType attributes ( default behaviour ) if (widthAttr && widthTypeAttr === 'pixel') { return true; } return false; } createDomRef() { const domRef = document.createElement('div'); if (this.reactComponentProps.mediaOptions && this.reactComponentProps.mediaOptions.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'; } if (expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true)) { this.resizeListenerBinding = bind(domRef, { type: 'resized', listener: this.hasResizedListener }); } return domRef; } viewShouldUpdate(nextNode, decorations) { const hasMediaNodeSelectedDecoration = decorations.some(decoration => isMediaDecorationSpec(decoration) && decoration.spec.type === 'media' && decoration.spec.selected); if (this.isSelected !== hasMediaNodeSelectedDecoration) { this.isSelected = hasMediaNodeSelectedDecoration; return true; } const aiGenerating = hasAIGeneratingDecoration(decorations); if (this.isAIGenerating !== aiGenerating) { this.isAIGenerating = aiGenerating; return true; } if (this.node.attrs !== nextNode.attrs) { return true; } return super.viewShouldUpdate(nextNode, decorations); } stopEvent(event) { // Don't trap right click events on media node if (['mousedown', 'contextmenu'].indexOf(event.type) !== -1) { const mouseEvent = event; if (mouseEvent.button === 2) { return true; } } return false; } getAttrs() { const { attrs } = this.node; return attrs; } isMediaBlobUrl() { const attrs = this.getAttrs(); return isMediaBlobUrlFromAttrs(attrs); } render() { const { providerFactory } = this.reactComponentProps; return /*#__PURE__*/React.createElement(WithProviders // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , { providers: ['contextIdentifierProvider'], providerFactory: providerFactory, renderNode: this.renderMediaNodeWithProviders }); } destroy() { if (this.resizeListenerBinding) { this.resizeListenerBinding(); } super.destroy(); } } export const ReactMediaNode = (portalProviderAPI, eventDispatcher, providerFactory, mediaOptions = {}, pluginInjectionApi) => (node, view, getPos) => { return new MediaNodeView(node, view, getPos, portalProviderAPI, eventDispatcher, { eventDispatcher, providerFactory, mediaOptions, pluginInjectionApi }).init(); };