UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

370 lines (365 loc) 18.4 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } 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 { DEFAULT_IMAGE_WIDTH, getMaxWidthForNestedNodeNext, getMediaSingleInitialWidth, MEDIA_SINGLE_DEFAULT_MIN_PIXEL_WIDTH, MEDIA_SINGLE_VIDEO_MIN_PIXEL_WIDTH } from '@atlaskit/editor-common/media-single'; import { atTheBeginningOfBlock, selectionIsAtTheBeginningOfBlock } 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, removeSelectedNode } from '@atlaskit/editor-prosemirror/utils'; import { fg } from '@atlaskit/platform-feature-flags'; import { copyOptionalAttrsFromMediaState } from '../utils/media-common'; import { findChangeFromLocation, getChangeMediaAnalytics } from './analytics'; import { isImage } from './is-type'; var getInsertMediaAnalytics = function getInsertMediaAnalytics(inputMethod, fileExtension, insertMediaVia) { return { action: ACTION.INSERTED, actionSubject: ACTION_SUBJECT.DOCUMENT, actionSubjectId: ACTION_SUBJECT_ID.MEDIA, attributes: { inputMethod: inputMethod, insertMediaVia: insertMediaVia, fileExtension: 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(_ref) { var nodes = _ref.nodes, _ref$analyticsAttribu = _ref.analyticsAttributes, analyticsAttributes = _ref$analyticsAttribu === void 0 ? {} : _ref$analyticsAttribu, editorAnalyticsAPI = _ref.editorAnalyticsAPI, insertMediaVia = _ref.insertMediaVia; return function (state, dispatch) { var tr = state.tr; if (fg('platform_editor_introduce_insert_media_command')) { var _updatedTr = insertNodesWithOptionalParagraphCommand({ nodes: nodes, analyticsAttributes: analyticsAttributes, editorAnalyticsAPI: editorAnalyticsAPI, insertMediaVia: insertMediaVia })({ tr: tr }); if (_updatedTr && dispatch) { dispatch === null || dispatch === void 0 || dispatch(_updatedTr); return true; } return false; } var inputMethod = analyticsAttributes.inputMethod, fileExtension = analyticsAttributes.fileExtension, newType = analyticsAttributes.newType, previousType = analyticsAttributes.previousType; var updatedTr = tr; var openEnd = 0; if (state.selection.empty) { var insertFrom = atTheBeginningOfBlock(state) ? state.selection.$from.before() : state.selection.from; // the use of pmSafeInsert causes the node selection to media single node. // It leads to discrepancy between the full-page and comment editor - not sure why :shrug: // When multiple images are uploaded, the node selection is set to the previous node // and got overridden by the next node inserted. // It also causes the images position shifted when the images are uploaded. // E.g the images are uploaded after a table, the images will be inserted inside the table. // so we revert to use tr.insert instead. No extra paragraph is added. updatedTr = updatedTr.insert(insertFrom, nodes); } else { updatedTr.replaceSelection(new Slice(Fragment.from(nodes), 0, openEnd)); } if (inputMethod) { editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent(getInsertMediaAnalytics(inputMethod, fileExtension, insertMediaVia))(updatedTr); } if (newType && previousType) { editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent(getChangeMediaAnalytics(previousType, newType, findChangeFromLocation(state.selection)))(updatedTr); } if (dispatch) { dispatch(updatedTr); } return true; }; } function insertNodesWithOptionalParagraphCommand(_ref2) { var nodes = _ref2.nodes, _ref2$analyticsAttrib = _ref2.analyticsAttributes, analyticsAttributes = _ref2$analyticsAttrib === void 0 ? {} : _ref2$analyticsAttrib, editorAnalyticsAPI = _ref2.editorAnalyticsAPI, insertMediaVia = _ref2.insertMediaVia; return function (_ref3) { var tr = _ref3.tr; var inputMethod = analyticsAttributes.inputMethod, fileExtension = analyticsAttributes.fileExtension, newType = analyticsAttributes.newType, previousType = analyticsAttributes.previousType; var updatedTr = tr; var openEnd = 0; if (tr.selection.empty) { var insertFrom = selectionIsAtTheBeginningOfBlock(tr.selection) ? tr.selection.$from.before() : tr.selection.from; // the use of pmSafeInsert causes the node selection to media single node. // It leads to discrepancy between the full-page and comment editor - not sure why :shrug: // When multiple images are uploaded, the node selection is set to the previous node // and got overridden by the next node inserted. // It also causes the images position shifted when the images are uploaded. // E.g the images are uploaded after a table, the images will be inserted inside the table. // so we revert to use tr.insert instead. No extra paragraph is added. updatedTr = updatedTr.insert(insertFrom, nodes); } else { updatedTr.replaceSelection(new Slice(Fragment.from(nodes), 0, openEnd)); } if (inputMethod) { editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent(getInsertMediaAnalytics(inputMethod, fileExtension, insertMediaVia))(updatedTr); } if (newType && previousType) { editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent(getChangeMediaAnalytics(previousType, newType, findChangeFromLocation(tr.selection)))(updatedTr); } return updatedTr; }; } export var isMediaSingle = function isMediaSingle(schema, fileMimeType) { return !!schema.nodes.mediaSingle && isImage(fileMimeType); }; export var insertMediaAsMediaSingle = function insertMediaAsMediaSingle(view, node, inputMethod, editorAnalyticsAPI, insertMediaVia, allowPixelResizing) { var _node$attrs$width; var state = view.state, dispatch = view.dispatch; var _state$schema$nodes = state.schema.nodes, mediaSingle = _state$schema$nodes.mediaSingle, media = _state$schema$nodes.media; 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; } if (fg('platform_editor_introduce_insert_media_command')) { var updatedTr = createInsertMediaAsMediaSingleCommand(node.attrs, inputMethod, editorAnalyticsAPI, insertMediaVia, allowPixelResizing)({ tr: state.tr }); if (updatedTr && dispatch) { dispatch === null || dispatch === void 0 || dispatch(updatedTr); return true; } return false; } var mediaSingleAttrs = allowPixelResizing ? { widthType: 'pixel', width: getMediaSingleInitialWidth((_node$attrs$width = node.attrs.width) !== null && _node$attrs$width !== void 0 ? _node$attrs$width : DEFAULT_IMAGE_WIDTH), layout: 'center' } : {}; var mediaSingleNode = mediaSingle.create(mediaSingleAttrs, node); var nodes = [mediaSingleNode]; var analyticsAttributes = { inputMethod: inputMethod, fileExtension: node.attrs.__fileMimeType }; return insertNodesWithOptionalParagraph({ nodes: nodes, analyticsAttributes: analyticsAttributes, editorAnalyticsAPI: editorAnalyticsAPI, insertMediaVia: insertMediaVia })(state, dispatch); }; export var createInsertMediaAsMediaSingleCommand = function createInsertMediaAsMediaSingleCommand(mediaAttrs, inputMethod, editorAnalyticsAPI, insertMediaVia, allowPixelResizing) { return function (_ref4) { var _mediaAttrs$__fileMim, _mediaAttrs$width, _mediaAttrs$__fileMim2; var tr = _ref4.tr; var _tr$doc$type$schema$n = tr.doc.type.schema.nodes, mediaSingle = _tr$doc$type$schema$n.mediaSingle, media = _tr$doc$type$schema$n.media; if (!mediaSingle || !media) { return null; } if (mediaAttrs.type !== 'external' && !isImage((_mediaAttrs$__fileMim = mediaAttrs.__fileMimeType) !== null && _mediaAttrs$__fileMim !== void 0 ? _mediaAttrs$__fileMim : undefined)) { return null; } var mediaSingleAttrs = allowPixelResizing ? { widthType: 'pixel', width: getMediaSingleInitialWidth((_mediaAttrs$width = mediaAttrs.width) !== null && _mediaAttrs$width !== void 0 ? _mediaAttrs$width : DEFAULT_IMAGE_WIDTH), layout: 'center' } : {}; var mediaNode = media.create(mediaAttrs); var mediaSingleNode = mediaSingle.create(mediaSingleAttrs, mediaNode); var nodes = [mediaSingleNode]; var analyticsAttributes = { inputMethod: inputMethod, // External images have no file extension fileExtension: mediaAttrs.type !== 'external' && mediaAttrs.__fileMimeType ? (_mediaAttrs$__fileMim2 = mediaAttrs.__fileMimeType) !== null && _mediaAttrs$__fileMim2 !== void 0 ? _mediaAttrs$__fileMim2 : undefined : undefined }; return insertNodesWithOptionalParagraphCommand({ nodes: nodes, analyticsAttributes: analyticsAttributes, editorAnalyticsAPI: editorAnalyticsAPI, insertMediaVia: insertMediaVia })({ tr: tr }); }; }; var getFileExtension = function getFileExtension(fileName) { if (fileName) { var extensionIdx = fileName.lastIndexOf('.'); return extensionIdx >= 0 ? fileName.substring(extensionIdx + 1) : undefined; } return undefined; }; export var insertMediaSingleNode = function insertMediaSingleNode(view, mediaState, inputMethod, collection, alignLeftOnInsert, widthPluginState, editorAnalyticsAPI, onNodeInserted, insertMediaVia, allowPixelResizing) { var _state$selection$$fro; if (collection === undefined) { return false; } var state = view.state, dispatch = view.dispatch; var grandParentNodeType = (_state$selection$$fro = state.selection.$from.node(-1)) === null || _state$selection$$fro === void 0 ? void 0 : _state$selection$$fro.type; var 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 var 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; var 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, allowPixelResizing)(mediaState); var fileExtension; if (mediaState.fileName) { var 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: parentNodeType, grandParentNodeType: grandParentNodeType, content: node })) { insertNodesWithOptionalParagraph({ nodes: [node], analyticsAttributes: { fileExtension: fileExtension, inputMethod: inputMethod }, editorAnalyticsAPI: editorAnalyticsAPI, insertMediaVia: insertMediaVia })(state, dispatch); } else { var tr = null; tr = safeInsert(node, state.selection.from)(state.tr); if (!tr) { var 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 || editorAnalyticsAPI.attachAnalyticsEvent(getInsertMediaAnalytics(inputMethod, fileExtension, insertMediaVia))(tr); } dispatch(tr); } if (onNodeInserted) { onNodeInserted(mediaState.id, view.state.selection.to); } return true; }; export var changeFromMediaInlineToMediaSingleNode = function changeFromMediaInlineToMediaSingleNode(view, fromNode, widthPluginState, editorAnalyticsAPI, allowPixelResizing) { var _state$selection$$fro2; var state = view.state, dispatch = view.dispatch; var mediaInline = state.schema.nodes.mediaInline; if (fromNode.type !== mediaInline) { return false; } var grandParentNodeType = (_state$selection$$fro2 = state.selection.$from.node(-1)) === null || _state$selection$$fro2 === void 0 ? void 0 : _state$selection$$fro2.type; var 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 var 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; var node = replaceWithMediaSingleNode(state.schema, contentWidth, MEDIA_SINGLE_DEFAULT_MIN_PIXEL_WIDTH, allowPixelResizing)(fromNode); var fileExtension = getFileExtension(fromNode.attrs.__fileName); // should split if media is valid content for the grandparent of the selected node // and the parent node is a paragraph if (shouldSplitSelectedNodeOnNodeInsertion({ parentNodeType: parentNodeType, grandParentNodeType: grandParentNodeType, content: node })) { return insertNodesWithOptionalParagraph({ nodes: [node], analyticsAttributes: { fileExtension: fileExtension, newType: ACTION_SUBJECT_ID.MEDIA_SINGLE, previousType: ACTION_SUBJECT_ID.MEDIA_INLINE }, editorAnalyticsAPI: editorAnalyticsAPI })(state, dispatch); } else { var nodePos = state.tr.doc.resolve(state.selection.from).end(); var tr = null; tr = removeSelectedNode(state.tr); tr = safeInsert(node, nodePos)(tr); if (!tr) { var content = shouldAddParagraph(view.state) ? Fragment.fromArray([node, state.schema.nodes.paragraph.create()]) : node; tr = pmSafeInsert(content, undefined, true)(state.tr); } editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent(getChangeMediaAnalytics(ACTION_SUBJECT_ID.MEDIA_INLINE, ACTION_SUBJECT_ID.MEDIA_SINGLE, findChangeFromLocation(state.selection)))(tr); dispatch(tr); } return true; }; var createMediaSingleNode = function createMediaSingleNode(schema, collection, maxWidth, minWidth, alignLeftOnInsert, allowPixelResizing) { return function (mediaState) { var id = mediaState.id, dimensions = mediaState.dimensions, contextId = mediaState.contextId, _mediaState$scaleFact = mediaState.scaleFactor, scaleFactor = _mediaState$scaleFact === void 0 ? 1 : _mediaState$scaleFact, fileName = mediaState.fileName; var _ref5 = dimensions || { height: undefined, width: undefined }, width = _ref5.width, height = _ref5.height; var _schema$nodes = schema.nodes, media = _schema$nodes.media, mediaSingle = _schema$nodes.mediaSingle; var scaledWidth = width && Math.round(width / scaleFactor); var mediaNode = media.create(_objectSpread({ id: id, type: 'file', collection: collection, contextId: contextId, width: scaledWidth, height: height && Math.round(height / scaleFactor) }, fileName && { alt: fileName })); var mediaSingleAttrs = alignLeftOnInsert ? { layout: 'align-start' } : {}; var extendedMediaSingleAttrs = allowPixelResizing ? _objectSpread(_objectSpread({}, mediaSingleAttrs), {}, { width: getMediaSingleInitialWidth(scaledWidth, maxWidth, minWidth), // TODO: ED-26962 - change to use enum widthType: 'pixel' }) : mediaSingleAttrs; copyOptionalAttrsFromMediaState(mediaState, mediaNode); return mediaSingle.createChecked(extendedMediaSingleAttrs, mediaNode); }; }; var replaceWithMediaSingleNode = function replaceWithMediaSingleNode(schema, maxWidth, minWidth, allowPixelResizing) { return function (mediaNode) { var width = mediaNode.attrs.width; var _schema$nodes2 = schema.nodes, media = _schema$nodes2.media, mediaSingle = _schema$nodes2.mediaSingle; var copiedMediaNode = media.create(_objectSpread(_objectSpread({}, mediaNode.attrs), {}, { type: 'file' }), mediaNode.content, mediaNode.marks); var extendedMediaSingleAttrs = allowPixelResizing ? { width: getMediaSingleInitialWidth(width, maxWidth, minWidth), widthType: 'pixel' } : {}; return mediaSingle.createChecked(extendedMediaSingleAttrs, copiedMediaNode); }; }; export var isVideo = memoizeOne(function (fileType) { return !!fileType && fileType.includes('video'); });