@patternfly/react-code-editor
Version:
This package provides a PatternFly wrapper for the Monaco code editor
268 lines • 16.7 kB
JavaScript
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { useEffect, useRef, useState } from 'react';
import { css } from '@patternfly/react-styles';
import styles from '@patternfly/react-styles/css/components/CodeEditor/code-editor.mjs';
import fileUploadStyles from '@patternfly/react-styles/css/components/FileUpload/file-upload.mjs';
import monoFont from '@patternfly/react-tokens/dist/esm/t_global_font_family_mono';
import { Button, ButtonVariant } from '@patternfly/react-core/dist/esm/components/Button';
import { EmptyState, EmptyStateActions, EmptyStateBody, EmptyStateFooter, EmptyStateVariant } from '@patternfly/react-core/dist/esm/components/EmptyState';
import { Popover } from '@patternfly/react-core/dist/esm/components/Popover';
import { getResizeObserver } from '@patternfly/react-core/dist/esm/helpers/resizeObserver';
import Editor from '@monaco-editor/react';
import CopyIcon from '@patternfly/react-icons/dist/esm/icons/copy-icon';
import UploadIcon from '@patternfly/react-icons/dist/esm/icons/upload-icon';
import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon';
import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon';
import HelpIcon from '@patternfly/react-icons/dist/esm/icons/help-icon';
import Dropzone from 'react-dropzone';
import { CodeEditorContext } from './CodeEditorUtils';
import { CodeEditorControl } from './CodeEditorControl';
import { defineThemes } from './CodeEditorTheme';
export var Language;
(function (Language) {
Language["abap"] = "abap";
Language["aes"] = "aes";
Language["apex"] = "apex";
Language["azcli"] = "azcli";
Language["bat"] = "bat";
Language["bicep"] = "bicep";
Language["c"] = "c";
Language["cameligo"] = "cameligo";
Language["clojure"] = "clojure";
Language["coffeescript"] = "coffeescript";
Language["cpp"] = "cpp";
Language["csharp"] = "csharp";
Language["csp"] = "csp";
Language["css"] = "css";
Language["dart"] = "dart";
Language["dockerfile"] = "dockerfile";
Language["ecl"] = "ecl";
Language["elixir"] = "elixir";
Language["fsharp"] = "fsharp";
Language["go"] = "go";
Language["graphql"] = "graphql";
Language["handlebars"] = "handlebars";
Language["hcl"] = "hcl";
Language["html"] = "html";
Language["ini"] = "ini";
Language["java"] = "java";
Language["javascript"] = "javascript";
Language["json"] = "json";
Language["julia"] = "julia";
Language["kotlin"] = "kotlin";
Language["less"] = "less";
Language["lexon"] = "lexon";
Language["liquid"] = "liquid";
Language["lua"] = "lua";
Language["m3"] = "m3";
Language["markdown"] = "markdown";
Language["mips"] = "mips";
Language["msdax"] = "msdax";
Language["mysql"] = "mysql";
Language["objective-c"] = "objective-c";
Language["pascal"] = "pascal";
Language["pascaligo"] = "pascaligo";
Language["perl"] = "perl";
Language["pgsql"] = "pgsql";
Language["php"] = "php";
Language["plaintext"] = "plaintext";
Language["postiats"] = "postiats";
Language["powerquery"] = "powerquery";
Language["powershell"] = "powershell";
Language["pug"] = "pug";
Language["python"] = "python";
Language["r"] = "r";
Language["razor"] = "razor";
Language["redis"] = "redis";
Language["redshift"] = "redshift";
Language["restructuredtext"] = "restructuredtext";
Language["ruby"] = "ruby";
Language["rust"] = "rust";
Language["sb"] = "sb";
Language["scala"] = "scala";
Language["scheme"] = "scheme";
Language["scss"] = "scss";
Language["shell"] = "shell";
Language["sol"] = "sol";
Language["sql"] = "sql";
Language["st"] = "st";
Language["swift"] = "swift";
Language["systemverilog"] = "systemverilog";
Language["tcl"] = "tcl";
Language["twig"] = "twig";
Language["typescript"] = "typescript";
Language["vb"] = "vb";
Language["verilog"] = "verilog";
Language["xml"] = "xml";
Language["yaml"] = "yaml";
})(Language || (Language = {}));
const getExtensionFromLanguage = (language) => {
switch (language) {
case Language.shell:
return 'sh';
case Language.ruby:
return 'rb';
case Language.perl:
return 'pl';
case Language.python:
return 'py';
case Language.mysql:
return 'sql';
case Language.javascript:
return 'js';
case Language.typescript:
return 'ts';
case Language.markdown:
return 'md';
case Language.plaintext:
return 'txt';
default:
return language.toString();
}
};
/**
* Downloads a file to a users device given its string content and a full file name.
*/
const defaultOnDownload = (value, fileName) => {
const link = document.createElement('a');
link.href = URL.createObjectURL(new Blob([value], { type: 'text' }));
link.download = fileName;
link.click();
};
export const CodeEditor = ({ className = '', code = '', copyButtonAriaLabel = 'Copy code to clipboard', copyButtonSuccessTooltipText = 'Content added to clipboard', copyButtonToolTipText = 'Copy to clipboard', customControls = null, downloadButtonAriaLabel = 'Download code', downloadButtonToolTipText = 'Download', downloadFileName = Date.now().toString(), editorProps, emptyState = '', emptyStateBody = 'Drag and drop a file or upload one.', emptyStateButton = 'Browse', emptyStateLink = 'Start from scratch', emptyStateTitle = 'Start editing', headerMainContent = '', height, isCopyEnabled = false, isDarkTheme = false, isDownloadEnabled = false, isFullHeight = false, isHeaderPlain = false, isLanguageLabelVisible = false, isLineNumbersVisible = true, isMinimapVisible = false, isReadOnly = false, isUploadEnabled = false, language = Language.plaintext, loading = '', onChange = () => { }, onCodeChange = () => { }, onDownload = defaultOnDownload, onEditorDidMount = () => { }, options = {}, overrideServices = {}, shortcutsPopoverButtonText = 'View Shortcuts', shortcutsPopoverProps = { bodyContent: '', 'aria-label': 'Keyboard Shortcuts' }, showEditor = true, toolTipCopyExitDelay = 1600, toolTipDelay = 300, toolTipMaxWidth = '100px', toolTipPosition = 'top', uploadButtonAriaLabel = 'Upload code', uploadButtonToolTipText = 'Upload', width = '' }) => {
const [value, setValue] = useState(code);
const [isLoading, setIsLoading] = useState(false);
const [showEmptyState, setShowEmptyState] = useState(true);
const [copied, setCopied] = useState(false);
const editorRef = useRef(null);
const wrapperRef = useRef(null);
const ref = useRef(null);
const observer = useRef(() => { });
const setHeightToFitContent = () => {
var _a, _b, _c;
const contentHeight = (_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.getContentHeight();
const layoutInfo = (_b = editorRef.current) === null || _b === void 0 ? void 0 : _b.getLayoutInfo();
(_c = editorRef.current) === null || _c === void 0 ? void 0 : _c.layout({ width: layoutInfo.width, height: contentHeight });
};
const onModelChange = (value, event) => {
if (height === 'sizeToFit') {
setHeightToFitContent();
}
if (onChange) {
onChange(value, event);
}
setValue(value);
onCodeChange(value);
};
const handleResize = () => {
if (editorRef.current) {
editorRef.current.layout({ width: 0, height: 0 }); // ensures the editor won't take up more space than it needs
editorRef.current.layout();
}
};
const handleGlobalKeys = (event) => {
var _a;
if (wrapperRef.current === document.activeElement && (event.key === 'ArrowDown' || event.key === ' ')) {
(_a = editorRef.current) === null || _a === void 0 ? void 0 : _a.focus();
event.preventDefault();
}
};
// if the code changes due to the prop changing
// set the value to the code prop
useEffect(() => setValue(code), [code]);
useEffect(() => {
document.addEventListener('keydown', handleGlobalKeys);
observer.current = getResizeObserver(ref.current, handleResize, true);
handleResize();
return () => {
document.removeEventListener('keydown', handleGlobalKeys);
observer.current();
};
}, []);
const editorBeforeMount = (monaco) => {
var _a;
defineThemes(monaco.editor);
(_a = editorProps === null || editorProps === void 0 ? void 0 : editorProps.beforeMount) === null || _a === void 0 ? void 0 : _a.call(editorProps, monaco);
};
const editorDidMount = (editor, monaco) => {
// eslint-disable-next-line no-bitwise
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Tab, () => wrapperRef.current.focus());
Array.from(document.getElementsByClassName('monaco-editor')).forEach((editorElement) => editorElement.removeAttribute('role'));
onEditorDidMount(editor, monaco);
editorRef.current = editor;
if (height === 'sizeToFit') {
setHeightToFitContent();
}
};
const handleFileChange = (value) => {
setValue(value);
onCodeChange(value);
};
const handleFileReadStarted = () => setIsLoading(true);
const handleFileReadFinished = () => setIsLoading(false);
const readFile = (fileHandle) => new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(fileHandle);
});
const onDropAccepted = (acceptedFiles) => {
if (acceptedFiles.length > 0) {
const fileHandle = acceptedFiles[0];
handleFileChange(''); // Show the filename while reading
handleFileReadStarted();
readFile(fileHandle)
.then((data) => {
handleFileReadFinished();
setShowEmptyState(false);
handleFileChange(data);
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error('error', error);
handleFileReadFinished();
handleFileChange('');
});
}
};
const onDropRejected = (rejectedFiles) => {
if (rejectedFiles.length > 0) {
// eslint-disable-next-line no-console
console.error('There was an error accepting that dropped file'); // TODO
}
};
const copyCode = () => {
navigator.clipboard.writeText(value);
setCopied(true);
};
const editorOptions = Object.assign({ fontFamily: monoFont.var, scrollBeyondLastLine: height !== 'sizeToFit', readOnly: isReadOnly, cursorStyle: 'line', lineNumbers: isLineNumbersVisible ? 'on' : 'off', tabIndex: -1, minimap: {
enabled: isMinimapVisible
} }, options);
const tooltipProps = {
position: toolTipPosition,
exitDelay: toolTipDelay,
entryDelay: toolTipDelay,
maxWidth: toolTipMaxWidth,
trigger: 'mouseenter focus'
};
const hasEditorHeaderContent = ((isCopyEnabled || isDownloadEnabled) && (!showEmptyState || !!value)) ||
isUploadEnabled ||
customControls ||
headerMainContent ||
!!shortcutsPopoverProps.bodyContent;
return (_jsx(Dropzone, { multiple: false, onDropAccepted: onDropAccepted, onDropRejected: onDropRejected, children: ({ getRootProps, getInputProps, isDragActive, open }) => {
const hiddenFileInput = _jsx("input", Object.assign({}, getInputProps(), { hidden: true }));
const editorEmptyState = emptyState ||
(isUploadEnabled ? (_jsxs(EmptyState, { variant: EmptyStateVariant.sm, titleText: emptyStateTitle, icon: CodeIcon, headingLevel: "h4", children: [_jsx(EmptyStateBody, { children: emptyStateBody }), !isReadOnly && (_jsxs(EmptyStateFooter, { children: [_jsx(EmptyStateActions, { children: _jsx(Button, { variant: "primary", onClick: open, children: emptyStateButton }) }), _jsx(EmptyStateActions, { children: _jsx(Button, { variant: "link", onClick: () => setShowEmptyState(false), children: emptyStateLink }) })] }))] })) : (_jsx(EmptyState, { variant: EmptyStateVariant.sm, titleText: emptyStateTitle, icon: CodeIcon, headingLevel: "h4", children: !isReadOnly && (_jsx(EmptyStateFooter, { children: _jsx(EmptyStateActions, { children: _jsx(Button, { variant: "primary", onClick: () => setShowEmptyState(false), children: emptyStateLink }) }) })) })));
const editorHeaderContent = (_jsxs(_Fragment, { children: [_jsx("div", { className: css(styles.codeEditorControls), children: _jsxs(CodeEditorContext.Provider, { value: { code: value }, children: [isCopyEnabled && (!showEmptyState || !!value) && (_jsx(CodeEditorControl, { icon: _jsx(CopyIcon, {}), "aria-label": copyButtonAriaLabel, tooltipProps: Object.assign(Object.assign({}, tooltipProps), { 'aria-live': 'polite', content: _jsx("div", { children: copied ? copyButtonSuccessTooltipText : copyButtonToolTipText }), exitDelay: copied ? toolTipCopyExitDelay : toolTipDelay, onTooltipHidden: () => setCopied(false) }), onClick: copyCode })), isUploadEnabled && (_jsx(CodeEditorControl, { icon: _jsx(UploadIcon, {}), "aria-label": uploadButtonAriaLabel, tooltipProps: Object.assign({ content: _jsx("div", { children: uploadButtonToolTipText }) }, tooltipProps), onClick: open })), isDownloadEnabled && (!showEmptyState || !!value) && (_jsx(CodeEditorControl, { icon: _jsx(DownloadIcon, {}), "aria-label": downloadButtonAriaLabel, tooltipProps: Object.assign({ content: _jsx("div", { children: downloadButtonToolTipText }) }, tooltipProps), onClick: () => {
onDownload(value, `${downloadFileName}.${getExtensionFromLanguage(language)}`);
} })), customControls && customControls] }) }), headerMainContent && _jsx("div", { className: css(styles.codeEditorHeaderMain), children: headerMainContent }), !!shortcutsPopoverProps.bodyContent && (_jsx("div", { className: `${styles.codeEditor}__keyboard-shortcuts`, children: _jsx(Popover, Object.assign({}, shortcutsPopoverProps, { children: _jsx(Button, { variant: ButtonVariant.link, icon: _jsx(HelpIcon, {}), children: shortcutsPopoverButtonText }) })) }))] }));
const editorHeader = (_jsxs("div", { className: css(styles.codeEditorHeader, isHeaderPlain && styles.modifiers.plain), children: [hasEditorHeaderContent && _jsx("div", { className: css(styles.codeEditorHeaderContent), children: editorHeaderContent }), isLanguageLabelVisible && (_jsxs("div", { className: css(styles.codeEditorTab), children: [_jsx("span", { className: css(styles.codeEditorTabIcon), children: _jsx(CodeIcon, {}) }), _jsx("span", { className: css(styles.codeEditorTabText), children: language.toUpperCase() })] }))] }));
const editor = (_jsx("div", { className: css(styles.codeEditorCode), ref: wrapperRef, tabIndex: 0, dir: "ltr", children: _jsx(Editor, Object.assign({ height: height === '100%' ? undefined : height, width: width, language: language, value: value, options: editorOptions, overrideServices: overrideServices, onChange: onModelChange, onMount: editorDidMount, loading: loading, theme: isDarkTheme ? 'pf-v6-theme-dark' : 'pf-v6-theme-light' }, editorProps, { beforeMount: editorBeforeMount })) }));
return (_jsx("div", { className: css(styles.codeEditor, isReadOnly && styles.modifiers.readOnly, (height === '100%' ? true : isFullHeight) && styles.modifiers.fullHeight, className), ref: ref, children: isUploadEnabled || emptyState ? (_jsxs("div", Object.assign({}, getRootProps({
onClick: (event) => event.stopPropagation() // Prevents clicking TextArea from opening file dialog
}), { className: css(styles.codeEditorContainer, isLoading && fileUploadStyles.modifiers.loading), children: [editorHeader, _jsx("div", { className: css(styles.codeEditorMain, isDragActive && styles.modifiers.dragHover), children: (showEmptyState || emptyState) && !value ? (_jsxs("div", { className: css(styles.codeEditorUpload), children: [hiddenFileInput, editorEmptyState] })) : (_jsxs(_Fragment, { children: [hiddenFileInput, editor] })) })] }))) : (_jsxs(_Fragment, { children: [editorHeader, showEditor && (_jsxs("div", { className: css(styles.codeEditorMain), children: [hiddenFileInput, editor] }))] })) }));
} }));
};
CodeEditor.displayName = 'CodeEditor';
//# sourceMappingURL=CodeEditor.js.map