@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
311 lines (302 loc) • 9.98 kB
JavaScript
import { atTheBeginningOfBlock, atTheBeginningOfDoc, atTheEndOfBlock, endPositionOfParent, GapCursorSelection, startPositionOfParent } from '@atlaskit/editor-common/selection';
import { createNewParagraphBelow, createParagraphNear } from '@atlaskit/editor-common/utils';
import { deleteSelection, splitBlock } from '@atlaskit/editor-prosemirror/commands';
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
import { findPositionOfNodeBefore } from '@atlaskit/editor-prosemirror/utils';
import { isExternalImageIdentifier, isMediaBlobUrl } from '@atlaskit/media-client';
import { isExternalMedia } from '../../ui/toolbar/utils';
import { getMediaPluginState } from '../main';
import { isVideo } from './media-single';
const isTemporary = id => {
return id.indexOf('temporary:') === 0;
};
export const isMediaBlobUrlFromAttrs = attrs => {
return !!(attrs && attrs.type === 'external' && isMediaBlobUrl(attrs.url));
};
export const posOfMediaGroupNearby = state => {
return posOfParentMediaGroup(state) || posOfFollowingMediaGroup(state) || posOfPrecedingMediaGroup(state) || posOfMediaGroupNextToGapCursor(state);
};
export const isSelectionNonMediaBlockNode = state => {
const {
node
} = state.selection;
return node && node.type !== state.schema.nodes.media && node.isBlock;
};
export const isSelectionMediaSingleNode = state => {
const {
node
} = state.selection;
return node && node.type === state.schema.nodes.mediaSingle;
};
const isSelectionMediaInlineNode = state => {
const {
node
} = state.selection;
return node && node.type === state.schema.nodes.mediaInline;
};
export const posOfPrecedingMediaGroup = state => {
if (!atTheBeginningOfBlock(state)) {
return;
}
return posOfMediaGroupAbove(state, state.selection.$from);
};
const posOfMediaGroupNextToGapCursor = state => {
const {
selection
} = state;
if (selection instanceof GapCursorSelection) {
const $pos = state.selection.$from;
const mediaGroupType = state.schema.nodes.mediaGroup;
return posOfImmediatePrecedingMediaGroup($pos, mediaGroupType) || posOfImmediateFollowingMediaGroup($pos, mediaGroupType);
}
};
const posOfImmediatePrecedingMediaGroup = ($pos, mediaGroupType) => {
if ($pos.nodeBefore && $pos.nodeBefore.type === mediaGroupType) {
return $pos.pos - $pos.nodeBefore.nodeSize + 1;
}
};
const posOfImmediateFollowingMediaGroup = ($pos, mediaGroupType) => {
if ($pos.nodeAfter && $pos.nodeAfter.type === mediaGroupType) {
return $pos.pos + 1;
}
};
const posOfFollowingMediaGroup = state => {
if (!atTheEndOfBlock(state)) {
return;
}
return posOfMediaGroupBelow(state, state.selection.$to);
};
const posOfMediaGroupAbove = (state, $pos) => {
let adjacentPos;
let adjacentNode;
if (isSelectionNonMediaBlockNode(state)) {
adjacentPos = $pos.pos;
adjacentNode = $pos.nodeBefore;
} else {
adjacentPos = startPositionOfParent($pos) - 1;
adjacentNode = state.doc.resolve(adjacentPos).nodeBefore;
}
if (adjacentNode && adjacentNode.type === state.schema.nodes.mediaGroup) {
return adjacentPos - adjacentNode.nodeSize + 1;
}
return;
};
/**
* Determine whether the cursor is inside empty paragraph
* or the selection is the entire paragraph
*/
export const isInsidePotentialEmptyParagraph = state => {
const {
$from
} = state.selection;
return $from.parent.type === state.schema.nodes.paragraph && atTheBeginningOfBlock(state) && atTheEndOfBlock(state);
};
const posOfMediaGroupBelow = (state, $pos, prepend = true) => {
let adjacentPos;
let adjacentNode;
if (isSelectionNonMediaBlockNode(state)) {
adjacentPos = $pos.pos;
adjacentNode = $pos.nodeAfter;
} else {
adjacentPos = endPositionOfParent($pos);
adjacentNode = state.doc.nodeAt(adjacentPos);
}
if (adjacentNode && adjacentNode.type === state.schema.nodes.mediaGroup) {
return prepend ? adjacentPos + 1 : adjacentPos + adjacentNode.nodeSize - 1;
}
return;
};
export const posOfParentMediaGroup = (state, $pos, prepend = false) => {
const {
$from
} = state.selection;
$pos = $pos || $from;
if ($pos.parent.type === state.schema.nodes.mediaGroup) {
return prepend ? startPositionOfParent($pos) : endPositionOfParent($pos) - 1;
}
return;
};
export const removeMediaNode = (view, node, getPos) => {
const {
id
} = node.attrs;
const {
state
} = view;
const {
tr,
selection,
doc
} = state;
const currentMediaNodePos = getPos();
if (typeof currentMediaNodePos !== 'number') {
return;
}
tr.deleteRange(currentMediaNodePos, currentMediaNodePos + node.nodeSize);
if (isTemporary(id)) {
tr.setMeta('addToHistory', false);
}
const $currentMediaNodePos = doc.resolve(currentMediaNodePos);
const {
nodeBefore,
parent
} = $currentMediaNodePos;
const isLastMediaNode = $currentMediaNodePos.index() === parent.childCount - 1;
// If deleting a selected media node, we need to tell where the cursor to go next.
// Prosemirror didn't gave us the behaviour of moving left if the media node is not the last one.
// So we handle it ourselves.
if (selection.from === currentMediaNodePos && !isLastMediaNode && !atTheBeginningOfDoc(state) && nodeBefore && nodeBefore.type.name === 'media') {
const nodeBefore = findPositionOfNodeBefore(tr.selection);
if (nodeBefore) {
tr.setSelection(NodeSelection.create(tr.doc, nodeBefore));
}
}
view.dispatch(tr);
};
export const splitMediaGroup = view => {
const {
selection
} = view.state;
// if selection is not a media node, do nothing.
if (!(selection instanceof NodeSelection) || selection.node.type !== view.state.schema.nodes.media) {
return false;
}
deleteSelection(view.state, view.dispatch);
if (selection.$to.nodeAfter) {
splitBlock(view.state, view.dispatch);
createParagraphNear(false)(view.state, view.dispatch);
} else {
createNewParagraphBelow(view.state, view.dispatch);
}
return true;
};
const isOptionalAttr = attr => attr.length > 1 && attr[0] === '_' && attr[1] === '_';
export const copyOptionalAttrsFromMediaState = (mediaState, node) => {
Object.keys(node.attrs).filter(isOptionalAttr).forEach(key => {
const mediaStateKey = key.substring(2);
const attrValue = mediaState[mediaStateKey];
if (attrValue !== undefined) {
// @ts-ignore - [unblock prosemirror bump] assigning to readonly prop
node.attrs[key] = attrValue;
}
});
};
export const getMediaNodeFromSelection = state => {
if (!isSelectionMediaSingleNode(state)) {
return null;
}
const tr = state.tr;
const pos = tr.selection.from + 1;
const mediaNode = tr.doc.nodeAt(pos);
if (mediaNode && mediaNode.type === state.schema.nodes.media) {
return mediaNode;
}
return null;
};
const getMediaInlineNodeFromSelection = state => {
if (!isSelectionMediaInlineNode(state)) {
return null;
}
const tr = state.tr;
const pos = tr.selection.from;
const mediaNode = tr.doc.nodeAt(pos);
return mediaNode;
};
export const isMediaSingleOrInlineNodeSelected = state => {
const {
allowInlineImages
} = getMediaPluginState(state);
return isSelectionMediaSingleNode(state) || allowInlineImages && isSelectionMediaInlineNode(state);
};
export const getMediaSingleOrInlineNodeFromSelection = state => {
const {
allowInlineImages
} = getMediaPluginState(state);
const mediaNode = getMediaNodeFromSelection(state) || allowInlineImages && getMediaInlineNodeFromSelection(state);
return mediaNode || null;
};
export const getMediaFromSupportedMediaNodesFromSelection = state => {
const {
node
} = state.selection;
// Specifically for media supported nodes, double click could have been initiated on the media caption
if (!node) {
return null;
}
switch (node.type) {
case node.type.schema.nodes.media:
case node.type.schema.nodes.mediaInline:
return node;
case node.type.schema.nodes.mediaSingle:
case node.type.schema.nodes.mediaGroup:
return node.firstChild;
default:
return null;
}
};
export const isNodeDoubleClickSupportedInLivePagesViewMode = (isViewMode, node) => {
if (!node) {
return false;
}
// Double Click is not supported for video nodes on both views
if (isVideo(node.attrs.__fileMimeType)) {
return false;
}
// Double click is supported for all editor media nodes
if (!isViewMode) {
return true;
}
// Double Click is not supported for mediaGroup and mediaInline nodes that are file
if (node.type === node.type.schema.nodes.mediaGroup || node.type === node.type.schema.nodes.mediaInline && node.attrs.type === 'file') {
return false;
}
// Double Click supported for all other media nodes
return true;
};
export const getIdentifier = attrs => {
if (isExternalMedia(attrs)) {
return {
mediaItemType: 'external-image',
dataURI: attrs.url
};
} else {
const {
id,
collection = ''
} = attrs;
return {
id,
mediaItemType: 'file',
collectionName: collection
};
}
};
export const extractMediaNodes = doc => {
const mediaNodes = [];
doc.descendants(node => {
if (node.type.name === 'media' || node.type.name === 'mediaInline') {
mediaNodes.push(node);
}
});
return mediaNodes;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const createMediaIdentifierArray = mediaNodes => {
const mediaIdentifierMap = new Map();
mediaNodes.forEach(item => {
const attrs = item.attrs;
if (!attrs) {
return;
}
if (isVideo(attrs.__fileMimeType)) {
return;
}
const identifier = getIdentifier(attrs);
// Add only if not already processed
const idKey = isExternalImageIdentifier(identifier) ? identifier.dataURI : identifier.id;
if (!mediaIdentifierMap.has(idKey)) {
mediaIdentifierMap.set(idKey, identifier);
}
});
return [...mediaIdentifierMap.values()];
};