@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
150 lines (118 loc) • 4.06 kB
text/typescript
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);
}
};