@atlaskit/editor-plugin-media-insert
Version:
Media Insert plugin for @atlaskit/editor-core
359 lines (358 loc) • 17.1 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _extends from "@babel/runtime/helpers/extends";
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 from 'react';
import { useIntl } from 'react-intl';
import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD, MEDIA_INSERT_TAB } from '@atlaskit/editor-common/analytics';
import { getDomRefFromSelection } from '@atlaskit/editor-common/get-dom-ref-from-selection';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { mediaInsertMessages } from '@atlaskit/editor-common/messages';
import { PlainOutsideClickTargetRefContext, Popup, withOuterListeners } from '@atlaskit/editor-common/ui';
import { akEditorFloatingDialogZIndex } from '@atlaskit/editor-shared-styles';
import { Box, Focusable, Text } from '@atlaskit/primitives/compiled';
import Tabs, { TabList, useTab, useTabPanel } from '@atlaskit/tabs';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import { useFocusEditor } from './hooks/use-focus-editor';
import { useUnholyAutofocus } from './hooks/use-unholy-autofocus';
import { LocalMedia } from './LocalMedia';
import { MediaFromURL } from './MediaFromURL';
import { MediaInsertWrapper } from './MediaInsertWrapper';
var PopupWithListeners = withOuterListeners(Popup);
var MEDIA_INSERT_PICKER_ANALYTICS_SOURCE = 'MediaInsertPicker';
var EMPTY_REGISTERED_TABS = [];
var getMediaInsertPickerTabSource = function getMediaInsertPickerTabSource(selectedTab) {
return "".concat(MEDIA_INSERT_PICKER_ANALYTICS_SOURCE, " - ").concat(selectedTab);
};
var getNextTabIndexForKey = function getNextTabIndexForKey(currentTabIndex, tabCount, key) {
if (key === 'Home') {
return 0;
}
if (key === 'End') {
return tabCount - 1;
}
if (key === 'ArrowRight') {
return currentTabIndex === tabCount - 1 ? 0 : currentTabIndex + 1;
}
if (key === 'ArrowLeft') {
return currentTabIndex === 0 ? tabCount - 1 : currentTabIndex - 1;
}
return undefined;
};
var getLinkTabIndex = function getLinkTabIndex(registeredTabCount, isOnlyExternalLinks) {
return registeredTabCount + (isOnlyExternalLinks ? 0 : 1);
};
var TabWithAnalytics = function TabWithAnalytics(_ref) {
var children = _ref.children,
onSelectTabForAnalytics = _ref.onSelectTabForAnalytics,
selectedTabIndex = _ref.selectedTabIndex,
tabCount = _ref.tabCount;
var _useTab = useTab(),
onClick = _useTab.onClick,
id = _useTab.id,
ariaControls = _useTab['aria-controls'],
ariaPosinset = _useTab['aria-posinset'],
ariaSelected = _useTab['aria-selected'],
ariaSetsize = _useTab['aria-setsize'],
onKeyDown = _useTab.onKeyDown,
role = _useTab.role,
tabIndex = _useTab.tabIndex;
var handleClick = React.useCallback(function () {
onSelectTabForAnalytics(selectedTabIndex);
onClick();
}, [onClick, onSelectTabForAnalytics, selectedTabIndex]);
var handleKeyDown = React.useCallback(function (event) {
var nextTabIndex = getNextTabIndexForKey(selectedTabIndex, tabCount, event.key);
if (nextTabIndex !== undefined) {
onSelectTabForAnalytics(nextTabIndex);
}
onKeyDown(event);
}, [onKeyDown, onSelectTabForAnalytics, selectedTabIndex, tabCount]);
return /*#__PURE__*/React.createElement(Focusable, {
as: "div",
isInset: true,
onClick: handleClick,
id: id,
"aria-controls": ariaControls,
"aria-posinset": ariaPosinset,
"aria-selected": ariaSelected,
"aria-setsize": ariaSetsize,
onKeyDown: handleKeyDown,
role: role,
tabIndex: tabIndex
}, /*#__PURE__*/React.createElement(Text, {
weight: "medium",
color: "inherit",
maxLines: 1
}, children));
};
/**
* A custom TabPanel that is non-focusable.
*/
var CustomTabPanel = function CustomTabPanel(_ref2) {
var children = _ref2.children,
_ref2$disablePaddingB = _ref2.disablePaddingBlockEnd,
disablePaddingBlockEnd = _ref2$disablePaddingB === void 0 ? false : _ref2$disablePaddingB;
var tabPanelAttributes = useTabPanel();
return /*#__PURE__*/React.createElement(Box, _extends({
paddingBlockEnd: disablePaddingBlockEnd ? 'space.0' : 'space.150'
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
}, tabPanelAttributes, {
tabIndex: -1
}), children);
};
export var MediaInsertPicker = function MediaInsertPicker(_ref3) {
var _api$mediaInsert$acti, _api$mediaInsert, _api$mediaInsert$getI;
var api = _ref3.api,
editorView = _ref3.editorView,
dispatchAnalyticsEvent = _ref3.dispatchAnalyticsEvent,
popupsMountPoint = _ref3.popupsMountPoint,
popupsBoundariesElement = _ref3.popupsBoundariesElement,
popupsScrollableElement = _ref3.popupsScrollableElement,
_closeMediaInsertPicker = _ref3.closeMediaInsertPicker,
insertMediaSingle = _ref3.insertMediaSingle,
insertExternalMediaSingle = _ref3.insertExternalMediaSingle,
insertFile = _ref3.insertFile,
_ref3$isOnlyExternalL = _ref3.isOnlyExternalLinks,
isOnlyExternalLinks = _ref3$isOnlyExternalL === void 0 ? false : _ref3$isOnlyExternalL,
customizedUrlValidation = _ref3.customizedUrlValidation,
customizedHelperMessage = _ref3.customizedHelperMessage;
// Tabs registered by other plugins via `api.mediaInsert.actions.registerInsertTab(...)`.
// Read once per render; the registry is mutated only at plugin setup time so this is stable
// for the lifetime of an editor instance.
var registeredTabs = (_api$mediaInsert$acti = api === null || api === void 0 || (_api$mediaInsert = api.mediaInsert) === null || _api$mediaInsert === void 0 || (_api$mediaInsert = _api$mediaInsert.actions) === null || _api$mediaInsert === void 0 || (_api$mediaInsert$getI = _api$mediaInsert.getInsertTabs) === null || _api$mediaInsert$getI === void 0 ? void 0 : _api$mediaInsert$getI.call(_api$mediaInsert)) !== null && _api$mediaInsert$acti !== void 0 ? _api$mediaInsert$acti : EMPTY_REGISTERED_TABS;
var _useSharedPluginState = useSharedPluginStateWithSelector(api, ['media', 'mediaInsert'], function (states) {
var _states$mediaState, _states$mediaInsertSt, _states$mediaInsertSt2;
return {
mediaProvider: (_states$mediaState = states.mediaState) === null || _states$mediaState === void 0 ? void 0 : _states$mediaState.mediaProvider,
isOpen: (_states$mediaInsertSt = states.mediaInsertState) === null || _states$mediaInsertSt === void 0 ? void 0 : _states$mediaInsertSt.isOpen,
mountInfo: (_states$mediaInsertSt2 = states.mediaInsertState) === null || _states$mediaInsertSt2 === void 0 ? void 0 : _states$mediaInsertSt2.mountInfo
};
}),
mediaProvider = _useSharedPluginState.mediaProvider,
isOpen = _useSharedPluginState.isOpen,
mountInfo = _useSharedPluginState.mountInfo;
var targetRef;
var mountPoint;
if (mountInfo) {
targetRef = mountInfo.ref;
mountPoint = mountInfo.mountPoint;
} else {
var _api$analytics;
// If targetRef is undefined, target the selection in the editor
targetRef = getDomRefFromSelection(editorView, ACTION_SUBJECT_ID.PICKER_MEDIA, api === null || api === void 0 || (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
mountPoint = popupsMountPoint;
}
var intl = useIntl();
var focusEditor = useFocusEditor({
editorView: editorView
});
var _useUnholyAutofocus = useUnholyAutofocus(),
autofocusRef = _useUnholyAutofocus.autofocusRef,
onPositionCalculated = _useUnholyAutofocus.onPositionCalculated;
var tabCount = registeredTabs.length + (isOnlyExternalLinks ? 1 : 2);
var linkTabIndex = getLinkTabIndex(registeredTabs.length, isOnlyExternalLinks);
var getTabAnalyticsMetadata = React.useCallback(function (selectedTabIndex) {
var registeredTab = registeredTabs[selectedTabIndex];
if (registeredTab) {
return {
selectedTab: registeredTab.key,
selectedTabIndex: selectedTabIndex
};
}
var uploadTabIndex = registeredTabs.length;
if (!isOnlyExternalLinks && selectedTabIndex === uploadTabIndex) {
return {
selectedTab: MEDIA_INSERT_TAB.UPLOAD,
selectedTabIndex: selectedTabIndex
};
}
if (selectedTabIndex === linkTabIndex) {
return {
selectedTab: MEDIA_INSERT_TAB.LINK,
selectedTabIndex: selectedTabIndex
};
}
return {
selectedTab: 'unknown',
selectedTabIndex: selectedTabIndex
};
}, [isOnlyExternalLinks, linkTabIndex, registeredTabs]);
var selectedTabAnalyticsMetadataRef = React.useRef(getTabAnalyticsMetadata(0));
var tabsAnalyticsContext = React.useMemo(function () {
return {
get source() {
return getMediaInsertPickerTabSource(selectedTabAnalyticsMetadataRef.current.selectedTab);
}
};
}, []);
var setSelectedTabAnalyticsMetadata = React.useCallback(function (selectedTabIndex) {
selectedTabAnalyticsMetadataRef.current = getTabAnalyticsMetadata(selectedTabIndex);
}, [getTabAnalyticsMetadata]);
var hasDispatchedInitialTabViewedEventRef = React.useRef(false);
// Atlaskit Tabs only calls `onChange` after the user changes tabs, so the
// initially opened tab needs its viewed analytics event dispatched from an
// effect. The ref keeps this to once per picker open without setting state.
React.useEffect(function () {
if (!isOpen) {
hasDispatchedInitialTabViewedEventRef.current = false;
return;
}
if (!mediaProvider || !dispatchAnalyticsEvent || hasDispatchedInitialTabViewedEventRef.current) {
return;
}
var selectedTabMetadata = getTabAnalyticsMetadata(0);
selectedTabAnalyticsMetadataRef.current = selectedTabMetadata;
var payload = {
action: ACTION.VIEWED,
actionSubject: ACTION_SUBJECT.PICKER,
actionSubjectId: ACTION_SUBJECT_ID.PICKER_MEDIA,
eventType: EVENT_TYPE.UI,
attributes: {
selectedTab: selectedTabMetadata.selectedTab,
selectedTabIndex: selectedTabMetadata.selectedTabIndex
}
};
dispatchAnalyticsEvent(payload);
hasDispatchedInitialTabViewedEventRef.current = true;
}, [dispatchAnalyticsEvent, getTabAnalyticsMetadata, isOpen, mediaProvider]);
var handleTabChange = React.useCallback(function (selectedTabIndex, analyticsEvent) {
var selectedTabMetadata = getTabAnalyticsMetadata(selectedTabIndex);
selectedTabAnalyticsMetadataRef.current = selectedTabMetadata;
analyticsEvent.update(function (payload) {
return _objectSpread(_objectSpread({}, payload), {}, {
attributes: _objectSpread(_objectSpread({}, payload.attributes), {}, {
selectedTab: selectedTabMetadata.selectedTab,
selectedTabIndex: selectedTabIndex
})
});
}).fire();
if (dispatchAnalyticsEvent) {
var payload = {
action: ACTION.VIEWED,
actionSubject: ACTION_SUBJECT.PICKER,
actionSubjectId: ACTION_SUBJECT_ID.PICKER_MEDIA,
eventType: EVENT_TYPE.UI,
attributes: {
selectedTab: selectedTabMetadata.selectedTab,
selectedTabIndex: selectedTabIndex
}
};
dispatchAnalyticsEvent(payload);
}
}, [dispatchAnalyticsEvent, getTabAnalyticsMetadata]);
if (!isOpen || !mediaProvider) {
return null;
}
var handleClose = function handleClose(exitMethod) {
return function (event) {
// Same as AIImageGenerationPopup: react-select can detach the option
// before `click` fires, so withOuterListeners treats it as outside.
if (exitMethod === INPUT_METHOD.MOUSE && event.target instanceof Node && !event.target.isConnected) {
return;
}
event.preventDefault();
if (dispatchAnalyticsEvent) {
var payload = {
action: ACTION.CLOSED,
actionSubject: ACTION_SUBJECT.PICKER,
actionSubjectId: ACTION_SUBJECT_ID.PICKER_MEDIA,
eventType: EVENT_TYPE.UI,
attributes: {
exitMethod: exitMethod
}
};
dispatchAnalyticsEvent(payload);
}
_closeMediaInsertPicker();
focusEditor();
};
};
var fileTabTitle = expValEqualsNoExposure('cc_page_experiences_editor_image_generation', 'isEnabled', true) ? intl.formatMessage(mediaInsertMessages.uploadTabTitle) : intl.formatMessage(mediaInsertMessages.fileTabTitle);
return /*#__PURE__*/React.createElement(PopupWithListeners, {
ariaLabel: intl.formatMessage(mediaInsertMessages.mediaPickerPopupAriaLabel)
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
offset: [0, 12],
target: targetRef,
zIndex: akEditorFloatingDialogZIndex,
fitHeight: 390,
fitWidth: 340,
mountTo: mountPoint,
boundariesElement: popupsBoundariesElement,
handleClickOutside: handleClose(INPUT_METHOD.MOUSE),
handleEscapeKeydown: handleClose(INPUT_METHOD.KEYBOARD),
scrollableElement: popupsScrollableElement,
preventOverflow: true,
onPositionCalculated: onPositionCalculated,
focusTrap: true
}, /*#__PURE__*/React.createElement(PlainOutsideClickTargetRefContext.Consumer, null, function (setOutsideClickTargetRef) {
return /*#__PURE__*/React.createElement(MediaInsertWrapper, {
ref: setOutsideClickTargetRef
}, /*#__PURE__*/React.createElement(Tabs, {
id: "media-insert-tab-navigation",
analyticsContext: tabsAnalyticsContext,
onChange: handleTabChange
}, /*#__PURE__*/React.createElement(Box, {
paddingBlockEnd: "space.150"
}, /*#__PURE__*/React.createElement(TabList, null, registeredTabs.map(function (tab, index) {
return /*#__PURE__*/React.createElement(TabWithAnalytics, {
key: tab.key,
onSelectTabForAnalytics: setSelectedTabAnalyticsMetadata,
selectedTabIndex: index,
tabCount: tabCount
}, tab.label);
}), !isOnlyExternalLinks && /*#__PURE__*/React.createElement(TabWithAnalytics, {
onSelectTabForAnalytics: setSelectedTabAnalyticsMetadata,
selectedTabIndex: registeredTabs.length,
tabCount: tabCount
}, fileTabTitle), /*#__PURE__*/React.createElement(TabWithAnalytics, {
onSelectTabForAnalytics: setSelectedTabAnalyticsMetadata,
selectedTabIndex: linkTabIndex,
tabCount: tabCount
}, intl.formatMessage(mediaInsertMessages.linkTabTitle)))), registeredTabs.map(function (_ref4) {
var key = _ref4.key,
TabComponent = _ref4.component;
return /*#__PURE__*/React.createElement(CustomTabPanel, {
key: key,
disablePaddingBlockEnd: true
}, /*#__PURE__*/React.createElement(TabComponent
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
, {
closeMediaInsertPicker: function closeMediaInsertPicker() {
_closeMediaInsertPicker();
focusEditor();
},
dispatchAnalyticsEvent: dispatchAnalyticsEvent,
insertMediaSingle: insertMediaSingle,
mediaProvider: mediaProvider
}));
}), !isOnlyExternalLinks && /*#__PURE__*/React.createElement(CustomTabPanel, null, /*#__PURE__*/React.createElement(LocalMedia, {
ref: autofocusRef,
mediaProvider: mediaProvider
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
closeMediaInsertPicker: function closeMediaInsertPicker() {
_closeMediaInsertPicker();
focusEditor();
},
dispatchAnalyticsEvent: dispatchAnalyticsEvent,
insertFile: insertFile
})), /*#__PURE__*/React.createElement(CustomTabPanel, null, /*#__PURE__*/React.createElement(MediaFromURL, {
mediaProvider: mediaProvider,
dispatchAnalyticsEvent: dispatchAnalyticsEvent
// eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed)
,
closeMediaInsertPicker: function closeMediaInsertPicker() {
_closeMediaInsertPicker();
focusEditor();
},
insertMediaSingle: insertMediaSingle,
insertExternalMediaSingle: insertExternalMediaSingle,
isOnlyExternalLinks: isOnlyExternalLinks,
customizedUrlValidation: customizedUrlValidation,
customizedHelperMessage: customizedHelperMessage
}))));
}));
};