UNPKG

@atlaskit/editor-plugin-type-ahead

Version:

Type-ahead plugin for @atlaskit/editor-core

370 lines (357 loc) 18.6 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.TypeAheadPopup = void 0; var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _react = _interopRequireWildcard(require("react")); var _react2 = require("@emotion/react"); var _rafSchd = _interopRequireDefault(require("raf-schd")); var _analytics = require("@atlaskit/editor-common/analytics"); var _hooks = require("@atlaskit/editor-common/hooks"); var _typeAhead = require("@atlaskit/editor-common/type-ahead"); var _ui = require("@atlaskit/editor-common/ui"); var _editorSharedStyles = require("@atlaskit/editor-shared-styles"); var _featureGateJsClient = _interopRequireDefault(require("@atlaskit/feature-gate-js-client")); var _experiments = require("@atlaskit/tmp-editor-statsig/experiments"); var _analytics2 = require("../pm-plugins/analytics"); var _constants = require("../pm-plugins/constants"); var _utils = require("../pm-plugins/utils"); var _TypeAheadErrorFallback = require("./TypeAheadErrorFallback"); var _TypeAheadList = require("./TypeAheadList"); function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); } /** * @jsxRuntime classic * @jsx jsx */ // eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports var DEFAULT_TYPEAHEAD_MENU_HEIGHT = 380; var VIEWMORE_BUTTON_HEIGHT = 53; var DEFAULT_TYPEAHEAD_MENU_HEIGHT_NEW = 480; var ITEM_PADDING = 12; var typeAheadContent = (0, _react2.css)({ background: "var(--ds-surface-overlay, #FFFFFF)", borderRadius: "var(--ds-radius-small, 3px)", boxShadow: "var(--ds-shadow-overlay, 0px 8px 12px #1E1F2126, 0px 0px 1px #1E1F214f)", padding: "var(--ds-space-050, 4px)".concat(" 0"), width: '320px', maxHeight: '380px' /* ~5.5 visibile items */, overflowY: 'auto', MsOverflowStyle: '-ms-autohiding-scrollbar', position: 'relative' }); var typeAheadContentOverride = (0, _react2.css)({ maxHeight: "".concat(DEFAULT_TYPEAHEAD_MENU_HEIGHT_NEW, "px") }); var typeAheadWrapperWithViewMoreOverride = (0, _react2.css)({ display: 'flex', flexDirection: 'column' }); var Highlight = function Highlight(_ref) { var state = _ref.state, triggerHandler = _ref.triggerHandler; if (!(triggerHandler !== null && triggerHandler !== void 0 && triggerHandler.getHighlight)) { return null; } return triggerHandler.getHighlight(state); }; var OFFSET = [0, 8]; var TypeAheadPopup = exports.TypeAheadPopup = /*#__PURE__*/_react.default.memo(function (props) { var _getPluginState; var editorView = props.editorView, triggerHandler = props.triggerHandler, anchorElement = props.anchorElement, popupsMountPoint = props.popupsMountPoint, popupsBoundariesElement = props.popupsBoundariesElement, popupsScrollableElement = props.popupsScrollableElement, items = props.items, errorInfo = props.errorInfo, selectedIndex = props.selectedIndex, onItemInsert = props.onItemInsert, isEmptyQuery = props.isEmptyQuery, cancel = props.cancel, api = props.api, showMoreOptionsButton = props.showMoreOptionsButton; var ref = (0, _react.useRef)(null); var _useSharedPluginState = (0, _hooks.useSharedPluginStateWithSelector)(api, ['featureFlags'], function (states) { var _states$featureFlagsS; return { moreElementsInQuickInsertView: (_states$featureFlagsS = states.featureFlagsState) === null || _states$featureFlagsS === void 0 ? void 0 : _states$featureFlagsS.moreElementsInQuickInsertView }; }), moreElementsInQuickInsertView = _useSharedPluginState.moreElementsInQuickInsertView; var moreElementsInQuickInsertViewEnabled = moreElementsInQuickInsertView && triggerHandler.id === _typeAhead.TypeAheadAvailableNodes.QUICK_INSERT; var isEditorControlsEnabled = (0, _experiments.editorExperiment)('platform_editor_controls', 'variant1'); var defaultMenuHeight = (0, _react.useMemo)(function () { return moreElementsInQuickInsertViewEnabled ? DEFAULT_TYPEAHEAD_MENU_HEIGHT_NEW : DEFAULT_TYPEAHEAD_MENU_HEIGHT; }, [moreElementsInQuickInsertViewEnabled]); var activityStateRef = (0, _react.useRef)({ inputMethod: null, closeAction: null, invocationMethod: (_getPluginState = (0, _utils.getPluginState)(editorView.state)) === null || _getPluginState === void 0 ? void 0 : _getPluginState.inputMethod }); var startTime = (0, _react.useMemo)(function () { return performance.now(); }, // In case those props changes // we need to recreate the startTime [items, isEmptyQuery, triggerHandler] // eslint-disable-line react-hooks/exhaustive-deps ); (0, _react.useEffect)(function () { var _api$analytics, _api$analytics2; if (!(api !== null && api !== void 0 && (_api$analytics = api.analytics) !== null && _api$analytics !== void 0 && (_api$analytics = _api$analytics.actions) !== null && _api$analytics !== void 0 && _api$analytics.fireAnalyticsEvent)) { return; } var stopTime = performance.now(); var time = stopTime - startTime; // eslint-disable-next-line @atlaskit/platform/use-recommended-utils _featureGateJsClient.default.getExperimentValue('cc_fd_db_quick_insert_options', 'isEnabled', false); api === null || api === void 0 || (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 || (_api$analytics2 = _api$analytics2.actions) === null || _api$analytics2 === void 0 || _api$analytics2.fireAnalyticsEvent({ action: _analytics.ACTION.RENDERED, actionSubject: _analytics.ACTION_SUBJECT.TYPEAHEAD, eventType: _analytics.EVENT_TYPE.OPERATIONAL, attributes: { time: time, items: items.length, initial: isEmptyQuery } }); }, [startTime, items, isEmptyQuery, // In case the current triggerHandler changes // e.g: Inserting a mention using the quick insert // we need to send the event again // eslint-disable-next-line react-hooks/exhaustive-deps triggerHandler, api]); (0, _react.useEffect)(function () { var _api$analytics3, _api$analytics4; if (!(api !== null && api !== void 0 && (_api$analytics3 = api.analytics) !== null && _api$analytics3 !== void 0 && (_api$analytics3 = _api$analytics3.actions) !== null && _api$analytics3 !== void 0 && _api$analytics3.fireAnalyticsEvent)) { return; } api === null || api === void 0 || (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 || (_api$analytics4 = _api$analytics4.actions) === null || _api$analytics4 === void 0 || _api$analytics4.fireAnalyticsEvent({ action: _analytics.ACTION.VIEWED, actionSubject: _analytics.ACTION_SUBJECT.TYPEAHEAD_ITEM, eventType: _analytics.EVENT_TYPE.OPERATIONAL, attributes: { index: selectedIndex, items: items.length } }); }, [items, api, selectedIndex, // In case the current triggerHandler changes // e.g: Inserting a mention using the quick insert // we need to send the event again // eslint-disable-next-line react-hooks/exhaustive-deps triggerHandler]); var _useState = (0, _react.useState)(defaultMenuHeight), _useState2 = (0, _slicedToArray2.default)(_useState, 2), fitHeight = _useState2[0], setFitHeight = _useState2[1]; var fitHeightWithViewMore = (0, _react.useMemo)(function () { if (showMoreOptionsButton) { return fitHeight - VIEWMORE_BUTTON_HEIGHT; } return fitHeight; }, [fitHeight, showMoreOptionsButton]); var getFitHeight = (0, _react.useCallback)(function () { if (!anchorElement || !popupsMountPoint) { return; } var target = anchorElement; var _target$getBoundingCl = target.getBoundingClientRect(), targetTop = _target$getBoundingCl.top, targetHeight = _target$getBoundingCl.height; var boundariesElement = popupsBoundariesElement || document.body; var _boundariesElement$ge = boundariesElement.getBoundingClientRect(), boundariesHeight = _boundariesElement$ge.height, boundariesTop = _boundariesElement$ge.top; // Calculating the space above and space below our decoration var spaceAbove = targetTop - (boundariesTop - boundariesElement.scrollTop); var spaceBelow = boundariesTop + boundariesHeight - (targetTop + targetHeight); // Keep default height if more than enough space if (spaceBelow >= defaultMenuHeight) { return setFitHeight(defaultMenuHeight); } // Determines whether typeahead will fit above or below decoration // and return the space available. var newFitHeight = spaceBelow >= spaceAbove ? spaceBelow : spaceAbove; // Each typeahead item has some padding // We want to leave some space at the top so first item // is not partially cropped var fitHeightWithSpace = newFitHeight - ITEM_PADDING * 2; // Ensure typeahead height is max its default height var minFitHeight = Math.min(defaultMenuHeight, fitHeightWithSpace); return setFitHeight(minFitHeight); }, [anchorElement, defaultMenuHeight, popupsBoundariesElement, popupsMountPoint]); var getFitHeightDebounced = (0, _rafSchd.default)(getFitHeight); (0, _react.useLayoutEffect)(function () { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion var scrollableElement = popupsScrollableElement || (0, _ui.findOverflowScrollParent)(anchorElement); getFitHeight(); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners window.addEventListener('resize', getFitHeightDebounced); if (scrollableElement) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners scrollableElement.addEventListener('scroll', getFitHeightDebounced); } return function () { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners window.removeEventListener('resize', getFitHeightDebounced); if (scrollableElement) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners scrollableElement.removeEventListener('scroll', getFitHeightDebounced); } }; }, [anchorElement, popupsScrollableElement, getFitHeightDebounced, getFitHeight]); (0, _react.useLayoutEffect)(function () { var _ref2 = (0, _utils.getPluginState)(editorView.state) || {}, removePrefixTriggerOnCancel = _ref2.removePrefixTriggerOnCancel; var focusOut = function focusOut(event) { var _window$getSelection; var relatedTarget = event.relatedTarget; // Given the user is changing the focus // When the target is inside the TypeAhead Popup // Then the popup should stay open if (relatedTarget instanceof HTMLElement && relatedTarget.closest && (relatedTarget.closest(".".concat(_constants.TYPE_AHEAD_POPUP_CONTENT_CLASS)) || relatedTarget.closest("[data-type-ahead=\"".concat(_constants.TYPE_AHEAD_DECORATION_DATA_ATTRIBUTE, "\"]")))) { return; } // Handles cases like emoji (:) and mention (@) typeaheads that open new typeaheads var isTextSelected = ((_window$getSelection = window.getSelection()) === null || _window$getSelection === void 0 ? void 0 : _window$getSelection.type) === 'Range'; var innerEditor = anchorElement.closest('.extension-editable-area'); if (innerEditor) { // When there is no related target, we default to not closing the popup var newFocusInsideCurrentEditor = !relatedTarget; if (relatedTarget instanceof HTMLElement) { // check if the new focus is inside inner editor, keep popup opens newFocusInsideCurrentEditor = innerEditor.contains(relatedTarget); } if (!isTextSelected && newFocusInsideCurrentEditor) { return; } } else { // if the current focus in outer editor, keep the existing behaviour, do not close the pop up if text is not selected if (!isTextSelected) { return; } } cancel({ addPrefixTrigger: isEditorControlsEnabled ? !removePrefixTriggerOnCancel : true, setSelectionAt: _constants.CloseSelectionOptions.AFTER_TEXT_INSERTED, forceFocusOnEditor: false }); }; var element = ref.current; // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element === null || element === void 0 || element.addEventListener('focusout', focusOut); return function () { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element === null || element === void 0 || element.removeEventListener('focusout', focusOut); }; }, [ref, cancel, editorView.state, isEditorControlsEnabled, anchorElement]); // TODO: ED-17443 - When you press escape on typeahead panel, it should remove focus and close the panel // This is the expected keyboard behaviour advised by the Accessibility team (0, _react.useLayoutEffect)(function () { var _ref3 = (0, _utils.getPluginState)(editorView.state) || {}, removePrefixTriggerOnCancel = _ref3.removePrefixTriggerOnCancel; var escape = function escape(event) { if (event.key === 'Escape') { activityStateRef.current.inputMethod = _analytics.INPUT_METHOD.KEYBOARD; cancel({ addPrefixTrigger: isEditorControlsEnabled ? !removePrefixTriggerOnCancel : true, setSelectionAt: _constants.CloseSelectionOptions.AFTER_TEXT_INSERTED, forceFocusOnEditor: true }); } }; var element = ref.current; // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element === null || element === void 0 || element.addEventListener('keydown', escape); return function () { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element === null || element === void 0 || element.removeEventListener('keydown', escape); }; }, [ref, cancel, editorView.state, isEditorControlsEnabled]); // @ts-ignore var openElementBrowserModal = triggerHandler === null || triggerHandler === void 0 ? void 0 : triggerHandler.openElementBrowserModal; var onMoreOptionsClicked = (0, _react.useCallback)(function () { if ( // eslint-disable-next-line @atlaskit/platform/no-preconditioning openElementBrowserModal && (0, _experiments.editorExperiment)('platform_editor_controls', 'variant1') && triggerHandler.id === _typeAhead.TypeAheadAvailableNodes.QUICK_INSERT) { activityStateRef.current = { inputMethod: _analytics.INPUT_METHOD.MOUSE, closeAction: _analytics.ACTION.VIEW_MORE, invocationMethod: activityStateRef.current.invocationMethod }; } }, [openElementBrowserModal, triggerHandler.id]); return (0, _react2.jsx)(_ui.Popup, { zIndex: _editorSharedStyles.akEditorFloatingDialogZIndex, target: anchorElement, mountTo: popupsMountPoint, boundariesElement: popupsBoundariesElement, scrollableElement: popupsScrollableElement, fitHeight: fitHeight, fitWidth: 340, offset: OFFSET, ariaLabel: null, preventOverflow: true // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onUnmount: function onUnmount() { if (selectedIndex > -1 && (0, _experiments.editorExperiment)('platform_editor_controls', 'variant1')) { // if selectedIndex is -1, it means that the user has not selected any item // will be handled by WrapperTypeAhead (0, _analytics2.fireTypeAheadClosedAnalyticsEvent)(api, activityStateRef.current.closeAction || _analytics.ACTION.CANCELLED, !isEmptyQuery, activityStateRef.current.inputMethod || _analytics.INPUT_METHOD.MOUSE, activityStateRef.current.invocationMethod); // reset activity state activityStateRef.current = { inputMethod: null, closeAction: null, invocationMethod: null }; } } }, (0, _react2.jsx)("div", { css: [typeAheadContent, moreElementsInQuickInsertViewEnabled && typeAheadContentOverride, showMoreOptionsButton && typeAheadWrapperWithViewMoreOverride] // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766 , className: _constants.TYPE_AHEAD_POPUP_CONTENT_CLASS, ref: ref }, errorInfo ? (0, _react2.jsx)(_TypeAheadErrorFallback.TypeAheadErrorFallback, null) : (0, _react2.jsx)(_react.default.Fragment, null, (0, _react2.jsx)(Highlight, { state: editorView.state, triggerHandler: triggerHandler }), (0, _react2.jsx)(_TypeAheadList.TypeAheadList, { items: items, selectedIndex: selectedIndex // eslint-disable-next-line @atlassian/perf-linting/no-unstable-inline-props -- Ignored via go/ees017 (to be fixed) , onItemClick: function onItemClick(mode, index, inputMethod) { if ((0, _experiments.editorExperiment)('platform_editor_controls', 'variant1')) { activityStateRef.current = { inputMethod: inputMethod || null, closeAction: _analytics.ACTION.INSERTED, invocationMethod: activityStateRef.current.invocationMethod }; } onItemInsert(mode, index); }, fitHeight: fitHeightWithViewMore, editorView: editorView, decorationElement: anchorElement, triggerHandler: triggerHandler, moreElementsInQuickInsertViewEnabled: moreElementsInQuickInsertViewEnabled, api: api, showMoreOptionsButton: showMoreOptionsButton, onMoreOptionsClicked: onMoreOptionsClicked })))); }); TypeAheadPopup.displayName = 'TypeAheadPopup';