@craftercms/studio-ui
Version:
Services, components, models & utils to build CrafterCMS authoring extensions.
266 lines (264 loc) • 9.42 kB
JavaScript
/*
* 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;