UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

179 lines (177 loc) 6.92 kB
import memoizeOne from 'memoize-one'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics'; import { safeInsert, shouldSplitSelectedNodeOnNodeInsertion } from '@atlaskit/editor-common/insert'; import { getMaxWidthForNestedNodeNext, getMediaSingleInitialWidth, MEDIA_SINGLE_DEFAULT_MIN_PIXEL_WIDTH, MEDIA_SINGLE_VIDEO_MIN_PIXEL_WIDTH } from '@atlaskit/editor-common/media-single'; import { atTheBeginningOfBlock } from '@atlaskit/editor-common/selection'; import { checkNodeDown, isEmptyParagraph } from '@atlaskit/editor-common/utils'; import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model'; import { safeInsert as pmSafeInsert } from '@atlaskit/editor-prosemirror/utils'; import { getBooleanFF } from '@atlaskit/platform-feature-flags'; import { copyOptionalAttrsFromMediaState } from '../utils/media-common'; import { isImage } from './is-type'; const getInsertMediaAnalytics = (inputMethod, fileExtension) => ({ action: ACTION.INSERTED, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId: ACTION_SUBJECT_ID.MEDIA, attributes: { inputMethod, fileExtension, type: ACTION_SUBJECT_ID.MEDIA_SINGLE }, eventType: EVENT_TYPE.TRACK }); function shouldAddParagraph(state) { return atTheBeginningOfBlock(state) && !checkNodeDown(state.selection, state.doc, isEmptyParagraph); } function insertNodesWithOptionalParagraph(nodes, analyticsAttributes = {}, editorAnalyticsAPI) { return function (state, dispatch) { const { tr, schema } = state; const { paragraph } = schema.nodes; const { inputMethod, fileExtension } = analyticsAttributes; let openEnd = 0; if (shouldAddParagraph(state)) { nodes.push(paragraph.create()); openEnd = 1; } tr.replaceSelection(new Slice(Fragment.from(nodes), 0, openEnd)); if (inputMethod) { editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(getInsertMediaAnalytics(inputMethod, fileExtension))(tr); } if (dispatch) { dispatch(tr); } return true; }; } export const isMediaSingle = (schema, fileMimeType) => !!schema.nodes.mediaSingle && isImage(fileMimeType); export const insertMediaAsMediaSingle = (view, node, inputMethod, editorAnalyticsAPI) => { const { state, dispatch } = view; const { mediaSingle, media } = state.schema.nodes; if (!mediaSingle) { return false; } // if not an image type media node if (node.type !== media || !isImage(node.attrs.__fileMimeType) && node.attrs.type !== 'external') { return false; } const mediaSingleNode = mediaSingle.create({}, node); const nodes = [mediaSingleNode]; const analyticsAttributes = { inputMethod, fileExtension: node.attrs.__fileMimeType }; return insertNodesWithOptionalParagraph(nodes, analyticsAttributes, editorAnalyticsAPI)(state, dispatch); }; export const insertMediaSingleNode = (view, mediaState, inputMethod, collection, alignLeftOnInsert, newInsertionBehaviour, widthPluginState, editorAnalyticsAPI) => { var _state$selection$$fro; if (collection === undefined) { return false; } const { state, dispatch } = view; const grandParentNodeType = (_state$selection$$fro = state.selection.$from.node(-1)) === null || _state$selection$$fro === void 0 ? void 0 : _state$selection$$fro.type; const parentNodeType = state.selection.$from.parent.type; // add undefined as fallback as we don't want media single width to have upper limit as 0 // if widthPluginState.width is 0, default 760 will be used const contentWidth = getMaxWidthForNestedNodeNext(view, state.selection.$from.pos, true) || (widthPluginState === null || widthPluginState === void 0 ? void 0 : widthPluginState.lineLength) || (widthPluginState === null || widthPluginState === void 0 ? void 0 : widthPluginState.width) || undefined; const node = createMediaSingleNode(state.schema, collection, contentWidth, mediaState.status !== 'error' && isVideo(mediaState.fileMimeType) ? MEDIA_SINGLE_VIDEO_MIN_PIXEL_WIDTH : MEDIA_SINGLE_DEFAULT_MIN_PIXEL_WIDTH, alignLeftOnInsert)(mediaState); let fileExtension; if (mediaState.fileName) { const extensionIdx = mediaState.fileName.lastIndexOf('.'); fileExtension = extensionIdx >= 0 ? mediaState.fileName.substring(extensionIdx + 1) : undefined; } // should split if media is valid content for the grandparent of the selected node // and the parent node is a paragraph if (shouldSplitSelectedNodeOnNodeInsertion({ parentNodeType, grandParentNodeType, content: node })) { insertNodesWithOptionalParagraph([node], { fileExtension, inputMethod }, editorAnalyticsAPI)(state, dispatch); } else { let tr = null; if (newInsertionBehaviour) { tr = safeInsert(node, state.selection.from)(state.tr); } if (!tr) { const content = shouldAddParagraph(view.state) ? Fragment.fromArray([node, state.schema.nodes.paragraph.create()]) : node; tr = pmSafeInsert(content, undefined, true)(state.tr); } if (inputMethod) { editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent(getInsertMediaAnalytics(inputMethod, fileExtension))(tr); } dispatch(tr); } return true; }; export const createMediaSingleNode = (schema, collection, maxWidth, minWidth, alignLeftOnInsert) => mediaState => { const { id, dimensions, contextId, scaleFactor = 1 } = mediaState; const { width, height } = dimensions || { height: undefined, width: undefined }; const { media, mediaSingle } = schema.nodes; const scaledWidth = width && Math.round(width / scaleFactor); const mediaNode = media.create({ id, type: 'file', collection, contextId, width: scaledWidth, height: height && Math.round(height / scaleFactor) }); const mediaSingleAttrs = alignLeftOnInsert ? { layout: 'align-start' } : {}; const extendedMediaSingleAttrs = getBooleanFF('platform.editor.media.extended-resize-experience') ? { ...mediaSingleAttrs, width: getMediaSingleInitialWidth(scaledWidth, maxWidth, minWidth), // TODO: change to use enum widthType: 'pixel' } : mediaSingleAttrs; copyOptionalAttrsFromMediaState(mediaState, mediaNode); return mediaSingle.createChecked(extendedMediaSingleAttrs, mediaNode); }; export function isCaptionNode(editorView) { const { $from } = editorView.state.selection; const immediateWrapperParentNode = editorView.state.doc.nodeAt($from.before(Math.max($from.depth, 1))); if (immediateWrapperParentNode && immediateWrapperParentNode.type.name === 'caption') { return true; } return false; } export const isVideo = memoizeOne(fileType => { return !!fileType && fileType.includes('video'); });