UNPKG

@craftercms/studio-ui

Version:

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

266 lines (264 loc) 9.42 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/>. */ var __rest = (this && this.__rest) || function (s, e) { var t = {}; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) t[p] = s[p]; if (s != null && typeof Object.getOwnPropertySymbols === 'function') for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) t[p[i]] = s[p[i]]; } return t; }; 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: Object.assign({ position: 'relative', display: 'contents' }, root), editorRoot: Object.assign( { top: 0, left: 0, right: 0, bottom: 0, margin: 0, width: '100%', height: '100%' }, editorRoot ) })); function AceEditorComp(props, ref) { var _a, _b, _c; const { value = '', classes: propClasses, autoFocus = false, styles, extensions = [], onChange, onInit } = props, options = __rest(props, ['value', 'classes', 'autoFocus', 'styles', 'extensions', 'onChange', 'onInit']); const { classes, cx } = useStyles(styles); const editorRootClasses = propClasses === null || propClasses === void 0 ? void 0 : 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 = (_a = options.theme) !== null && _a !== void 0 ? _a : `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); 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 === null || onInit === void 0 ? void 0 : 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; // Emmet const emmetScript = document.createElement('script'); emmetScript.src = '/studio/static-assets/libs/ace/ext-language_tools.js'; emmetScript.onload = () => { deps.languageTools = true; init(); }; // Language tools const languageToolsScript = document.createElement('script'); languageToolsScript.src = '/studio/static-assets/libs/ace/ext-emmet.js'; languageToolsScript.onload = () => { deps.emmet = true; init(); }; document.head.appendChild(emmetScript); document.head.appendChild(languageToolsScript); }; 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 === null || options === void 0 ? void 0 : options.readOnly ) ? 'none' : ''; } }, [initialized, options === null || options === void 0 ? void 0 : options.readOnly]); // If the Editor is inside a dialog, resize when fullscreen changes const isFullScreen = (_b = useEnhancedDialogContext()) === null || _b === void 0 ? void 0 : _b.isFullScreen; useEffect(() => { var _a; (_a = refs.current.ace) === null || _a === void 0 ? void 0 : _a.resize(); }, [isFullScreen]); useEffect(() => { if (initialized) { const ace = refs.current.ace; const onChange = (e) => { var _a, _b; (_b = (_a = refs.current).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, 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 === null || classes === void 0 ? void 0 : classes.editorRoot, editorRootClasses)}`; } }, [classes.editorRoot, editorRootClasses]); return React.createElement('div', { ref: (e) => { if (e) { refs.current.elem = e; } }, className: cx(classes.root, (_c = props.classes) === null || _c === void 0 ? void 0 : _c.root) }); } export const AceEditor = React.forwardRef(AceEditorComp); export default AceEditor;