UNPKG

@patternfly/react-code-editor

Version:

This package provides a PatternFly wrapper for the Monaco code editor

343 lines • 21.1 kB
"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