UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

569 lines (567 loc) 24.4 kB
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; };