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