UNPKG

@atlaskit/editor-plugin-media

Version:

Media plugin for @atlaskit/editor-core

418 lines (416 loc) 21.6 kB
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 React, { useMemo } from 'react'; import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics'; import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks'; import { toolbarInsertBlockMessages as messages } from '@atlaskit/editor-common/messages'; import { IconImages } from '@atlaskit/editor-common/quick-insert'; import { SafePlugin } from '@atlaskit/editor-common/safe-plugin'; import { areToolbarFlagsEnabled } from '@atlaskit/editor-common/toolbar-flag-check'; import { NodeSelection, PluginKey } from '@atlaskit/editor-prosemirror/state'; import { getMediaFeatureFlag } from '@atlaskit/media-common'; import { fg } from '@atlaskit/platform-feature-flags'; import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals'; import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments'; import { lazyMediaGroupView } from './nodeviews/lazy-media-group'; import { lazyMediaInlineView } from './nodeviews/lazy-media-inline'; import { ReactMediaNode } from './nodeviews/mediaNodeView'; import { ReactMediaSingleNode } from './nodeviews/mediaSingle'; import { mediaSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/media'; import { mediaGroupSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaGroup'; import { mediaInlineSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaInline'; import { mediaSingleSpecWithFixedToDOM } from './nodeviews/toDOM-fixes/mediaSingle'; import { createAIGeneratingDecorationPlugin } from './pm-plugins/ai-generating-decoration'; import { createPlugin as createMediaAltTextPlugin } from './pm-plugins/alt-text'; import keymapMediaAltTextPlugin from './pm-plugins/alt-text/keymap'; import { clearAIGenerating, hideMediaViewer, insertMediaAsMediaSingleCommand, setAIGenerating, showMediaViewer, trackMediaPaste } from './pm-plugins/commands'; import keymapPlugin from './pm-plugins/keymap'; import keymapMediaSinglePlugin from './pm-plugins/keymap-media'; import linkingPlugin from './pm-plugins/linking'; import keymapLinkingPlugin from './pm-plugins/linking/keymap'; import { createPlugin } from './pm-plugins/main'; import { createPlugin as createMediaPixelResizingPlugin } from './pm-plugins/pixel-resizing'; import { stateKey } from './pm-plugins/plugin-key'; import { createMediaIdentifierArray, extractMediaNodes } from './pm-plugins/utils/media-common'; import { insertMediaAsMediaSingle as _insertMediaAsMediaSingle } from './pm-plugins/utils/media-single'; import { MediaPickerComponents } from './ui/MediaPicker'; import { RenderMediaViewer } from './ui/MediaViewer/PortalWrapper'; import { floatingToolbar as _floatingToolbar } from './ui/toolbar'; import ToolbarMedia from './ui/ToolbarMedia'; var selector = function selector(states) { var _states$mediaState, _states$mediaState2; return { onPopupToggle: (_states$mediaState = states.mediaState) === null || _states$mediaState === void 0 ? void 0 : _states$mediaState.onPopupToggle, setBrowseFn: (_states$mediaState2 = states.mediaState) === null || _states$mediaState2 === void 0 ? void 0 : _states$mediaState2.setBrowseFn }; }; var MediaPickerFunctionalComponent = function MediaPickerFunctionalComponent(_ref) { var api = _ref.api, editorDomElement = _ref.editorDomElement, appearance = _ref.appearance; var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['media'], selector), onPopupToggle = _useSharedPluginState.onPopupToggle, setBrowseFn = _useSharedPluginState.setBrowseFn; if (!onPopupToggle || !setBrowseFn) { return null; } return /*#__PURE__*/React.createElement(MediaPickerComponents, { onPopupToggle: onPopupToggle, setBrowseFn: setBrowseFn, editorDomElement: editorDomElement, appearance: appearance, api: api }); }; var mediaViewerStateSelector = function mediaViewerStateSelector(states) { var _states$mediaState3, _states$mediaState4, _states$mediaState5; return { isMediaViewerVisible: (_states$mediaState3 = states.mediaState) === null || _states$mediaState3 === void 0 ? void 0 : _states$mediaState3.isMediaViewerVisible, mediaViewerSelectedMedia: (_states$mediaState4 = states.mediaState) === null || _states$mediaState4 === void 0 ? void 0 : _states$mediaState4.mediaViewerSelectedMedia, mediaClientConfig: (_states$mediaState5 = states.mediaState) === null || _states$mediaState5 === void 0 ? void 0 : _states$mediaState5.mediaClientConfig }; }; var MediaViewerFunctionalComponent = function MediaViewerFunctionalComponent(_ref2) { var api = _ref2.api, editorView = _ref2.editorView, mediaViewerExtensions = _ref2.mediaViewerExtensions; // Only traverse document once when media viewer is visible, media viewer items will not update // when document changes are made while media viewer is open var _useSharedPluginState2 = useSharedPluginStateWithSelector(api, ['media'], mediaViewerStateSelector), isMediaViewerVisible = _useSharedPluginState2.isMediaViewerVisible, mediaViewerSelectedMedia = _useSharedPluginState2.mediaViewerSelectedMedia, mediaClientConfig = _useSharedPluginState2.mediaClientConfig; var mediaItems = useMemo(function () { if (isMediaViewerVisible) { var mediaNodes = extractMediaNodes(editorView.state.doc); return createMediaIdentifierArray(mediaNodes); } // eslint-disable-next-line react-hooks/exhaustive-deps -- only update mediaItems once when media viewer is visible }, [isMediaViewerVisible]); // Viewer does not have required attributes to render the media viewer if (!isMediaViewerVisible || !mediaViewerSelectedMedia || !mediaClientConfig) { return null; } var handleOnClose = function handleOnClose() { // Run Command to hide the media viewer api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 ? void 0 : api.media.commands.hideMediaViewer); }; return /*#__PURE__*/React.createElement(RenderMediaViewer, { mediaClientConfig: mediaClientConfig, onClose: handleOnClose, selectedNodeAttrs: mediaViewerSelectedMedia, items: mediaItems, extensions: mediaViewerExtensions }); }; export var mediaPlugin = function mediaPlugin(_ref3) { var _api$analytics3; var _ref3$config = _ref3.config, options = _ref3$config === void 0 ? {} : _ref3$config, api = _ref3.api; var previousMediaProvider; var mediaErrorLocalIds = new Set(); return { name: 'media', getSharedState: function getSharedState(editorState) { if (!editorState) { return null; } return stateKey.getState(editorState) || null; }, actions: { handleMediaNodeRenderError: function handleMediaNodeRenderError(node, reason, nestedUnder) { var _api$analytics; var isDuplicateError = false; if (expValEquals('platform_editor_media_reliability_observability', 'isEnabled', true)) { if (mediaErrorLocalIds.has(node.attrs.localId)) { // we mark duplicate errors in the case of nested media nodes // renderering to avoid firing multiple errored events for the same underlying issue, // which can happen more often with nested media nodes isDuplicateError = true; } else { mediaErrorLocalIds.add(node.attrs.localId); } } api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 || _api$analytics.actions.fireAnalyticsEvent({ action: ACTION.ERRORED, actionSubject: ACTION_SUBJECT.EDITOR, actionSubjectId: ACTION_SUBJECT_ID.MEDIA, eventType: EVENT_TYPE.UI, attributes: _objectSpread(_objectSpread({ reason: reason, external: node.attrs.__external }, nestedUnder && editorExperiment('platform_synced_block', true) ? { nestedUnder: nestedUnder } : {}), editorExperiment('platform_synced_block', true) ? { isDuplicateError: isDuplicateError } : {}) }); }, insertMediaAsMediaSingle: function insertMediaAsMediaSingle(view, node, inputMethod, insertMediaVia) { var _api$analytics2; return _insertMediaAsMediaSingle(view, node, inputMethod, api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, insertMediaVia, options === null || options === void 0 ? void 0 : options.allowPixelResizing); }, setProvider: function setProvider(provider) { var _api$core$actions$exe; // Prevent someone trying to set the exact same provider twice for performance reasons if (previousMediaProvider === provider) { return false; } previousMediaProvider = provider; return (_api$core$actions$exe = api === null || api === void 0 ? void 0 : api.core.actions.execute(function (_ref4) { var tr = _ref4.tr; return tr.setMeta(stateKey, { mediaProvider: provider }); })) !== null && _api$core$actions$exe !== void 0 ? _api$core$actions$exe : false; } }, commands: { showMediaViewer: showMediaViewer, hideMediaViewer: hideMediaViewer, trackMediaPaste: trackMediaPaste, setAIGenerating: setAIGenerating, clearAIGenerating: clearAIGenerating, insertMediaSingle: insertMediaAsMediaSingleCommand(api === null || api === void 0 || (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions, options.allowPixelResizing) }, nodes: function nodes() { var _ref5 = options || {}, _ref5$allowMediaGroup = _ref5.allowMediaGroup, allowMediaGroup = _ref5$allowMediaGroup === void 0 ? true : _ref5$allowMediaGroup, _ref5$allowMediaSingl = _ref5.allowMediaSingle, allowMediaSingle = _ref5$allowMediaSingl === void 0 ? false : _ref5$allowMediaSingl, _ref5$allowPixelResiz = _ref5.allowPixelResizing, allowPixelResizing = _ref5$allowPixelResiz === void 0 ? false : _ref5$allowPixelResiz, allowCaptions = _ref5.allowCaptions, allowMediaInlineImages = _ref5.allowMediaInlineImages, mediaFeatureFlags = _ref5.featureFlags; var allowMediaInline = fg('platform_editor_remove_media_inline_feature_flag') ? allowMediaInlineImages : getMediaFeatureFlag('mediaInline', mediaFeatureFlags); var mediaSingleOption = { withCaption: allowCaptions, withExtendedWidthTypes: allowPixelResizing }; return [{ name: 'mediaGroup', node: mediaGroupSpecWithFixedToDOM() }, { name: 'mediaSingle', node: mediaSingleSpecWithFixedToDOM(mediaSingleOption) }, { name: 'media', node: mediaSpecWithFixedToDOM() }, { name: 'mediaInline', node: mediaInlineSpecWithFixedToDOM() }].filter(function (node) { if (node.name === 'mediaGroup') { return allowMediaGroup; } if (node.name === 'mediaSingle') { return allowMediaSingle; } if (node.name === 'mediaInline') { return allowMediaInline; } return true; }); }, pmPlugins: function pmPlugins() { var pmPlugins = [{ name: 'media', plugin: function plugin(_ref6) { var schema = _ref6.schema, dispatch = _ref6.dispatch, getIntl = _ref6.getIntl, eventDispatcher = _ref6.eventDispatcher, providerFactory = _ref6.providerFactory, errorReporter = _ref6.errorReporter, portalProviderAPI = _ref6.portalProviderAPI, dispatchAnalyticsEvent = _ref6.dispatchAnalyticsEvent, nodeViewPortalProviderAPI = _ref6.nodeViewPortalProviderAPI; return createPlugin(schema, { providerFactory: providerFactory, nodeViews: { mediaGroup: lazyMediaGroupView(portalProviderAPI, eventDispatcher, providerFactory, options, api), mediaSingle: ReactMediaSingleNode(portalProviderAPI, eventDispatcher, providerFactory, api, dispatchAnalyticsEvent, options), media: ReactMediaNode(portalProviderAPI, eventDispatcher, providerFactory, options, api), mediaInline: lazyMediaInlineView(portalProviderAPI, eventDispatcher, providerFactory, api, undefined, options === null || options === void 0 ? void 0 : options.fallbackMediaNameFetcher) }, errorReporter: errorReporter, uploadErrorHandler: options && options.uploadErrorHandler, waitForMediaUpload: options && options.waitForMediaUpload, customDropzoneContainer: options && options.customDropzoneContainer, customMediaPicker: options && options.customMediaPicker, allowResizing: !!(options && options.allowResizing) }, getIntl, api, nodeViewPortalProviderAPI, dispatch, options); } }, { name: 'mediaKeymap', plugin: function plugin(_ref7) { var _api$analytics4, _api$selection; var getIntl = _ref7.getIntl; return keymapPlugin(options, api === null || api === void 0 || (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : _api$analytics4.actions, api === null || api === void 0 || (_api$selection = api.selection) === null || _api$selection === void 0 ? void 0 : _api$selection.actions, api === null || api === void 0 ? void 0 : api.width, getIntl); } }]; if (options && options.allowMediaSingle) { pmPlugins.push({ name: 'mediaSingleKeymap', plugin: function plugin(_ref8) { var schema = _ref8.schema; return keymapMediaSinglePlugin(schema); } }); } if (options && options.allowAltTextOnImages) { pmPlugins.push({ name: 'mediaAltText', plugin: createMediaAltTextPlugin }); pmPlugins.push({ name: 'mediaAltTextKeymap', plugin: function plugin(_ref9) { var _api$analytics5; var schema = _ref9.schema; return keymapMediaAltTextPlugin(schema, api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions); } }); } if (options && options.allowLinking) { pmPlugins.push({ name: 'mediaLinking', plugin: function plugin(_ref0) { var dispatch = _ref0.dispatch; return linkingPlugin(dispatch); } }); pmPlugins.push({ name: 'mediaLinkingKeymap', plugin: function plugin(_ref1) { var schema = _ref1.schema; return keymapLinkingPlugin(schema); } }); } if (options && options.allowAdvancedToolBarOptions && options.allowResizing && areToolbarFlagsEnabled(Boolean(api === null || api === void 0 ? void 0 : api.toolbar)) && options.allowPixelResizing) { pmPlugins.push({ name: 'mediaPixelResizing', plugin: createMediaPixelResizingPlugin }); } pmPlugins.push({ name: 'mediaAIGeneratingDecoration', plugin: function plugin() { return createAIGeneratingDecorationPlugin(); } }); pmPlugins.push({ name: 'mediaSelectionHandler', plugin: function plugin() { var mediaSelectionHandlerPlugin = new SafePlugin({ key: new PluginKey('mediaSelectionHandlerPlugin'), props: { handleScrollToSelection: function handleScrollToSelection(view) { var selection = view.state.selection; if (!(selection instanceof NodeSelection) || selection.node.type.name !== 'media') { return false; } var _view$domAtPos = view.domAtPos(selection.from), node = _view$domAtPos.node, offset = _view$domAtPos.offset; if ( // Is the media element mounted already? offset === node.childNodes.length) { // Media is not ready, so stop the scroll request return true; } // Media is ready, keep the scrolling request return false; } } }); return mediaSelectionHandlerPlugin; } }); return pmPlugins; }, contentComponent: function contentComponent(_ref10) { var editorView = _ref10.editorView, appearance = _ref10.appearance; if (!editorView) { return null; } return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(MediaViewerFunctionalComponent, { api: api, editorView: editorView, mediaViewerExtensions: options === null || options === void 0 ? void 0 : options.mediaViewerExtensions }), /*#__PURE__*/React.createElement(MediaPickerFunctionalComponent, { editorDomElement: editorView.dom, appearance: appearance, api: api })); }, secondaryToolbarComponent: function secondaryToolbarComponent(_ref11) { var disabled = _ref11.disabled; return /*#__PURE__*/React.createElement(ToolbarMedia, { isDisabled: disabled, isReducedSpacing: true, api: api }); }, pluginsOptions: { quickInsert: function quickInsert(_ref12) { var formatMessage = _ref12.formatMessage; return !(api !== null && api !== void 0 && api.mediaInsert) ? [{ id: 'media', title: formatMessage(messages.mediaFiles), description: formatMessage(messages.mediaFilesDescription), priority: 400, keywords: ['attachment', 'gif', 'media', 'picture', 'image', 'video', 'file'], icon: function icon() { return /*#__PURE__*/React.createElement(IconImages, null); }, isDisabledOffline: true, action: function action(insert, state) { var _api$analytics6; var pluginState = stateKey.getState(state); pluginState === null || pluginState === void 0 || pluginState.showMediaPicker(); var tr = insert(''); api === null || api === void 0 || (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 || _api$analytics6.actions.attachAnalyticsEvent({ action: ACTION.OPENED, actionSubject: ACTION_SUBJECT.PICKER, actionSubjectId: ACTION_SUBJECT_ID.PICKER_CLOUD, attributes: { inputMethod: INPUT_METHOD.QUICK_INSERT }, eventType: EVENT_TYPE.UI })(tr); return tr; } }] : []; }, floatingToolbar: function floatingToolbar(state, intl, providerFactory) { var _api$editorViewMode; return _floatingToolbar(state, intl, { providerFactory: providerFactory, allowMediaInline: options && getMediaFeatureFlag('mediaInline', options.featureFlags), allowResizing: options && options.allowResizing, allowResizingInTables: options && options.allowResizingInTables, allowCommentsOnMedia: options && options.allowCommentsOnMedia, allowLinking: options && options.allowLinking, allowAdvancedToolBarOptions: options && options.allowAdvancedToolBarOptions, allowAltTextOnImages: options && options.allowAltTextOnImages, allowImageEditing: !!(api !== null && api !== void 0 && api.mediaEditing) && options && options.allowImageEditing, allowImagePreview: options && options.allowImagePreview, altTextValidator: options && options.altTextValidator, fullWidthEnabled: options && options.fullWidthEnabled, allowMediaInlineImages: options && options.allowMediaInlineImages, isViewOnly: (api === null || api === void 0 || (_api$editorViewMode = api.editorViewMode) === null || _api$editorViewMode === void 0 || (_api$editorViewMode = _api$editorViewMode.sharedState.currentState()) === null || _api$editorViewMode === void 0 ? void 0 : _api$editorViewMode.mode) === 'view', allowPixelResizing: options === null || options === void 0 ? void 0 : options.allowPixelResizing, onCommentButtonMount: options === null || options === void 0 ? void 0 : options.onCommentButtonMount, createCommentExperience: options === null || options === void 0 ? void 0 : options.createCommentExperience }, api); } } }; };