@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
1,368 lines • 65.1 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
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 { v4 as uuid } from 'uuid';
import { SetAttrsStep } from '@atlaskit/adf-schema/steps/set-attrs';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics/types/enums';
import { getBrowserInfo } from '@atlaskit/editor-common/browser';
import { ErrorReporter } from '@atlaskit/editor-common/error-reporter';
import { mediaInlineImagesEnabled } from '@atlaskit/editor-common/media-inline/utils';
import { CAPTION_PLACEHOLDER_ID } from '@atlaskit/editor-common/media-single/constants';
import { getMaxWidthForNestedNodeNext } from '@atlaskit/editor-common/media-single/utils';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
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/is-file-identifier';
import { getMediaFeatureFlag } from '@atlaskit/media-common';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { createMediaNodeUpdater } from '../nodeviews/mediaNodeUpdater';
// 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 { isRemixGeneratedMedia } from '../pm-plugins/utils/remix-media';
import DropPlaceholder from '../ui/Media/DropPlaceholder';
import { MediaDropIndicator } from '../ui/Media/MediaDropIndicator';
import { ACTIONS } from './actions';
import { isMediaDropPositionEnabled } from './media-drop-position-experiment';
import { findMediaDropPosition, findMediaDropReplacement, MediaDropTargets, mediaDropSelection } from './media-drop-target';
import { MediaTaskManager } from './mediaTaskManager';
import PickerFacade from './picker-facade';
import { stateKey } from './plugin-key';
import { reportMediaViewerItem } from './utils/report-media-viewer-item';
export const MEDIA_CONTENT_WRAP_CLASS_NAME = 'media-content-wrap';
export const MEDIA_PLUGIN_IS_RESIZING_KEY = 'mediaSinglePlugin.isResizing';
export const MEDIA_PLUGIN_RESIZING_WIDTH_KEY = 'mediaSinglePlugin.resizing-width';
const createDropPlaceholder = (intl, nodeViewPortalProviderAPI, dropPlaceholderKey, allowDropLine, inline) => {
const usePointerIndicator = isMediaDropPositionEnabled();
// eslint-disable-next-line @atlaskit/platform/no-direct-document-usage
const dropPlaceholder = document.createElement(usePointerIndicator ? 'span' : 'div');
const createElement = React.createElement;
if (usePointerIndicator) {
dropPlaceholder.dataset.mediaDropPlaceholder = 'true';
dropPlaceholder.style.display = 'contents';
nodeViewPortalProviderAPI.render(() => createElement(MediaDropIndicator, {
inline: inline !== null && inline !== void 0 ? inline : false
}), dropPlaceholder, dropPlaceholderKey);
return dropPlaceholder;
}
if (allowDropLine) {
nodeViewPortalProviderAPI.render(() => createElement(RawIntlProvider, {
value: intl
}, createElement(DropPlaceholder, {
type: 'single'
})), dropPlaceholder, dropPlaceholderKey);
} else {
nodeViewPortalProviderAPI.render(() => createElement(RawIntlProvider, {
value: intl
}, createElement(DropPlaceholder)), dropPlaceholder, dropPlaceholderKey);
}
return dropPlaceholder;
};
const MEDIA_RESOLVED_STATES = ['ready', 'error', 'cancelled'];
export class MediaPluginStateImplementation {
/**
* Pending and hovered drop targets, allocated on first use and only for the test cohort, so
* the control cohort runs none of this code and retains none of its state.
*/
get dropTargets() {
if (!isMediaDropPositionEnabled()) {
return undefined;
}
this.mediaDropTargets ??= new MediaDropTargets();
return this.mediaDropTargets;
}
constructor(_state, options, mediaOptions, _dispatch, pluginInjectionApi) {
var _this$mediaOptions2, _this$mediaOptions3;
_defineProperty(this, "allowsUploads", false);
_defineProperty(this, "uploadStatus", 'pending');
_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", () => {});
_defineProperty(this, "onPopupToggleCallback", () => {});
// When non-null, holds the file ID of the media node being replaced. Used both as a
// flag (non-null = replace mode) and as a cross-check to ensure the correct node is
// updated if the selection moves between the picker opening and the file being picked.
_defineProperty(this, "replaceMediaFileId", null);
// The display height (in pixels) of the mediaSingle being replaced, computed at replace time
// from its display width and the old media node's intrinsic aspect ratio.
// Used by the nodeview to recompute display width from the new file's aspect ratio after
// dimensions are fetched, preserving visual height rather than visual width.
_defineProperty(this, "replaceMediaTargetDisplayHeight", null);
_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", () => this.options);
_defineProperty(this, "isMediaSchemaNode", ({
type
}) => {
var _this$mediaOptions;
const {
mediaInline,
mediaSingle,
media
} = this.view.state.schema.nodes;
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", (id, selectionPosition) => {
this.lastAddedMediaSingleFileIds.unshift({
id,
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", (mediaState, onMediaStateChanged, pickerType, insertMediaVia) => {
var _mediaState$fileMimeT, _this$pluginInjection, _this$pluginInjection2, _mediaState$collectio, _this$pluginInjection6, _this$pluginInjection7, _this$dropTargets3;
const dropTargets = this.dropTargets;
if (dropTargets && this.destroyed) {
return;
}
const dropPos = dropTargets === null || dropTargets === void 0 ? void 0 : dropTargets.positionFor(mediaState.id);
const replacement = dropPos !== undefined && (_mediaState$fileMimeT = mediaState.fileMimeType) !== null && _mediaState$fileMimeT !== void 0 && _mediaState$fileMimeT.startsWith('image/') ? dropTargets === null || dropTargets === void 0 ? void 0 : dropTargets.replacementFor(mediaState.id, this.view.state.doc) : undefined;
if (dropPos !== undefined) {
this.replaceMediaFileId = null;
this.replaceMediaTargetDisplayHeight = null;
const selection = replacement ? NodeSelection.create(this.view.state.doc, replacement.pos) : mediaDropSelection(this.view.state.doc, dropPos);
this.view.dispatch(this.view.state.tr.setSelection(selection));
}
const {
state
} = this.view;
const editorAnalyticsAPI = (_this$pluginInjection = this.pluginInjectionApi) === null || _this$pluginInjection === void 0 ? void 0 : (_this$pluginInjection2 = _this$pluginInjection.analytics) === null || _this$pluginInjection2 === void 0 ? void 0 : _this$pluginInjection2.actions;
const mediaStateWithContext = {
...mediaState,
contextId: this.contextIdentifierProvider ? this.contextIdentifierProvider.objectId : undefined
};
const collection = (_mediaState$collectio = mediaState.collection) !== null && _mediaState$collectio !== void 0 ? _mediaState$collectio : this.collectionFromProvider();
if (collection === undefined) {
var _this$dropTargets;
(_this$dropTargets = this.dropTargets) === null || _this$dropTargets === void 0 ? void 0 : _this$dropTargets.inserted(mediaState.id);
return;
}
// If replace mode was set but the selection has moved away from a mediaSingle
// (e.g. the user cancelled the replace picker and then inserted media elsewhere),
// clear replace state so this insertion proceeds as a normal insert.
if (this.replaceMediaFileId !== null) {
var _state$selection$node;
const {
mediaSingle
} = state.schema.nodes;
const isStillOnTargetMedia = state.selection instanceof NodeSelection && state.selection.node.type === mediaSingle && ((_state$selection$node = state.selection.node.firstChild) === null || _state$selection$node === void 0 ? void 0 : _state$selection$node.attrs.id) === this.replaceMediaFileId;
if (!isStillOnTargetMedia) {
this.replaceMediaFileId = null;
this.replaceMediaTargetDisplayHeight = null;
}
}
// 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(fn => fn(true));
this.uploadInProgressSubscriptionsNotified = true;
}
// Replace mode: if a media node is being replaced, update its attrs in-place
// rather than inserting a new node. This preserves layout, width, and caption.
if (this.replaceMediaFileId !== null || replacement !== undefined) {
var _mediaState$fileMimeT2, _mediaState$fileName, _mediaState$fileSize;
// Clear replace mode immediately so subsequent insertions behave normally
this.replaceMediaFileId = null;
const {
state: currentState
} = this.view;
const mediaSinglePos = currentState.selection.from;
const mediaPos = mediaSinglePos + 1;
const mediaNode = currentState.doc.nodeAt(mediaPos);
if (!mediaNode || mediaNode.type.name !== 'media') {
return;
}
// Alt text that Rovo Remix generated describes the image being replaced, so clear it.
// Alt text on other images is kept. The experiment is only checked when there is
// Remix alt text to clear.
const shouldClearRemixAlt = Boolean(mediaNode.attrs.alt) && isRemixGeneratedMedia(mediaNode) && isExperimentEnabled('ccaig_clear-remix-alt-on-replace');
// Build a single transaction that:
// 1. Updates the media child node attrs (new file identity + cleared dimensions)
// 2. Re-creates the NodeSelection so the toolbar picks up the fresh node
const tr = currentState.tr;
tr.step(new SetAttrsStep(mediaPos, {
id: mediaState.id,
collection,
type: 'file',
__fileMimeType: (_mediaState$fileMimeT2 = mediaState.fileMimeType) !== null && _mediaState$fileMimeT2 !== void 0 ? _mediaState$fileMimeT2 : null,
__fileName: (_mediaState$fileName = mediaState.fileName) !== null && _mediaState$fileName !== void 0 ? _mediaState$fileName : null,
__fileSize: (_mediaState$fileSize = mediaState.fileSize) !== null && _mediaState$fileSize !== void 0 ? _mediaState$fileSize : null,
__mediaTraceId: null,
// Clear intrinsic dimensions — they'll be fetched once the file
// is processed and applied via updateDimensions in a single tx
// with the height-preserving mediaSingle width adjustment.
width: null,
height: null,
...(shouldClearRemixAlt ? {
alt: ''
} : {})
}));
// Re-create the selection so the floating toolbar picks up
// the updated node and renders the full set of controls.
tr.setSelection(NodeSelection.create(tr.doc, mediaSinglePos));
tr.setMeta('scrollIntoView', false);
this.view.dispatch(tr);
// Still register the state-change listener so upload completion is tracked
onMediaStateChanged(this.handleMediaState);
const isEndState = state => state.status && MEDIA_RESOLVED_STATES.includes(state.status);
// After the file finishes uploading/processing, trigger a dimension fetch.
// getRemoteDimensions may fail if called too early (isImageRepresentationReady
// returns false while processing), so we wait for the ready state first.
const triggerDimensionFetch = () => {
// Find the media node in the doc by id and create a temporary
// MediaNodeUpdater to fetch and apply dimensions
const {
state: editorState
} = this.view;
const {
mediaSingle: mediaSingleType
} = editorState.schema.nodes;
let mediaChildNode = null;
editorState.doc.descendants(node => {
if (mediaChildNode) {
return false;
}
if (node.type === mediaSingleType) {
const child = node.firstChild;
if (child && child.attrs.id === mediaState.id) {
mediaChildNode = child;
}
}
return true;
});
if (mediaChildNode) {
var _this$pluginInjection3, _this$pluginInjection4, _this$pluginInjection5;
const updater = createMediaNodeUpdater({
view: this.view,
mediaProvider: this.mediaProvider ? Promise.resolve(this.mediaProvider) : undefined,
contextIdentifierProvider: this.contextIdentifierProvider ? Promise.resolve(this.contextIdentifierProvider) : undefined,
node: mediaChildNode,
isMediaSingle: true,
lineLength: (_this$pluginInjection3 = this.pluginInjectionApi) === null || _this$pluginInjection3 === void 0 ? void 0 : (_this$pluginInjection4 = _this$pluginInjection3.width) === null || _this$pluginInjection4 === void 0 ? void 0 : (_this$pluginInjection5 = _this$pluginInjection4.sharedState.currentState()) === null || _this$pluginInjection5 === void 0 ? void 0 : _this$pluginInjection5.lineLength
});
updater.getRemoteDimensions().then(dims => {
if (dims) {
updater.updateDimensions(dims);
}
}).catch(() => {
// Silently ignore — if dimensions can't be fetched (e.g. network error),
// the image will render at its current size without the height-preserving
// width adjustment. This is an acceptable degraded experience.
});
}
};
if (!isEndState(mediaStateWithContext)) {
const uploadingPromise = new Promise(resolve => {
onMediaStateChanged(newState => {
if (isEndState(newState)) {
resolve(newState);
}
});
});
this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(() => {
this.updateAndDispatch({
allUploadsFinished: true
});
triggerDimensionFetch();
});
} else {
// File is already in a resolved state — fetch dimensions immediately
triggerDimensionFetch();
}
if (dropPos !== undefined) {
var _this$dropTargets2;
(_this$dropTargets2 = this.dropTargets) === null || _this$dropTargets2 === void 0 ? void 0 : _this$dropTargets2.inserted(mediaState.id);
}
const {
view
} = this;
if (!view.hasFocus()) {
view.focus();
}
return;
}
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
const widthPluginState = (_this$pluginInjection6 = this.pluginInjectionApi) === null || _this$pluginInjection6 === void 0 ? void 0 : (_this$pluginInjection7 = _this$pluginInjection6.width) === null || _this$pluginInjection7 === void 0 ? void 0 : _this$pluginInjection7.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, dropPos);
break;
}
(_this$dropTargets3 = this.dropTargets) === null || _this$dropTargets3 === void 0 ? void 0 : _this$dropTargets3.inserted(mediaState.id);
// do events when media state changes
onMediaStateChanged(this.handleMediaState);
// handle waiting for upload complete
const isEndState = state => state.status && MEDIA_RESOLVED_STATES.indexOf(state.status) !== -1;
if (!isEndState(mediaStateWithContext)) {
const uploadingPromise = new Promise(resolve => {
onMediaStateChanged(newState => {
// When media item reaches its final state, remove listener and resolve
if (isEndState(newState)) {
resolve(newState);
}
});
});
this.taskManager.addPendingTask(uploadingPromise, mediaStateWithContext.id).then(() => {
this.updateAndDispatch({
allUploadsFinished: true
});
});
}
// refocus the view
const {
view
} = this;
if (!view.hasFocus()) {
view.focus();
}
this.waitForPendingTasks().then(() => {
if (this.uploadInProgressSubscriptions.length > 0 && this.uploadInProgressSubscriptionsNotified) {
this.uploadInProgressSubscriptions.forEach(fn => fn(false));
this.uploadInProgressSubscriptionsNotified = false;
}
});
this.selectLastAddedMediaNode();
});
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_defineProperty(this, "addPendingTask", task => {
this.taskManager.addPendingTask(task);
});
_defineProperty(this, "splitMediaGroup", () => splitMediaGroup(this.view));
_defineProperty(this, "onPopupPickerClose", () => {
this.onPopupToggleCallback(false);
});
_defineProperty(this, "showMediaPicker", () => {
if (this.openMediaPickerBrowser) {
return this.openMediaPickerBrowser();
}
this.onPopupToggleCallback(true);
});
/**
* Opens the media picker in "replace" mode. The next file selected/uploaded
* will replace the currently selected mediaSingle node's media child in-place,
* preserving layout, width, and caption.
*
* The display height is computed and stored so that after the new file's intrinsic
* dimensions are fetched, the mediaSingle display width can be adjusted to maintain
* visual height stability rather than width stability.
*/
_defineProperty(this, "showMediaPickerForReplace", () => {
var _this$pluginInjection8, _this$pluginInjection9, _this$pluginInjection0, _this$pluginInjection1;
const {
state
} = this.view;
const {
mediaSingle
} = state.schema.nodes;
const {
selection
} = state;
// Only activate replace mode when a mediaSingle is selected
if (!(selection instanceof NodeSelection) || selection.node.type !== mediaSingle) {
return;
}
const mediaSingleNode = selection.node;
const mediaNode = mediaSingleNode.firstChild;
if (!mediaNode) {
return;
}
// Store the current media node's id so insertFile can identify and replace it
this.replaceMediaFileId = mediaNode.attrs.id;
// Compute and store the current display height so we can preserve it after
// the new file's intrinsic dimensions are known.
// displayHeight = displayWidth * (intrinsicHeight / intrinsicWidth)
const widthAttr = mediaSingleNode.attrs.width;
const widthType = mediaSingleNode.attrs.widthType;
const intrinsicWidth = mediaNode.attrs.width;
const intrinsicHeight = mediaNode.attrs.height;
// Resolve actual pixel display width from mediaSingle attrs.
const lineLength = (_this$pluginInjection8 = (_this$pluginInjection9 = this.pluginInjectionApi) === null || _this$pluginInjection9 === void 0 ? void 0 : (_this$pluginInjection0 = _this$pluginInjection9.width) === null || _this$pluginInjection0 === void 0 ? void 0 : (_this$pluginInjection1 = _this$pluginInjection0.sharedState.currentState()) === null || _this$pluginInjection1 === void 0 ? void 0 : _this$pluginInjection1.lineLength) !== null && _this$pluginInjection8 !== void 0 ? _this$pluginInjection8 : 760;
let displayWidth = null;
if (widthAttr && widthType === 'pixel') {
displayWidth = widthAttr;
} else if (widthAttr) {
// Default widthType is 'percentage' — convert to pixels
displayWidth = widthAttr / 100 * lineLength;
} else if (intrinsicWidth) {
// No width set at all (never resized) — fall back to intrinsic width
displayWidth = intrinsicWidth;
}
if (displayWidth && intrinsicWidth && intrinsicHeight && intrinsicWidth > 0) {
this.replaceMediaTargetDisplayHeight = displayWidth * (intrinsicHeight / intrinsicWidth);
} else {
// Can't compute display height — fall back to preserving width
this.replaceMediaTargetDisplayHeight = null;
}
// Finally, show the media picker
this.showMediaPicker();
});
_defineProperty(this, "setBrowseFn", browseFn => {
this.openMediaPickerBrowser = browseFn;
});
_defineProperty(this, "onPopupToggle", 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", (node, getPos) => {
let getNode = node;
if (!getNode) {
const pos = getPos();
if (typeof pos !== 'number') {
return;
}
getNode = this.view.state.doc.nodeAt(pos);
}
removeMediaNode(this.view, getNode, getPos);
});
_defineProperty(this, "getIdentifierKey", identifier => {
if (identifier.mediaItemType === 'file') {
return identifier.id;
} else {
return identifier.dataURI;
}
});
_defineProperty(this, "trackMediaNodeAddition", node => {
var _this$identifierCount;
const identifier = getIdentifier(node.attrs);
const key = this.getIdentifierKey(identifier);
const {
count
} = (_this$identifierCount = this.identifierCount.get(key)) !== null && _this$identifierCount !== void 0 ? _this$identifierCount : {
count: 0
};
if (count === 0) {
this.taskManager.resumePendingTask(key);
}
this.identifierCount.set(key, {
identifier,
count: count + 1
});
});
_defineProperty(this, "trackMediaNodeRemoval", node => {
var _this$identifierCount2;
const identifier = getIdentifier(node.attrs);
const key = this.getIdentifierKey(identifier);
const {
count
} = (_this$identifierCount2 = this.identifierCount.get(key)) !== null && _this$identifierCount2 !== void 0 ? _this$identifierCount2 : {
count: 0
};
if (count === 1) {
this.taskManager.cancelPendingTask(key);
}
this.identifierCount.set(key, {
identifier,
count: count - 1
});
});
_defineProperty(this, "isIdentifierInEditorScope", identifier => {
const 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", identifier => {
const key = this.getIdentifierKey(identifier);
this.outOfEditorScopeIdentifierMap.set(key, {
identifier
});
});
/**
* Called from React UI Component on componentDidMount
*/
_defineProperty(this, "handleMediaNodeMount", (node, getPos) => {
this.trackMediaNodeAddition(node);
this.mediaNodes.unshift({
node,
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", oldNode => {
this.trackMediaNodeRemoval(oldNode);
this.mediaNodes = this.mediaNodes.filter(({
node
}) => oldNode !== node);
});
_defineProperty(this, "handleMediaGroupUpdate", (oldNodes, newNodes) => {
const addedNodes = newNodes.filter(node => oldNodes.every(oldNode => oldNode.attrs.id !== node.attrs.id));
const removedNodes = oldNodes.filter(node => newNodes.every(newNode => newNode.attrs.id !== node.attrs.id));
addedNodes.forEach(node => {
this.trackMediaNodeAddition(node);
});
removedNodes.forEach(oldNode => {
this.trackMediaNodeRemoval(oldNode);
});
});
_defineProperty(this, "findMediaNode", id => {
return helpers.findMediaNode(this, id);
});
_defineProperty(this, "destroyAllPickers", pickers => {
pickers.forEach(picker => picker.destroy());
this.pickers.splice(0, this.pickers.length);
});
_defineProperty(this, "destroyPickers", () => {
const {
pickers,
pickerPromises
} = this;
// 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(resolvedPickers => this.destroyAllPickers(resolvedPickers));
}
this.customPicker = undefined;
});
_defineProperty(this, "getInputMethod", 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", (id, attrs) => {
const {
view
} = this;
if (!view) {
return;
}
return updateMediaNodeAttrs(id, attrs)(view.state, view.dispatch);
});
_defineProperty(this, "handleMediaState", state => {
switch (state.status) {
case 'error':
const {
uploadErrorHandler
} = this.options;
if (uploadErrorHandler) {
uploadErrorHandler(state);
}
break;
}
});
_defineProperty(this, "removeSelectedMediaContainer", () => {
const {
view
} = this;
const selectedNode = this.selectedMediaContainerNode();
if (!selectedNode) {
return false;
}
const {
from
} = view.state.selection;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
removeMediaNode(view, selectedNode.firstChild, () => from + 1);
return true;
});
_defineProperty(this, "selectedMediaContainerNode", () => {
var _this$view, _this$view$state;
const selection = (_this$view = this.view) === null || _this$view === void 0 ? void 0 : (_this$view$state = _this$view.state) === null || _this$view$state === void 0 ? void 0 : _this$view$state.selection;
if (selection instanceof NodeSelection && this.isMediaSchemaNode(selection.node)) {
return selection.node;
}
return;
});
_defineProperty(this, "getDropTarget", event => {
var _this$dropTargets4;
if (!event.dataTransfer || !this.allowsUploads || this.destroyed) {
return;
}
const hover = (_this$dropTargets4 = this.dropTargets) === null || _this$dropTargets4 === void 0 ? void 0 : _this$dropTargets4.hover;
if (event.target instanceof Element && event.target.closest('[data-media-drop-placeholder]') && hover !== undefined) {
return {
pos: hover
};
}
const files = Array.from(event.dataTransfer.files);
const fileTypes = files.length ? files.map(file => file.type) : Array.from(event.dataTransfer.items || []).filter(item => item.kind === 'file').map(item => item.type);
const replacement = findMediaDropReplacement(this.view, event, fileTypes);
if (replacement) {
var _node$nodeSize;
const node = this.view.state.doc.nodeAt(replacement.pos);
return {
pos: replacement.pos + ((_node$nodeSize = node === null || node === void 0 ? void 0 : node.nodeSize) !== null && _node$nodeSize !== void 0 ? _node$nodeSize : 0),
replacement
};
}
const pos = findMediaDropPosition(this.view, {
left: event.clientX,
top: event.clientY
}, fileTypes.length ? fileTypes : [''], this.mediaOptions);
return pos === undefined ? undefined : {
pos
};
});
_defineProperty(this, "handleDragOver", event => {
var _dropTargets$hoverRep, _target$replacement, _dropTargets$hoverRep2, _target$replacement2;
const dropTargets = this.dropTargets;
if (!dropTargets) {
return;
}
const target = this.getDropTarget(event);
const isActive = target !== undefined;
if (dropTargets.hover === (target === null || target === void 0 ? void 0 : target.pos) && this.showDropzone === isActive && ((_dropTargets$hoverRep = dropTargets.hoverReplacement) === null || _dropTargets$hoverRep === void 0 ? void 0 : _dropTargets$hoverRep.pos) === (target === null || target === void 0 ? void 0 : (_target$replacement = target.replacement) === null || _target$replacement === void 0 ? void 0 : _target$replacement.pos) && ((_dropTargets$hoverRep2 = dropTargets.hoverReplacement) === null || _dropTargets$hoverRep2 === void 0 ? void 0 : _dropTargets$hoverRep2.media) === (target === null || target === void 0 ? void 0 : (_target$replacement2 = target.replacement) === null || _target$replacement2 === void 0 ? void 0 : _target$replacement2.media)) {
return;
}
dropTargets.hover = target === null || target === void 0 ? void 0 : target.pos;
dropTargets.hoverReplacement = target === null || target === void 0 ? void 0 : target.replacement;
this.showDropzone = isActive;
this.view.dispatch(this.view.state.tr);
});
_defineProperty(this, "handleDrop", (event, traceContext) => {
const dropTargets = this.dropTargets;
if (!dropTargets) {
return false;
}
const target = this.getDropTarget(event);
this.handleDrag('leave');
if (!target) {
return false;
}
return dropTargets.start(traceContext.traceId, target.pos, target.replacement);
});
_defineProperty(this, "handleDrag", dragState => {
const dropTargets = this.dropTargets;
if (dropTargets) {
if (dragState === 'enter' || this.destroyed) {
return;
}
if (!this.showDropzone && dropTargets.hover === undefined) {
return;
}
this.showDropzone = false;
dropTargets.clearHover();
this.view.dispatch(this.view.state.tr);
return;
}
const isActive = dragState === 'enter';
if (this.showDropzone === isActive) {
return;
}
this.showDropzone = isActive;
const {
dispatch,
state
} = this.view;
const {
tr,
selection,
doc
} = state;
const {
media,
mediaGroup
} = state.schema.nodes;
const pos = Math.max(0, selection.$from.pos - 1);
const sel = Selection.findFrom(doc.resolve(pos), -1);
if (sel && findSelectedNodeOfType(media)(sel)) {
const parent = findParentNodeOfType(mediaGroup)(sel);
if (parent) {
tr.setSelection(NodeSelection.create(tr.doc, parent.start));
}
}
dispatch(tr);
});
this.options = options;
this.mediaOptions = mediaOptions;
this.dispatch = _dispatch;
this.pluginInjectionApi = pluginInjectionApi;
this.waitForMediaUpload = options.waitForMediaUpload === undefined ? true : options.waitForMediaUpload;
const {
nodes
} = _state.schema;
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.syncProvider);
if (mediaOptions.provider && isExperimentEnabled('platform_editor_ssr_toolbar_optimistic')) {
// The synchronous SSR provider may only support viewing. Keep its view
// config, but wait for the full provider before deciding upload availability.
if (!this.allowsUploads) {
this.uploadStatus = 'pending';
}
this.setMediaProvider(mediaOptions.provider);
}
} else if (mediaOptions !== null && mediaOptions !== void 0 && mediaOptions.provider) {
this.setMediaProvider(mediaOptions === null || mediaOptions === void 0 ? void 0 : mediaOptions.provider);
}
if (mediaInlineImagesEnabled(getMediaFeatureFlag('mediaInline', (_this$mediaOptions2 = this.mediaOptions) === null || _this$mediaOptions2 === void 0 ? void 0 : _this$mediaOptions2.featureFlags), (_this$mediaOptions3 = this.mediaOptions) === null || _this$mediaOptions3 === void 0 ? void 0 : _this$mediaOptions3.allowMediaInlineImages)) {
this.allowInlineImages = true;
}
this.errorReporter = options.errorReporter || new ErrorReporter();
this.singletonCreatedAt = (performance || Date).now();
}
clone() {
var _originalTarget;
const 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(target, prop, receiver) {
if (prop === 'singletonCreatedAt') {
return clonedAt;
}
if (prop === 'originalTarget') {
return target;
}
return Reflect.get(target, prop, receiver);
}
});
}
subscribeToUploadInProgressState(fn) {
this.uploadInProgressSubscriptions.push(fn);
}
unsubscribeFromUploadInProgressState(fn) {
this.uploadInProgressSubscriptions = this.uploadInProgressSubscriptions.filter(subscribedFn => subscribedFn !== fn);
}
async setMediaProvider(mediaProvider) {
// Prevent someone trying to set the exact same provider twice for performance reasons
if (this.previousMediaProvider === mediaProvider && !(isExperimentEnabled('platform_editor_ssr_toolbar_optimistic') && this.uploadStatus === 'pending' && !mediaProvider)) {
return;
}
this.previousMediaProvider = mediaProvider;
if (!mediaProvider) {
this.destroyPickers();
this.allowsUploads = false;
this.uploadStatus = 'unavailable';
if (!this.destroyed && (this.view || !isExperimentEnabled('platform_editor_ssr_toolbar_optimistic'))) {
this.view.dispatch(this.view.state.tr.setMeta(stateKey, {
allowsUploads: this.allowsUploads
}));
}
return;
}
// Ignored via go/ees007
// eslint-disable-next-line @atlaskit/editor/enforce-todo-comment-format
// TODO disable (not destroy!) pickers until mediaProvider is resolved
try {
const resolvedProvider = mediaProvider instanceof Promise ? await mediaProvider : mediaProvider;
if (isExperimentEnabled('platform_editor_ssr_toolbar_optimistic') && (this.destroyed || this.previousMediaProvider !== mediaProvider)) {
return;
}
this.mediaProvider = resolvedProvider;
// 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) {
const 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 - ${this.mediaProvider}`);
} catch (err) {
if (isExperimentEnabled('platform_editor_ssr_toolbar_optimistic') && (this.destroyed || this.previousMediaProvider !== mediaProvider)) {
return;
}
const wrappedError = new Error(`Media functionality disabled due to rejected provider: ${err instanceof Error ? err.message : String(err)}`);
this.errorReporter.captureException(wrappedError);
this.destroyPickers();
this.allowsUploads = false;
this.uploadStatus = 'unavailable';
if (!this.destroyed && (this.view || !isExperimentEnabled('platform_editor_ssr_toolbar_optimistic'))) {
this.view.dispatch(this.view.state.tr.setMeta(stateKey, {
allowsUploads: this.allowsUploads
}));
}
return;
}
this.mediaClientConfig = this.mediaProvider.viewMediaClientConfig;
this.allowsUploads = !!this.mediaProvider.uploadMediaClientConfig;
this.uploadStatus = this.allowsUploads ? 'available' : 'unavailable';
const {
view,
allowsUploads
} = this;
// 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
}));
}
if (this.allowsUploads) {
this.uploadMediaClientConfig = this.mediaProvider.uploadMediaClientConfig;
if (this.mediaProvider.uploadParams && this.uploadMediaClientConfig) {
await this.initPickers(this.mediaProvider.uploadParams, PickerFacade);
} else {
this.destroyPickers();
}
} else {
this.destroyPickers();
}
}
setIsResizing(isResizing) {
this.isResizing = isResizing;
}
setResizingWidth(width) {
this.resizingWidth = width;
}
updateElement() {
let newElement;
const 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;
}
}
getDomElement(domAtPos) {
const {
selection
} = this.view.state;
if (!(selection instanceof NodeSelection)) {
return;
}
if (!this.isMediaSchemaNode(selection.node)) {
return;
}
const node = findDomRefAtPos(selection.from, domAtPos);
if (node) {
if (!node.childNodes.length) {
return node.parentNode;
}
return node;
}
return;
}
get contextIdentifierProvider() {
var _this$pluginInjection10, _this$pluginInjection11, _this$pluginInjection12;
return (_this$pluginInjection10 = this.pluginInjectionApi) === null || _this$pluginInjection10 === void 0 ? void 0 : (_this$pluginInjection11 = _this$pluginInjection10.contextIdentifier) === null || _this$pluginInjection11 === void 0 ? void 0 : (_this$pluginInjection12 = _this$pluginInjection11.sharedState.currentState()) === null || _this$pluginInjection12 === void 0 ? void 0 : _this$pluginInjection12.contextIdentifierProvider;
}
selectLastAddedMediaNode() {
var _this$mediaOptions4;
// if preventAutoFocusOnUpload is enabled, skip auto-selection and just clear the tracking array
if ((_this$mediaOptions4 = this.mediaOptions) !== null && _this$mediaOptions4 !== void 0 && _this$mediaOptions4.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(() => {
const lastTrackedAddedNode = this.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) === this.view.state.selection.from) {
const lastAddedNode = this.mediaNodes.find(node => {
return node.node.attrs.id === lastTrackedAddedNode.id;
});
const lastAddedNodePos = lastAddedNode === null || lastAddedNode === void 0 ? void 0 : lastAddedNode.getPos();
if (lastAddedNodePos) {
const {
dispatch,
state
} = this.view;
const tr = state.tr;
tr.setSelection(NodeSelection.create(tr.doc, lastAddedNodePos));
if (dispatch) {
dispatch(tr);
}
}
}
// reset temp constant after uploads finished
this.lastAddedMediaSingleFileIds = [];
});
}
}
setView(view) {
this.view = view;
if (isExperimentEnabled('platform_editor_ssr_toolbar_optimistic') && this.uploadStatus === 'pending' && !this.previousMediaProvider) {
// No provider was configured for this client editor. SSR has no view and stays pending.
this.uploadStatus = 'unavailable';
}
}
destroy() {
var _this$mediaDropTarget;
if (this.destroyed) {
return;
}
this.destroyed = true;
(_this$mediaDropTarget = this.mediaDropTargets) === null || _this$mediaDropTarget === void 0 ? void 0 : _this$mediaDropTarget.clear();
const {
mediaNodes
} = this;
mediaNodes.splice(0, mediaNodes.length);
this.removeOnCloseListener();
this.destroyPickers();
}
async initPickers(uploadParams, Picker) {
if (this.destroyed || !this.uploadMediaClientConfig) {
return;
}
const {
errorReporter,
pickers,
pickerPromises
} = this;
// create pickers if they don't exist, re-use otherwise
if (!pickers.length) {
const pickerFacadeConfig = {
mediaClientConfig: this.uploadMediaClientConfig,
errorReporter
};
if (this.options.customMediaPicker) {
const customPicker = new Picker('customMediaPicker', pickerFacadeConfig, this.options.customMediaPicker).init();
pickerPromises.push(customPicker);
pickers.push(this.customPicker = await customPicker);
}
pickers.forEach(picker => {
picker.onNewMedia(this.insertFile);
});
}
// set new upload params for the pickers
pickers.forEach(picker => picker.setUploadParams(uploadParams));
}
collectionFromProvider() {
return this.mediaProvider && this.mediaProvider.uploadParams && this.mediaProvider.uploadParams.collection;
}
updateAndDispatch(props) {
// update plugin state
Object.keys(props).forEach(_key => {
const key = _key;
const value = props[key];
if (value !== undefined) {
this[key] = value;
}
});
if (this.dispatch) {
this.dispatch(stateKey, {
...this
});
}
}
}
export const getMediaPluginState = state => stateKey.getState(state);
export const createPlugin = (_schema, options, getIntl, pluginInjectionApi, nodeViewPortalProviderAPI, dispatch, mediaOptions) => {
const intl = getIntl();
return new SafePlugin({
state: {
init(_config, state) {
return new MediaPluginStateImplementation(state, options, mediaOptions, dispatch, pluginInjectionApi);
},
apply(tr, pluginState) {
var _pluginState$dropTarg, _tr$getMeta;
(_pluginState$dropTarg = pluginState.dropTargets) === null || _pluginState$dropTarg === void 0 ? void 0 : _pluginState$dropTarg.map(tr.mapping);
const isResizing = tr.getMeta(MEDIA_PLUGIN_IS_RESIZING_KEY);
const resizingWidth = tr.getMeta(MEDIA_PLUGIN_RESIZING_WIDTH_KEY);
const 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.
let 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();
}
const meta = tr.getMeta(stateKey);
if (meta) {
const {
allowsUploads
} = meta;
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:
const {
identifier
} = meta;
const 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(transactions, _oldState, newState) {
for (const transaction of transactions) {
const 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));
}
}
return;
},
key: stateKey,
view: view => {
const pluginState = getMediaPluginState(view.state);
pluginState.setView(view);
pluginState.updateElement();
// Can't compare against `prevState`: this plugin's state is one shared object, so the previous
// state already reads the new visibility. Remember it here instead.
let wasMediaViewerVisible = !!pluginState.isMediaViewerVisible;
return {
update: () => {
pluginState.updateElement();
const isMediaViewerVisible = !!pluginState.isMediaViewerVisible;
// Report when the viewer's visibility changes: the item it opens on, or null when it
// closes. This runs before the viewer renders, so the consumer already knows the item.
if (isMediaViewerVisible !== wasMediaViewerVisible) {
var _pluginState$mediaOpt, _pluginState$mediaVie;
wasMediaViewerVisible = isMediaViewerVisible;
reportMediaViewerItem((_pluginState$mediaOpt = pluginState.mediaOptions) === null || _pluginState$mediaOpt === void 0 ? void 0 : _pluginState$mediaOpt.mediaViewerExtensions, isMediaViewerVisible ? (_pluginState$mediaVie = pluginState.mediaViewerSelectedMedia) !== null && _pluginState$mediaVie !== void 0 ? _pluginState$mediaVie : null : null);
}
}
};
},
props: {
decorations: state => {
// Use this to indicate that the media node is selected
const mediaNodes = [];
const {
schema,
doc
} = state;
// 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, (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) {
const {
node,
$from
} = state.selection;
if (node.type === schema.nodes.mediaSingle || node.type === schema.nodes.mediaGroup) {
doc.nodesBetween($from.pos, $from.pos + node.nodeSize, (mediaNode, mediaPos) => {
if (mediaNode.type === schema.nodes.media) {
mediaNodes.push(Decoration.node(mediaPos, mediaPos + mediaNode.nodeSize, {}, {
type: 'media',
selected: true
}));
return false;
}
return true;
});
}
}
const pluginState = getMediaPluginState(state);
if (!pluginState.showDropzone) {
return DecorationSet.create(state.doc, mediaNodes);
}
const dropTargets = pluginState.dropTargets;
let pos;
if (dropTargets) {
const replacement = dropTargets.hoverReplacementFor(state.doc);
if (replacement) {
const node = state.doc.nodeAt(replacement.pos);
if (node) {
return DecorationSet.create(state.doc, [Decoration.node(replacement.pos, replacement.pos + node.nodeSize, {
class: 'richMedia-selected'
}), ...mediaNodes]);
}
}
pos = dropTargets.hover;
} else {
if (state.selection instanceof NodeSelection) {
const node = state.selection.node;
if (node.type === schema.nodes.mediaSingle) {
const deco = Decoration.node(state.selection.from, state.selection.to, {
class: 'richMedia-selected'
});
return DecorationSet.create(state.doc, [deco, ...mediaNodes]);
}
return DecorationSet.create(state.doc, mediaNodes);
}
const {
$anchor
} = state.selection;
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
const dropPlaceholderKey = uuid();
const dropPlaceholders = [Decoration.widget(pos, () => {
return createDropPlaceholder(intl, nodeViewPortalProviderAPI, dropPlaceholderKey, mediaOptions && mediaOptions.allowDropzoneDropLine, dropTargets !== undefined && state.doc.resolve(pos).parent.inlineContent);
}, {
key: 'drop-placeholder',
destroy: elem => {
if (elem instanceof HTMLElement) {
nodeViewPortalProviderAPI.remove(dropPlaceholderKey);
}
}
}), ...mediaNodes];
return DecorationSet.create(state.doc, dropPlaceholders);
},
nodeViews: options.nodeViews,
handleTextInput(view, from, to, text) {
const {
selection
} = view.state;
if (text === ' ' && selection instanceof NodeSelection && selection.node.type.name === 'mediaSingle') {
var _stateKey$getState;
const videoControlsWrapperRef = (_stateKey$getState = stateKey.getState(view.state)) === null || _stateKey$getState === void 0 ? void 0 : _stateKey$getState.element;
const videoControls = videoControlsWrapperRef === null || videoControlsWrapperRef === void 0 ? void 0 : videoControlsWrapperRef.querySelectorAll('button, [tabindex]:not([tabindex="-1"])');
if (videoControls) {
const isVideoControl = Array.from(videoControls).some(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: (_editorView, _pos, event) => {
var _event$target;
// Ignored via go/ees005
// eslint-disable-next-line @atlaskit/editor/no-as-casting
const clickedInsideCaptionPlaceholder = (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(`[data-id="${CAPTION_PLACEHOLDER_ID}"]`);
const 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="${MEDIA_CONTENT_WRAP_CLASS_NAME}"]`));
}
return false;
},
handleDoubleClickOn: view => {
var _pluginState$mediaOpt2, _pluginInjectionApi$e, _pluginInjectionApi$e2;
// Check if media viewer is enabled
const pluginState = getMediaPluginState(view.state);
if (!((_pluginState$mediaOpt2 = pluginState.mediaOptions) !== null && _pluginState$mediaOpt2 !== void 0 && _pluginState$mediaOpt2.allowImagePreview)) {
return false;
}
const isLivePagesViewMode = (pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$e = pluginInjectionApi.editorViewMode) === null || _pluginInjectionApi$e === void 0 ? void 0 : (_pluginInjectionApi$e2 = _pluginInjectionApi$e.sharedState.currentState()) === null || _pluginInjectionApi$e2 === void 0 ? void 0 : _pluginInjectionApi$e2.mode) === 'view';
// Double Click support for Media Viewer Nodes
const maybeMediaNode = getMediaFromSupportedMediaNodesFromSelection(view.state);
if (maybeMediaNode) {
var _pluginInjectionApi$a;
// If media type is video, do not open media viewer
if (!isNodeDoubleClickSupportedInLivePagesViewMode(isLivePagesViewMode, maybeMediaNode)) {
return false;
}
// Show media viewer
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.core.actions.execute(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.media.commands.showMediaViewer(maybeMediaNode.attrs));
// Call analytics event
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions.fireAnalyticsEvent({
action: ACTION.OPENED,
actionSubject: ACTION_SUBJECT.MEDIA_VIEWER,
actionSubjectId: ACTION_SUBJECT_ID.MEDIA,
eventType: EVENT_TYPE.UI,
attributes: {
nodeType: maybeMediaNode.type.name,
inputMethod: INPUT_METHOD.DOUBLE_CLICK
}
});
return true;
}
return false;
},
handleDOMEvents: {
keydown: (view, event) => {
const {
selection
} = view.state;
if (selection instanceof NodeSelection && selection.node.type.name === 'mediaSingle') {
// handle keydown events for video controls panel to prevent fire of rest prosemirror listeners;
if ((event === null || event === void 0 ? void 0 : event.target) instanceof HTMLElement) {
const a11yDefaultKeys = ['Tab', 'Space', 'Enter', 'Shift', 'Esc'];
const targetsAndButtons = {
button: a11yDefaultKeys,
range: [...a11yDefaultKeys, 'ArrowDown', 'ArrowUp', 'ArrowLeft', 'ArrowRight'],
combobox: [...a11yDefaultKeys, 'ArrowDown', 'ArrowUp', 'Esc'],
slider: ['Tab', 'Shift', 'ArrowLeft', 'ArrowRight']
};
const targetRole = event.target.role;
const targetType = event.target.type;
const allowedTargets = targetRole || targetType;
// only if targeting interactive elements fe. button, slider, range, dropdown
if (allowedTargets && allowedTargets in targetsAndButtons) {
const targetRelatedA11YKeys = targetsAndButtons[allowedTargets];
const allowedKeys = new Set(targetRelatedA11YKeys);
if (allowedKeys.has(event.key) || allowedKeys.has(event.code)) {
// allow event to bubble to be handled by react handlers
return true;
} else {
// otherwise focus editor to allow setting gapCursor. (e.g.: arrowRightFromMediaSingle)
view.focus();
}
}
}
}
// fire regular prosemirror listeners;
return false;
}
}
}
});
};