@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
432 lines (427 loc) • 22.5 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, startPositionOfParent } 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 { akEditorDefaultLayoutWidth } from '@atlaskit/editor-shared-styles';
import { TableMap } from '@atlaskit/editor-tables/table-map';
import { fg } from '@atlaskit/platform-feature-flags';
import { copyOptionalAttrsFromMediaState } from '../utils/media-common';
import { findChangeFromLocation, getChangeMediaAnalytics } from './analytics';
import { isImage } from './is-type';
var TABLE_CELL_HORIZONTAL_PADDING = 16;
var getPositiveWidth = function getPositiveWidth(width) {
return typeof width === 'number' && width > 0 ? width : undefined;
};
var getClosestNodeDepth = function getClosestNodeDepth($pos, nodeNames) {
for (var depth = $pos.depth; depth > 0; depth--) {
if (nodeNames.includes($pos.node(depth).type.name)) {
return depth;
}
}
};
var getTableCellWidthFromPosition = function getTableCellWidthFromPosition(doc, pos) {
if (typeof pos !== 'number') {
return undefined;
}
try {
var _getPositiveWidth, _getPositiveWidth2;
var $pos = doc.resolve(pos);
var cellDepth = getClosestNodeDepth($pos, ['tableCell', 'tableHeader']);
var tableDepth = getClosestNodeDepth($pos, ['table']);
if (cellDepth === undefined || tableDepth === undefined || tableDepth >= cellDepth) {
return undefined;
}
var tableNode = $pos.node(tableDepth);
var cellNode = $pos.node(cellDepth);
var tableMap = TableMap.get(tableNode);
var tableStart = $pos.before(tableDepth) + 1;
var cellPositionInTable = $pos.before(cellDepth) - tableStart;
var cellRect = tableMap.findCell(cellPositionInTable);
var columnSpan = cellRect.right - cellRect.left;
var colwidth = cellNode.attrs.colwidth;
var explicitCellWidth = colwidth === null || colwidth === void 0 ? void 0 : colwidth.slice(0, columnSpan).reduce(function (total, width) {
return total + width;
}, 0);
var tableWidth = (_getPositiveWidth = getPositiveWidth(tableNode.attrs.width)) !== null && _getPositiveWidth !== void 0 ? _getPositiveWidth : akEditorDefaultLayoutWidth;
var inferredCellWidth = tableWidth / tableMap.width * columnSpan;
var cellWidth = (_getPositiveWidth2 = getPositiveWidth(explicitCellWidth)) !== null && _getPositiveWidth2 !== void 0 ? _getPositiveWidth2 : getPositiveWidth(inferredCellWidth);
return cellWidth ? cellWidth - TABLE_CELL_HORIZONTAL_PADDING : undefined;
} catch (_unused) {
return undefined;
}
};
var getMediaSingleMaxWidthForInsertion = function getMediaSingleMaxWidthForInsertion(view, pos, widthPluginState) {
var _ref, _ref2, _getPositiveWidth3;
return (_ref = (_ref2 = (_getPositiveWidth3 = getPositiveWidth(getMaxWidthForNestedNodeNext(view, pos, true))) !== null && _getPositiveWidth3 !== void 0 ? _getPositiveWidth3 : getPositiveWidth(getTableCellWidthFromPosition(view.state.doc, pos))) !== null && _ref2 !== void 0 ? _ref2 : getPositiveWidth(widthPluginState === null || widthPluginState === void 0 ? void 0 : widthPluginState.lineLength)) !== null && _ref !== void 0 ? _ref : getPositiveWidth(widthPluginState === null || widthPluginState === void 0 ? void 0 : widthPluginState.width);
};
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(_ref3) {
var nodes = _ref3.nodes,
_ref3$analyticsAttrib = _ref3.analyticsAttributes,
analyticsAttributes = _ref3$analyticsAttrib === void 0 ? {} : _ref3$analyticsAttrib,
editorAnalyticsAPI = _ref3.editorAnalyticsAPI,
insertMediaVia = _ref3.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(_ref4) {
var nodes = _ref4.nodes,
positions = _ref4.positions,
_ref4$analyticsAttrib = _ref4.analyticsAttributes,
analyticsAttributes = _ref4$analyticsAttrib === void 0 ? {} : _ref4$analyticsAttrib,
editorAnalyticsAPI = _ref4.editorAnalyticsAPI,
insertMediaVia = _ref4.insertMediaVia;
return function (_ref5) {
var tr = _ref5.tr;
var inputMethod = analyticsAttributes.inputMethod,
fileExtension = analyticsAttributes.fileExtension,
newType = analyticsAttributes.newType,
previousType = analyticsAttributes.previousType;
var updatedTr = tr;
var openEnd = 0;
var selection = tr.selection;
var from = positions ? positions[0] : selection.from;
var to = positions ? positions[1] : selection.to;
var $from = positions ? tr.doc.resolve(from) : selection.$from;
var isEmpty = positions ? from === to : selection.empty;
if (isEmpty) {
var isAtTheBeginningOfBlock = positions ? startPositionOfParent($from) === $from.pos : selectionIsAtTheBeginningOfBlock(selection);
var insertFrom = isAtTheBeginningOfBlock ? $from.before() : 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 {
var slice = new Slice(Fragment.from(nodes), 0, openEnd);
updatedTr = positions ? updatedTr.replaceRange(from, to, slice) : updatedTr.replaceSelection(slice);
}
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;
}
var mediaSingleMaxWidth = getMediaSingleMaxWidthForInsertion(view, state.selection.$from.pos);
if (fg('platform_editor_introduce_insert_media_command')) {
var updatedTr = createInsertMediaAsMediaSingleCommand(node.attrs, inputMethod, editorAnalyticsAPI, insertMediaVia, allowPixelResizing, undefined, undefined, mediaSingleMaxWidth)({
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, mediaSingleMaxWidth),
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, positions, dataConsumerSource, mediaSingleMaxWidth) {
return function (_ref6) {
var _mediaAttrs$__fileMim, _getPositiveWidth4, _mediaAttrs$width, _mediaAttrs$__fileMim2;
var tr = _ref6.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 resolvedMediaSingleMaxWidth = (_getPositiveWidth4 = getPositiveWidth(mediaSingleMaxWidth)) !== null && _getPositiveWidth4 !== void 0 ? _getPositiveWidth4 : getPositiveWidth(getTableCellWidthFromPosition(tr.doc, positions ? positions[0] : tr.selection.from));
var mediaSingleAttrs = allowPixelResizing ? {
widthType: 'pixel',
width: getMediaSingleInitialWidth((_mediaAttrs$width = mediaAttrs.width) !== null && _mediaAttrs$width !== void 0 ? _mediaAttrs$width : DEFAULT_IMAGE_WIDTH, resolvedMediaSingleMaxWidth),
layout: 'center'
} : {};
var dataConsumerMarkType = dataConsumerSource ? tr.doc.type.schema.marks.dataConsumer : undefined;
var mediaNode = dataConsumerSource && dataConsumerMarkType ? media.create(mediaAttrs, undefined, [dataConsumerMarkType.create({
sources: [dataConsumerSource]
})]) : 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,
positions: positions,
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, _getMediaSingleMaxWid;
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 = (_getMediaSingleMaxWid = getMediaSingleMaxWidthForInsertion(view, state.selection.$from.pos, widthPluginState)) !== null && _getMediaSingleMaxWid !== void 0 ? _getMediaSingleMaxWid : 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, _getMediaSingleMaxWid2;
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 = (_getMediaSingleMaxWid2 = getMediaSingleMaxWidthForInsertion(view, state.selection.$from.pos, widthPluginState)) !== null && _getMediaSingleMaxWid2 !== void 0 ? _getMediaSingleMaxWid2 : 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 _ref7 = dimensions || {
height: undefined,
width: undefined
},
width = _ref7.width,
height = _ref7.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');
});