@grafana/ui
Version:
Grafana Components Library
249 lines (246 loc) • 8.67 kB
JavaScript
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
import { debounce, sortBy } from 'lodash';
import { Typeahead } from '../components/Typeahead/Typeahead.mjs';
import { SearchFunctionType, SearchFunctionMap } from '../utils/searchFunctions.mjs';
import { makeFragment } from '../utils/slate.mjs';
import TOKEN_MARK from './slate-prism/TOKEN_MARK.mjs';
;
const TYPEAHEAD_DEBOUNCE = 250;
function SuggestionsPlugin({
onTypeahead,
cleanText,
onWillApplySuggestion,
portalOrigin
}) {
let typeaheadRef;
let state = {
groupedItems: [],
typeaheadPrefix: "",
typeaheadContext: "",
typeaheadText: ""
};
const handleTypeaheadDebounced = debounce(handleTypeahead, TYPEAHEAD_DEBOUNCE);
const setState = (update) => {
state = {
...state,
...update
};
};
return {
onBlur: (event, editor, next) => {
state = {
...state,
groupedItems: []
};
return next();
},
onClick: (event, editor, next) => {
state = {
...state,
groupedItems: []
};
return next();
},
onKeyDown: (event, editor, next) => {
const currentSuggestions = state.groupedItems;
const hasSuggestions = currentSuggestions.length;
switch (event.key) {
case "Escape": {
if (hasSuggestions) {
event.preventDefault();
state = {
...state,
groupedItems: []
};
return editor.insertText("");
}
break;
}
case "ArrowDown":
case "ArrowUp":
if (hasSuggestions) {
event.preventDefault();
typeaheadRef.moveMenuIndex(event.key === "ArrowDown" ? 1 : -1);
return;
}
break;
case "Enter": {
if (!(event.shiftKey || event.ctrlKey) && hasSuggestions) {
event.preventDefault();
return typeaheadRef.insertSuggestion();
}
break;
}
case "Tab": {
if (hasSuggestions) {
event.preventDefault();
return typeaheadRef.insertSuggestion();
}
break;
}
default: {
if (event.key.length === 1) {
handleTypeaheadDebounced(editor, setState, onTypeahead, cleanText);
}
break;
}
}
return next();
},
commands: {
selectSuggestion: (editor, suggestion) => {
const suggestions = state.groupedItems;
if (!suggestions || !suggestions.length) {
return editor;
}
const ed = editor.applyTypeahead(suggestion);
handleTypeaheadDebounced(editor, setState, onTypeahead, cleanText);
return ed;
},
applyTypeahead: (editor, suggestion) => {
let suggestionText = suggestion.insertText || suggestion.label;
const preserveSuffix = suggestion.kind === "function";
const move = suggestion.move || 0;
const moveForward = move > 0 ? move : 0;
const moveBackward = move < 0 ? -move : 0;
const { typeaheadPrefix, typeaheadText, typeaheadContext } = state;
if (onWillApplySuggestion) {
suggestionText = onWillApplySuggestion(suggestionText, {
groupedItems: state.groupedItems,
typeaheadContext,
typeaheadPrefix,
typeaheadText
});
}
const { forward, backward } = getNumCharsToDelete(
suggestionText,
typeaheadPrefix,
typeaheadText,
preserveSuffix,
suggestion.deleteBackwards,
cleanText
);
if (suggestionText.match(/\n/)) {
const fragment = makeFragment(suggestionText);
editor.deleteBackward(backward).deleteForward(forward).insertFragment(fragment).focus();
return editor;
}
state = {
...state,
groupedItems: []
};
editor.snapshotSelection().deleteBackward(backward).deleteForward(forward).insertText(suggestionText).moveForward(moveForward).moveBackward(moveBackward).focus();
return editor;
}
},
renderEditor(props, editor, next) {
if (editor.value.selection.isExpanded) {
return next();
}
const children = next();
return /* @__PURE__ */ jsxs(Fragment, { children: [
children,
/* @__PURE__ */ jsx(
Typeahead,
{
menuRef: (menu) => typeaheadRef = menu,
origin: portalOrigin,
prefix: state.typeaheadPrefix,
isOpen: !!state.groupedItems.length,
groupedItems: state.groupedItems,
onSelectSuggestion: editor.selectSuggestion
}
)
] });
}
};
}
const handleTypeahead = async (editor, onStateChange, onTypeahead, cleanText) => {
if (!onTypeahead) {
return;
}
const { value } = editor;
const { selection } = value;
const parentBlock = value.document.getClosestBlock(value.focusBlock.key);
const selectionStartOffset = value.selection.start.offset - 1;
const decorations = parentBlock && parentBlock.getDecorations(editor);
const filteredDecorations = decorations ? decorations.filter(
(decoration) => decoration.start.offset <= selectionStartOffset && decoration.end.offset > selectionStartOffset && decoration.type === TOKEN_MARK
).toArray() : [];
const labelKeyDec = decorations && decorations.filter(
(decoration) => decoration.end.offset <= selectionStartOffset && decoration.type === TOKEN_MARK && decoration.data.get("className").includes("label-key")
).last();
const labelKey = labelKeyDec && value.focusText.text.slice(labelKeyDec.start.offset, labelKeyDec.end.offset);
const wrapperClasses = filteredDecorations.map((decoration) => decoration.data.get("className")).join(" ").split(" ").filter((className) => className.length);
let text = value.focusText.text;
let prefix = text.slice(0, selection.focus.offset);
if (filteredDecorations.length) {
text = value.focusText.text.slice(filteredDecorations[0].start.offset, filteredDecorations[0].end.offset);
prefix = value.focusText.text.slice(filteredDecorations[0].start.offset, selection.focus.offset);
}
const labelValueMatch = prefix.match(/(?:!?=~?"?|")(.*)/);
if (labelValueMatch) {
prefix = labelValueMatch[1];
} else if (cleanText) {
prefix = cleanText(prefix);
}
const { suggestions, context } = await onTypeahead({
prefix,
text,
value,
wrapperClasses,
labelKey: labelKey || void 0,
editor
});
const filteredSuggestions = suggestions.map((group) => {
if (!group.items) {
return group;
}
const searchFunctionType = group.searchFunctionType || (group.prefixMatch ? SearchFunctionType.Prefix : SearchFunctionType.Word);
const searchFunction = SearchFunctionMap[searchFunctionType];
let newGroup = { ...group };
if (prefix) {
if (!group.skipFilter) {
newGroup.items = newGroup.items.filter((c) => (c.filterText || c.label).length >= prefix.length);
newGroup.items = searchFunction(newGroup.items, prefix);
}
newGroup.items = newGroup.items.filter(
(c) => {
var _a;
return !(c.insertText === prefix || ((_a = c.filterText) != null ? _a : c.label) === prefix);
}
);
}
if (!group.skipSort) {
newGroup.items = sortBy(newGroup.items, (item) => {
if (item.sortText === void 0) {
return item.sortValue !== void 0 ? item.sortValue : item.label;
} else {
return item.sortText || item.label;
}
});
}
return newGroup;
}).filter((gr) => gr.items && gr.items.length);
onStateChange({
groupedItems: filteredSuggestions,
typeaheadPrefix: prefix,
typeaheadContext: context,
typeaheadText: text
});
editor.blur().focus();
};
function getNumCharsToDelete(suggestionText, typeaheadPrefix, typeaheadText, preserveSuffix, deleteBackwards, cleanText) {
const backward = deleteBackwards || typeaheadPrefix.length;
const text = cleanText ? cleanText(typeaheadText) : typeaheadText;
const offset = typeaheadText.indexOf(typeaheadPrefix);
const suffixLength = offset > -1 ? text.length - offset - typeaheadPrefix.length : text.length - typeaheadPrefix.length;
const midWord = Boolean(typeaheadPrefix && suffixLength > 0 || suggestionText === typeaheadText);
const forward = midWord && !preserveSuffix ? suffixLength + offset : 0;
return {
forward,
backward
};
}
export { SuggestionsPlugin, TYPEAHEAD_DEBOUNCE, getNumCharsToDelete };
//# sourceMappingURL=suggestions.mjs.map