@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
1,055 lines (1,038 loc) • 53.9 kB
JavaScript
import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _createClass from "@babel/runtime/helpers/createClass";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
import _regeneratorRuntime from "@babel/runtime/regenerator";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import assert from 'assert';
import React from 'react';
import { RawIntlProvider } from 'react-intl';
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
import uuid from 'uuid';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { getBrowserInfo } from '@atlaskit/editor-common/browser';
import { mediaInlineImagesEnabled } from '@atlaskit/editor-common/media-inline';
import { CAPTION_PLACEHOLDER_ID, getMaxWidthForNestedNodeNext } from '@atlaskit/editor-common/media-single';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { ErrorReporter } from '@atlaskit/editor-common/utils';
import { AllSelection, NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { insertPoint } from '@atlaskit/editor-prosemirror/transform';
import { findDomRefAtPos, findParentNodeOfType, findSelectedNodeOfType, isNodeSelection } from '@atlaskit/editor-prosemirror/utils';
import { Decoration, DecorationSet } from '@atlaskit/editor-prosemirror/view';
import { CellSelection } from '@atlaskit/editor-tables/cell-selection';
import { isFileIdentifier } from '@atlaskit/media-client';
import { getMediaFeatureFlag } from '@atlaskit/media-common';
import { fg } from '@atlaskit/platform-feature-flags';
// Ignored via go/ees005
// eslint-disable-next-line import/no-namespace
import * as helpers from '../pm-plugins/commands/helpers';
import { updateMediaNodeAttrs } from '../pm-plugins/commands/helpers';
import { getIdentifier, getMediaFromSupportedMediaNodesFromSelection, isNodeDoubleClickSupportedInLivePagesViewMode, removeMediaNode, splitMediaGroup } from '../pm-plugins/utils/media-common';
import { insertMediaGroupNode, insertMediaInlineNode } from '../pm-plugins/utils/media-files';
import { getMediaNodeInsertionType } from '../pm-plugins/utils/media-inline';
import { insertMediaSingleNode } from '../pm-plugins/utils/media-single';
import DropPlaceholder from '../ui/Media/DropPlaceholder';
import { ACTIONS } from './actions';
import { MediaTaskManager } from './mediaTaskManager';
import PickerFacade from './picker-facade';
import { stateKey } from './plugin-key';
export var MEDIA_CONTENT_WRAP_CLASS_NAME = 'media-content-wrap';
export var MEDIA_PLUGIN_IS_RESIZING_KEY = 'mediaSinglePlugin.isResizing';
export var MEDIA_PLUGIN_RESIZING_WIDTH_KEY = 'mediaSinglePlugin.resizing-width';
var createDropPlaceholder = function createDropPlaceholder(intl, nodeViewPortalProviderAPI, dropPlaceholderKey, allowDropLine) {
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
var dropPlaceholder = document.createElement('div');
var createElement = React.createElement;
if (allowDropLine) {
nodeViewPortalProviderAPI.render(function () {
return createElement(RawIntlProvider, {
value: intl
}, createElement(DropPlaceholder, {
type: 'single'
}));
}, dropPlaceholder, dropPlaceholderKey);
} else {
nodeViewPortalProviderAPI.render(function () {
return createElement(RawIntlProvider, {
value: intl
}, createElement(DropPlaceholder));
}, dropPlaceholder, dropPlaceholderKey);
}
return dropPlaceholder;
};
var MEDIA_RESOLVED_STATES = ['ready', 'error', 'cancelled'];
export var MediaPluginStateImplementation = /*#__PURE__*/function () {
function MediaPluginStateImplementation(_state, options, mediaOptions, _dispatch, pluginInjectionApi) {
var _this = this;
_classCallCheck(this, MediaPluginStateImplementation);
_defineProperty(this, "allowsUploads", false);
_defineProperty(this, "ignoreLinks", false);
_defineProperty(this, "waitForMediaUpload", true);
_defineProperty(this, "allUploadsFinished", true);
_defineProperty(this, "showDropzone", false);
_defineProperty(this, "isFullscreen", false);
_defineProperty(this, "layout", 'center');
_defineProperty(this, "mediaNodes", []);
_defineProperty(this, "isResizing", false);
_defineProperty(this, "resizingWidth", 0);
_defineProperty(this, "allowInlineImages", false);
_defineProperty(this, "uploadInProgressSubscriptions", []);
_defineProperty(this, "uploadInProgressSubscriptionsNotified", false);
// this is only a temporary variable, which gets cleared after the last inserted node has been selected
_defineProperty(this, "lastAddedMediaSingleFileIds", []);
_defineProperty(this, "destroyed", false);
_defineProperty(this, "removeOnCloseListener", function () {});
_defineProperty(this, "onPopupToggleCallback", function () {});
_defineProperty(this, "identifierCount", new Map());
// This is to enable mediaShallowCopySope to enable only shallow copying media referenced within the edtior
// see: trackOutOfScopeIdentifier
_defineProperty(this, "outOfEditorScopeIdentifierMap", new Map());
_defineProperty(this, "taskManager", new MediaTaskManager());
_defineProperty(this, "pickers", []);
_defineProperty(this, "pickerPromises", []);
_defineProperty(this, "getMediaOptions", function () {
return _this.options;
});
_defineProperty(this, "isMediaSchemaNode", function (_ref) {
var _this$mediaOptions;
var type = _ref.type;
var _this$view$state$sche = _this.view.state.schema.nodes,
mediaInline = _this$view$state$sche.mediaInline,
mediaSingle = _this$view$state$sche.mediaSingle,
media = _this$view$state$sche.media;
if (getMediaFeatureFlag('mediaInline', (_this$mediaOptions = _this.mediaOptions) === null || _this$mediaOptions === void 0 ? void 0 : _this$mediaOptions.featureFlags)) {
return type === mediaSingle || type === media || type === mediaInline;
}
return type === mediaSingle;
});
// callback to flag that a node has been inserted
_defineProperty(this, "onNodeInserted", function (id, selectionPosition) {
_this.lastAddedMediaSingleFileIds.unshift({
id: id,
selectionPosition: selectionPosition
});
});
/**
* we insert a new file by inserting a initial state for that file.
*
* called when we insert a new file via the picker (connected via pickerfacade)
*/
_defineProperty(this, "insertFile", function (mediaState, onMediaStateChanged, pickerType, insertMediaVia) {
var _this$pluginInjection, _mediaState$collectio, _this$pluginInjection2;
var state = _this.view.state;
var editorAnalyticsAPI = (_this$pluginInjection = _this.pluginInjectionApi) === null || _this$pluginInjection === void 0 || (_this$pluginInjection = _this$pluginInjection.analytics) === null || _this$pluginInjection === void 0 ? void 0 : _this$pluginInjection.actions;
var mediaStateWithContext = _objectSpread(_objectSpread({}, mediaState), {}, {
contextId: _this.contextIdentifierProvider ? _this.contextIdentifierProvider.objectId : undefined
});
var collection = (_mediaState$collectio = mediaState.collection) !== null && _mediaState$collectio !== void 0 ? _mediaState$collectio : _this.collectionFromProvider();
if (collection === undefined) {
return;
}
// We need to dispatch the change to event dispatcher only for successful files
if (mediaState.status !== 'error') {
_this.updateAndDispatch({
allUploadsFinished: false
});
}
if (_this.uploadInProgressSubscriptions.length > 0 && !_this.uploadInProgressSubscriptionsNotified) {
_this.uploadInProgressSubscriptions.forEach(function (fn) {
return fn(true);
});
_this.uploadInProgressSubscriptionsNotified = true;
}
switch (getMediaNodeInsertionType(state, _this.mediaOptions, mediaStateWithContext.fileMimeType)) {
case 'inline':
insertMediaInlineNode(editorAnalyticsAPI)(_this.view, mediaStateWithContext, collection, _this.allowInlineImages, _this.getInputMethod(pickerType), insertMediaVia);
break;
case 'block':
// read width state right before inserting to get up-to-date and define values
var widthPluginState = (_this$pluginInjection2 = _this.pluginInjectionApi) === null || _this$pluginInjection2 === void 0 || (_this$pluginInjection2 = _this$pluginInjection2.width) === null || _this$pluginInjection2 === void 0 ? void 0 : _this$pluginInjection2.sharedState.currentState();
insertMediaSingleNode(_this.view, mediaStateWithContext, _this.getInputMethod(pickerType), collection, _this.mediaOptions && _this.mediaOptions.alignLeftOnInsert, widthPluginState, editorAnalyticsAPI, _this.onNodeInserted, insertMediaVia, _this.mediaOptions && _this.mediaOptions.allowPixelResizing);
break;
case 'group':
insertMediaGroupNode(editorAnalyticsAPI)(_this.view, [mediaStateWithContext], collection, _this.getInputMethod(pickerType), insertMediaVia);
break;
}
// do events when media state changes
onMediaStateChanged(_this.handleMediaState);
// handle waiting for upload complete
var isEndState = function isEndState(state) {
return state.status && MEDIA_RESOLVED_STATES.indexOf(state.status) !== -1;
};
if (!isEndState(mediaStateWithContext)) {
var uploadingPromise = new Promise(function (resolve) {
onMediaStateChanged(function (newState) {
// When media item reaches its final state, remove listener and resolve
if (isEndState(newState)) {
resolve(newState);
}
});
});
if (fg('platform_editor_media_disable_save_during_upload')) {
_this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(function () {
_this.updateAndDispatch({
allUploadsFinished: true
});
_this.waitForPendingTasks().then(function () {
if (_this.uploadInProgressSubscriptions.length > 0 && _this.uploadInProgressSubscriptionsNotified) {
_this.uploadInProgressSubscriptions.forEach(function (fn) {
return fn(false);
});
_this.uploadInProgressSubscriptionsNotified = false;
}
});
});
} else {
_this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(function () {
_this.updateAndDispatch({
allUploadsFinished: true
});
});
}
}
// refocus the view
var view = _this.view;
if (!view.hasFocus()) {
view.focus();
}
if (isEndState(mediaStateWithContext) || !fg('platform_editor_media_disable_save_during_upload')) {
_this.waitForPendingTasks().then(function () {
if (_this.uploadInProgressSubscriptions.length > 0 && _this.uploadInProgressSubscriptionsNotified) {
_this.uploadInProgressSubscriptions.forEach(function (fn) {
return fn(false);
});
_this.uploadInProgressSubscriptionsNotified = false;
}
});
}
_this.selectLastAddedMediaNode();
});
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_defineProperty(this, "addPendingTask", function (task) {
_this.taskManager.addPendingTask(task);
});
_defineProperty(this, "splitMediaGroup", function () {
return splitMediaGroup(_this.view);
});
_defineProperty(this, "onPopupPickerClose", function () {
_this.onPopupToggleCallback(false);
});
_defineProperty(this, "showMediaPicker", function () {
if (_this.openMediaPickerBrowser) {
return _this.openMediaPickerBrowser();
}
_this.onPopupToggleCallback(true);
});
_defineProperty(this, "setBrowseFn", function (browseFn) {
_this.openMediaPickerBrowser = browseFn;
});
_defineProperty(this, "onPopupToggle", function (onPopupToggleCallback) {
_this.onPopupToggleCallback = onPopupToggleCallback;
});
/**
* Returns a promise that is resolved after all pending operations have been finished.
* An optional timeout will cause the promise to reject if the operation takes too long
*
* NOTE: The promise will resolve even if some of the media have failed to process.
*/
_defineProperty(this, "waitForPendingTasks", this.taskManager.waitForPendingTasks);
/**
* Called from React UI Component when user clicks on "Delete" icon
* inside of it
*/
_defineProperty(this, "handleMediaNodeRemoval", function (node, getPos) {
var getNode = node;
if (!getNode) {
var pos = getPos();
if (typeof pos !== 'number') {
return;
}
getNode = _this.view.state.doc.nodeAt(pos);
}
removeMediaNode(_this.view, getNode, getPos);
});
_defineProperty(this, "getIdentifierKey", function (identifier) {
if (identifier.mediaItemType === 'file') {
return identifier.id;
} else {
return identifier.dataURI;
}
});
_defineProperty(this, "trackMediaNodeAddition", function (node) {
var _this$identifierCount;
var identifier = getIdentifier(node.attrs);
var key = _this.getIdentifierKey(identifier);
var _ref2 = (_this$identifierCount = _this.identifierCount.get(key)) !== null && _this$identifierCount !== void 0 ? _this$identifierCount : {
count: 0
},
count = _ref2.count;
if (count === 0) {
_this.taskManager.resumePendingTask(key);
}
_this.identifierCount.set(key, {
identifier: identifier,
count: count + 1
});
});
_defineProperty(this, "trackMediaNodeRemoval", function (node) {
var _this$identifierCount2;
var identifier = getIdentifier(node.attrs);
var key = _this.getIdentifierKey(identifier);
var _ref3 = (_this$identifierCount2 = _this.identifierCount.get(key)) !== null && _this$identifierCount2 !== void 0 ? _this$identifierCount2 : {
count: 0
},
count = _ref3.count;
if (count === 1) {
_this.taskManager.cancelPendingTask(key);
}
_this.identifierCount.set(key, {
identifier: identifier,
count: count - 1
});
});
_defineProperty(this, "isIdentifierInEditorScope", function (identifier) {
var key = _this.getIdentifierKey(identifier);
// rely on has instead of count > 0 because if the user cuts and pastes the same media
// the count will temporarily be 0 but the media is still in the scope of editor.
return !_this.outOfEditorScopeIdentifierMap.has(key) && _this.identifierCount.has(key);
});
/**
* This is used in on Paste of media, this tracks which if the pasted media originated from a outside the editor
* i.e. the pasted media was not uplaoded to the current editor.
* This is to enable mediaShallowCopySope to enable only shallow copying media referenced within the edtior
*/
_defineProperty(this, "trackOutOfScopeIdentifier", function (identifier) {
var key = _this.getIdentifierKey(identifier);
_this.outOfEditorScopeIdentifierMap.set(key, {
identifier: identifier
});
});
/**
* Called from React UI Component on componentDidMount
*/
_defineProperty(this, "handleMediaNodeMount", function (node, getPos) {
_this.trackMediaNodeAddition(node);
_this.mediaNodes.unshift({
node: node,
getPos: getPos
});
});
/**
* Called from React UI Component on componentWillUnmount and UNSAFE_componentWillReceiveProps
* when React component's underlying node property is replaced with a new node
*/
_defineProperty(this, "handleMediaNodeUnmount", function (oldNode) {
_this.trackMediaNodeRemoval(oldNode);
_this.mediaNodes = _this.mediaNodes.filter(function (_ref4) {
var node = _ref4.node;
return oldNode !== node;
});
});
_defineProperty(this, "handleMediaGroupUpdate", function (oldNodes, newNodes) {
var addedNodes = newNodes.filter(function (node) {
return oldNodes.every(function (oldNode) {
return oldNode.attrs.id !== node.attrs.id;
});
});
var removedNodes = oldNodes.filter(function (node) {
return newNodes.every(function (newNode) {
return newNode.attrs.id !== node.attrs.id;
});
});
addedNodes.forEach(function (node) {
_this.trackMediaNodeAddition(node);
});
removedNodes.forEach(function (oldNode) {
_this.trackMediaNodeRemoval(oldNode);
});
});
_defineProperty(this, "findMediaNode", function (id) {
return helpers.findMediaNode(_this, id);
});
_defineProperty(this, "destroyAllPickers", function (pickers) {
pickers.forEach(function (picker) {
return picker.destroy();
});
_this.pickers.splice(0, _this.pickers.length);
});
_defineProperty(this, "destroyPickers", function () {
var pickers = _this.pickers,
pickerPromises = _this.pickerPromises;
// If pickerPromises and pickers are the same length
// All pickers have resolved and we safely destroy them
// Otherwise wait for them to resolve then destroy.
if (pickerPromises.length === pickers.length) {
_this.destroyAllPickers(_this.pickers);
} else {
Promise.all(pickerPromises).then(function (resolvedPickers) {
return _this.destroyAllPickers(resolvedPickers);
});
}
_this.customPicker = undefined;
});
_defineProperty(this, "getInputMethod", function (pickerType) {
switch (pickerType) {
case INPUT_METHOD.PICKER_CLOUD:
return INPUT_METHOD.PICKER_CLOUD;
case INPUT_METHOD.MEDIA_PICKER:
return INPUT_METHOD.MEDIA_PICKER;
case 'clipboard':
return INPUT_METHOD.CLIPBOARD;
case 'dropzone':
return INPUT_METHOD.DRAG_AND_DROP;
case 'browser':
return INPUT_METHOD.BROWSER;
}
return;
});
_defineProperty(this, "updateMediaSingleNodeAttrs", function (id, attrs) {
var view = _this.view;
if (!view) {
return;
}
return updateMediaNodeAttrs(id, attrs)(view.state, view.dispatch);
});
_defineProperty(this, "handleMediaState", function (state) {
switch (state.status) {
case 'error':
var uploadErrorHandler = _this.options.uploadErrorHandler;
if (uploadErrorHandler) {
uploadErrorHandler(state);
}
break;
}
});
_defineProperty(this, "removeSelectedMediaContainer", function () {
var view = _this.view;
var selectedNode = _this.selectedMediaContainerNode();
if (!selectedNode) {
return false;
}
var from = view.state.selection.from;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
removeMediaNode(view, selectedNode.firstChild, function () {
return from + 1;
});
return true;
});
_defineProperty(this, "selectedMediaContainerNode", function () {
var _this$view;
var selection = (_this$view = _this.view) === null || _this$view === void 0 || (_this$view = _this$view.state) === null || _this$view === void 0 ? void 0 : _this$view.selection;
if (selection instanceof NodeSelection && _this.isMediaSchemaNode(selection.node)) {
return selection.node;
}
return;
});
_defineProperty(this, "handleDrag", function (dragState) {
var isActive = dragState === 'enter';
if (_this.showDropzone === isActive) {
return;
}
_this.showDropzone = isActive;
var _this$view2 = _this.view,
dispatch = _this$view2.dispatch,
state = _this$view2.state;
var tr = state.tr,
selection = state.selection,
doc = state.doc;
var _state$schema$nodes = state.schema.nodes,
media = _state$schema$nodes.media,
mediaGroup = _state$schema$nodes.mediaGroup;
// Workaround for wrong upload position
// @see https://product-fabric.atlassian.net/browse/MEX-2457
// If the media node is the last selectable item in the current cursor position and it is located within a mediaGroup,
// we relocate the cursor to the first child of the mediaGroup.
var pos = Math.max(0, selection.$from.pos - 1);
var sel = Selection.findFrom(doc.resolve(pos), -1);
if (sel && findSelectedNodeOfType(media)(sel)) {
var parent = findParentNodeOfType(mediaGroup)(sel);
if (parent) {
tr.setSelection(NodeSelection.create(tr.doc, parent.start));
}
}
// Trigger state change to be able to pick it up in the decorations handler
dispatch(tr);
});
this.options = options;
this.mediaOptions = mediaOptions;
this.dispatch = _dispatch;
this.pluginInjectionApi = pluginInjectionApi;
this.waitForMediaUpload = options.waitForMediaUpload === undefined ? true : options.waitForMediaUpload;
var nodes = _state.schema.nodes;
assert(nodes.media && (nodes.mediaGroup || nodes.mediaSingle), 'Editor: unable to init media plugin - media or mediaGroup/mediaSingle node absent in schema');
if (mediaOptions !== null && mediaOptions !== void 0 && mediaOptions.syncProvider) {
this.setMediaProvider(mediaOptions === null || mediaOptions === void 0 ? void 0 : mediaOptions.syncProvider);
} else if (mediaOptions !== null && mediaOptions !== void 0 && mediaOptions.provider) {
this.setMediaProvider(mediaOptions === null || mediaOptions === void 0 ? void 0 : mediaOptions.provider);
}
if (fg('platform_editor_remove_media_inline_feature_flag')) {
var _this$mediaOptions2;
if ((_this$mediaOptions2 = this.mediaOptions) !== null && _this$mediaOptions2 !== void 0 && _this$mediaOptions2.allowMediaInlineImages) {
this.allowInlineImages = true;
}
} else {
var _this$mediaOptions3, _this$mediaOptions4;
if (mediaInlineImagesEnabled(getMediaFeatureFlag('mediaInline', (_this$mediaOptions3 = this.mediaOptions) === null || _this$mediaOptions3 === void 0 ? void 0 : _this$mediaOptions3.featureFlags), (_this$mediaOptions4 = this.mediaOptions) === null || _this$mediaOptions4 === void 0 ? void 0 : _this$mediaOptions4.allowMediaInlineImages)) {
this.allowInlineImages = true;
}
}
this.errorReporter = options.errorReporter || new ErrorReporter();
this.singletonCreatedAt = (performance || Date).now();
}
return _createClass(MediaPluginStateImplementation, [{
key: "clone",
value: function clone() {
var _originalTarget;
var clonedAt = (performance || Date).now();
// Prevent double wrapping
// If clone is repeatedly called, we want to proxy the underlying MediaPluginStateImplementation target, rather than the proxy itself
// If we proxy the proxy, then calling get in future will need to recursively unwrap proxies to find the original target, which causes performance issues
// Instead, we check if there is an original target stored on "this", and if so, we use that as the proxy target instead
return new Proxy((_originalTarget = this.originalTarget) !== null && _originalTarget !== void 0 ? _originalTarget : this, {
get: function get(target, prop, receiver) {
if (prop === 'singletonCreatedAt') {
return clonedAt;
}
if (prop === 'originalTarget') {
return target;
}
return Reflect.get(target, prop, receiver);
}
});
}
}, {
key: "subscribeToUploadInProgressState",
value: function subscribeToUploadInProgressState(fn) {
this.uploadInProgressSubscriptions.push(fn);
}
}, {
key: "unsubscribeFromUploadInProgressState",
value: function unsubscribeFromUploadInProgressState(fn) {
this.uploadInProgressSubscriptions = this.uploadInProgressSubscriptions.filter(function (subscribedFn) {
return subscribedFn !== fn;
});
}
}, {
key: "setMediaProvider",
value: function () {
var _setMediaProvider = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(mediaProvider) {
var viewMediaClientConfig, wrappedError, view, allowsUploads;
return _regeneratorRuntime.wrap(function _callee$(_context) {
while (1) switch (_context.prev = _context.next) {
case 0:
if (!(this.previousMediaProvider === mediaProvider)) {
_context.next = 2;
break;
}
return _context.abrupt("return");
case 2:
this.previousMediaProvider = mediaProvider;
if (mediaProvider) {
_context.next = 8;
break;
}
this.destroyPickers();
this.allowsUploads = false;
if (!this.destroyed) {
this.view.dispatch(this.view.state.tr.setMeta(stateKey, {
allowsUploads: this.allowsUploads
}));
}
return _context.abrupt("return");
case 8:
_context.prev = 8;
if (!(mediaProvider instanceof Promise)) {
_context.next = 15;
break;
}
_context.next = 12;
return mediaProvider;
case 12:
this.mediaProvider = _context.sent;
_context.next = 16;
break;
case 15:
this.mediaProvider = mediaProvider;
case 16:
// Ignored via go/ees007
// eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format
// TODO [MS-2038]: remove once context api is removed
// We want to re assign the view and upload configs if they are missing for backwards compatibility
// as currently integrators can pass context || mediaClientConfig
if (!this.mediaProvider.viewMediaClientConfig) {
viewMediaClientConfig = this.mediaProvider.viewMediaClientConfig;
if (viewMediaClientConfig) {
this.mediaProvider.viewMediaClientConfig = viewMediaClientConfig;
}
}
assert(this.mediaProvider.viewMediaClientConfig, "MediaProvider promise did not resolve to a valid instance of MediaProvider - ".concat(this.mediaProvider));
_context.next = 28;
break;
case 20:
_context.prev = 20;
_context.t0 = _context["catch"](8);
wrappedError = new Error("Media functionality disabled due to rejected provider: ".concat(_context.t0 instanceof Error ? _context.t0.message : String(_context.t0)));
this.errorReporter.captureException(wrappedError);
this.destroyPickers();
this.allowsUploads = false;
if (!this.destroyed) {
this.view.dispatch(this.view.state.tr.setMeta(stateKey, {
allowsUploads: this.allowsUploads
}));
}
return _context.abrupt("return");
case 28:
this.mediaClientConfig = this.mediaProvider.viewMediaClientConfig;
this.allowsUploads = !!this.mediaProvider.uploadMediaClientConfig;
view = this.view, allowsUploads = this.allowsUploads; // make sure editable DOM node is mounted
if (!this.destroyed && view && view.dom.parentNode) {
// make PM plugin aware of the state change to update UI during 'apply' hook
view.dispatch(view.state.tr.setMeta(stateKey, {
allowsUploads: allowsUploads
}));
}
if (!this.allowsUploads) {
_context.next = 42;
break;
}
this.uploadMediaClientConfig = this.mediaProvider.uploadMediaClientConfig;
if (!(this.mediaProvider.uploadParams && this.uploadMediaClientConfig)) {
_context.next = 39;
break;
}
_context.next = 37;
return this.initPickers(this.mediaProvider.uploadParams, PickerFacade);
case 37:
_context.next = 40;
break;
case 39:
this.destroyPickers();
case 40:
_context.next = 43;
break;
case 42:
this.destroyPickers();
case 43:
case "end":
return _context.stop();
}
}, _callee, this, [[8, 20]]);
}));
function setMediaProvider(_x) {
return _setMediaProvider.apply(this, arguments);
}
return setMediaProvider;
}()
}, {
key: "setIsResizing",
value: function setIsResizing(isResizing) {
this.isResizing = isResizing;
}
}, {
key: "setResizingWidth",
value: function setResizingWidth(width) {
this.resizingWidth = width;
}
}, {
key: "updateElement",
value: function updateElement() {
var newElement;
var selectedContainer = this.selectedMediaContainerNode();
if (selectedContainer && this.isMediaSchemaNode(selectedContainer)) {
newElement = this.getDomElement(this.view.domAtPos.bind(this.view));
if (selectedContainer.type === this.view.state.schema.nodes.mediaSingle) {
this.currentMaxWidth = getMaxWidthForNestedNodeNext(this.view, this.view.state.selection.$anchor.pos) || undefined;
} else {
this.currentMaxWidth = undefined;
}
}
if (this.element !== newElement) {
this.element = newElement;
}
}
}, {
key: "getDomElement",
value: function getDomElement(domAtPos) {
var selection = this.view.state.selection;
if (!(selection instanceof NodeSelection)) {
return;
}
if (!this.isMediaSchemaNode(selection.node)) {
return;
}
var node = findDomRefAtPos(selection.from, domAtPos);
if (node) {
if (!node.childNodes.length) {
return node.parentNode;
}
return node;
}
return;
}
}, {
key: "contextIdentifierProvider",
get: function get() {
var _this$pluginInjection3;
return (_this$pluginInjection3 = this.pluginInjectionApi) === null || _this$pluginInjection3 === void 0 || (_this$pluginInjection3 = _this$pluginInjection3.contextIdentifier) === null || _this$pluginInjection3 === void 0 || (_this$pluginInjection3 = _this$pluginInjection3.sharedState.currentState()) === null || _this$pluginInjection3 === void 0 ? void 0 : _this$pluginInjection3.contextIdentifierProvider;
}
}, {
key: "selectLastAddedMediaNode",
value: function selectLastAddedMediaNode() {
var _this$mediaOptions5,
_this2 = this;
// if preventAutoFocusOnUpload is enabled, skip auto-selection and just clear the tracking array
if ((_this$mediaOptions5 = this.mediaOptions) !== null && _this$mediaOptions5 !== void 0 && _this$mediaOptions5.preventAutoFocusOnUpload) {
this.lastAddedMediaSingleFileIds = [];
return;
}
// if lastAddedMediaSingleFileIds is empty exit because there are no added media single nodes to be selected
if (this.lastAddedMediaSingleFileIds.length !== 0) {
this.waitForPendingTasks().then(function () {
var lastTrackedAddedNode = _this2.lastAddedMediaSingleFileIds[0];
// execute selection only if selection did not change after the node has been inserted
if ((lastTrackedAddedNode === null || lastTrackedAddedNode === void 0 ? void 0 : lastTrackedAddedNode.selectionPosition) === _this2.view.state.selection.from) {
var lastAddedNode = _this2.mediaNodes.find(function (node) {
return node.node.attrs.id === lastTrackedAddedNode.id;
});
var lastAddedNodePos = lastAddedNode === null || lastAddedNode === void 0 ? void 0 : lastAddedNode.getPos();
if (lastAddedNodePos) {
var _this2$view = _this2.view,
dispatch = _this2$view.dispatch,
state = _this2$view.state;
var tr = state.tr;
tr.setSelection(NodeSelection.create(tr.doc, lastAddedNodePos));
if (dispatch) {
dispatch(tr);
}
}
}
// reset temp constant after uploads finished
_this2.lastAddedMediaSingleFileIds = [];
});
}
}
}, {
key: "setView",
value: function setView(view) {
this.view = view;
}
}, {
key: "destroy",
value: function destroy() {
if (this.destroyed) {
return;
}
this.destroyed = true;
var mediaNodes = this.mediaNodes;
mediaNodes.splice(0, mediaNodes.length);
this.removeOnCloseListener();
this.destroyPickers();
}
}, {
key: "initPickers",
value: function () {
var _initPickers = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2(uploadParams, Picker) {
var _this3 = this;
var errorReporter, pickers, pickerPromises, pickerFacadeConfig, customPicker;
return _regeneratorRuntime.wrap(function _callee2$(_context2) {
while (1) switch (_context2.prev = _context2.next) {
case 0:
if (!(this.destroyed || !this.uploadMediaClientConfig)) {
_context2.next = 2;
break;
}
return _context2.abrupt("return");
case 2:
errorReporter = this.errorReporter, pickers = this.pickers, pickerPromises = this.pickerPromises; // create pickers if they don't exist, re-use otherwise
if (pickers.length) {
_context2.next = 14;
break;
}
pickerFacadeConfig = {
mediaClientConfig: this.uploadMediaClientConfig,
errorReporter: errorReporter
};
if (!this.options.customMediaPicker) {
_context2.next = 13;
break;
}
customPicker = new Picker('customMediaPicker', pickerFacadeConfig, this.options.customMediaPicker).init();
pickerPromises.push(customPicker);
_context2.t0 = pickers;
_context2.next = 11;
return customPicker;
case 11:
_context2.t1 = this.customPicker = _context2.sent;
_context2.t0.push.call(_context2.t0, _context2.t1);
case 13:
pickers.forEach(function (picker) {
picker.onNewMedia(_this3.insertFile);
});
case 14:
// set new upload params for the pickers
pickers.forEach(function (picker) {
return picker.setUploadParams(uploadParams);
});
case 15:
case "end":
return _context2.stop();
}
}, _callee2, this);
}));
function initPickers(_x2, _x3) {
return _initPickers.apply(this, arguments);
}
return initPickers;
}()
}, {
key: "collectionFromProvider",
value: function collectionFromProvider() {
return this.mediaProvider && this.mediaProvider.uploadParams && this.mediaProvider.uploadParams.collection;
}
}, {
key: "updateAndDispatch",
value: function updateAndDispatch(props) {
var _this4 = this;
// update plugin state
Object.keys(props).forEach(function (_key) {
var key = _key;
var value = props[key];
if (value !== undefined) {
_this4[key] = value;
}
});
if (this.dispatch) {
this.dispatch(stateKey, _objectSpread({}, this));
}
}
}]);
}();
export var getMediaPluginState = function getMediaPluginState(state) {
return stateKey.getState(state);
};
export var createPlugin = function createPlugin(_schema, options, getIntl, pluginInjectionApi, nodeViewPortalProviderAPI, dispatch, mediaOptions) {
var intl = getIntl();
return new SafePlugin({
state: {
init: function init(_config, state) {
return new MediaPluginStateImplementation(state, options, mediaOptions, dispatch, pluginInjectionApi);
},
apply: function apply(tr, pluginState) {
var _tr$getMeta;
var isResizing = tr.getMeta(MEDIA_PLUGIN_IS_RESIZING_KEY);
var resizingWidth = tr.getMeta(MEDIA_PLUGIN_RESIZING_WIDTH_KEY);
var mediaProvider = (_tr$getMeta = tr.getMeta(stateKey)) === null || _tr$getMeta === void 0 ? void 0 : _tr$getMeta.mediaProvider;
// Yes, I agree with you; this approach, using the clone() fuction, below is horrifying.
// However, we needed to implement this workaround to solve the singleton Media PluginState.
// The entire PluginInjectionAPI relies on the following axiom: "A PluginState that reflects a new EditorState.". We can not have the mutable singleton instance for all EditorState.
// Unfortunately, we can't implement a proper fix for this media state situation. So, we are faking a new object using a Proxy instance.
var nextPluginState = pluginState;
if (isResizing !== undefined) {
pluginState.setIsResizing(isResizing);
nextPluginState = nextPluginState.clone();
}
if (mediaProvider) {
pluginState.setMediaProvider(mediaProvider);
}
if (resizingWidth) {
pluginState.setResizingWidth(resizingWidth);
nextPluginState = nextPluginState.clone();
}
// remap editing media single position if we're in collab
if (typeof pluginState.editingMediaSinglePos === 'number') {
pluginState.editingMediaSinglePos = tr.mapping.map(pluginState.editingMediaSinglePos);
nextPluginState = nextPluginState.clone();
}
var meta = tr.getMeta(stateKey);
if (meta) {
var allowsUploads = meta.allowsUploads;
pluginState.updateAndDispatch({
allowsUploads: typeof allowsUploads === 'undefined' ? pluginState.allowsUploads : allowsUploads
});
nextPluginState = nextPluginState.clone();
}
// ACTIONS
switch (meta === null || meta === void 0 ? void 0 : meta.type) {
case ACTIONS.SHOW_MEDIA_VIEWER:
pluginState.mediaViewerSelectedMedia = meta.mediaViewerSelectedMedia;
pluginState.isMediaViewerVisible = meta.isMediaViewerVisible;
nextPluginState = nextPluginState.clone();
break;
case ACTIONS.HIDE_MEDIA_VIEWER:
pluginState.mediaViewerSelectedMedia = undefined;
pluginState.isMediaViewerVisible = meta.isMediaViewerVisible;
nextPluginState = nextPluginState.clone();
break;
case ACTIONS.TRACK_MEDIA_PASTE:
var identifier = meta.identifier;
var isIdentifierInEditorScope = pluginState.isIdentifierInEditorScope(identifier);
if (!isIdentifierInEditorScope && isFileIdentifier(identifier)) {
pluginState.trackOutOfScopeIdentifier(identifier);
nextPluginState = pluginState.clone();
}
break;
}
// NOTE: We're not calling passing new state to the Editor, because we depend on the view.state reference
// throughout the lifetime of view. We injected the view into the plugin state, because we dispatch()
// transformations from within the plugin state (i.e. when adding a new file).
return nextPluginState;
}
},
appendTransaction: function appendTransaction(transactions, _oldState, newState) {
var _iterator = _createForOfIteratorHelper(transactions),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var transaction = _step.value;
var isSelectionOnMediaInsideMediaSingle = transaction.selectionSet && isNodeSelection(transaction.selection) && transaction.selection.node.type === newState.schema.nodes.media && transaction.selection.$anchor.parent.type === newState.schema.nodes.mediaSingle;
// Note: this causes an additional transaction when selecting a media node
// through clicking on it with the cursor.
if (isSelectionOnMediaInsideMediaSingle) {
// If a selection has been placed on a media inside a media single,
// we shift it to the media single parent as other code is opinionated about
// the selection landing there. In particular the caption insertion and selection
// action.
return newState.tr.setSelection(NodeSelection.create(newState.doc, transaction.selection.$from.pos - 1));
}
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
return;
},
key: stateKey,
view: function view(_view) {
var pluginState = getMediaPluginState(_view.state);
pluginState.setView(_view);
pluginState.updateElement();
return {
update: function update() {
pluginState.updateElement();
}
};
},
props: {
decorations: function decorations(state) {
// Use this to indicate that the media node is selected
var mediaNodes = [];
var schema = state.schema,
$anchor = state.selection.$anchor,
doc = state.doc;
// Find any media nodes in the current selection
if (state.selection instanceof TextSelection || state.selection instanceof AllSelection || state.selection instanceof CellSelection) {
doc.nodesBetween(state.selection.from, state.selection.to, function (node, pos) {
if (node.type === schema.nodes.media) {
mediaNodes.push(Decoration.node(pos, pos + node.nodeSize, {}, {
type: 'media',
selected: true
}));
return false;
}
return true;
});
} else if (state.selection instanceof NodeSelection) {
var _state$selection = state.selection,
node = _state$selection.node,
$from = _state$selection.$from;
if (node.type === schema.nodes.mediaSingle || node.type === schema.nodes.mediaGroup) {
doc.nodesBetween($from.pos, $from.pos + node.nodeSize, function (mediaNode, mediaPos) {
if (mediaNode.type === schema.nodes.media) {
mediaNodes.push(Decoration.node(mediaPos, mediaPos + mediaNode.nodeSize, {}, {
type: 'media',
selected: true
}));
return false;
}
return true;
});
}
}
var pluginState = getMediaPluginState(state);
if (!pluginState.showDropzone) {
return DecorationSet.create(state.doc, mediaNodes);
}
// When a media is already selected
if (state.selection instanceof NodeSelection) {
var _node = state.selection.node;
if (_node.type === schema.nodes.mediaSingle) {
var deco = Decoration.node(state.selection.from, state.selection.to, {
class: 'richMedia-selected'
});
return DecorationSet.create(state.doc, [deco].concat(mediaNodes));
}
return DecorationSet.create(state.doc, mediaNodes);
}
var pos = $anchor.pos;
if ($anchor.parent.type !== schema.nodes.paragraph && $anchor.parent.type !== schema.nodes.codeBlock) {
pos = insertPoint(state.doc, pos, schema.nodes.mediaGroup);
}
if (pos === null || pos === undefined) {
return DecorationSet.create(state.doc, mediaNodes);
}
// eslint-disable-next-line @atlaskit/platform/prefer-crypto-random-uuid -- Use crypto.randomUUID instead
var dropPlaceholderKey = uuid();
var dropPlaceholders = [Decoration.widget(pos, function () {
return createDropPlaceholder(intl, nodeViewPortalProviderAPI, dropPlaceholderKey, mediaOptions && mediaOptions.allowDropzoneDropLine);
}, {
key: 'drop-placeholder',
destroy: function destroy(elem) {
if (elem instanceof HTMLElement) {
nodeViewPortalProviderAPI.remove(dropPlaceholderKey);
}
}
})].concat(mediaNodes);
return DecorationSet.create(state.doc, dropPlaceholders);
},
nodeViews: options.nodeViews,
handleTextInput: function handleTextInput(view, from, to, text) {
var selection = view.state.selection;
if (text === ' ' && selection instanceof NodeSelection && selection.node.type.name === 'mediaSingle') {
var _stateKey$getState;
var videoControlsWrapperRef = (_stateKey$getState = stateKey.getState(view.state)) === null || _stateKey$getState === void 0 ? void 0 : _stateKey$getState.element;
var videoControls = videoControlsWrapperRef === null || videoControlsWrapperRef === void 0 ? void 0 : videoControlsWrapperRef.querySelectorAll('button, [tabindex]:not([tabindex="-1"])');
if (videoControls) {
var isVideoControl = Array.from(videoControls).some(function (videoControl) {
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
return document.activeElement === videoControl;
});
if (isVideoControl) {
return true;
}
}
}
getMediaPluginState(view.state).splitMediaGroup();
return false;
},
handleClick: function handleClick(_editorView, _pos, event) {
var _event$target;
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
var clickedInsideCaptionPlaceholder = (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest("[data-id=\"".concat(CAPTION_PLACEHOLDER_ID, "\"]"));
var browser = getBrowserInfo();
// Workaround for Chrome given a regression introduced in prosemirror-view@1.18.6
// Returning true prevents that updateSelection() is getting called in the commit below:
// @see https://github.com/ProseMirror/prosemirror-view/compare/1.18.5...1.18.6
if ((browser.chrome || browser.safari) && clickedInsideCaptionPlaceholder) {
return true;
}
// Workaound for iOS 16 Caption selection issue
// @see https://product-fabric.atlassian.net/browse/MEX-2012
if (browser.ios) {
var _event$target2;
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
return !!((_event$target2 = event.target) !== null && _event$target2 !== void 0 && _event$target2.closest("[class=\"".concat(MEDIA_CONTENT_WRAP_CLASS_NAME, "\"]")));
}
return false;
},
handleDoubleClickOn: function handleDoubleClickOn(view) {
var _pluginState$mediaOpt, _pluginInjectionApi$e;
// Check if media view