UNPKG

@craftercms/studio-ui

Version:

Services, components, models & utils to build CrafterCMS authoring extensions.

269 lines (267 loc) 8.52 kB
/* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ /* * Copyright (C) 2007-2022 Crafter Software Corporation. All Rights Reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as published by * the Free Software Foundation. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ import React, { useEffect, useRef, useState } from 'react'; import { makeStyles } from 'tss-react/mui'; import { useMount } from '../../hooks/useMount'; import { useTheme } from '@mui/material/styles'; import clsx from 'clsx'; import { useEnhancedDialogContext } from '../EnhancedDialog/useEnhancedDialogContext'; const aceOptions = [ 'selectionStyle', 'highlightActiveLine', 'highlightSelectedWord', 'readOnly', 'cursorStyle', 'mergeUndoDeltas', 'behavioursEnabled', 'wrapBehavioursEnabled', 'autoScrollEditorIntoView', 'copyWithEmptySelection', 'useSoftTabs', 'navigateWithinSoftTabs', 'enableMultiselect', 'hScrollBarAlwaysVisible', 'vScrollBarAlwaysVisible', 'highlightGutterLine', 'animatedScroll', 'showInvisibles', 'showPrintMargin', 'printMarginColumn', 'printMargin', 'fadeFoldWidgets', 'showFoldWidgets', 'showLineNumbers', 'showGutter', 'displayIndentGuides', 'fontSize', 'fontFamily', 'maxLines', 'minLines', 'scrollPastEnd', 'fixedWidthGutter', 'theme', 'scrollSpeed', 'dragDelay', 'dragEnabled', 'focusTimout', 'tooltipFollowsMouse', 'firstLineNumber', 'overwrite', 'newLineMode', 'useWorker', 'tabSize', 'wrap', 'foldStyle', 'mode', 'enableBasicAutocompletion', 'enableLiveAutocompletion', 'enableSnippets', 'enableEmmet', 'useElasticTabstops' ]; // const aceModes = []; // const aceThemes = []; const useStyles = makeStyles()((_theme, { root, editorRoot } = {}) => ({ root: { position: 'relative', display: 'contents', ...root }, editorRoot: { top: 0, left: 0, right: 0, bottom: 0, margin: 0, width: '100%', height: '100%', ...editorRoot } })); function AceEditorComp(props, ref) { const { value = '', classes: propClasses, autoFocus = false, styles, extensions = [], onChange, onInit, ...options } = props; const { classes, cx } = useStyles(styles); const editorRootClasses = propClasses?.editorRoot; const refs = useRef({ ace: null, elem: null, pre: null, onChange: null, options: null }); const [initialized, setInitialized] = useState(false); const { palette: { mode } } = useTheme(); options.theme = options.theme ?? `ace/theme/${mode === 'light' ? 'chrome' : 'tomorrow_night'}`; refs.current.onChange = onChange; refs.current.options = options; useMount(() => { let unmounted = false; let initialized = false; let aceEditor; let deps = { ace: false, emmet: false, languageTools: false }; const init = () => { deps.ace && deps.emmet && deps.languageTools && // @ts-ignore - Ace types are incorrect; the require function takes a callback window.ace.require(['ace/ace', 'ace/ext/language_tools', 'ace/ext/emmet', ...extensions], (ace) => { if (!unmounted) { const pre = document.createElement('pre'); pre.className = cx(classes.editorRoot, editorRootClasses); refs.current.pre = pre; refs.current.elem.appendChild(pre); // @ts-ignore - Ace types are incorrect; they don't implement the constructor that receives options. aceEditor = ace.edit(pre, refs.current.options); aceEditor.setOptions({ fontSize: '15px', fontFamily: "'Monaco', 'Menlo', 'Consolas', 'Source Code Pro', 'source-code-pro', monospace" }); autoFocus && aceEditor.focus(); if (refs.current.options.readOnly) { // This setting of the cursor to not display is unnecessary as the // options.readOnly effect takes care of doing so. Nevertheless, this // eliminates the delay in hiding the cursor if left up to the effect only. // @ts-ignore - $cursorLayer.element typings are missing aceEditor.renderer.$cursorLayer.element.style.display = 'none'; } refs.current.ace = aceEditor; onInit?.(aceEditor); if (ref) { typeof ref === 'function' ? ref(aceEditor) : (ref.current = aceEditor); } setInitialized((initialized = true)); } }); }; // TODO: Loading mechanisms very rudimentary. Must research better ways. if (!window.ace) { const aceScript = document.createElement('script'); aceScript.src = '/studio/static-assets/libs/ace/ace.js'; aceScript.onload = () => { deps.ace = true; // Language tools const languageToolsScript = document.createElement('script'); languageToolsScript.src = '/studio/static-assets/libs/ace/ext-language_tools.js'; languageToolsScript.onload = () => { deps.emmet = true; init(); }; document.head.appendChild(languageToolsScript); // Emmet const emmetScript = document.createElement('script'); emmetScript.src = '/studio/static-assets/libs/ace/ext-emmet.js'; emmetScript.onload = () => { deps.languageTools = true; init(); }; document.head.appendChild(emmetScript); }; document.head.appendChild(aceScript); } else { deps.ace = true; deps.emmet = true; deps.languageTools = true; init(); } return () => { unmounted = true; if (initialized) { aceEditor.destroy(); } }; }); useEffect( () => { if (initialized) { refs.current.ace.setOptions(options); } }, // eslint-disable-next-line react-hooks/exhaustive-deps [ initialized, // eslint-disable-next-line react-hooks/exhaustive-deps ...aceOptions.map((o) => options[o]) ] ); useEffect(() => { if (initialized) { const editor = refs.current.ace; editor.renderer.$cursorLayer.element.style.display = options?.readOnly ? 'none' : ''; } }, [initialized, options?.readOnly]); // If the Editor is inside a dialog, resize when fullscreen changes const isFullScreen = useEnhancedDialogContext()?.isFullScreen; useEffect(() => { refs.current.ace?.resize(); }, [isFullScreen]); useEffect(() => { if (initialized) { const ace = refs.current.ace; const onChange = (e) => { refs.current.onChange?.(e); }; ace.setValue(value, -1); ace.session.getUndoManager().reset(); ace.getSession().on('change', onChange); return () => { ace.getSession().off('change', onChange); }; } }, [initialized, value]); useEffect(() => { if (refs.current.pre) { refs.current.pre.className = `${[...refs.current.pre.classList] .filter((value) => !/craftercms-|makeStyles-/.test(value)) .join(' ')} ${clsx(classes?.editorRoot, editorRootClasses)}`; } }, [classes.editorRoot, editorRootClasses]); return React.createElement('div', { ref: (e) => { if (e) { refs.current.elem = e; } }, className: cx(classes.root, props.classes?.root) }); } export const AceEditor = React.forwardRef(AceEditorComp); export default AceEditor;