@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
189 lines (186 loc) • 6.48 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 { isMediaBlobUrl } from '@atlaskit/media-client';
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;
};
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;
};