UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

189 lines (186 loc) 6.48 kB
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; };