@atlaskit/editor-plugin-insert-block
Version:
Insert block plugin for @atlaskit/editor-core
240 lines (236 loc) • 11.4 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
/**
* @jsxRuntime classic
* @jsx jsx
*/
import { useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react';
/* eslint-disable @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports, jsdoc/require-description -- Ignored via go/DSP-18766; jsdoc debt surfaced by this mechanical PR */
import { css, jsx } from '@emotion/react';
import { useIntl } from 'react-intl';
import { CellMeasurerCache } from 'react-virtualized/dist/commonjs/CellMeasurer';
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
import { ELEMENT_ITEM_HEIGHT, ElementBrowser } from '@atlaskit/editor-common/element-browser';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
import { messages, IconCode, IconDate, IconDecision, IconDivider, IconExpand, IconPanel, IconQuote, IconStatus } from '@atlaskit/editor-common/quick-insert';
import { OutsideClickTargetRefContext, withReactEditorViewOuterListeners as withOuterListeners } from '@atlaskit/editor-common/ui-react';
import { isOfflineMode } from '@atlaskit/editor-plugin-connectivity';
import { expVal, expValNoExposure } from '@atlaskit/tmp-editor-statsig/expVal';
export const DEFAULT_HEIGHT = 560;
/**
* Exported helper to allow testing of InsertMenu pinning logic.
*
* The `cc_fd_db_top_editor_toolbar` experiment adds new logic to sort elements by `priority`.
* This newer implementation matches how the quick insert menu sorts elements.
*/
export const sortFeaturedItems = (featuredItems, formatMessage) => {
if (['new-description', 'orig-description'].includes(expVal('cc_fd_db_top_editor_toolbar', 'cohort', 'control')) || expValNoExposure('cc_fd_wb_jira_quick_insert_experiment', 'isEnabled', false) || ['slot-two', 'slot-four'].includes(expValNoExposure('cc_fd_cwr_quick_insert', 'cohort', 'control'))) {
// Sort by priority (lower first) on the concatenated list so items
// with "priority" are at the top (e.g. Whiteboard before Database)
return featuredItems.slice(0).sort((a, b) => (a.priority || Number.POSITIVE_INFINITY) - (b.priority || Number.POSITIVE_INFINITY));
}
// NOTE: this is *not* the ideal way to approach this. Old logic sort whiteboards to top
const DIAGRAM_KEY = 'whiteboard-extension:create-diagram';
const isDiagram = item => item.key === DIAGRAM_KEY;
const featuredWhiteboardsPresent = featuredItems.some(isDiagram);
if (featuredWhiteboardsPresent) {
const pin = key => {
const idx = featuredItems.findIndex(item => item.key === key);
const filtered = featuredItems.filter(item => !isDiagram(item));
if (idx === -1) {
return filtered;
}
const picked = {
...featuredItems[idx],
description: formatMessage(messages.featuredWhiteboardDescription)
};
return [picked, ...filtered];
};
return pin(DIAGRAM_KEY);
}
return featuredItems;
};
const selector = states => {
var _states$connectivityS;
return {
connectivityMode: (_states$connectivityS = states.connectivityState) === null || _states$connectivityS === void 0 ? void 0 : _states$connectivityS.mode
};
};
const InsertMenu = ({
editorView,
dropdownItems,
showElementBrowserLink,
onInsert,
toggleVisiblity,
pluginInjectionApi
}) => {
var _pluginInjectionApi$q8, _pluginInjectionApi$q9, _pluginInjectionApi$q0;
const [itemCount, setItemCount] = useState(0);
const [height, setHeight] = useState(DEFAULT_HEIGHT);
const {
formatMessage
} = useIntl();
const cache = useMemo(() => {
return new CellMeasurerCache({
fixedWidth: true,
defaultHeight: ELEMENT_ITEM_HEIGHT,
minHeight: ELEMENT_ITEM_HEIGHT
});
}, []);
useLayoutEffect(() => {
// Figure based on visuals to exclude the searchbar, padding/margin, and the ViewMore item.
const EXTRA_SPACE_EXCLUDING_ELEMENTLIST = 128;
const totalItemHeight =
// eslint-disable-next-line @atlassian/perf-linting/no-expensive-computations-in-render -- Ignored via go/ees017 (to be fixed)
[...Array(itemCount)].reduce((sum, _, index) => sum + cache.rowHeight({
index
}), 0) + EXTRA_SPACE_EXCLUDING_ELEMENTLIST;
if (itemCount > 0 && totalItemHeight < DEFAULT_HEIGHT) {
setHeight(totalItemHeight);
} else {
setHeight(DEFAULT_HEIGHT);
}
}, [cache, itemCount]);
const transform = useCallback(item => ({
title: item.content,
description: item.tooltipDescription,
keyshortcut: item.shortcut,
icon: () => getSvgIconForItem({
name: item.value.name
}) || item.elemBefore,
/**
* @note This transformed items action is only used when a quick insert item has been
* called from the quick insert menu and a search has not been performed.
*/
action: () => onInsert({
item
}),
// "insertInsertMenuItem" expects these 2 properties.
onClick: item.onClick,
value: item.value
}), [onInsert]);
const quickInsertDropdownItems = dropdownItems.map(transform);
const onInsertItem = useCallback(item => {
var _pluginInjectionApi$q;
toggleVisiblity();
if (!editorView.hasFocus()) {
editorView.focus();
}
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$q = pluginInjectionApi.quickInsert) === null || _pluginInjectionApi$q === void 0 ? void 0 : _pluginInjectionApi$q.actions.insertItem(item, INPUT_METHOD.TOOLBAR)(editorView.state, editorView.dispatch);
}, [editorView, toggleVisiblity, pluginInjectionApi]);
const {
connectivityMode
} = useSharedPluginStateWithSelector(pluginInjectionApi, ['connectivity'], selector);
const getItems = useCallback((query, category) => {
let result;
/**
* @warning The results if there is a query are not the same as the results if there is no query.
* For example: If you have a typed panel and then select the panel item then it will call a different action
* than is specified on the editor plugins quick insert
* @see above transform function for more details.
*/
if (query) {
var _pluginInjectionApi$q2, _pluginInjectionApi$q3, _pluginInjectionApi$q4;
result = (_pluginInjectionApi$q2 = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$q3 = pluginInjectionApi.quickInsert) === null || _pluginInjectionApi$q3 === void 0 ? void 0 : (_pluginInjectionApi$q4 = _pluginInjectionApi$q3.actions.getSuggestions({
query,
category
})) === null || _pluginInjectionApi$q4 === void 0 ? void 0 : _pluginInjectionApi$q4.map(item => isOfflineMode(connectivityMode) && item.isDisabledOffline ? {
...item,
isDisabled: true
} : item)) !== null && _pluginInjectionApi$q2 !== void 0 ? _pluginInjectionApi$q2 : [];
} else {
var _pluginInjectionApi$q5, _pluginInjectionApi$q6, _pluginInjectionApi$q7;
const featuredQuickInsertSuggestions = (_pluginInjectionApi$q5 = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$q6 = pluginInjectionApi.quickInsert) === null || _pluginInjectionApi$q6 === void 0 ? void 0 : (_pluginInjectionApi$q7 = _pluginInjectionApi$q6.actions.getSuggestions({
category,
featuredItems: true
})) === null || _pluginInjectionApi$q7 === void 0 ? void 0 : _pluginInjectionApi$q7.map(item => isOfflineMode(connectivityMode) && item.isDisabledOffline ? {
...item,
isDisabled: true
} : item)) !== null && _pluginInjectionApi$q5 !== void 0 ? _pluginInjectionApi$q5 : [];
const unfilteredResult = quickInsertDropdownItems.concat(featuredQuickInsertSuggestions);
// need to sort on the concatenated list so desired elements are at the top
result = sortFeaturedItems(unfilteredResult, formatMessage);
}
setItemCount(result.length);
return result;
}, [pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$q8 = pluginInjectionApi.quickInsert) === null || _pluginInjectionApi$q8 === void 0 ? void 0 : _pluginInjectionApi$q8.actions, quickInsertDropdownItems, connectivityMode, formatMessage]);
const emptyStateHandler = pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$q9 = pluginInjectionApi.quickInsert) === null || _pluginInjectionApi$q9 === void 0 ? void 0 : (_pluginInjectionApi$q0 = _pluginInjectionApi$q9.sharedState.currentState()) === null || _pluginInjectionApi$q0 === void 0 ? void 0 : _pluginInjectionApi$q0.emptyStateHandler;
const onViewMore = useCallback(() => {
var _pluginInjectionApi$c, _pluginInjectionApi$q1;
toggleVisiblity();
pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$c = pluginInjectionApi.core) === null || _pluginInjectionApi$c === void 0 ? void 0 : _pluginInjectionApi$c.actions.execute(pluginInjectionApi === null || pluginInjectionApi === void 0 ? void 0 : (_pluginInjectionApi$q1 = pluginInjectionApi.quickInsert) === null || _pluginInjectionApi$q1 === void 0 ? void 0 : _pluginInjectionApi$q1.commands.openElementBrowserModal);
}, [pluginInjectionApi, toggleVisiblity]);
return (
// eslint-disable-next-line @atlaskit/design-system/consistent-css-prop-usage
jsx("div", {
css: insertMenuWrapper(height)
}, jsx(ElementBrowserWrapper, {
handleClickOutside: toggleVisiblity,
handleEscapeKeydown: toggleVisiblity,
closeOnTab: false
}, jsx(ElementBrowser, {
mode: "inline",
getItems: getItems,
emptyStateHandler: emptyStateHandler,
onInsertItem: onInsertItem,
showSearch: true,
showCategories: false
// On page resize we want the InlineElementBrowser to show updated tools/overflow items
,
key: quickInsertDropdownItems.length,
onViewMore: showElementBrowserLink ? onViewMore : undefined,
cache: cache
})))
);
};
const getSvgIconForItem = ({
name
}) => {
const Icon = {
codeblock: IconCode,
panel: IconPanel,
blockquote: IconQuote,
decision: IconDecision,
horizontalrule: IconDivider,
expand: IconExpand,
date: IconDate,
status: IconStatus
}[name];
return Icon ? jsx(Icon, {
label: ""
}) : undefined;
};
const insertMenuWrapper = height => {
return css({
display: 'flex',
flexDirection: 'column',
width: '320px',
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-unsafe-values, @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
height: `${height}px`,
backgroundColor: `${"var(--ds-surface-overlay, #FFFFFF)"}`,
borderRadius: "var(--ds-radius-small, 3px)",
boxShadow: `${"var(--ds-shadow-overlay, 0px 8px 12px #1E1F2126, 0px 0px 1px #1E1F214f)"}`
});
};
const flexWrapperStyles = css({
display: 'flex',
flex: 1,
boxSizing: 'border-box',
overflow: 'hidden'
});
const FlexWrapper = props => {
const setOutsideClickTargetRef = useContext(OutsideClickTargetRefContext);
const {
children,
...divProps
} = props;
return (
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
jsx("div", _extends({
ref: setOutsideClickTargetRef,
css: flexWrapperStyles
}, divProps), children)
);
};
const ElementBrowserWrapper = withOuterListeners(FlexWrapper);
export default InsertMenu;