@atlaskit/editor-plugin-insert-block
Version:
Insert block plugin for @atlaskit/editor-core
212 lines (209 loc) • 10.2 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 { CellMeasurerCache } from 'react-virtualized/dist/commonjs/CellMeasurer';
import { INPUT_METHOD } from '@atlaskit/editor-common/analytics/types/enums';
import { IconCode, IconDate, IconDecision, IconDivider, IconExpand, IconPanel, IconQuote, IconStatus } from '@atlaskit/editor-common/assets';
import { isOfflineMode } from '@atlaskit/editor-common/connectivity/isOfflineMode';
import { ELEMENT_ITEM_HEIGHT } from '@atlaskit/editor-common/element-browser/constants';
import ElementBrowser from '@atlaskit/editor-common/element-browser/ElementBrowserLoader';
import { OutsideClickTargetRefContext } from '@atlaskit/editor-common/ui-react';
import withOuterListeners from '@atlaskit/editor-common/ui-react/with-react-editor-view-outer-listeners';
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/useSharedPluginStateWithSelector';
export const DEFAULT_HEIGHT = 560;
/**
* Sort featured elements by priority, matching the quick insert menu.
*/
export const sortFeaturedItems = featuredItems => {
return featuredItems.slice(0).sort((a, b) => {
var _a$priority, _b$priority;
return ((_a$priority = a.priority) !== null && _a$priority !== void 0 ? _a$priority : Number.POSITIVE_INFINITY) - ((_b$priority = b.priority) !== null && _b$priority !== void 0 ? _b$priority : Number.POSITIVE_INFINITY);
});
};
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 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);
}
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]);
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;