@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
393 lines (387 loc) • 20.1 kB
JavaScript
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _possibleConstructorReturn from "@babel/runtime/helpers/possibleConstructorReturn";
import _getPrototypeOf from "@babel/runtime/helpers/getPrototypeOf";
import _inherits from "@babel/runtime/helpers/inherits";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _regeneratorRuntime from "@babel/runtime/regenerator";
function _callSuper(t, o, e) { return o = _getPrototypeOf(o), _possibleConstructorReturn(t, _isNativeReflectConstruct() ? Reflect.construct(o, e || [], _getPrototypeOf(t).constructor) : o.apply(t, e)); }
function _isNativeReflectConstruct() { try { var t = !Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {})); } catch (t) {} return (_isNativeReflectConstruct = function _isNativeReflectConstruct() { return !!t; })(); }
import React, { Component } from 'react';
import { bind } from 'bind-event-listener';
import memoizeOne from 'memoize-one';
import { MEDIA_CONTEXT } from '@atlaskit/analytics-namespaced-context';
import { AnalyticsContext } from '@atlaskit/analytics-next';
import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check';
import { setNodeSelection, setTextSelection, withImageLoader } from '@atlaskit/editor-common/utils';
import { findParentNodeClosestToPos } from '@atlaskit/editor-prosemirror/utils';
import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
import { Card, CardLoading } from '@atlaskit/media-card';
import { fg } from '@atlaskit/platform-feature-flags';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { stateKey as mediaStateKey } from '../../pm-plugins/plugin-key';
import { MediaCardWrapper } from '../styles';
// This is being used by DropPlaceholder now
export var MEDIA_HEIGHT = 125;
export var FILE_WIDTH = 156;
// eslint-disable-next-line @repo/internal/react/no-class-components
export var MediaNode = /*#__PURE__*/function (_Component) {
function MediaNode(_props) {
var _this;
_classCallCheck(this, MediaNode);
_this = _callSuper(this, MediaNode, [_props]);
_defineProperty(_this, "state", {});
_defineProperty(_this, "videoControlsWrapperRef", /*#__PURE__*/React.createRef());
_defineProperty(_this, "unbindKeyDown", null);
_defineProperty(_this, "setViewMediaClientConfig", /*#__PURE__*/_asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee() {
var mediaProvider, viewMediaClientConfig, viewAndUploadMediaClientConfig;
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return _this.props.mediaProvider;
case 2:
mediaProvider = _context.sent;
if (mediaProvider) {
viewMediaClientConfig = mediaProvider.viewMediaClientConfig;
viewAndUploadMediaClientConfig = mediaProvider.viewAndUploadMediaClientConfig;
if (expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true)) {
// Only update state if new configs are available and different from current state
if (viewMediaClientConfig && _this.state.viewMediaClientConfig !== viewMediaClientConfig || viewAndUploadMediaClientConfig && _this.state.viewAndUploadMediaClientConfig !== viewAndUploadMediaClientConfig) {
_this.setState({
viewMediaClientConfig: viewMediaClientConfig,
viewAndUploadMediaClientConfig: viewAndUploadMediaClientConfig
});
}
} else {
_this.setState({
viewMediaClientConfig: viewMediaClientConfig,
viewAndUploadMediaClientConfig: viewAndUploadMediaClientConfig
});
}
}
case 4:
case "end":
return _context.stop();
}
}, _callee);
})));
_defineProperty(_this, "selectMediaSingleFromCard", function (_ref2) {
var _this$props$pluginInj;
var event = _ref2.event;
_this.selectMediaSingle(event);
// In edit mode (node content wrapper has contenteditable set to true), link redirection is disabled by default
// We need to call "stopPropagation" here in order to prevent in editor view mode, the browser from navigating to
// another URL if the media node is wrapped in a link mark.
if (_this.props.isViewOnly && areToolbarFlagsEnabled(Boolean((_this$props$pluginInj = _this.props.pluginInjectionApi) === null || _this$props$pluginInj === void 0 ? void 0 : _this$props$pluginInj.toolbar))) {
event.preventDefault();
}
});
_defineProperty(_this, "selectMediaSingle", function (event) {
var propPos = _this.props.getPos();
if (typeof propPos !== 'number') {
return;
}
// NOTE: This does not prevent the link navigation in the editor view mode, .preventDefault is needed (see selectMediaSingleFromCard)
// Hence it should be removed
// We need to call "stopPropagation" here in order to prevent the browser from navigating to
// another URL if the media node is wrapped in a link mark.
if (editorExperiment('platform_editor_controls', 'control')) {
event.stopPropagation();
}
var state = _this.props.view.state;
if (event.shiftKey) {
// don't select text if there is current selection in a table (as this would override selected cells)
if (state.selection instanceof CellSelection) {
return;
}
setTextSelection(_this.props.view, state.selection.from < propPos ? state.selection.from : propPos - 1,
// + 3 needed for offset of the media inside mediaSingle and cursor to make whole mediaSingle selected
state.selection.to > propPos ? state.selection.to : propPos + 2);
} else {
setNodeSelection(_this.props.view, propPos - 1);
}
});
_defineProperty(_this, "getMediaSettings", memoizeOne(function (viewAndUploadMediaClientConfig) {
return {
canUpdateVideoCaptions: fg('platform_media_video_captions') ? !!viewAndUploadMediaClientConfig : false
};
}));
_defineProperty(_this, "onError", function (reason) {
var _this$props$api;
var nestedUnder = _this.getNestedUnder();
(_this$props$api = _this.props.api) === null || _this$props$api === void 0 || _this$props$api.media.actions.handleMediaNodeRenderError(_this.props.node, reason, nestedUnder);
});
/**
* This function checks if the media node is nested under a certain nodes, and if so,
* returns the name of the parent node type. This is used for providing more context in media render errors.
* @returns
*/
_defineProperty(_this, "getNestedUnder", function () {
var pos = _this.props.getPos();
if (typeof pos !== 'number') {
return undefined;
}
var _this$props$view$stat = _this.props.view.state,
doc = _this$props$view$stat.doc,
schema = _this$props$view$stat.schema;
var bodiedSyncBlock = schema.nodes.bodiedSyncBlock;
if (!bodiedSyncBlock) {
return undefined;
}
var resolvedPos = doc.resolve(pos);
var bodiedSyncBlockNode = findParentNodeClosestToPos(resolvedPos, function (currentNode) {
return currentNode.type === bodiedSyncBlock;
});
return bodiedSyncBlockNode === null || bodiedSyncBlockNode === void 0 ? void 0 : bodiedSyncBlockNode.node.type.name;
});
_defineProperty(_this, "onFullscreenChange", function (fullscreen) {
var _this$mediaPluginStat;
(_this$mediaPluginStat = _this.mediaPluginState) === null || _this$mediaPluginStat === void 0 || _this$mediaPluginStat.updateAndDispatch({
isFullscreen: fullscreen
});
});
_defineProperty(_this, "handleNewNode", function (props) {
var _this$mediaPluginStat2;
var node = props.node;
(_this$mediaPluginStat2 = _this.mediaPluginState) === null || _this$mediaPluginStat2 === void 0 || _this$mediaPluginStat2.handleMediaNodeMount(node, function () {
return _this.props.getPos();
});
});
var _this$props = _this.props,
view = _this$props.view,
syncProvider = _this$props.syncProvider;
_this.mediaPluginState = mediaStateKey.getState(view.state);
// Initialize state from syncProvider (available on both server and client for SSR)
if (expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true) && syncProvider) {
_this.state = {
viewMediaClientConfig: syncProvider.viewMediaClientConfig,
viewAndUploadMediaClientConfig: syncProvider.viewAndUploadMediaClientConfig
};
}
return _this;
}
_inherits(MediaNode, _Component);
return _createClass(MediaNode, [{
key: "shouldComponentUpdate",
value: function shouldComponentUpdate(nextProps, nextState) {
var hasNewViewMediaClientConfig = !this.state.viewMediaClientConfig && nextState.viewMediaClientConfig;
var hasNewViewAndUploadMediaClientConfig = !this.state.viewAndUploadMediaClientConfig && nextState.viewAndUploadMediaClientConfig;
if (this.props.selected !== nextProps.selected || this.props.node.attrs.id !== nextProps.node.attrs.id || this.props.node.attrs.collection !== nextProps.node.attrs.collection || this.props.isAIGenerating !== nextProps.isAIGenerating || this.props.maxDimensions.height !== nextProps.maxDimensions.height || this.props.maxDimensions.width !== nextProps.maxDimensions.width || this.props.contextIdentifierProvider !== nextProps.contextIdentifierProvider || this.props.isLoading !== nextProps.isLoading || this.props.mediaProvider !== nextProps.mediaProvider || expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true) && this.props.syncProvider !== nextProps.syncProvider || hasNewViewMediaClientConfig || hasNewViewAndUploadMediaClientConfig) {
return true;
}
return false;
}
}, {
key: "componentDidMount",
value: function () {
var _componentDidMount = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
var contextIdentifierProvider;
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
this.handleNewNode(this.props);
contextIdentifierProvider = this.props.contextIdentifierProvider;
_context2.t0 = this;
_context2.next = 5;
return contextIdentifierProvider;
case 5:
_context2.t1 = _context2.sent;
_context2.t2 = {
contextIdentifierProvider: _context2.t1
};
_context2.t0.setState.call(_context2.t0, _context2.t2);
_context2.next = 10;
return this.setViewMediaClientConfig();
case 10:
case "end":
return _context2.stop();
}
}, _callee2, this);
}));
function componentDidMount() {
return _componentDidMount.apply(this, arguments);
}
return componentDidMount;
}()
}, {
key: "componentWillUnmount",
value: function componentWillUnmount() {
var _this$mediaPluginStat3;
var node = this.props.node;
(_this$mediaPluginStat3 = this.mediaPluginState) === null || _this$mediaPluginStat3 === void 0 || _this$mediaPluginStat3.handleMediaNodeUnmount(node);
if (this.unbindKeyDown && typeof this.unbindKeyDown === 'function') {
this.unbindKeyDown();
}
}
}, {
key: "componentDidUpdate",
value: function componentDidUpdate(prevProps) {
var _this$mediaPluginStat5;
if (prevProps.node.attrs.id !== this.props.node.attrs.id) {
var _this$mediaPluginStat4;
(_this$mediaPluginStat4 = this.mediaPluginState) === null || _this$mediaPluginStat4 === void 0 || _this$mediaPluginStat4.handleMediaNodeUnmount(prevProps.node);
this.handleNewNode(this.props);
}
(_this$mediaPluginStat5 = this.mediaPluginState) === null || _this$mediaPluginStat5 === void 0 || _this$mediaPluginStat5.updateElement();
this.setViewMediaClientConfig();
// this.videoControlsWrapperRef is null on componentDidMount. We need to wait until it has value
if (this.videoControlsWrapperRef && this.videoControlsWrapperRef.current) {
var _this$mediaPluginStat6;
if (!((_this$mediaPluginStat6 = this.mediaPluginState) !== null && _this$mediaPluginStat6 !== void 0 && _this$mediaPluginStat6.videoControlsWrapperRef)) {
var _this$mediaPluginStat7;
this.bindKeydown();
(_this$mediaPluginStat7 = this.mediaPluginState) === null || _this$mediaPluginStat7 === void 0 || _this$mediaPluginStat7.updateAndDispatch({
videoControlsWrapperRef: this.videoControlsWrapperRef.current
});
}
}
}
}, {
key: "bindKeydown",
value: function bindKeydown() {
var _this2 = this,
_this$videoControlsWr;
var onKeydown = function onKeydown(event) {
if (event.key === 'Tab') {
var _this2$videoControlsW;
// Add focus trap for controls panel
var firstElement;
var lastElement;
var focusableElements = (_this2$videoControlsW = _this2.videoControlsWrapperRef) === null || _this2$videoControlsW === void 0 || (_this2$videoControlsW = _this2$videoControlsW.current) === null || _this2$videoControlsW === void 0 ? void 0 : _this2$videoControlsW.querySelectorAll('button, input, [tabindex]:not([tabindex="-1"])');
if (focusableElements && focusableElements.length) {
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
firstElement = focusableElements[0];
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
lastElement = focusableElements[focusableElements.length - 1];
if (event.shiftKey && document.activeElement === firstElement) {
event.preventDefault();
lastElement.focus();
} else if (!event.shiftKey && document.activeElement === lastElement) {
var _firstElement;
event.preventDefault();
(_firstElement = firstElement) === null || _firstElement === void 0 || _firstElement.focus();
}
}
}
};
if ((_this$videoControlsWr = this.videoControlsWrapperRef) !== null && _this$videoControlsWr !== void 0 && _this$videoControlsWr.current) {
this.unbindKeyDown = bind(this.videoControlsWrapperRef.current, {
type: 'keydown',
listener: onKeydown,
options: {
capture: true,
passive: false
}
});
}
}
}, {
key: "render",
value: function render() {
var _this$props2 = this.props,
node = _this$props2.node,
selected = _this$props2.selected,
originalDimensions = _this$props2.originalDimensions,
isLoading = _this$props2.isLoading,
maxDimensions = _this$props2.maxDimensions,
mediaOptions = _this$props2.mediaOptions;
var borderMark = node.marks.find(function (m) {
return m.type.name === 'border';
});
var _this$state = this.state,
viewMediaClientConfig = _this$state.viewMediaClientConfig,
viewAndUploadMediaClientConfig = _this$state.viewAndUploadMediaClientConfig,
contextIdentifierProvider = _this$state.contextIdentifierProvider;
var _node$attrs = node.attrs,
id = _node$attrs.id,
type = _node$attrs.type,
collection = _node$attrs.collection,
url = _node$attrs.url,
alt = _node$attrs.alt;
// Check if we have any media client config available (syncProvider, state, or upload config)
var hasNoMediaClientConfig = !viewMediaClientConfig && (fg('platform_media_video_captions') ? !viewAndUploadMediaClientConfig : true);
if (isLoading || type !== 'external' && hasNoMediaClientConfig) {
if (expValEquals('platform_editor_media_vc_fixes', 'isEnabled', true)) {
return /*#__PURE__*/React.createElement(MediaCardWrapper, {
dimensions: originalDimensions,
borderWidth: borderMark === null || borderMark === void 0 ? void 0 : borderMark.attrs.size,
selected: selected
}, /*#__PURE__*/React.createElement(CardLoading, {
interactionName: "editor-media-card-loading"
}));
}
return /*#__PURE__*/React.createElement(MediaCardWrapper, {
dimensions: originalDimensions
}, /*#__PURE__*/React.createElement(CardLoading, {
interactionName: "editor-media-card-loading"
}));
}
var contextId = contextIdentifierProvider && contextIdentifierProvider.objectId;
var identifier = type === 'external' ? {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
dataURI: url,
name: url,
mediaItemType: 'external-image'
} : {
id: id,
mediaItemType: 'file',
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
collectionName: collection
};
var resolvedViewAndUploadMediaClientConfig = fg('platform_media_video_captions') ? viewAndUploadMediaClientConfig : undefined;
// mediaClientConfig is not needed for "external" case. So we have to cheat here.
// there is a possibility mediaClientConfig will be part of a identifier,
// so this might be not an issue
var mediaClientConfig = resolvedViewAndUploadMediaClientConfig || viewMediaClientConfig || {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
authProvider: function authProvider() {
return {};
}
};
var ssr = process.env.REACT_SSR ? 'server' : 'client';
return /*#__PURE__*/React.createElement(MediaCardWrapper, {
dimensions: originalDimensions,
onContextMenu: this.selectMediaSingle,
borderWidth: borderMark === null || borderMark === void 0 ? void 0 : borderMark.attrs.size,
selected: selected
}, /*#__PURE__*/React.createElement(AnalyticsContext
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
, {
data: _defineProperty({}, MEDIA_CONTEXT, {
border: !!borderMark
})
}, /*#__PURE__*/React.createElement(Card, {
mediaClientConfig: mediaClientConfig,
resizeMode: "stretchy-fit",
dimensions: maxDimensions,
originalDimensions: originalDimensions,
identifier: identifier,
selectable: true,
selected: selected,
disableOverlay: true,
onFullscreenChange: this.onFullscreenChange,
onClick: this.selectMediaSingleFromCard,
useInlinePlayer: mediaOptions && mediaOptions.allowLazyLoading,
isLazy: mediaOptions && mediaOptions.allowLazyLoading,
featureFlags: mediaOptions && mediaOptions.featureFlags,
contextId: contextId,
alt: alt,
videoControlsWrapperRef: this.videoControlsWrapperRef,
ssr: ssr,
mediaSettings: this.getMediaSettings(viewAndUploadMediaClientConfig),
isAIGenerating: !!this.props.isAIGenerating,
onError: expValEquals('platform_editor_media_error_analytics', 'isEnabled', true) ? this.onError : undefined
})));
}
}]);
}(Component);
var _default_1 = withImageLoader(MediaNode);
export default _default_1;