mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
refactor: introduce codemirror for CSS editor (#2039)
* refactor: introduce codemirror for CSS editor * chore: fix formatting
This commit is contained in:
@@ -5,6 +5,10 @@
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@base-ui/react": "1.0.0",
|
||||
"@codemirror/commands": "^6.0.0",
|
||||
"@codemirror/lang-css": "^6.0.0",
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.0.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@dnd-kit/utilities": "^3.2.2",
|
||||
@@ -15,10 +19,10 @@
|
||||
"@tanstack/react-query": "^5.85.9",
|
||||
"@tanstack/react-query-devtools": "^5.85.9",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@uiw/codemirror-theme-vscode": "^4.25.9",
|
||||
"autosize": "^6.0.1",
|
||||
"axios": "^1.12.2",
|
||||
"csv-stringify": "^6.6.0",
|
||||
"prismjs": "^1.30.0",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "^19.2.3",
|
||||
"react-colorful": "^5.6.1",
|
||||
@@ -27,7 +31,6 @@
|
||||
"react-hook-form": "^7.62.0",
|
||||
"react-icons": "5.5.0",
|
||||
"react-router": "^7.11.0",
|
||||
"react-simple-code-editor": "^0.14.1",
|
||||
"react-virtuoso": "^4.17.0",
|
||||
"web-vitals": "^5.1.0",
|
||||
"zustand": "^5.0.9"
|
||||
@@ -58,7 +61,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sentry/vite-plugin": "5.1.1",
|
||||
"@types/prismjs": "^1.26.6",
|
||||
"@types/qrcode": "^1.5.6",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
@@ -70,7 +72,6 @@
|
||||
"typescript": "catalog:",
|
||||
"vite": "8.0.1",
|
||||
"vite-plugin-compression2": "2.5.1",
|
||||
"vite-plugin-prismjs-plus": "1.1.1",
|
||||
"vite-plugin-svgr": "4.5.0",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "catalog:"
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
declare module 'virtual:prismjs' {
|
||||
const prism: typeof import('prismjs');
|
||||
export default prism;
|
||||
}
|
||||
+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>
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
"types": [
|
||||
"vite/client",
|
||||
"vite-plugin-svgr/client",
|
||||
"vite-plugin-prismjs-plus/client",
|
||||
],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
|
||||
@@ -4,7 +4,6 @@ import { sentryVitePlugin } from '@sentry/vite-plugin';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { defineConfig } from 'vite';
|
||||
import { compression } from 'vite-plugin-compression2';
|
||||
import prismjsPlugin from 'vite-plugin-prismjs-plus';
|
||||
import svgrPlugin from 'vite-plugin-svgr';
|
||||
|
||||
import { ONTIME_VERSION } from './src/ONTIME_VERSION';
|
||||
@@ -17,11 +16,6 @@ const ReactCompilerConfig = {
|
||||
|
||||
export default defineConfig({
|
||||
base: './', // Ontime cloud: we use relative paths to allow them to reference a dynamic base set at runtime
|
||||
legacy: {
|
||||
// Temporary compatibility for CJS packages affected by Vite 8 interop changes.
|
||||
// ie: react-simple-code-editor
|
||||
inconsistentCjsInterop: true,
|
||||
},
|
||||
define: {
|
||||
// we pass along the NODE_ENV here in case it is a docker build
|
||||
'import.meta.env.IS_DOCKER': process.env.NODE_ENV === 'docker',
|
||||
@@ -54,11 +48,6 @@ export default defineConfig({
|
||||
algorithm: 'brotliCompress',
|
||||
exclude: /\.(html)$/, // Ontime cloud: Exclude HTML files from compression so we can change the base property at runtime
|
||||
}),
|
||||
prismjsPlugin({
|
||||
manual: true,
|
||||
languages: ['css'],
|
||||
css: true,
|
||||
}),
|
||||
],
|
||||
server: {
|
||||
port: 3000,
|
||||
|
||||
Reference in New Issue
Block a user