@atlaskit/editor-plugin-type-ahead
Version:
Type-ahead plugin for @atlaskit/editor-core
477 lines (470 loc) • 20.3 kB
JavaScript
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React, { Fragment, useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
import { css, jsx } from '@emotion/react';
import { useIntl } from 'react-intl';
import { keyName as keyNameNormalized } from 'w3c-keyname';
import { getBrowserInfo } from '@atlaskit/editor-common/browser';
import { SelectItemMode, typeAheadListMessages } from '@atlaskit/editor-common/type-ahead';
import { AssistiveText } from '@atlaskit/editor-common/ui';
import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
import { blockNodesVerticalMargin } from '@atlaskit/editor-shared-styles';
import { fg } from '@atlaskit/platform-feature-flags';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { CloseSelectionOptions, TYPE_AHEAD_DECORATION_ELEMENT_ID, TYPE_AHEAD_POPUP_CONTENT_CLASS } from '../pm-plugins/constants';
import { getPluginState } from '../pm-plugins/utils';
var placeholderStyles = 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 = css({
'&::after': {
content: "''"
}
});
var querySpanStyles = 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: 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 = keyNameNormalized(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 typeAheadListMessages.mentionInputLabel;
case '/':
return typeAheadListMessages.quickInsertInputLabel;
case ':':
return typeAheadListMessages.emojiInputLabel;
default:
return typeAheadListMessages.quickInsertInputLabel;
}
};
export var InputQuery = /*#__PURE__*/React.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 = useRef(document.createElement('span'));
var inputRef = useRef(null);
var _useState = useState(null),
_useState2 = _slicedToArray(_useState, 2),
query = _useState2[0],
setQuery = _useState2[1];
var isEditorControlsEnabled = editorExperiment('platform_editor_controls', 'variant1');
var isSearchPlaceholderEnabled = editorExperiment('platform_editor_controls', 'variant1') && fg('platform_editor_quick_insert_placeholder');
var selection = editorView.state.selection;
var table = editorView.state.schema.nodes.table;
var _useState3 = 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
!findParentNodeOfType(table)(selection)),
_useState4 = _slicedToArray(_useState3, 2),
showPlaceholder = _useState4[0],
setShowPlaceholder = _useState4[1];
var cleanedInputContent = 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 = useCallback(function (_event) {
var text = cleanedInputContent();
onQueryChange(text);
}, [onQueryChange, cleanedInputContent]);
var onInput = useCallback(function () {
if (cleanedInputContent()) {
setShowPlaceholder(false);
}
}, [cleanedInputContent]);
var _useState5 = useState(false),
_useState6 = _slicedToArray(_useState5, 2),
isInFocus = _useState6[0],
setInFocus = _useState6[1];
var checkKeyEvent = useCallback(function (event) {
var _ref$current2;
var key = keyNameNormalized(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 = 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: 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: 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: 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 ? SelectItemMode.SHIFT_ENTER : 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]);
useLayoutEffect(function () {
if (!ref.current) {
return;
}
var browser = getBrowserInfo();
var element = ref.current;
var _ref3 = getPluginState(editorView.state) || {},
removePrefixTriggerOnCancel = _ref3.removePrefixTriggerOnCancel;
var onFocusIn = function onFocusIn(_event) {
onQueryFocus();
};
var keyDown = function keyDown(event) {
var key = keyNameNormalized(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 ? CloseSelectionOptions.AFTER_TEXT_INSERTED : 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(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: CloseSelectionOptions.BEFORE_TEXT_INSERTED,
forceFocusOnEditor: false
});
};
var close = function close() {
cancel({
addPrefixTrigger: false,
text: '',
forceFocusOnEditor: true,
setSelectionAt: 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]);
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 = 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 = TYPE_AHEAD_DECORATION_ELEMENT_ID + '__assistiveHint';
var intl = useIntl();
return jsx(Fragment, null, triggerQueryPrefix, jsx("span", {
css: [querySpanStyles, isSearchPlaceholderEnabled && queryWithoutPlaceholderStyles, showPlaceholder && placeholderStyles],
contentEditable: true,
ref: ref,
onKeyUp: onKeyUp,
tabIndex: -1,
onInput: isSearchPlaceholderEnabled ? onInput : undefined,
role: "combobox",
"aria-controls": 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(typeAheadListMessages.quickInsertInputPlaceholderLabel)
}, query === null ? jsx("input", {
ref: inputRef,
type: "text",
"aria-label": intl.formatMessage(getAriaLabel(triggerQueryPrefix, intl))
}) : query), jsx("span", {
id: assistiveHintID,
style: {
display: 'none'
}
}, intl.formatMessage(typeAheadListMessages.inputQueryAssistiveLabel)), jsx(AssistiveText, {
assistiveText: items.length === 0 ? intl.formatMessage(typeAheadListMessages.noSearchResultsLabel, {
itemsLength: items.length
}) : '',
isInFocus: items.length === 0 || isInFocus,
id: TYPE_AHEAD_DECORATION_ELEMENT_ID
}));
});
InputQuery.displayName = 'InputQuery';