@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
704 lines (687 loc) • 28.7 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import assert from 'assert';
import React from 'react';
import ReactDOM from 'react-dom';
import { RawIntlProvider } from 'react-intl-next';
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { CAPTION_PLACEHOLDER_ID, getMaxWidthForNestedNodeNext } from '@atlaskit/editor-common/media-single';
import { SafePlugin } from '@atlaskit/editor-common/safe-plugin';
import { browser, 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 { getMediaFeatureFlag } from '@atlaskit/media-common';
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
import * as helpers from '../commands/helpers';
import { updateMediaSingleNodeAttrs } from '../commands/helpers';
import PickerFacade from '../picker-facade';
import DropPlaceholder from '../ui/Media/DropPlaceholder';
import { removeMediaNode, splitMediaGroup } from '../utils/media-common';
import { insertMediaGroupNode, insertMediaInlineNode } from '../utils/media-files';
import { getMediaNodeInsertionType } from '../utils/media-inline';
import { insertMediaSingleNode } from '../utils/media-single';
import { MediaTaskManager } from './mediaTaskManager';
import { stateKey } from './plugin-key';
export { stateKey } from './plugin-key';
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, allowDropLine) => {
const dropPlaceholder = document.createElement('div');
const createElement = React.createElement;
if (allowDropLine) {
ReactDOM.render(createElement(RawIntlProvider, {
value: intl
}, createElement(DropPlaceholder, {
type: 'single'
})), dropPlaceholder);
} else {
ReactDOM.render(createElement(RawIntlProvider, {
value: intl
}, createElement(DropPlaceholder)), dropPlaceholder);
}
return dropPlaceholder;
};
const MEDIA_RESOLVED_STATES = ['ready', 'error', 'cancelled'];
export class MediaPluginStateImplementation {
constructor(_state, options, mediaOptions, newInsertionBehaviour, _dispatch, pluginInjectionApi) {
_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, "destroyed", false);
_defineProperty(this, "removeOnCloseListener", () => {});
_defineProperty(this, "onPopupToggleCallback", () => {});
_defineProperty(this, "nodeCount", new Map());
_defineProperty(this, "taskManager", new MediaTaskManager());
_defineProperty(this, "pickers", []);
_defineProperty(this, "pickerPromises", []);
_defineProperty(this, "onContextIdentifierProvider", async (_name, provider) => {
if (provider) {
this.contextIdentifierProvider = await provider;
}
});
_defineProperty(this, "setMediaProvider", async mediaProvider => {
if (!mediaProvider) {
this.destroyPickers();
this.allowsUploads = false;
if (!this.destroyed) {
this.view.dispatch(this.view.state.tr.setMeta(stateKey, {
allowsUploads: this.allowsUploads
}));
}
return;
}
// TODO disable (not destroy!) pickers until mediaProvider is resolved
try {
this.mediaProvider = await mediaProvider;
// 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) {
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;
if (!this.destroyed) {
this.view.dispatch(this.view.state.tr.setMeta(stateKey, {
allowsUploads: this.allowsUploads
}));
}
return;
}
this.mediaClientConfig = this.mediaProvider.viewMediaClientConfig;
this.allowsUploads = !!this.mediaProvider.uploadMediaClientConfig;
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();
}
});
_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;
});
/**
* 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) => {
var _this$pluginInjection, _this$pluginInjection2, _mediaState$collectio, _this$mediaOptions2, _this$pluginInjection3;
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) {
return;
}
// We need to dispatch the change to event dispatcher only for successful files
if (mediaState.status !== 'error') {
this.updateAndDispatch({
allUploadsFinished: false
});
}
switch (getMediaNodeInsertionType(state, (_this$mediaOptions2 = this.mediaOptions) === null || _this$mediaOptions2 === void 0 ? void 0 : _this$mediaOptions2.featureFlags, mediaStateWithContext.fileMimeType)) {
case 'inline':
insertMediaInlineNode(editorAnalyticsAPI)(this.view, mediaStateWithContext, collection, this.getInputMethod(pickerType));
break;
case 'block':
// read width state right before inserting to get up-to-date and define values
const widthPluginState = (_this$pluginInjection3 = this.pluginInjectionApi) === null || _this$pluginInjection3 === void 0 ? void 0 : _this$pluginInjection3.width.sharedState.currentState();
insertMediaSingleNode(this.view, mediaStateWithContext, this.getInputMethod(pickerType), collection, this.mediaOptions && this.mediaOptions.alignLeftOnInsert, this.newInsertionBehaviour, widthPluginState, editorAnalyticsAPI);
break;
case 'group':
insertMediaGroupNode(editorAnalyticsAPI)(this.view, [mediaStateWithContext], collection, this.getInputMethod(pickerType));
break;
}
// 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();
}
});
_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);
});
_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, "trackMediaNodeAddition", node => {
var _this$nodeCount$get;
const id = node.attrs.id;
const count = (_this$nodeCount$get = this.nodeCount.get(id)) !== null && _this$nodeCount$get !== void 0 ? _this$nodeCount$get : 0;
if (count === 0) {
this.taskManager.resumePendingTask(id);
}
this.nodeCount.set(id, count + 1);
});
_defineProperty(this, "trackMediaNodeRemoval", node => {
var _this$nodeCount$get2;
const id = node.attrs.id;
const count = (_this$nodeCount$get2 = this.nodeCount.get(id)) !== null && _this$nodeCount$get2 !== void 0 ? _this$nodeCount$get2 : 0;
if (count === 1) {
this.taskManager.cancelPendingTask(id);
}
this.nodeCount.set(id, count - 1);
});
/**
* 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.findMediaSingleNode(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 'clipboard':
return INPUT_METHOD.CLIPBOARD;
case 'dropzone':
return INPUT_METHOD.DRAG_AND_DROP;
}
return;
});
_defineProperty(this, "updateMediaSingleNodeAttrs", (id, attrs) => {
const {
view
} = this;
if (!view) {
return;
}
return updateMediaSingleNodeAttrs(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;
}
let {
from
} = view.state.selection;
removeMediaNode(view, selectedNode.firstChild, () => from + 1);
return true;
});
_defineProperty(this, "selectedMediaContainerNode", () => {
const {
selection
} = this.view.state;
if (selection instanceof NodeSelection && this.isMediaSchemaNode(selection.node)) {
return selection.node;
}
return;
});
_defineProperty(this, "handleDrag", dragState => {
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;
// 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.
const sel = Selection.findFrom(doc.resolve(selection.$from.pos - 1), -1);
if (sel && findSelectedNodeOfType(media)(sel)) {
const 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.newInsertionBehaviour = newInsertionBehaviour;
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');
options.providerFactory.subscribe('mediaProvider', (_name, provider) => this.setMediaProvider(provider));
options.providerFactory.subscribe('contextIdentifierProvider', this.onContextIdentifierProvider);
if (getBooleanFF('platform.editor.media.inline-image.base-support')) {
this.allowInlineImages = true;
}
this.errorReporter = options.errorReporter || new ErrorReporter();
this.singletonCreatedAt = (performance || Date).now();
}
clone() {
const clonedAt = (performance || Date).now();
return new Proxy(this, {
get(target, prop, receiver) {
if (prop === 'singletonCreatedAt') {
return clonedAt;
}
return Reflect.get(target, prop, receiver);
}
});
}
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;
}
setView(view) {
this.view = view;
}
destroy() {
if (this.destroyed) {
return;
}
this.destroyed = true;
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, reactContext, getIntl, pluginInjectionApi, dispatch, mediaOptions, newInsertionBehaviour) => {
const intl = getIntl();
const dropPlaceholder = createDropPlaceholder(intl, mediaOptions && mediaOptions.allowDropzoneDropLine);
return new SafePlugin({
state: {
init(_config, state) {
return new MediaPluginStateImplementation(state, options, mediaOptions, newInsertionBehaviour, dispatch, pluginInjectionApi);
},
apply(tr, pluginState) {
const isResizing = tr.getMeta(MEDIA_PLUGIN_IS_RESIZING_KEY);
const resizingWidth = tr.getMeta(MEDIA_PLUGIN_RESIZING_WIDTH_KEY);
// 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 (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();
}
// 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();
return {
update: () => {
pluginState.updateElement();
}
};
},
props: {
decorations: state => {
// Use this to indicate that the media node is selected
const mediaNodes = [];
const {
schema,
selection: {
$anchor
},
doc
} = state;
// Find any media nodes in the current selection
if (state.selection instanceof TextSelection || state.selection instanceof AllSelection || state.selection instanceof NodeSelection || 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;
});
}
const pluginState = getMediaPluginState(state);
if (!pluginState.showDropzone) {
return DecorationSet.create(state.doc, mediaNodes);
}
// When a media is already selected
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);
}
let 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);
}
const dropPlaceholders = [Decoration.widget(pos, dropPlaceholder, {
key: 'drop-placeholder'
}), ...mediaNodes];
return DecorationSet.create(state.doc, dropPlaceholders);
},
nodeViews: options.nodeViews,
handleTextInput(view) {
getMediaPluginState(view.state).splitMediaGroup();
return false;
},
handleClick: (_editorView, _pos, event) => {
var _event$target;
const clickedInsideCaptionPlaceholder = (_event$target = event.target) === null || _event$target === void 0 ? void 0 : _event$target.closest(`[data-id="${CAPTION_PLACEHOLDER_ID}"]`);
// 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;
return !!((_event$target2 = event.target) !== null && _event$target2 !== void 0 && _event$target2.closest(`[class="${MEDIA_CONTENT_WRAP_CLASS_NAME}"]`));
}
return false;
}
}
});
};