UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

364 lines (350 loc) 14.1 kB
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH } from '@atlaskit/editor-common/media-inline'; import { atTheBeginningOfBlock, atTheEndOfBlock, atTheEndOfDoc, endPositionOfParent, startPositionOfParent } from '@atlaskit/editor-common/selection'; import { findFarthestParentNode, insideTableCell, isInLayoutColumn, isInListItem, isSupportedInParent, setNodeSelection, setTextSelection } from '@atlaskit/editor-common/utils'; import { Fragment } from '@atlaskit/editor-prosemirror/model'; import { ReplaceStep } from '@atlaskit/editor-prosemirror/transform'; import { canInsert, hasParentNode, safeInsert } from '@atlaskit/editor-prosemirror/utils'; import { isImage } from './is-type'; import { copyOptionalAttrsFromMediaState, isInsidePotentialEmptyParagraph, isSelectionNonMediaBlockNode, posOfMediaGroupNearby, posOfParentMediaGroup, posOfPrecedingMediaGroup } from './media-common'; import { isInSupportedInlineImageParent } from './media-inline'; export var canInsertMediaInline = function canInsertMediaInline(state) { var node = state.schema.nodes.mediaInline.create({}); return canInsert(state.selection.$to, Fragment.from(node)); }; var getInsertMediaGroupAnalytics = function getInsertMediaGroupAnalytics(mediaState, inputMethod, insertMediaVia) { var media = ''; if (mediaState.length === 1) { media = mediaState[0].fileMimeType || 'unknown'; } else if (mediaState.length > 1) { media = 'multiple'; } return { action: ACTION.INSERTED, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId: ACTION_SUBJECT_ID.MEDIA, attributes: { type: ACTION_SUBJECT_ID.MEDIA_GROUP, inputMethod: inputMethod, fileExtension: media, insertMediaVia: insertMediaVia }, eventType: EVENT_TYPE.TRACK }; }; var getInsertMediaInlineAnalytics = function getInsertMediaInlineAnalytics(mediaState, inputMethod, insertMediaVia) { var media = mediaState.fileMimeType || 'unknown'; return { action: ACTION.INSERTED, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId: ACTION_SUBJECT_ID.MEDIA, attributes: { type: ACTION_SUBJECT_ID.MEDIA_INLINE, inputMethod: inputMethod, fileExtension: media, insertMediaVia: insertMediaVia }, eventType: EVENT_TYPE.TRACK }; }; export var getFailToInsertAnalytics = function getFailToInsertAnalytics(mediaState, actionSubjectId, inputMethod, insertMediaVia, reason) { var media = mediaState.fileMimeType || 'unknown'; return { action: ACTION.FAILED_TO_INSERT, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId: actionSubjectId, attributes: { inputMethod: inputMethod, fileExtension: media, insertMediaVia: insertMediaVia, reason: reason }, eventType: EVENT_TYPE.OPERATIONAL }; }; /** * Check if current editor selections is a media group or not. * @param state Editor state */ function isSelectionMediaGroup(state) { var schema = state.schema; var selectionParent = state.selection.$anchor.node(); return selectionParent && selectionParent.type === schema.nodes.mediaGroup; } /** * 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 * @param node Node at insertion point * @param state Editor state */ function shouldAppendParagraph(state, node) { var media = state.schema.nodes.media; var wasMediaNode = node && node.type === media; return (insideTableCell(state) || isInListItem(state) || isInLayoutColumn(state) || atTheEndOfDoc(state) && (!posOfPrecedingMediaGroup(state) || isSelectionNonMediaBlockNode(state))) && !wasMediaNode; } /** * Check if node of type has been inserted successfully */ var hasInsertedNodeOfType = function hasInsertedNodeOfType(tr, nodeType) { var _tr$doc$nodeAt; var insertPos = -1; tr.steps.forEach(function (step) { if (step instanceof ReplaceStep) { step.slice.content.forEach(function (node) { if (node.type.name === nodeType) { insertPos = step.from; } }); } }); if (insertPos === -1 || ((_tr$doc$nodeAt = tr.doc.nodeAt(insertPos)) === null || _tr$doc$nodeAt === void 0 ? void 0 : _tr$doc$nodeAt.type.name) !== nodeType) { return false; } return true; }; /** * Create a new media inline to insert the new media. * @param view Editor view * @param mediaState Media file to be added to the editor * @param allowInlineImages Configuration for allowing adding of inline images * @param collection Collection for the media to be added */ export var insertMediaInlineNode = function insertMediaInlineNode(editorAnalyticsAPI) { return function (view, mediaState, collection, allowInlineImages, inputMethod, insertMediaVia) { var state = view.state, dispatch = view.dispatch; var schema = state.schema, tr = state.tr; var mediaInline = schema.nodes.mediaInline; // Do nothing if no media found if (!mediaInline || !mediaState || collection === undefined) { return false; } var id = mediaState.id, dimensions = mediaState.dimensions, _mediaState$scaleFact = mediaState.scaleFactor, scaleFactor = _mediaState$scaleFact === void 0 ? 1 : _mediaState$scaleFact, fileName = mediaState.fileName; var mediaInlineAttrs = { id: id, collection: collection }; if (allowInlineImages && isImage(mediaState.fileMimeType)) { var _ref = dimensions || { width: undefined, height: undefined }, width = _ref.width, height = _ref.height; var scaledWidth = width ? Math.round(width / scaleFactor) : DEFAULT_IMAGE_WIDTH; var scaledHeight = height ? Math.round(height / scaleFactor) : DEFAULT_IMAGE_HEIGHT; mediaInlineAttrs.width = scaledWidth; mediaInlineAttrs.height = scaledHeight; mediaInlineAttrs.type = 'image'; mediaInlineAttrs.alt = fileName; } var mediaInlineNode = mediaInline.create(mediaInlineAttrs); var space = state.schema.text(' '); var pos = state.selection.$to.pos; // If the selection is inside an empty list item or panel set pos inside paragraph if (isInSupportedInlineImageParent(state) && isInsidePotentialEmptyParagraph(state)) { pos = pos + 1; } var content = Fragment.from([mediaInlineNode, space]); // Delete the selection if a selection is made var deleteRange = findDeleteRange(state); var payload; try { if (!deleteRange) { tr.insert(pos, content); } else { tr.insert(pos, content).deleteRange(deleteRange.start, deleteRange.end); } if (hasInsertedNodeOfType(tr, 'mediaInline')) { payload = getInsertMediaInlineAnalytics(mediaState, inputMethod, insertMediaVia); } else { payload = getFailToInsertAnalytics(mediaState, ACTION_SUBJECT_ID.MEDIA_INLINE, inputMethod, insertMediaVia); } } catch (error) { payload = getFailToInsertAnalytics(mediaState, ACTION_SUBJECT_ID.MEDIA_INLINE, inputMethod, insertMediaVia, // eslint-disable-next-line @typescript-eslint/no-explicit-any error.toString()); } editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent(payload)(tr); dispatch(tr); return true; }; }; /** * Insert a media into an existing media group * or create a new media group to insert the new media. * @param view Editor view * @param mediaStates Media files to be added to the editor * @param collection Collection for the media to be added */ export var insertMediaGroupNode = function insertMediaGroupNode(editorAnalyticsAPI) { return function (view, mediaStates, collection, inputMethod, insertMediaVia) { var state = view.state, dispatch = view.dispatch; var tr = state.tr, schema = state.schema; var _schema$nodes = schema.nodes, media = _schema$nodes.media, paragraph = _schema$nodes.paragraph; // Do nothing if no media found if (!media || !mediaStates.length) { return; } var mediaNodes = createMediaFileNodes(mediaStates, collection, media); var mediaInsertPos = findMediaInsertPos(state); var resolvedInsertPos = tr.doc.resolve(mediaInsertPos); var parent = resolvedInsertPos.parent; var nodeAtInsertionPoint = tr.doc.nodeAt(mediaInsertPos); var shouldSplit = !isSelectionMediaGroup(state) && isSupportedInParent(state, Fragment.from(state.schema.nodes.mediaGroup.createChecked({}, mediaNodes))); var withParagraph = shouldAppendParagraph(state, nodeAtInsertionPoint); var content = parent.type === schema.nodes.mediaGroup ? mediaNodes // If parent is a mediaGroup do not wrap items again. : [schema.nodes.mediaGroup.createChecked({}, mediaNodes)]; if (shouldSplit) { content = withParagraph ? content.concat(paragraph.create()) : content; // delete the selection or empty paragraph var deleteRange = findDeleteRange(state); if (!deleteRange) { tr.insert(mediaInsertPos, content); } else if (mediaInsertPos <= deleteRange.start) { tr.deleteRange(deleteRange.start, deleteRange.end).insert(mediaInsertPos, content); } else { tr.insert(mediaInsertPos, content).deleteRange(deleteRange.start, deleteRange.end); } editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent(getInsertMediaGroupAnalytics(mediaStates, inputMethod, insertMediaVia))(tr); dispatch(tr); setSelectionAfterMediaInsertion(view); return; } // Don't append new paragraph when adding media to a existing mediaGroup if (withParagraph && parent.type !== schema.nodes.mediaGroup) { content.push(paragraph.create()); } editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent(getInsertMediaGroupAnalytics(mediaStates, inputMethod, insertMediaVia))(tr); dispatch(safeInsert(Fragment.fromArray(content), mediaInsertPos)(tr)); }; }; var createMediaFileNodes = function createMediaFileNodes(mediaStates, collection, media) { var nodes = mediaStates.map(function (mediaState) { var id = mediaState.id; var node = media.create({ id: id, type: 'file', collection: collection }); copyOptionalAttrsFromMediaState(mediaState, node); return node; }); return nodes; }; /** * Find root list node if exist from current selection * @param state Editor state */ var findRootListNode = function findRootListNode(state) { var _state$schema$nodes = state.schema.nodes, bulletList = _state$schema$nodes.bulletList, orderedList = _state$schema$nodes.orderedList; return findFarthestParentNode(function (node) { return node.type === bulletList || node.type === orderedList; })(state.selection.$from); }; /** * Return position of media to be inserted, if it is inside a list * @param content Content to be inserted * @param state Editor State */ export var getPosInList = function getPosInList(state) { var _state$schema$nodes2 = state.schema.nodes, mediaGroup = _state$schema$nodes2.mediaGroup, listItem = _state$schema$nodes2.listItem; // 1. Check if I am inside a list. if (hasParentNode(function (node) { return node.type === listItem; })(state.selection)) { // 2. Get end position of root list var rootListNode = findRootListNode(state); if (rootListNode) { var pos = rootListNode.pos + rootListNode.node.nodeSize; // 3. Fint the first location inside the media group var nextNode = state.doc.nodeAt(pos); if (nextNode && nextNode.type === mediaGroup) { return pos + 1; } return pos; } } return; }; /** * Find insertion point, * If it is in a List it will return position to the next block, * If there are any media group close it will return this position * Otherwise, It will return the respective block given selection. * @param content Content to be inserted * @param state Editor state */ var findMediaInsertPos = function findMediaInsertPos(state) { var _state$selection = state.selection, $from = _state$selection.$from, $to = _state$selection.$to; // Check if selection is inside a list. var posInList = getPosInList(state); if (posInList) { // If I have a position in lists, I should return, otherwise I am not inside a list return posInList; } var nearbyMediaGroupPos = posOfMediaGroupNearby(state); if (nearbyMediaGroupPos && (!isSelectionNonMediaBlockNode(state) || $from.pos < nearbyMediaGroupPos && $to.pos < 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; }; var findDeleteRange = function findDeleteRange(state) { var _state$selection2 = state.selection, $from = _state$selection2.$from, $to = _state$selection2.$to; if (posOfParentMediaGroup(state)) { return; } if (!isInsidePotentialEmptyParagraph(state) || posOfMediaGroupNearby(state)) { return range($from.pos, $to.pos); } return range(startPositionOfParent($from) - 1, endPositionOfParent($to)); }; var range = function range(start) { var end = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : start; return { start: start, end: end }; }; var setSelectionAfterMediaInsertion = function setSelectionAfterMediaInsertion(view) { var state = view.state; var doc = state.doc; var mediaPos = posOfMediaGroupNearby(state); if (!mediaPos) { return; } var $mediaPos = doc.resolve(mediaPos); var 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); } };