@patternfly/react-code-editor
Version:
This package provides a PatternFly wrapper for the Monaco code editor
343 lines • 21.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CodeEditor = exports.Language = void 0;
const tslib_1 = require("tslib");
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const react_styles_1 = require("@patternfly/react-styles");
const code_editor_1 = tslib_1.__importDefault(require("@patternfly/react-styles/css/components/CodeEditor/code-editor"));
const file_upload_1 = tslib_1.__importDefault(require("@patternfly/react-styles/css/components/FileUpload/file-upload"));
const Button_1 = require('@patternfly/react-core/dist/js/components/Button');
const EmptyState_1 = require('@patternfly/react-core/dist/js/components/EmptyState');
const Popover_1 = require('@patternfly/react-core/dist/js/components/Popover');
const resizeObserver_1 = require('@patternfly/react-core/dist/js/helpers/resizeObserver');
const react_2 = tslib_1.__importDefault(require("@monaco-editor/react"));
const copy_icon_1 = tslib_1.__importDefault(require('@patternfly/react-icons/dist/js/icons/copy-icon'));
const upload_icon_1 = tslib_1.__importDefault(require('@patternfly/react-icons/dist/js/icons/upload-icon'));
const download_icon_1 = tslib_1.__importDefault(require('@patternfly/react-icons/dist/js/icons/download-icon'));
const code_icon_1 = tslib_1.__importDefault(require('@patternfly/react-icons/dist/js/icons/code-icon'));
const help_icon_1 = tslib_1.__importDefault(require('@patternfly/react-icons/dist/js/icons/help-icon'));
const react_dropzone_1 = tslib_1.__importDefault(require("react-dropzone"));
const CodeEditorUtils_1 = require("./CodeEditorUtils");
const CodeEditorControl_1 = require("./CodeEditorControl");
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 || (exports.Language = Language = {}));
class CodeEditor extends react_1.Component {
static 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();
}
}
constructor(props) {
super(props);
this.editor = null;
this.wrapperRef = (0, react_1.createRef)();
this.ref = (0, react_1.createRef)();
this.timer = null;
this.observer = () => { };
this.onChange = (value, event) => {
if (this.props.height === 'sizeToFit') {
this.setHeightToFitContent();
}
if (this.props.onChange) {
this.props.onChange(value, event);
}
this.setState({ value });
this.props.onCodeChange(value);
};
this.handleResize = () => {
if (this.editor) {
this.editor.layout({ width: 0, height: 0 }); // ensures the editor won't take up more space than it needs
this.editor.layout();
}
};
this.handleGlobalKeys = (event) => {
var _a;
if (this.wrapperRef.current === document.activeElement && (event.key === 'ArrowDown' || event.key === ' ')) {
(_a = this.editor) === null || _a === void 0 ? void 0 : _a.focus();
event.preventDefault();
}
};
this.editorDidMount = (editor, monaco) => {
// eslint-disable-next-line no-bitwise
editor.addCommand(monaco.KeyMod.Shift | monaco.KeyCode.Tab, () => this.wrapperRef.current.focus());
Array.from(document.getElementsByClassName('monaco-editor')).forEach((editorElement) => editorElement.removeAttribute('role'));
this.props.onEditorDidMount(editor, monaco);
this.editor = editor;
if (this.props.height === 'sizeToFit') {
this.setHeightToFitContent();
}
};
this.handleFileChange = (value, filename) => {
this.setState({
value,
filename
});
this.props.onCodeChange(value);
};
this.handleFileReadStarted = () => this.setState({ isLoading: true });
this.handleFileReadFinished = () => this.setState({ isLoading: false });
this.onDropAccepted = (acceptedFiles) => {
if (acceptedFiles.length > 0) {
const fileHandle = acceptedFiles[0];
this.handleFileChange('', fileHandle.name); // Show the filename while reading
this.handleFileReadStarted();
this.readFile(fileHandle)
.then((data) => {
this.handleFileReadFinished();
this.toggleEmptyState();
this.handleFileChange(data, fileHandle.name);
})
.catch((error) => {
// eslint-disable-next-line no-console
console.error('error', error);
this.handleFileReadFinished();
this.handleFileChange('', ''); // Clear the filename field on a failure
});
}
};
this.onDropRejected = (rejectedFiles) => {
if (rejectedFiles.length > 0) {
// eslint-disable-next-line no-console
console.error('There was an error accepting that dropped file'); // TODO
}
};
this.copyCode = () => {
navigator.clipboard.writeText(this.state.value);
this.setState({ copied: true });
};
this.download = () => {
const { value } = this.state;
const element = document.createElement('a');
const file = new Blob([value], { type: 'text' });
element.href = URL.createObjectURL(file);
element.download = `${this.props.downloadFileName}.${CodeEditor.getExtensionFromLanguage(this.props.language)}`;
document.body.appendChild(element); // Required for this to work in FireFox
element.click();
};
this.toggleEmptyState = () => {
this.setState({ showEmptyState: false });
};
this.state = {
height: this.props.height,
prevPropsCode: this.props.code,
value: this.props.code,
filename: '',
isLoading: false,
showEmptyState: true,
copied: false
};
}
setHeightToFitContent() {
const contentHeight = this.editor.getContentHeight();
const layoutInfo = this.editor.getLayoutInfo();
this.editor.layout({ width: layoutInfo.width, height: contentHeight });
}
// this function is only called when the props change
// the only conflict is between props.code and state.value
static getDerivedStateFromProps(nextProps, prevState) {
// if the code changes due to the props.code changing
// set the value to props.code
if (nextProps.code !== prevState.prevPropsCode) {
return {
value: nextProps.code,
prevPropsCode: nextProps.code
};
}
// else, don't need to change the state.value
// because the onChange function will do all the work
return null;
}
componentDidMount() {
document.addEventListener('keydown', this.handleGlobalKeys);
this.observer = (0, resizeObserver_1.getResizeObserver)(this.ref.current, this.handleResize, true);
this.handleResize();
}
componentWillUnmount() {
document.removeEventListener('keydown', this.handleGlobalKeys);
this.observer();
}
readFile(fileHandle) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
reader.readAsText(fileHandle);
});
}
render() {
const { height, value, isLoading, showEmptyState, copied } = this.state;
const { isDarkTheme, width, className, isCopyEnabled, copyButtonSuccessTooltipText, isReadOnly, isUploadEnabled, isLanguageLabelVisible, copyButtonAriaLabel, copyButtonToolTipText, uploadButtonAriaLabel, uploadButtonToolTipText, downloadButtonAriaLabel, downloadButtonToolTipText, toolTipDelay, toolTipCopyExitDelay, toolTipMaxWidth, toolTipPosition, isLineNumbersVisible, isDownloadEnabled, language, emptyState: providedEmptyState, emptyStateTitle, emptyStateBody, emptyStateButton, emptyStateLink, customControls, isMinimapVisible, isHeaderPlain, headerMainContent, shortcutsPopoverButtonText, shortcutsPopoverProps: shortcutsPopoverPropsProp, showEditor, options: optionsProp, overrideServices, loading, editorProps } = this.props;
const shortcutsPopoverProps = Object.assign(Object.assign({}, CodeEditor.defaultProps.shortcutsPopoverProps), shortcutsPopoverPropsProp);
const options = Object.assign({ scrollBeyondLastLine: height !== 'sizeToFit', readOnly: isReadOnly, cursorStyle: 'line', lineNumbers: isLineNumbersVisible ? 'on' : 'off', tabIndex: -1, minimap: {
enabled: isMinimapVisible
} }, optionsProp);
const isFullHeight = this.props.height === '100%' ? true : this.props.isFullHeight;
return ((0, jsx_runtime_1.jsx)(react_dropzone_1.default, { multiple: false, onDropAccepted: this.onDropAccepted, onDropRejected: this.onDropRejected, children: ({ getRootProps, getInputProps, isDragActive, open }) => {
const emptyState = providedEmptyState ||
(isUploadEnabled ? ((0, jsx_runtime_1.jsxs)(EmptyState_1.EmptyState, { variant: EmptyState_1.EmptyStateVariant.sm, titleText: emptyStateTitle, icon: code_icon_1.default, headingLevel: "h4", children: [(0, jsx_runtime_1.jsx)(EmptyState_1.EmptyStateBody, { children: emptyStateBody }), !isReadOnly && ((0, jsx_runtime_1.jsxs)(EmptyState_1.EmptyStateFooter, { children: [(0, jsx_runtime_1.jsx)(EmptyState_1.EmptyStateActions, { children: (0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "primary", onClick: open, children: emptyStateButton }) }), (0, jsx_runtime_1.jsx)(EmptyState_1.EmptyStateActions, { children: (0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "link", onClick: this.toggleEmptyState, children: emptyStateLink }) })] }))] })) : ((0, jsx_runtime_1.jsx)(EmptyState_1.EmptyState, { variant: EmptyState_1.EmptyStateVariant.sm, titleText: emptyStateTitle, icon: code_icon_1.default, headingLevel: "h4", children: !isReadOnly && ((0, jsx_runtime_1.jsx)(EmptyState_1.EmptyStateFooter, { children: (0, jsx_runtime_1.jsx)(EmptyState_1.EmptyStateActions, { children: (0, jsx_runtime_1.jsx)(Button_1.Button, { variant: "primary", onClick: this.toggleEmptyState, children: emptyStateLink }) }) })) })));
const tooltipProps = {
position: toolTipPosition,
exitDelay: toolTipDelay,
entryDelay: toolTipDelay,
maxWidth: toolTipMaxWidth,
trigger: 'mouseenter focus'
};
const hasEditorHeaderContent = ((isCopyEnabled || isDownloadEnabled) && (!showEmptyState || !!value)) ||
isUploadEnabled ||
customControls ||
headerMainContent ||
!!shortcutsPopoverProps.bodyContent;
const editorHeaderContent = ((0, jsx_runtime_1.jsxs)(react_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorControls), children: (0, jsx_runtime_1.jsxs)(CodeEditorUtils_1.CodeEditorContext.Provider, { value: { code: value }, children: [isCopyEnabled && (!showEmptyState || !!value) && ((0, jsx_runtime_1.jsx)(CodeEditorControl_1.CodeEditorControl, { icon: (0, jsx_runtime_1.jsx)(copy_icon_1.default, {}), "aria-label": copyButtonAriaLabel, tooltipProps: Object.assign(Object.assign({}, tooltipProps), { 'aria-live': 'polite', content: (0, jsx_runtime_1.jsx)("div", { children: copied ? copyButtonSuccessTooltipText : copyButtonToolTipText }), exitDelay: copied ? toolTipCopyExitDelay : toolTipDelay, onTooltipHidden: () => this.setState({ copied: false }) }), onClick: this.copyCode })), isUploadEnabled && ((0, jsx_runtime_1.jsx)(CodeEditorControl_1.CodeEditorControl, { icon: (0, jsx_runtime_1.jsx)(upload_icon_1.default, {}), "aria-label": uploadButtonAriaLabel, tooltipProps: Object.assign({ content: (0, jsx_runtime_1.jsx)("div", { children: uploadButtonToolTipText }) }, tooltipProps), onClick: open })), isDownloadEnabled && (!showEmptyState || !!value) && ((0, jsx_runtime_1.jsx)(CodeEditorControl_1.CodeEditorControl, { icon: (0, jsx_runtime_1.jsx)(download_icon_1.default, {}), "aria-label": downloadButtonAriaLabel, tooltipProps: Object.assign({ content: (0, jsx_runtime_1.jsx)("div", { children: downloadButtonToolTipText }) }, tooltipProps), onClick: this.download })), customControls && customControls] }) }), headerMainContent && (0, jsx_runtime_1.jsx)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorHeaderMain), children: headerMainContent }), !!shortcutsPopoverProps.bodyContent && ((0, jsx_runtime_1.jsx)("div", { className: `${code_editor_1.default.codeEditor}__keyboard-shortcuts`, children: (0, jsx_runtime_1.jsx)(Popover_1.Popover, Object.assign({}, shortcutsPopoverProps, { children: (0, jsx_runtime_1.jsx)(Button_1.Button, { variant: Button_1.ButtonVariant.link, icon: (0, jsx_runtime_1.jsx)(help_icon_1.default, {}), children: shortcutsPopoverButtonText }) })) }))] }));
const editorHeader = ((0, jsx_runtime_1.jsxs)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorHeader, isHeaderPlain && code_editor_1.default.modifiers.plain), children: [hasEditorHeaderContent && ((0, jsx_runtime_1.jsx)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorHeaderContent), children: editorHeaderContent })), isLanguageLabelVisible && ((0, jsx_runtime_1.jsxs)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorTab), children: [(0, jsx_runtime_1.jsx)("span", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorTabIcon), children: (0, jsx_runtime_1.jsx)(code_icon_1.default, {}) }), (0, jsx_runtime_1.jsx)("span", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorTabText), children: language.toUpperCase() })] }))] }));
const editor = ((0, jsx_runtime_1.jsx)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorCode), ref: this.wrapperRef, tabIndex: 0, dir: "ltr", children: (0, jsx_runtime_1.jsx)(react_2.default, Object.assign({ height: height === '100%' ? undefined : height, width: width, language: language, value: value, options: options, overrideServices: overrideServices, onChange: this.onChange, onMount: this.editorDidMount, theme: isDarkTheme ? 'vs-dark' : 'vs-light', loading: loading }, editorProps)) }));
const hiddenFileInput = (0, jsx_runtime_1.jsx)("input", Object.assign({}, getInputProps(), { hidden: true }));
return ((0, jsx_runtime_1.jsx)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditor, isReadOnly && code_editor_1.default.modifiers.readOnly, isFullHeight && code_editor_1.default.modifiers.fullHeight, className), ref: this.ref, children: (isUploadEnabled || providedEmptyState) && !value ? ((0, jsx_runtime_1.jsxs)("div", Object.assign({}, getRootProps({
onClick: (event) => event.stopPropagation() // Prevents clicking TextArea from opening file dialog
}), { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorContainer, isLoading && file_upload_1.default.modifiers.loading), children: [editorHeader, (0, jsx_runtime_1.jsx)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorMain, isDragActive && code_editor_1.default.modifiers.dragHover), children: (showEmptyState || providedEmptyState) && !value ? ((0, jsx_runtime_1.jsxs)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorUpload), children: [hiddenFileInput, emptyState] })) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [hiddenFileInput, editor] })) })] }))) : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [editorHeader, showEditor && ((0, jsx_runtime_1.jsxs)("div", { className: (0, react_styles_1.css)(code_editor_1.default.codeEditorMain), children: [hiddenFileInput, editor] }))] })) }));
} }));
}
}
exports.CodeEditor = CodeEditor;
CodeEditor.displayName = 'CodeEditor';
CodeEditor.defaultProps = {
className: '',
code: '',
onEditorDidMount: () => { },
language: Language.plaintext,
isDarkTheme: false,
width: '',
isLineNumbersVisible: true,
isReadOnly: false,
isLanguageLabelVisible: false,
loading: '',
emptyState: '',
emptyStateTitle: 'Start editing',
emptyStateBody: 'Drag and drop a file or upload one.',
emptyStateButton: 'Browse',
emptyStateLink: 'Start from scratch',
downloadFileName: Date.now().toString(),
isUploadEnabled: false,
isDownloadEnabled: false,
isCopyEnabled: false,
isHeaderPlain: false,
copyButtonAriaLabel: 'Copy code to clipboard',
uploadButtonAriaLabel: 'Upload code',
downloadButtonAriaLabel: 'Download code',
copyButtonToolTipText: 'Copy to clipboard',
uploadButtonToolTipText: 'Upload',
downloadButtonToolTipText: 'Download',
copyButtonSuccessTooltipText: 'Content added to clipboard',
toolTipCopyExitDelay: 1600,
toolTipDelay: 300,
toolTipMaxWidth: '100px',
toolTipPosition: 'top',
customControls: null,
isMinimapVisible: false,
headerMainContent: '',
shortcutsPopoverButtonText: 'View Shortcuts',
shortcutsPopoverProps: {
bodyContent: '',
'aria-label': 'Keyboard Shortcuts'
},
showEditor: true,
options: {},
overrideServices: {},
onCodeChange: () => { }
};
//# sourceMappingURL=CodeEditor.js.map