UNPKG

@atlaskit/editor-plugin-insert-block

Version:

Insert block plugin for @atlaskit/editor-core

240 lines (236 loc) 11.4 kB
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;