@atlaskit/editor-plugin-media-insert
Version:
Media Insert plugin for @atlaskit/editor-core
396 lines (395 loc) • 18.4 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 { DEFAULT_MEDIA_INSERT_TAB_RANK, MEDIA_INSERT_TAB_RANK } from '@atlaskit/editor-common/media-insert/rank';
import { mediaInsertMessages } from '@atlaskit/editor-common/messages';
import { PlainOutsideClickTargetRefContext, Popup, withOuterListeners } from '@atlaskit/editor-common/ui';
import { akEditorFloatingDialogZIndex } from '@atlaskit/editor-shared-styles';
import { isExperimentEnabled } from '@atlaskit/platform-feature-experiments/is-experiment-enabled';
import { Box, Focusable, Text } from '@atlaskit/primitives/compiled';
import Tabs from '@atlaskit/tabs/tabs';
import TabList from '@atlaskit/tabs/tab-list';
import useTab from '@atlaskit/tabs/use-tab';
import useTabPanel from '@atlaskit/tabs/use-tab-panel';
import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
import { useFocus } from './hooks/use-focus';
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 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 focusButton = useFocus({
target: targetRef !== null && targetRef !== void 0 ? targetRef : editorView
});
var focusEditor = useFocus({
target: editorView
});
// returnFocusRef stores which function we call on unmount: focusButton | focusEditor
// Needs to be a ref so we can instantly toggle it before unmounting triggers the focus trap
var returnFocusRef = React.useRef(focusEditor);
// When inserting media, close picker and focus editor.
var closeAndFocusEditor = React.useCallback(function () {
returnFocusRef.current = focusEditor;
closeMediaInsertPicker();
}, [closeMediaInsertPicker, focusEditor]);
// When cancelling, close picker and return focus to button.
var closeAndReturnFocusToButton = React.useCallback(function () {
returnFocusRef.current = focusButton;
closeMediaInsertPicker();
}, [closeMediaInsertPicker, focusButton]);
var intl = useIntl();
var _useUnholyAutofocus = useUnholyAutofocus(),
autofocusRef = _useUnholyAutofocus.autofocusRef,
onPositionCalculated = _useUnholyAutofocus.onPositionCalculated;
var orderedTabs = React.useMemo(function () {
var tabs = [];
var tieBreaker = 0;
if (!isOnlyExternalLinks) {
tabs.push({
key: MEDIA_INSERT_TAB.UPLOAD,
rank: MEDIA_INSERT_TAB_RANK[MEDIA_INSERT_TAB.UPLOAD],
tieBreaker: tieBreaker++,
type: 'upload'
});
}
tabs.push({
key: MEDIA_INSERT_TAB.LINK,
rank: MEDIA_INSERT_TAB_RANK[MEDIA_INSERT_TAB.LINK],
tieBreaker: tieBreaker++,
type: 'link'
});
registeredTabs.forEach(function (registeredTab) {
var _registeredTab$rank;
tabs.push({
key: registeredTab.key,
rank: (_registeredTab$rank = registeredTab.rank) !== null && _registeredTab$rank !== void 0 ? _registeredTab$rank : DEFAULT_MEDIA_INSERT_TAB_RANK,
registeredTab: registeredTab,
tieBreaker: tieBreaker++,
type: 'registered'
});
});
return tabs.sort(function (a, b) {
return a.rank - b.rank || a.tieBreaker - b.tieBreaker;
});
}, [isOnlyExternalLinks, registeredTabs]);
var tabCount = orderedTabs.length;
var getTabAnalyticsMetadata = React.useCallback(function (selectedTabIndex) {
var selectedTab = orderedTabs[selectedTabIndex];
if (selectedTab) {
return {
selectedTab: selectedTab.key,
selectedTabIndex: selectedTabIndex
};
}
return {
selectedTab: 'unknown',
selectedTabIndex: selectedTabIndex
};
}, [orderedTabs]);
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);
}
if (isExperimentEnabled('platform_editor_fix_focus_MediaInsertPicker')) {
closeAndReturnFocusToButton();
} else {
closeMediaInsertPicker(); // Focuses editor on unmount
focusEditor();
}
};
};
var closePickerAndFocusEditor = function closePickerAndFocusEditor() {
if (isExperimentEnabled('platform_editor_fix_focus_MediaInsertPicker')) {
closeAndFocusEditor();
} else {
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,
onUnmount: isExperimentEnabled('platform_editor_fix_focus_MediaInsertPicker') ? function () {
return returnFocusRef.current();
} : undefined,
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, orderedTabs.map(function (tab, index) {
return /*#__PURE__*/React.createElement(TabWithAnalytics, {
key: tab.key,
onSelectTabForAnalytics: setSelectedTabAnalyticsMetadata,
selectedTabIndex: index,
tabCount: tabCount
}, tab.type === 'registered' ? tab.registeredTab.label : tab.type === 'upload' ? fileTabTitle : intl.formatMessage(mediaInsertMessages.linkTabTitle));
}))), orderedTabs.map(function (tab) {
if (tab.type === 'registered') {
var TabComponent = tab.registeredTab.component;
return /*#__PURE__*/React.createElement(CustomTabPanel, {
key: tab.key,
disablePaddingBlockEnd: true
}, /*#__PURE__*/React.createElement(TabComponent, {
closeMediaInsertPicker: closePickerAndFocusEditor,
dispatchAnalyticsEvent: dispatchAnalyticsEvent,
insertMediaSingle: insertMediaSingle,
mediaProvider: mediaProvider
}));
}
if (tab.type === 'upload') {
return /*#__PURE__*/React.createElement(CustomTabPanel, {
key: tab.key
}, /*#__PURE__*/React.createElement(LocalMedia, {
ref: autofocusRef,
mediaProvider: mediaProvider,
closeMediaInsertPicker: closePickerAndFocusEditor,
dispatchAnalyticsEvent: dispatchAnalyticsEvent,
insertFile: insertFile
}));
}
return /*#__PURE__*/React.createElement(CustomTabPanel, {
key: tab.key
}, /*#__PURE__*/React.createElement(MediaFromURL, {
mediaProvider: mediaProvider,
dispatchAnalyticsEvent: dispatchAnalyticsEvent,
closeMediaInsertPicker: closePickerAndFocusEditor,
cancelMediaInsertPicker: closeAndReturnFocusToButton,
insertMediaSingle: insertMediaSingle,
insertExternalMediaSingle: insertExternalMediaSingle,
isOnlyExternalLinks: isOnlyExternalLinks,
customizedUrlValidation: customizedUrlValidation,
customizedHelperMessage: customizedHelperMessage
}));
})));
}));
};