mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-14 11:53:49 +00:00
refactor: introduce codemirror for CSS editor (#2039)
* refactor: introduce codemirror for CSS editor * chore: fix formatting
This commit is contained in:
+12
-4
@@ -1,5 +1,13 @@
|
||||
.wrapper {
|
||||
min-height: 500px;
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
.editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
:global(.cm-editor) {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:global(.cm-scroller) {
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
+77
-29
@@ -1,38 +1,86 @@
|
||||
import { memo } from 'react';
|
||||
import Editor from 'react-simple-code-editor';
|
||||
|
||||
import 'prismjs/themes/prism-tomorrow.min.css';
|
||||
import Prism from 'virtual:prismjs';
|
||||
import { defaultKeymap, indentWithTab } from '@codemirror/commands';
|
||||
import { css } from '@codemirror/lang-css';
|
||||
import { Annotation, EditorState } from '@codemirror/state';
|
||||
import { EditorView, keymap } from '@codemirror/view';
|
||||
import { vscodeDark } from '@uiw/codemirror-theme-vscode';
|
||||
import { memo, useEffect, useRef } from 'react';
|
||||
|
||||
import style from './StyleEditor.module.scss';
|
||||
|
||||
interface CodeEditorProps {
|
||||
language: string;
|
||||
interface StyleEditorProps {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export default memo(CodeEditor);
|
||||
function CodeEditor({ language, onChange, value }: CodeEditorProps) {
|
||||
const highlight = (code: string) => {
|
||||
const grammar = Prism.languages[language];
|
||||
return grammar ? Prism.highlight(code, grammar, language) : code;
|
||||
};
|
||||
const editorExtensions = [keymap.of([indentWithTab, ...defaultKeymap]), css(), vscodeDark];
|
||||
const externalValueSync = Annotation.define<boolean>();
|
||||
|
||||
return (
|
||||
<div className={style.wrapper}>
|
||||
<Editor
|
||||
value={value}
|
||||
padding={15}
|
||||
onValueChange={onChange}
|
||||
highlight={highlight}
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
minHeight: 500,
|
||||
background: 'transparent',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
export default memo(StyleEditor);
|
||||
function StyleEditor({ onChange, value }: StyleEditorProps) {
|
||||
const hostRef = useRef<HTMLDivElement | null>(null);
|
||||
const viewRef = useRef<EditorView | null>(null);
|
||||
const onChangeRef = useRef(onChange);
|
||||
|
||||
// Keep the latest callback available to the editor listener without recreating the editor instance.
|
||||
useEffect(() => {
|
||||
onChangeRef.current = onChange;
|
||||
}, [onChange]);
|
||||
|
||||
// Create the CodeMirror editor once and wire document changes back to React state.
|
||||
useEffect(() => {
|
||||
if (viewRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const parent = hostRef.current;
|
||||
if (!parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updateListener = EditorView.updateListener.of((update) => {
|
||||
if (!update.docChanged) {
|
||||
return;
|
||||
}
|
||||
if (update.transactions.some((transaction) => transaction.annotation(externalValueSync))) {
|
||||
return;
|
||||
}
|
||||
onChangeRef.current(update.state.doc.toString());
|
||||
});
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: value,
|
||||
extensions: [...editorExtensions, updateListener],
|
||||
});
|
||||
|
||||
const view = new EditorView({ state, parent });
|
||||
viewRef.current = view;
|
||||
|
||||
return () => {
|
||||
view.destroy();
|
||||
viewRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Apply external value updates to the editor when they differ from the current document.
|
||||
useEffect(() => {
|
||||
const view = viewRef.current;
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentValue = view.state.doc.toString();
|
||||
if (currentValue === value) {
|
||||
return;
|
||||
}
|
||||
if (view.hasFocus) {
|
||||
return;
|
||||
}
|
||||
|
||||
view.dispatch({
|
||||
changes: { from: 0, to: view.state.doc.length, insert: value },
|
||||
annotations: externalValueSync.of(true),
|
||||
});
|
||||
}, [value]);
|
||||
|
||||
return <div className={style.editor} ref={hostRef} />;
|
||||
}
|
||||
|
||||
+1
-1
@@ -16,7 +16,7 @@
|
||||
|
||||
.editorBody {
|
||||
position: relative;
|
||||
min-height: 500px;
|
||||
height: 500px;
|
||||
background-color: $gray-1350;
|
||||
border: 1px solid $gray-1100;
|
||||
border-radius: 3px;
|
||||
|
||||
+4
-6
@@ -51,11 +51,11 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
|
||||
|
||||
const clear = () => setDraftCss('');
|
||||
|
||||
// Load the latest CSS from the server whenever the modal opens, and ignore stale responses on close.
|
||||
useEffect(() => {
|
||||
let isCancelled = false;
|
||||
|
||||
async function fetchServerCSS() {
|
||||
// check for isOpen to fetch recent css
|
||||
if (isOpen) {
|
||||
try {
|
||||
setError(null);
|
||||
@@ -97,11 +97,9 @@ export default function CodeEditorModal({ isOpen, onClose }: CodeEditorModalProp
|
||||
showBackdrop
|
||||
bodyElements={
|
||||
<div className={style.editorBody}>
|
||||
{!isLoadingCss && (
|
||||
<Suspense fallback={<Panel.Loader isLoading />}>
|
||||
<CodeEditor value={draftCss} onChange={setDraftCss} language='css' />
|
||||
</Suspense>
|
||||
)}
|
||||
<Suspense fallback={<Panel.Loader isLoading />}>
|
||||
<CodeEditor value={draftCss} onChange={setDraftCss} />
|
||||
</Suspense>
|
||||
<Panel.Loader isLoading={isLoadingCss} />
|
||||
</div>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user