@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
370 lines (365 loc) • 18.4 kB
JavaScript
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');
});