UNPKG

@grafana/ui

Version:
213 lines (210 loc) • 7.85 kB
import { jsx, jsxs } from 'react/jsx-runtime'; import { css, cx } from '@emotion/css'; import { offset, useFloating, autoUpdate } from '@floating-ui/react'; import Prism from 'prismjs'; import * as React from 'react'; import { memo, useRef, useState, useEffect } from 'react'; import { usePrevious } from 'react-use'; import Plain from 'slate-plain-serializer'; import { Editor } from 'slate-react'; import { VariableOrigin, DataLinkBuiltInVars } from '@grafana/data'; import { SlatePrism } from '../../slate-plugins/slate-prism/index.mjs'; import { useStyles2 } from '../../themes/ThemeContext.mjs'; import { getPositioningMiddleware } from '../../utils/floating.mjs'; import { makeValue, SCHEMA } from '../../utils/slate.mjs'; import { getInputStyles } from '../Input/Input.mjs'; import { Portal } from '../Portal/Portal.mjs'; import { ScrollContainer } from '../ScrollContainer/ScrollContainer.mjs'; import { DataLinkSuggestions } from './DataLinkSuggestions.mjs'; import { SelectionReference } from './SelectionReference.mjs'; "use strict"; const modulo = (a, n) => a - n * Math.floor(a / n); const datalinksSyntax = { builtInVariable: { pattern: /(\${\S+?})/ } }; const plugins = [ SlatePrism( { onlyIn: (node) => "type" in node && node.type === "code_block", getSyntax: () => "links" }, { ...Prism.languages, links: datalinksSyntax } ) ]; const getStyles = (theme) => ({ input: getInputStyles({ theme, invalid: false }).input, editor: css({ ".token.builtInVariable": { color: theme.colors.success.text }, ".token.variable": { color: theme.colors.primary.text } }), suggestionsWrapper: css({ boxShadow: theme.shadows.z2 }), // Wrapper with child selector needed. // When classnames are applied to the same element as the wrapper, it causes the suggestions to stop working wrapperOverrides: css({ width: "100%", "> .slate-query-field__wrapper": { padding: 0, backgroundColor: "transparent", border: "none" } }) }); const DataLinkInput = memo( ({ value, onChange, suggestions, placeholder = "http://your-grafana.com/d/000000010/annotations" }) => { const editorRef = useRef(null); const styles = useStyles2(getStyles); const [showingSuggestions, setShowingSuggestions] = useState(false); const [suggestionsIndex, setSuggestionsIndex] = useState(0); const [linkUrl, setLinkUrl] = useState(makeValue(value)); const prevLinkUrl = usePrevious(linkUrl); const [scrollTop, setScrollTop] = useState(0); const scrollRef = useRef(null); useEffect(() => { var _a; (_a = scrollRef.current) == null ? void 0 : _a.scrollTo(0, scrollTop); }, [scrollTop]); const middleware = [ offset(({ rects }) => ({ alignmentAxis: rects.reference.width })), ...getPositioningMiddleware() ]; const { refs, floatingStyles } = useFloating({ open: showingSuggestions, placement: "bottom-start", onOpenChange: setShowingSuggestions, middleware, whileElementsMounted: autoUpdate, strategy: "fixed" }); const stateRef = useRef({ showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange }); stateRef.current = { showingSuggestions, suggestions, suggestionsIndex, linkUrl, onChange }; const activeRef = useRef(null); useEffect(() => { setScrollTop(getElementPosition(activeRef.current, suggestionsIndex)); }, [suggestionsIndex]); const onKeyDown = React.useCallback((event, next) => { if (!stateRef.current.showingSuggestions) { if (event.key === "=" || event.key === "$" || event.keyCode === 32 && event.ctrlKey) { const selectionRef = new SelectionReference(); refs.setReference(selectionRef); return setShowingSuggestions(true); } return next(); } switch (event.key) { case "Backspace": if (stateRef.current.linkUrl.focusText.getText().length === 1) { next(); } case "Escape": setShowingSuggestions(false); return setSuggestionsIndex(0); case "Enter": event.preventDefault(); return onVariableSelect(stateRef.current.suggestions[stateRef.current.suggestionsIndex]); case "ArrowDown": case "ArrowUp": event.preventDefault(); const direction = event.key === "ArrowDown" ? 1 : -1; return setSuggestionsIndex((index) => modulo(index + direction, stateRef.current.suggestions.length)); default: return next(); } }, []); useEffect(() => { if (prevLinkUrl && prevLinkUrl.selection.isFocused && !linkUrl.selection.isFocused) { stateRef.current.onChange(Plain.serialize(linkUrl)); } }, [linkUrl, prevLinkUrl]); const onUrlChange = React.useCallback(({ value: value2 }) => { setLinkUrl(value2); }, []); const onVariableSelect = (item, editor = editorRef.current) => { const precedingChar = getCharactersAroundCaret(); const precedingDollar = precedingChar === "$"; if (item.origin !== VariableOrigin.Template || item.value === DataLinkBuiltInVars.includeVars) { editor.insertText(`${precedingDollar ? "" : "$"}{${item.value}}`); } else { editor.insertText(`${precedingDollar ? "" : "$"}{${item.value}:queryparam}`); } setLinkUrl(editor.value); setShowingSuggestions(false); setSuggestionsIndex(0); stateRef.current.onChange(Plain.serialize(editor.value)); }; const getCharactersAroundCaret = () => { const input = document.getElementById("data-link-input"); let precedingChar = "", sel, range; if (window.getSelection) { sel = window.getSelection(); if (sel && sel.rangeCount > 0) { range = sel.getRangeAt(0).cloneRange(); range.collapse(true); range.setStart(input, 0); precedingChar = range.toString().slice(-1); } } return precedingChar; }; return /* @__PURE__ */ jsx("div", { className: styles.wrapperOverrides, children: /* @__PURE__ */ jsx("div", { className: "slate-query-field__wrapper", children: /* @__PURE__ */ jsxs("div", { id: "data-link-input", className: "slate-query-field", children: [ showingSuggestions && /* @__PURE__ */ jsx(Portal, { children: /* @__PURE__ */ jsx("div", { ref: refs.setFloating, style: floatingStyles, children: /* @__PURE__ */ jsx( ScrollContainer, { maxHeight: "300px", ref: scrollRef, onScroll: (event) => setScrollTop(event.currentTarget.scrollTop), children: /* @__PURE__ */ jsx( DataLinkSuggestions, { activeRef, suggestions: stateRef.current.suggestions, onSuggestionSelect: onVariableSelect, onClose: () => setShowingSuggestions(false), activeIndex: suggestionsIndex } ) } ) }) }), /* @__PURE__ */ jsx( Editor, { schema: SCHEMA, ref: editorRef, placeholder, value: stateRef.current.linkUrl, onChange: onUrlChange, onKeyDown: (event, _editor, next) => onKeyDown(event, next), plugins, className: cx( styles.editor, styles.input, css({ padding: "3px 8px" }) ) } ) ] }) }) }); } ); DataLinkInput.displayName = "DataLinkInput"; function getElementPosition(suggestionElement, activeIndex) { var _a; return ((_a = suggestionElement == null ? void 0 : suggestionElement.clientHeight) != null ? _a : 0) * activeIndex; } export { DataLinkInput }; //# sourceMappingURL=DataLinkInput.mjs.map