UNPKG

@atlaskit/editor-core

Version:

A package contains Atlassian editor core functionality

150 lines (118 loc) 4.06 kB
import { atTheEndOfDoc, atTheEndOfBlock, atTheBeginningOfBlock, endPositionOfParent, startPositionOfParent, setNodeSelection, setTextSelection, } from '../../utils'; import { EditorView, EditorState, Node as PMNode, NodeType, } from '../../prosemirror'; import { MediaState } from '@atlaskit/media-core'; import { posOfPreceedingMediaGroup, posOfMediaGroupNearby, posOfParentMediaGroup, isSelectionNonMediaBlockNode, isInsidePotentialEmptyParagraph, } from './utils'; import { unsupportedNodeTypesForMediaCards } from '../../schema/unsupported'; import analyticsService from '../../analytics/service'; export interface Range { start: number; end: number; } export const insertFile = (view: EditorView, mediaState: MediaState, collection?: string): void => { const { state, dispatch } = view; const { $to } = state.selection; const { tr, schema } = state; const { media, paragraph } = schema.nodes; if (!collection || !media) { return; } // Don't support media in unsupported node types if (unsupportedNodeTypesForMediaCards.has($to.parent.type.name)) { analyticsService.trackEvent('atlassian.editor.media.file.unsupported.node'); return; } const node = createMediaFileNode(mediaState, collection, media); // insert a paragraph after if reach the end of doc // and there is no media group in the front or selection is a non media block node if (atTheEndOfDoc(state) && (!posOfPreceedingMediaGroup(state) || isSelectionNonMediaBlockNode(state))) { const paragraphInsertPos = isSelectionNonMediaBlockNode(state) ? $to.pos : $to.pos + 1; tr.insert(paragraphInsertPos, paragraph.create()); } const mediaInsertPos = findMediaInsertPos(state); // delete the selection or empty paragraph const deleteRange = findDeleteRange(state); if (!deleteRange) { tr.insert(mediaInsertPos, node); } else if (mediaInsertPos <= deleteRange.start) { tr.deleteRange(deleteRange.start, deleteRange.end).insert(mediaInsertPos, node); } else { tr.insert(mediaInsertPos, node).deleteRange(deleteRange.start, deleteRange.end); } dispatch(tr); setSelectionAfterMediaInsertion(view, mediaInsertPos); }; const createMediaFileNode = (mediaState: MediaState, collection: string, media: NodeType): PMNode => { const { id } = mediaState; const node = media.create({ id, type: 'file', collection }); ['fileName', 'fileSize', 'fileMimeType'].forEach(key => { if (mediaState[key]) { node.attrs[`__${key}`] = mediaState[key]; } }); return node; }; const findMediaInsertPos = (state: EditorState<any>): number => { const { $from, $to } = state.selection; const nearbyMediaGroupPos = posOfMediaGroupNearby(state); if (nearbyMediaGroupPos) { return nearbyMediaGroupPos; } if (isSelectionNonMediaBlockNode(state)) { return $to.pos; } if (atTheEndOfBlock(state)) { return $to.pos + 1; } if (atTheBeginningOfBlock(state)) { return $from.pos - 1; } return $to.pos; }; const findDeleteRange = (state: EditorState<any>): Range | undefined => { const { $from, $to } = state.selection; if (posOfParentMediaGroup(state)) { return; } if (!isInsidePotentialEmptyParagraph(state) || posOfMediaGroupNearby(state)) { return range($from.pos, $to.pos); } return range(startPositionOfParent($from) - 1, endPositionOfParent($to)); }; const range = (start: number, end: number = start) => { return { start, end }; }; const setSelectionAfterMediaInsertion = (view: EditorView, insertPos: number): void => { const { state } = view; const { doc } = state; const mediaPos = posOfMediaGroupNearby(state); if (!mediaPos) { return; } const $mediaPos = doc.resolve(mediaPos); const endOfMediaGroup = endPositionOfParent($mediaPos); if (endOfMediaGroup + 1 >= doc.nodeSize - 1) { // if nothing after the media group, fallback to select the newest uploaded media item setNodeSelection(view, mediaPos); } else { setTextSelection(view, endOfMediaGroup + 1); } };