UNPKG

@atlaskit/editor-plugin-type-ahead

Version:

Type-ahead plugin for @atlaskit/editor-core

487 lines (478 loc) 21.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); var _typeof = require("@babel/runtime/helpers/typeof"); Object.defineProperty(exports, "__esModule", { value: true }); exports.InputQuery = void 0; var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime/helpers/slicedToArray")); var _react = _interopRequireWildcard(require("react")); var _react2 = require("@emotion/react"); var _reactIntl = require("react-intl"); var _w3cKeyname = require("w3c-keyname"); var _browser = require("@atlaskit/editor-common/browser"); var _typeAhead = require("@atlaskit/editor-common/type-ahead"); var _ui = require("@atlaskit/editor-common/ui"); var _utils = require("@atlaskit/editor-prosemirror/utils"); var _editorSharedStyles = require("@atlaskit/editor-shared-styles"); var _platformFeatureFlags = require("@atlaskit/platform-feature-flags"); var _experiments = require("@atlaskit/tmp-editor-statsig/experiments"); var _constants = require("../pm-plugins/constants"); var _utils2 = require("../pm-plugins/utils"); 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 placeholderStyles = (0, _react2.css)({ '&::after': { content: 'attr(data-place-holder)', color: "var(--ds-text-subtlest, #6B6E76)", position: 'relative', padding: "var(--ds-space-025, 2px)", left: "var(--ds-space-negative-050, -4px)", backgroundColor: "var(--ds-background-neutral, #0515240F)", // eslint-disable-next-line @atlaskit/design-system/no-unsafe-design-token-usage borderRadius: "var(--ds-radius-small, 3px)" } }); var queryWithoutPlaceholderStyles = (0, _react2.css)({ '&::after': { content: "''" } }); var querySpanStyles = (0, _react2.css)({ outline: 'none', // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors -- Ignored via go/DSP-18766 '& input': { width: '5px', border: 'none', background: 'transparent', padding: 0, margin: 0, // TODO: ED-17022 - Fixes firefox caret position // Do not migrate font when em is used as unit // eslint-disable-next-line @atlaskit/design-system/use-tokens-typography fontSize: '1em', // eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766 height: _editorSharedStyles.blockNodesVerticalMargin, caretColor: "var(--ds-text-accent-blue, #1558BC)" } }); var isNavigationKey = function isNavigationKey(event) { return ['Enter', 'Tab', 'ArrowDown', 'ArrowUp'].includes(event.key); }; var isUndoRedoShortcut = function isUndoRedoShortcut(event) { var key = (0, _w3cKeyname.keyName)(event); if (event.ctrlKey && key === 'y') { return 'historyRedo'; } if ((event.ctrlKey || event.metaKey) && event.shiftKey && key === 'Z') { return 'historyRedo'; } if ((event.ctrlKey || event.metaKey) && key === 'z') { return 'historyUndo'; } return false; }; var getAriaLabel = function getAriaLabel(triggerPrefix, _intl) { switch (triggerPrefix) { case '@': return _typeAhead.typeAheadListMessages.mentionInputLabel; case '/': return _typeAhead.typeAheadListMessages.quickInsertInputLabel; case ':': return _typeAhead.typeAheadListMessages.emojiInputLabel; default: return _typeAhead.typeAheadListMessages.quickInsertInputLabel; } }; var InputQuery = exports.InputQuery = /*#__PURE__*/_react.default.memo(function (_ref) { var triggerQueryPrefix = _ref.triggerQueryPrefix, cancel = _ref.cancel, onQueryChange = _ref.onQueryChange, onItemSelect = _ref.onItemSelect, selectNextItem = _ref.selectNextItem, selectPreviousItem = _ref.selectPreviousItem, forceFocus = _ref.forceFocus, reopenQuery = _ref.reopenQuery, onQueryFocus = _ref.onQueryFocus, onUndoRedo = _ref.onUndoRedo, editorView = _ref.editorView, items = _ref.items; var ref = (0, _react.useRef)(document.createElement('span')); var inputRef = (0, _react.useRef)(null); var _useState = (0, _react.useState)(null), _useState2 = (0, _slicedToArray2.default)(_useState, 2), query = _useState2[0], setQuery = _useState2[1]; var isEditorControlsEnabled = (0, _experiments.editorExperiment)('platform_editor_controls', 'variant1'); var isSearchPlaceholderEnabled = (0, _experiments.editorExperiment)('platform_editor_controls', 'variant1') && (0, _platformFeatureFlags.fg)('platform_editor_quick_insert_placeholder'); var selection = editorView.state.selection; var table = editorView.state.schema.nodes.table; var _useState3 = (0, _react.useState)(isSearchPlaceholderEnabled && triggerQueryPrefix === '/' && // When triggered in very narrow table column, placeholder becomes ellipsis only // hence we disable it for now and revisit this scenario in ED-27480 !(0, _utils.findParentNodeOfType)(table)(selection)), _useState4 = (0, _slicedToArray2.default)(_useState3, 2), showPlaceholder = _useState4[0], setShowPlaceholder = _useState4[1]; var cleanedInputContent = (0, _react.useCallback)(function () { var _ref$current; var raw = ((_ref$current = ref.current) === null || _ref$current === void 0 ? void 0 : _ref$current.textContent) || ''; return raw; }, []); var onKeyUp = (0, _react.useCallback)(function (_event) { var text = cleanedInputContent(); onQueryChange(text); }, [onQueryChange, cleanedInputContent]); var onInput = (0, _react.useCallback)(function () { if (cleanedInputContent()) { setShowPlaceholder(false); } }, [cleanedInputContent]); var _useState5 = (0, _react.useState)(false), _useState6 = (0, _slicedToArray2.default)(_useState5, 2), isInFocus = _useState6[0], setInFocus = _useState6[1]; var checkKeyEvent = (0, _react.useCallback)(function (event) { var _ref$current2; var key = (0, _w3cKeyname.keyName)(event); var sel = document.getSelection(); var raw = ((_ref$current2 = ref.current) === null || _ref$current2 === void 0 ? void 0 : _ref$current2.textContent) || ''; var text = cleanedInputContent(); var stopDefault = false; var _ref2 = (0, _utils2.getPluginState)(editorView.state) || {}, selectedIndex = _ref2.selectedIndex, removePrefixTriggerOnCancel = _ref2.removePrefixTriggerOnCancel; setInFocus(true); switch (key) { case ' ': // space key if (text.length === 0) { cancel({ forceFocusOnEditor: true, text: ' ', addPrefixTrigger: isEditorControlsEnabled ? !removePrefixTriggerOnCancel : true, setSelectionAt: _constants.CloseSelectionOptions.AFTER_TEXT_INSERTED }); stopDefault = true; } break; case 'Escape': case 'PageUp': case 'PageDown': case 'Home': cancel({ text: text, forceFocusOnEditor: true, addPrefixTrigger: isEditorControlsEnabled ? !removePrefixTriggerOnCancel : true, setSelectionAt: _constants.CloseSelectionOptions.AFTER_TEXT_INSERTED }); stopDefault = true; break; case 'Backspace': if (raw.length === 0 || (sel === null || sel === void 0 ? void 0 : sel.anchorOffset) === 0) { event.stopPropagation(); event.preventDefault(); cancel({ forceFocusOnEditor: true, text: text, addPrefixTrigger: false, setSelectionAt: _constants.CloseSelectionOptions.BEFORE_TEXT_INSERTED }); } break; case 'Enter': // TODO: ED-14758 - Under the W3C specification, any keycode sent under IME would return a keycode 229 // event.isComposing can't be used alone as this also included a virtual keyboard under a keyboardless device, therefore, it seems the best practice would be intercepting the event as below. // Some suggested the other workaround maybe listen on`keypress` instead of `keydown` if (!event.isComposing && event.which !== 229 && event.keyCode !== 229) { if (selectedIndex === -1) { /** * TODO DTR-1401: (also see ED-17200) There are two options * here, either * - set the index directly to 1 in WrapperTypeAhead.tsx's * `insertSelectedItem` at the cost of breaking some of the a11y * focus changes, * - or do this jank at the cost of some small analytics noise. * * The focus behaviour still needs cleanup */ selectPreviousItem(); selectNextItem(); } onItemSelect(event.shiftKey ? _typeAhead.SelectItemMode.SHIFT_ENTER : _typeAhead.SelectItemMode.ENTER); } break; case 'Tab': event.shiftKey ? selectPreviousItem() : selectNextItem(); break; case 'ArrowDown': selectNextItem(); break; case 'ArrowUp': selectPreviousItem(); break; } var undoRedoType = isUndoRedoShortcut(event); if (onUndoRedo && undoRedoType && onUndoRedo(undoRedoType)) { stopDefault = true; } if (isNavigationKey(event) || stopDefault) { event.stopPropagation(); event.preventDefault(); return false; } }, [onUndoRedo, onItemSelect, selectNextItem, selectPreviousItem, cancel, cleanedInputContent, editorView.state, isEditorControlsEnabled]); (0, _react.useLayoutEffect)(function () { if (!ref.current) { return; } var browser = (0, _browser.getBrowserInfo)(); var element = ref.current; var _ref3 = (0, _utils2.getPluginState)(editorView.state) || {}, removePrefixTriggerOnCancel = _ref3.removePrefixTriggerOnCancel; var onFocusIn = function onFocusIn(_event) { onQueryFocus(); }; var keyDown = function keyDown(event) { var key = (0, _w3cKeyname.keyName)(event); if (['ArrowLeft', 'ArrowRight'].includes(key) && document.getSelection && document.getSelection()) { var _ref$current3; var q = ((_ref$current3 = ref.current) === null || _ref$current3 === void 0 ? void 0 : _ref$current3.textContent) || ''; var sel = document.getSelection(); var isMovingRight = sel && 'ArrowRight' === key && sel.anchorOffset === q.length; var isMovingLeft = sel && 'ArrowLeft' === key && (sel.anchorOffset === 0 || event.metaKey); if (!isMovingRight && !isMovingLeft) { return; } cancel({ forceFocusOnEditor: true, addPrefixTrigger: isEditorControlsEnabled ? !removePrefixTriggerOnCancel : true, text: cleanedInputContent(), setSelectionAt: isMovingRight ? _constants.CloseSelectionOptions.AFTER_TEXT_INSERTED : _constants.CloseSelectionOptions.BEFORE_TEXT_INSERTED }); event.preventDefault(); event.stopPropagation(); return; } checkKeyEvent(event); }; var onPaste = function onPaste(event) { var _event$clipboardData, _event$clipboardData2; var html = (_event$clipboardData = event.clipboardData) === null || _event$clipboardData === void 0 ? void 0 : _event$clipboardData.getData('text/html'); var plainText = (_event$clipboardData2 = event.clipboardData) === null || _event$clipboardData2 === void 0 ? void 0 : _event$clipboardData2.getData('text/plain'); if (html && plainText) { event.preventDefault(); // insert the plain text into the type-ahead input field var _selection = window.getSelection(); if (_selection && ref.current) { if (_selection.rangeCount > 0) { var range = _selection.getRangeAt(0); range.deleteContents(); range.insertNode(document.createTextNode(plainText)); range.collapse(false); } } } }; var onFocusOut = function onFocusOut(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))) { return; } // Chrome and Edge may emit focusout events without direct input. // This path handles dismissals that don't involve item selection, so we ignore these events. // In Edge this also lead to duplication in the trigger character (@, /, :) as `cancel` would be called twice if ((browser.ie || browser.chrome) && !(((_window$getSelection = window.getSelection()) === null || _window$getSelection === void 0 ? void 0 : _window$getSelection.type) === 'Range') && // eslint-disable-next-line @typescript-eslint/no-explicit-any !event.sourceCapabilities) { return; } cancel({ addPrefixTrigger: isEditorControlsEnabled ? !removePrefixTriggerOnCancel : true, text: cleanedInputContent(), setSelectionAt: _constants.CloseSelectionOptions.BEFORE_TEXT_INSERTED, forceFocusOnEditor: false }); }; var close = function close() { cancel({ addPrefixTrigger: false, text: '', forceFocusOnEditor: true, setSelectionAt: _constants.CloseSelectionOptions.BEFORE_TEXT_INSERTED }); }; var beforeinput = function beforeinput(e) { var _target$textContent; setInFocus(false); var target = e.target; if (e.isComposing || !(target instanceof HTMLElement)) { return; } if (e.inputType === 'historyUndo' && ((_target$textContent = target.textContent) === null || _target$textContent === void 0 ? void 0 : _target$textContent.length) === 0) { e.preventDefault(); e.stopPropagation(); close(); return; } if (e.data != null && inputRef.current === null) { setQuery(''); // We need to change the content on Safari // and set the cursor at the right place if (browser.safari) { e.preventDefault(); var dataElement = document.createTextNode(e.data); element.appendChild(dataElement); var sel = window.getSelection(); var range = document.createRange(); range.setStart(dataElement, dataElement.length); range.collapse(true); sel === null || sel === void 0 || sel.removeAllRanges(); sel === null || sel === void 0 || sel.addRange(range); } } }; var onInput = function onInput() {}; if (browser.safari) { // On Safari, for reasons beyond my understanding, // The undo behavior is totally different from other browsers // That why we need to have an specific branch only for Safari. var _onInput = function _onInput(e) { var _target$textContent2; var target = e.target; if (!(e instanceof InputEvent) || e.isComposing || !(target instanceof HTMLElement)) { return; } if (e.inputType === 'historyUndo' && ((_target$textContent2 = target.textContent) === null || _target$textContent2 === void 0 ? void 0 : _target$textContent2.length) === 1) { e.preventDefault(); e.stopPropagation(); close(); return; } }; // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.addEventListener('input', _onInput); } // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.addEventListener('focusout', onFocusOut); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.addEventListener('focusin', onFocusIn); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.addEventListener('keydown', keyDown); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.addEventListener('beforeinput', beforeinput); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.addEventListener('paste', onPaste); return function () { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.removeEventListener('focusout', onFocusOut); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.removeEventListener('focusin', onFocusIn); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.removeEventListener('keydown', keyDown); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.removeEventListener('beforeinput', beforeinput); // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.removeEventListener('paste', onPaste); if (browser.safari) { // Ignored via go/ees005 // eslint-disable-next-line @repo/internal/dom-events/no-unsafe-event-listeners element.removeEventListener('input', onInput); } }; }, [triggerQueryPrefix, cleanedInputContent, onQueryFocus, cancel, checkKeyEvent, editorView.state, isEditorControlsEnabled]); (0, _react.useLayoutEffect)(function () { var hasReopenQuery = typeof reopenQuery === 'string' && reopenQuery.trim().length > 0; if (ref.current && forceFocus) { // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion setQuery(hasReopenQuery ? reopenQuery : null); requestAnimationFrame(function () { if (!(ref !== null && ref !== void 0 && ref.current)) { return; } var sel = window.getSelection(); if (sel && hasReopenQuery && ref.current.lastChild instanceof Text) { var lastChild = ref.current.lastChild; var range = document.createRange(); range.setStart(ref.current.lastChild, lastChild.length); range.collapse(true); sel.removeAllRanges(); sel.addRange(range); } ref.current.focus(); setInFocus(true); }); } }, [forceFocus, reopenQuery]); var classNames = (0, _react.useMemo)(function () { var classes = []; if (showPlaceholder) { // to avoid the placeholder wrapped to next line when triggered at the end of the line // see placeholderWrapStyles in editor-core/src/ui/ContentStyles/index.tsx classes.push('placeholder-decoration-wrap'); if (selection.$from.depth > 1) { // to hide placeholder overflow as ellipsis // see placeholderWrapStyles in editor-core/src/ui/ContentStyles/index.tsx classes.push('placeholder-decoration-hide-overflow'); } } return classes.join(' '); }, [showPlaceholder, selection]); var assistiveHintID = _constants.TYPE_AHEAD_DECORATION_ELEMENT_ID + '__assistiveHint'; var intl = (0, _reactIntl.useIntl)(); return (0, _react2.jsx)(_react.Fragment, null, triggerQueryPrefix, (0, _react2.jsx)("span", { css: [querySpanStyles, isSearchPlaceholderEnabled && queryWithoutPlaceholderStyles, showPlaceholder && placeholderStyles], contentEditable: true, ref: ref, onKeyUp: onKeyUp, tabIndex: -1, onInput: isSearchPlaceholderEnabled ? onInput : undefined, role: "combobox", "aria-controls": _constants.TYPE_AHEAD_DECORATION_ELEMENT_ID, "aria-autocomplete": "list", "aria-expanded": items.length !== 0, "aria-labelledby": assistiveHintID, suppressContentEditableWarning: true, "data-query-prefix": triggerQueryPrefix // eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop , className: classNames, "data-place-holder": intl.formatMessage(_typeAhead.typeAheadListMessages.quickInsertInputPlaceholderLabel) }, query === null ? (0, _react2.jsx)("input", { ref: inputRef, type: "text", "aria-label": intl.formatMessage(getAriaLabel(triggerQueryPrefix, intl)) }) : query), (0, _react2.jsx)("span", { id: assistiveHintID, style: { display: 'none' } }, intl.formatMessage(_typeAhead.typeAheadListMessages.inputQueryAssistiveLabel)), (0, _react2.jsx)(_ui.AssistiveText, { assistiveText: items.length === 0 ? intl.formatMessage(_typeAhead.typeAheadListMessages.noSearchResultsLabel, { itemsLength: items.length }) : '', isInFocus: items.length === 0 || isInFocus, id: _constants.TYPE_AHEAD_DECORATION_ELEMENT_ID })); }); InputQuery.displayName = 'InputQuery';