@atlaskit/editor-plugin-media
Version:
Media plugin for @atlaskit/editor-core
569 lines (567 loc) • 24.4 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import React from 'react';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { alignmentIcons, buildLayoutButtons, layoutToMessages, wrappingIcons } from '@atlaskit/editor-common/card';
import { calcMinWidth, DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH } from '@atlaskit/editor-common/media-single';
import commonMessages, { cardMessages } from '@atlaskit/editor-common/messages';
import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
import { findParentNodeOfType, hasParentNode, hasParentNodeOfType, removeSelectedNode } from '@atlaskit/editor-prosemirror/utils';
import { akEditorDefaultLayoutWidth, akEditorFullWidthLayoutWidth } from '@atlaskit/editor-shared-styles';
import DownloadIcon from '@atlaskit/icon/glyph/download';
import RemoveIcon from '@atlaskit/icon/glyph/editor/remove';
import { mediaFilmstripItemDOMSelector } from '@atlaskit/media-filmstrip';
import { messages } from '@atlaskit/media-ui';
import { getBooleanFF } from '@atlaskit/platform-feature-flags';
import { showLinkingToolbar } from '../commands/linking';
import { MediaInlineNodeSelector, MediaSingleNodeSelector } from '../nodeviews/styles';
import { getPluginState as getMediaAltTextPluginState } from '../pm-plugins/alt-text';
import { getMediaLinkingState } from '../pm-plugins/linking';
import { stateKey } from '../pm-plugins/plugin-key';
import ImageBorderItem from '../ui/ImageBorder';
import { FullWidthDisplay, PixelEntry } from '../ui/PixelEntry';
import { currentMediaNodeBorderMark } from '../utils/current-media-node';
import { isVideo } from '../utils/media-single';
import { altTextButton, getAltTextToolbar } from './alt-text';
import { changeInlineToMediaCard, changeMediaCardToInline, removeInlineCard, setBorderMark, toggleBorderMark, updateMediaSingleWidth } from './commands';
import { FilePreviewItem } from './filePreviewItem';
import { shouldShowImageBorder } from './imageBorder';
import { LayoutGroup } from './layout-group';
import { getLinkingToolbar, shouldShowMediaLinkToolbar } from './linking';
import { LinkToolbarAppearance } from './linking-toolbar-appearance';
import { calcNewLayout, downloadMedia, getMaxToolbarWidth, getPixelWidthOfElement, getSelectedLayoutIcon, getSelectedMediaSingle, removeMediaGroupNode } from './utils';
const remove = (state, dispatch) => {
if (dispatch) {
dispatch(removeSelectedNode(state.tr));
}
return true;
};
const handleRemoveMediaGroup = (state, dispatch) => {
const tr = removeMediaGroupNode(state);
if (dispatch) {
dispatch(tr);
}
return true;
};
const generateMediaCardFloatingToolbar = (state, intl, mediaPluginState, hoverDecoration, editorAnalyticsAPI) => {
const {
mediaGroup
} = state.schema.nodes;
const items = [{
id: 'editor.media.view.switcher',
type: 'dropdown',
title: intl.formatMessage(messages.changeView),
options: [{
id: 'editor.media.view.switcher.inline',
title: intl.formatMessage(cardMessages.inline),
selected: false,
disabled: false,
onClick: changeMediaCardToInline(editorAnalyticsAPI),
testId: 'inline-appearance'
}, {
id: 'editor.media.view.switcher.thumbnail',
title: intl.formatMessage(messages.displayThumbnail),
selected: true,
disabled: false,
onClick: () => {
return true;
},
testId: 'thumbnail-appearance'
}]
}, {
type: 'separator'
}, {
type: 'custom',
fallback: [],
render: () => {
return /*#__PURE__*/React.createElement(FilePreviewItem, {
key: "editor.media.card.preview",
mediaPluginState: mediaPluginState,
intl: intl
});
}
}, {
type: 'separator'
}, {
id: 'editor.media.card.download',
type: 'button',
icon: DownloadIcon,
onClick: () => {
downloadMedia(mediaPluginState);
return true;
},
title: intl.formatMessage(messages.download)
}, {
type: 'separator'
}, {
type: 'copy-button',
items: [{
state,
formatMessage: intl.formatMessage,
nodeType: mediaGroup
}, {
type: 'separator'
}]
}, {
id: 'editor.media.delete',
type: 'button',
appearance: 'danger',
focusEditoronEnter: true,
icon: RemoveIcon,
onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaGroup, true),
onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaGroup, false),
onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaGroup, true),
onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaGroup, false),
title: intl.formatMessage(commonMessages.remove),
onClick: handleRemoveMediaGroup,
testId: 'media-toolbar-remove-button'
}];
return items;
};
const generateMediaInlineFloatingToolbar = (state, intl, mediaPluginState, hoverDecoration, editorAnalyticsAPI) => {
const {
mediaInline
} = state.schema.nodes;
const items = [{
id: 'editor.media.view.switcher',
type: 'dropdown',
title: intl.formatMessage(messages.changeView),
options: [{
id: 'editor.media.view.switcher.inline',
title: intl.formatMessage(cardMessages.inline),
selected: true,
disabled: false,
onClick: () => {
return true;
},
testId: 'inline-appearance'
}, {
id: 'editor.media.view.switcher.thumbnail',
title: intl.formatMessage(messages.displayThumbnail),
selected: false,
disabled: false,
onClick: changeInlineToMediaCard(editorAnalyticsAPI),
testId: 'thumbnail-appearance'
}]
}, {
type: 'separator'
}, {
type: 'custom',
fallback: [],
render: () => {
return /*#__PURE__*/React.createElement(FilePreviewItem, {
key: "editor.media.card.preview",
mediaPluginState: mediaPluginState,
intl: intl
});
}
}, {
type: 'separator'
}, {
id: 'editor.media.card.download',
type: 'button',
icon: DownloadIcon,
onClick: () => {
downloadMedia(mediaPluginState);
return true;
},
title: intl.formatMessage(messages.download)
}, {
type: 'separator'
}, {
type: 'copy-button',
items: [{
state,
formatMessage: intl.formatMessage,
nodeType: mediaInline
}, {
type: 'separator'
}]
}, {
id: 'editor.media.delete',
type: 'button',
appearance: 'danger',
focusEditoronEnter: true,
icon: RemoveIcon,
onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaInline, true),
onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaInline, false),
onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaInline, true),
onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaInline, false),
title: intl.formatMessage(commonMessages.remove),
onClick: removeInlineCard,
testId: 'media-toolbar-remove-button'
}];
return items;
};
const generateMediaSingleFloatingToolbar = (state, intl, options, pluginState, mediaLinkingState, pluginInjectionApi, getEditorFeatureFlags) => {
var _pluginInjectionApi$d;
const {
mediaSingle
} = state.schema.nodes;
const {
allowResizing,
allowLinking,
allowAdvancedToolBarOptions,
allowResizingInTables,
allowAltTextOnImages
} = options;
let toolbarButtons = [];
const {
hoverDecoration
} = (_pluginInjectionApi$d = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.decorations.actions) !== null && _pluginInjectionApi$d !== void 0 ? _pluginInjectionApi$d : {};
if (shouldShowImageBorder(state)) {
toolbarButtons.push({
type: 'custom',
fallback: [],
render: editorView => {
if (!editorView) {
return null;
}
const {
dispatch,
state
} = editorView;
const borderMark = currentMediaNodeBorderMark(state);
return /*#__PURE__*/React.createElement(ImageBorderItem, {
toggleBorder: () => {
var _pluginInjectionApi$a;
toggleBorderMark(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a === void 0 ? void 0 : _pluginInjectionApi$a.actions)(state, dispatch);
},
setBorder: attrs => {
var _pluginInjectionApi$a2;
setBorderMark(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a2 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a2 === void 0 ? void 0 : _pluginInjectionApi$a2.actions)(attrs)(state, dispatch);
},
borderMark: borderMark,
intl: intl
});
}
});
toolbarButtons.push({
type: 'separator'
});
}
if (allowAdvancedToolBarOptions) {
var _pluginInjectionApi$a3;
const widthPlugin = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.width;
let isChangingLayoutDisabled = false;
const selectedNode = getSelectedMediaSingle(state);
if (getBooleanFF('platform.editor.media.extended-resize-experience')) {
var _widthPlugin$sharedSt;
const contentWidth = widthPlugin === null || widthPlugin === void 0 ? void 0 : (_widthPlugin$sharedSt = widthPlugin.sharedState.currentState()) === null || _widthPlugin$sharedSt === void 0 ? void 0 : _widthPlugin$sharedSt.lineLength;
const selectedNodeMaxWidth = pluginState.currentMaxWidth || contentWidth;
if (selectedNode && selectedNodeMaxWidth && selectedNode.node.attrs.width >= selectedNodeMaxWidth) {
isChangingLayoutDisabled = true;
}
}
const layoutButtons = buildLayoutButtons(state, intl, state.schema.nodes.mediaSingle, widthPlugin, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a3 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a3 === void 0 ? void 0 : _pluginInjectionApi$a3.actions, allowResizing, allowResizingInTables, true, true, isChangingLayoutDisabled);
if (getBooleanFF('platform.editor.media.grouped-layout') && selectedNode) {
const selectedLayoutIcon = getSelectedLayoutIcon([...alignmentIcons, ...wrappingIcons], selectedNode.node);
if (selectedLayoutIcon && layoutButtons.length) {
const options = {
render: props => {
return /*#__PURE__*/React.createElement(LayoutGroup, _extends({
layoutButtons: layoutButtons
}, props));
},
width: 156,
height: 32
};
const trigger = {
id: 'media-single-layout',
testId: 'media-single-layout-dropdown-trigger',
type: 'dropdown',
options: options,
title: intl.formatMessage(layoutToMessages[selectedLayoutIcon.value]),
icon: selectedLayoutIcon.icon
};
toolbarButtons = [...toolbarButtons, trigger, {
type: 'separator'
}];
}
} else {
toolbarButtons = [...toolbarButtons, ...layoutButtons];
if (layoutButtons.length) {
toolbarButtons.push({
type: 'separator'
});
}
}
// Pixel Entry Toolbar Support
const {
selection
} = state;
const isWithinTable = hasParentNodeOfType([state.schema.nodes.table])(selection);
if (getBooleanFF('platform.editor.media.extended-resize-experience') && allowResizing && (!isWithinTable || allowResizingInTables === true)) {
const selectedMediaSingleNode = getSelectedMediaSingle(state);
const sizeInput = {
type: 'custom',
fallback: [],
render: editorView => {
var _widthPlugin$sharedSt2, _pluginInjectionApi$m;
if (!editorView || !selectedMediaSingleNode) {
return null;
}
const {
state,
dispatch
} = editorView;
const contentWidth = (widthPlugin === null || widthPlugin === void 0 ? void 0 : (_widthPlugin$sharedSt2 = widthPlugin.sharedState.currentState()) === null || _widthPlugin$sharedSt2 === void 0 ? void 0 : _widthPlugin$sharedSt2.lineLength) || akEditorDefaultLayoutWidth;
const selectedMediaNode = selectedMediaSingleNode.node.content.firstChild;
if (!selectedMediaNode) {
return null;
}
const {
width: mediaSingleWidth,
widthType,
layout
} = selectedMediaSingleNode.node.attrs;
const {
width: mediaWidth,
height: mediaHeight
} = selectedMediaNode.attrs;
const maxWidthForNestedNode = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$m = pluginInjectionApi.media.sharedState.currentState()) === null || _pluginInjectionApi$m === void 0 ? void 0 : _pluginInjectionApi$m.currentMaxWidth;
const maxWidth = maxWidthForNestedNode || akEditorFullWidthLayoutWidth;
const isVideoFile = isVideo(selectedMediaNode.attrs.__fileMimeType);
const minWidth = calcMinWidth(isVideoFile, maxWidthForNestedNode || contentWidth);
const hasPixelType = widthType === 'pixel';
const pixelWidthFromElement = getPixelWidthOfElement(editorView, selectedMediaSingleNode.pos + 1,
// get pos of media node
mediaWidth || DEFAULT_IMAGE_WIDTH);
const pixelWidth = hasPixelType ? mediaSingleWidth : pixelWidthFromElement;
//hasParentNode will return falsey value if selection depth === 0
const isNested = hasParentNode(n => n.type !== state.schema.nodes.doc)(state.selection);
return /*#__PURE__*/React.createElement(PixelEntry, {
intl: intl,
width: pluginState.isResizing ? pluginState.resizingWidth : pixelWidth,
showMigration: !pluginState.isResizing && !hasPixelType,
mediaWidth: mediaWidth || DEFAULT_IMAGE_WIDTH,
mediaHeight: mediaHeight || DEFAULT_IMAGE_HEIGHT,
minWidth: minWidth,
maxWidth: maxWidth,
onChange: valid => {
if (valid) {
hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaSingle, true, 'warning')(editorView.state, dispatch, editorView);
} else {
hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaSingle, false)(editorView.state, dispatch, editorView);
}
},
onSubmit: ({
width,
validation
}) => {
var _pluginInjectionApi$a4;
const newLayout = calcNewLayout(width, layout, contentWidth, options.fullWidthEnabled, isNested);
updateMediaSingleWidth(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a4 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a4 === void 0 ? void 0 : _pluginInjectionApi$a4.actions)(width, validation, newLayout)(state, dispatch);
},
onMigrate: () => {
const tr = state.tr.setNodeMarkup(selectedMediaSingleNode.pos, undefined, {
...selectedMediaSingleNode.node.attrs,
width: pixelWidthFromElement,
widthType: 'pixel'
});
tr.setMeta('scrollIntoView', false);
tr.setSelection(NodeSelection.create(tr.doc, selectedMediaSingleNode.pos));
dispatch(tr);
}
});
}
};
if (pluginState.isResizing) {
// If the image is resizing
// then return pixel entry component or full width label as the only toolbar item
if (!selectedMediaSingleNode) {
return [];
}
const {
layout
} = selectedMediaSingleNode.node.attrs;
if (layout === 'full-width') {
const fullWidthLabel = {
type: 'custom',
fallback: [],
render: () => {
return /*#__PURE__*/React.createElement(FullWidthDisplay, {
intl: intl
});
}
};
return [fullWidthLabel];
}
return [sizeInput];
}
toolbarButtons.push(sizeInput);
toolbarButtons.push({
type: 'separator'
});
}
if (allowLinking && shouldShowMediaLinkToolbar(state)) {
toolbarButtons.push({
type: 'custom',
fallback: [],
render: (editorView, idx) => {
if (editorView !== null && editorView !== void 0 && editorView.state) {
const editLink = () => {
if (editorView) {
const {
state,
dispatch
} = editorView;
showLinkingToolbar(state, dispatch);
}
};
const openLink = () => {
if (editorView) {
var _pluginInjectionApi$a5;
const {
state: {
tr
},
dispatch
} = editorView;
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a5 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a5 === void 0 ? void 0 : _pluginInjectionApi$a5.actions.attachAnalyticsEvent({
eventType: EVENT_TYPE.TRACK,
action: ACTION.VISITED,
actionSubject: ACTION_SUBJECT.MEDIA,
actionSubjectId: ACTION_SUBJECT_ID.LINK
})(tr);
dispatch(tr);
return true;
}
};
return /*#__PURE__*/React.createElement(LinkToolbarAppearance, {
key: idx,
editorState: editorView.state,
intl: intl,
mediaLinkingState: mediaLinkingState,
onAddLink: editLink,
onEditLink: editLink,
onOpenLink: openLink
});
}
return null;
}
});
}
}
if (allowAltTextOnImages) {
var _pluginInjectionApi$a6;
toolbarButtons.push(altTextButton(intl, state, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a6 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a6 === void 0 ? void 0 : _pluginInjectionApi$a6.actions), {
type: 'separator'
});
}
const removeButton = {
id: 'editor.media.delete',
type: 'button',
appearance: 'danger',
focusEditoronEnter: true,
icon: RemoveIcon,
onMouseEnter: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaSingle, true),
onMouseLeave: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaSingle, false),
onFocus: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaSingle, true),
onBlur: hoverDecoration === null || hoverDecoration === void 0 ? void 0 : hoverDecoration(mediaSingle, false),
title: intl.formatMessage(commonMessages.remove),
onClick: remove,
testId: 'media-toolbar-remove-button'
};
const items = [...toolbarButtons, {
type: 'copy-button',
items: [{
state,
formatMessage: intl.formatMessage,
nodeType: mediaSingle
}, {
type: 'separator'
}]
}, removeButton];
return items;
};
export const floatingToolbar = (state, intl, options = {}, pluginInjectionApi) => {
var _pluginInjectionApi$d2;
const {
media,
mediaInline,
mediaSingle,
mediaGroup
} = state.schema.nodes;
const {
altTextValidator,
allowLinking,
allowAltTextOnImages,
providerFactory,
allowMediaInline,
allowResizing,
getEditorFeatureFlags
} = options;
const mediaPluginState = stateKey.getState(state);
const mediaLinkingState = getMediaLinkingState(state);
const {
hoverDecoration
} = (_pluginInjectionApi$d2 = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : pluginInjectionApi.decorations.actions) !== null && _pluginInjectionApi$d2 !== void 0 ? _pluginInjectionApi$d2 : {};
if (!mediaPluginState) {
return;
}
const nodeType = allowMediaInline ? [mediaInline, mediaSingle, media] : [mediaSingle];
const baseToolbar = {
title: 'Media floating controls',
nodeType,
getDomRef: () => mediaPluginState.element
};
if (allowLinking && mediaLinkingState && mediaLinkingState.visible && shouldShowMediaLinkToolbar(state)) {
const linkingToolbar = getLinkingToolbar(baseToolbar, mediaLinkingState, state, intl, pluginInjectionApi, providerFactory);
if (linkingToolbar) {
return linkingToolbar;
}
}
if (allowAltTextOnImages) {
const mediaAltTextPluginState = getMediaAltTextPluginState(state);
if (mediaAltTextPluginState.isAltTextEditorOpen) {
var _pluginInjectionApi$f;
return getAltTextToolbar(baseToolbar, {
altTextValidator,
forceFocusSelector: pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$f = pluginInjectionApi.floatingToolbar.actions) === null || _pluginInjectionApi$f === void 0 ? void 0 : _pluginInjectionApi$f.forceFocusSelector
});
}
}
let items = [];
const parentMediaGroupNode = findParentNodeOfType(mediaGroup)(state.selection);
let selectedNodeType;
if (state.selection instanceof NodeSelection) {
selectedNodeType = state.selection.node.type;
}
if (allowMediaInline && (parentMediaGroupNode === null || parentMediaGroupNode === void 0 ? void 0 : parentMediaGroupNode.node.type) === mediaGroup) {
var _pluginInjectionApi$a7;
const mediaOffset = state.selection.$from.parentOffset + 1;
baseToolbar.getDomRef = () => {
var _mediaPluginState$ele;
const selector = mediaFilmstripItemDOMSelector(mediaOffset);
return (_mediaPluginState$ele = mediaPluginState.element) === null || _mediaPluginState$ele === void 0 ? void 0 : _mediaPluginState$ele.querySelector(selector);
};
items = generateMediaCardFloatingToolbar(state, intl, mediaPluginState, hoverDecoration, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a7 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a7 === void 0 ? void 0 : _pluginInjectionApi$a7.actions);
} else if (allowMediaInline && selectedNodeType && selectedNodeType === mediaInline) {
var _pluginInjectionApi$a8;
baseToolbar.getDomRef = () => {
var _mediaPluginState$ele2;
const element = (_mediaPluginState$ele2 = mediaPluginState.element) === null || _mediaPluginState$ele2 === void 0 ? void 0 : _mediaPluginState$ele2.querySelector(`.${MediaInlineNodeSelector}`);
return element || mediaPluginState.element;
};
items = generateMediaInlineFloatingToolbar(state, intl, mediaPluginState, hoverDecoration, pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$a8 = pluginInjectionApi.analytics) === null || _pluginInjectionApi$a8 === void 0 ? void 0 : _pluginInjectionApi$a8.actions);
} else {
baseToolbar.getDomRef = () => {
var _mediaPluginState$ele3;
const element = (_mediaPluginState$ele3 = mediaPluginState.element) === null || _mediaPluginState$ele3 === void 0 ? void 0 : _mediaPluginState$ele3.querySelector(`.${MediaSingleNodeSelector}`);
return element || mediaPluginState.element;
};
items = generateMediaSingleFloatingToolbar(state, intl, options, mediaPluginState, mediaLinkingState, pluginInjectionApi, getEditorFeatureFlags);
}
const toolbarConfig = {
...baseToolbar,
items,
scrollable: true
};
if (getBooleanFF('platform.editor.media.extended-resize-experience') && allowResizing) {
return {
...toolbarConfig,
width: mediaPluginState.isResizing ? undefined : getMaxToolbarWidth()
};
}
return toolbarConfig;
};