mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-08-14 21:00:37 +03:00
Replace RS editor and fix colors in dark mode
This commit is contained in:
parent
83e08ca7fc
commit
2bab898032
|
@ -15,21 +15,21 @@ createTheme('customDark', {
|
|||
disabled: 'rgba(228, 228, 231, 0.54)'
|
||||
},
|
||||
background: {
|
||||
default: '#002b36'
|
||||
default: '#002129'
|
||||
},
|
||||
context: {
|
||||
background: '#3e014d',
|
||||
text: 'rgba(228, 228, 231, 0.87)'
|
||||
},
|
||||
highlightOnHover: {
|
||||
default: '#3e014d',
|
||||
default: '#2d0138',
|
||||
text: 'rgba(228, 228, 231, 1)'
|
||||
},
|
||||
divider: {
|
||||
default: '#6b6b6b'
|
||||
},
|
||||
striped: {
|
||||
default: '#004859',
|
||||
default: '#003845',
|
||||
text: 'rgba(228, 228, 231, 1)'
|
||||
},
|
||||
selected: {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
@tailwind utilities;
|
||||
|
||||
.rdt_TableCell{
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
[data-color-scheme="dark"] {
|
||||
|
@ -59,7 +59,7 @@
|
|||
}
|
||||
|
||||
.clr-input {
|
||||
@apply dark:bg-black bg-white disabled:bg-[#f0f2f7] dark:disabled:bg-gray-700
|
||||
@apply dark:bg-[#070b12] bg-white disabled:bg-[#f0f2f7] dark:disabled:bg-gray-700
|
||||
}
|
||||
|
||||
.clr-footer {
|
||||
|
|
|
@ -171,8 +171,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
|
|||
<p>- столбец "Описание" содержит один из непустых текстовых атрибутов</p>
|
||||
<Divider margins='mt-2' />
|
||||
<h1>Статусы</h1>
|
||||
{ [... mapStatusInfo.values()].map(info => {
|
||||
return (<p className='py-1'>
|
||||
{ [... mapStatusInfo.values()].map((info, index) => {
|
||||
return (<p className='py-1' key={`status-info-${index}`}>
|
||||
<span className={`inline-block font-semibold min-w-[4rem] text-center border ${info.color}`}>
|
||||
{info.text}
|
||||
</span>
|
||||
|
|
|
@ -4,7 +4,6 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|||
import Button from '../../components/Common/Button';
|
||||
import Label from '../../components/Common/Label';
|
||||
import { Loader } from '../../components/Common/Loader';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { useRSForm } from '../../context/RSFormContext';
|
||||
import useCheckExpression from '../../hooks/useCheckExpression';
|
||||
import { TokenID } from '../../utils/enums';
|
||||
|
@ -33,15 +32,13 @@ interface EditorRSExpressionProps {
|
|||
}
|
||||
|
||||
function EditorRSExpression({
|
||||
id, activeCst, label, disabled, isActive, placeholder, value, setValue, onShowAST,
|
||||
id, activeCst, label, disabled, isActive, placeholder, value, onShowAST,
|
||||
toggleEditMode, setTypification, onChange
|
||||
}: EditorRSExpressionProps) {
|
||||
const { user } = useAuth();
|
||||
const { schema } = useRSForm();
|
||||
|
||||
const [isModified, setIsModified] = useState(false);
|
||||
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema });
|
||||
const expressionCtrl = useRef<HTMLTextAreaElement>(null);
|
||||
const rsInput = useRef<ReactCodeMirrorRef>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
|
@ -66,11 +63,10 @@ function EditorRSExpression({
|
|||
const expression = prefix + value;
|
||||
checkExpression(expression, parse => {
|
||||
if (parse.errors.length > 0) {
|
||||
const errorPosition = parse.errors[0].position - prefix.length
|
||||
expressionCtrl.current!.selectionStart = errorPosition;
|
||||
expressionCtrl.current!.selectionEnd = errorPosition;
|
||||
onShowError(parse.errors[0]);
|
||||
} else {
|
||||
rsInput.current?.view?.focus();
|
||||
}
|
||||
expressionCtrl.current!.focus();
|
||||
setIsModified(false);
|
||||
setTypification(getTypificationLabel({
|
||||
isValid: parse.parseResult,
|
||||
|
@ -82,38 +78,40 @@ function EditorRSExpression({
|
|||
|
||||
const onShowError = useCallback(
|
||||
(error: IRSErrorDescription) => {
|
||||
if (!activeCst || !expressionCtrl.current) {
|
||||
return;
|
||||
if (!activeCst || !rsInput.current) {
|
||||
return;
|
||||
}
|
||||
const prefix = getCstExpressionPrefix(activeCst);
|
||||
const errorPosition = error.position - prefix.length;
|
||||
rsInput.current?.view?.dispatch({
|
||||
selection: {
|
||||
anchor: errorPosition,
|
||||
head: errorPosition
|
||||
}
|
||||
const errorPosition = error.position - getCstExpressionPrefix(activeCst).length
|
||||
expressionCtrl.current.selectionStart = errorPosition;
|
||||
expressionCtrl.current.selectionEnd = errorPosition;
|
||||
expressionCtrl.current.focus();
|
||||
});
|
||||
rsInput.current?.view?.focus();
|
||||
}, [activeCst]);
|
||||
|
||||
const handleEdit = useCallback((id: TokenID, key?: string) => {
|
||||
if (!expressionCtrl.current) {
|
||||
if (!rsInput.current || !rsInput.current.editor || !rsInput.current.state || !rsInput.current.view) {
|
||||
return;
|
||||
}
|
||||
const text = new TextWrapper(expressionCtrl.current);
|
||||
const text = new TextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
|
||||
if (id === TokenID.ID_LOCAL) {
|
||||
text.insertChar(key ?? 'unknown_local');
|
||||
} else {
|
||||
text.insertToken(id);
|
||||
}
|
||||
text.finalize();
|
||||
text.focus();
|
||||
setValue(text.value);
|
||||
rsInput.current?.view?.focus();
|
||||
setIsModified(true);
|
||||
}, [setValue]);
|
||||
}, []);
|
||||
|
||||
const handleInput = useCallback(
|
||||
(event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
if (!expressionCtrl.current) {
|
||||
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (!rsInput.current) {
|
||||
return;
|
||||
}
|
||||
const text = new TextWrapper(expressionCtrl.current);
|
||||
// rsInput.current?.state?.selection
|
||||
const text = new TextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
|
||||
if (event.shiftKey && event.key === '*' && !event.altKey) {
|
||||
text.insertToken(TokenID.DECART);
|
||||
} else if (event.altKey) {
|
||||
|
@ -130,10 +128,8 @@ function EditorRSExpression({
|
|||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
text.finalize();
|
||||
setValue(text.value);
|
||||
setIsModified(true);
|
||||
}, [expressionCtrl, setValue]);
|
||||
}, []);
|
||||
|
||||
const EditButtons = useMemo(() => {
|
||||
return (<div className='flex items-center justify-between w-full'>
|
||||
|
@ -229,24 +225,15 @@ function EditorRSExpression({
|
|||
required={false}
|
||||
htmlFor={id}
|
||||
/>
|
||||
<textarea id={id} ref={expressionCtrl}
|
||||
className='w-full px-3 py-2 mt-2 leading-tight border shadow clr-input'
|
||||
rows={6}
|
||||
<RSInput innerref={rsInput}
|
||||
className='mt-2'
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
value={value}
|
||||
onChange={event => handleChange(event.target.value)}
|
||||
onFocus={handleFocusIn}
|
||||
onKeyDown={handleInput}
|
||||
disabled={disabled}
|
||||
spellCheck={false}
|
||||
/>
|
||||
{ user?.is_staff &&
|
||||
<RSInput ref={rsInput}
|
||||
value={value}
|
||||
editable={!disabled}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder}
|
||||
disabled={disabled}
|
||||
/> }
|
||||
onKeyDown={handleInput}
|
||||
onFocus={handleFocusIn}
|
||||
/>
|
||||
<div className='flex w-full gap-4 py-1 mt-1 justify-stretch'>
|
||||
<div className='flex flex-col gap-2'>
|
||||
<Button
|
||||
|
@ -257,7 +244,7 @@ function EditorRSExpression({
|
|||
onClick={handleCheckExpression}
|
||||
/>
|
||||
</div>
|
||||
{isActive && EditButtons}
|
||||
{isActive && !disabled && EditButtons}
|
||||
</div>
|
||||
{ (loading || parseData) &&
|
||||
<div className='w-full overflow-y-auto border mt-2 max-h-[14rem] min-h-[7rem]'>
|
||||
|
|
|
@ -18,9 +18,9 @@ function ParsingResult({ data, onShowAST, onShowError }: ParsingResultProps) {
|
|||
return (
|
||||
<div className='px-3 py-2'>
|
||||
<p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p>
|
||||
{data.errors.map(error => {
|
||||
{data.errors.map((error, index) => {
|
||||
return (
|
||||
<p className='cursor-pointer text-red' onClick={() => onShowError(error)}>
|
||||
<p key={`error-${index}`} className='cursor-pointer text-red' onClick={() => onShowError(error)}>
|
||||
<span className='mr-1 font-semibold underline'>{error.isCritical ? 'Ошибка' : 'Предупреждение'} {getRSErrorPrefix(error)}:</span>
|
||||
<span> {getRSErrorMessage(error)}</span>
|
||||
</p>
|
||||
|
|
|
@ -1,43 +1,12 @@
|
|||
import { bracketMatching } from '@codemirror/language';
|
||||
import { Extension } from '@codemirror/state';
|
||||
import { createTheme } from '@uiw/codemirror-themes';
|
||||
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
import { EditorView } from 'codemirror';
|
||||
import { Ref } from 'react';
|
||||
import { Ref, useMemo } from 'react';
|
||||
|
||||
import { useConceptTheme } from '../../../context/ThemeContext';
|
||||
|
||||
const lightTheme: Extension = createTheme({
|
||||
theme: 'light',
|
||||
settings: {
|
||||
fontFamily: 'inherit',
|
||||
background: '#ffffff',
|
||||
foreground: '#000000',
|
||||
selection: '#036dd626'
|
||||
},
|
||||
styles: [
|
||||
// { tag: t.comment, color: '#787b8099' },
|
||||
// { tag: t.variableName, color: '#0080ff' },
|
||||
// { tag: [t.string, t.special(t.brace)], color: '#5c6166' },
|
||||
// { tag: t.definition(t.typeName), color: '#5c6166' },
|
||||
]
|
||||
});
|
||||
|
||||
const darkTheme: Extension = createTheme({
|
||||
theme: 'dark',
|
||||
settings: {
|
||||
fontFamily: 'inherit',
|
||||
background: '#000000',
|
||||
foreground: '#ffffff',
|
||||
selection: '#036dd626'
|
||||
},
|
||||
styles: [
|
||||
// { tag: t.comment, color: '#787b8099' },
|
||||
// { tag: t.variableName, color: '#0080ff' },
|
||||
// { tag: [t.string, t.special(t.brace)], color: '#5c6166' },
|
||||
// { tag: t.definition(t.typeName), color: '#5c6166' },
|
||||
]
|
||||
});
|
||||
|
||||
const editorSetup: BasicSetupOptions = {
|
||||
highlightSpecialChars: true,
|
||||
history: true,
|
||||
|
@ -52,7 +21,7 @@ const editorSetup: BasicSetupOptions = {
|
|||
dropCursor: false,
|
||||
allowMultipleSelections: false,
|
||||
indentOnInput: false,
|
||||
bracketMatching: true,
|
||||
bracketMatching: false,
|
||||
closeBrackets: false,
|
||||
autocompletion: false,
|
||||
rectangularSelection: false,
|
||||
|
@ -67,35 +36,74 @@ const editorSetup: BasicSetupOptions = {
|
|||
};
|
||||
|
||||
const editorExtensions = [
|
||||
EditorView.lineWrapping
|
||||
EditorView.lineWrapping,
|
||||
bracketMatching()
|
||||
];
|
||||
|
||||
interface RSInputProps {
|
||||
ref?: Ref<ReactCodeMirrorRef>
|
||||
value?: string
|
||||
disabled?: boolean
|
||||
height?: string
|
||||
placeholder?: string
|
||||
interface RSInputProps
|
||||
extends Omit<ReactCodeMirrorProps, 'onChange'> {
|
||||
innerref?: Ref<ReactCodeMirrorRef> | undefined
|
||||
onChange: (newValue: string) => void
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
|
||||
}
|
||||
|
||||
function RSInput({
|
||||
disabled, onChange,
|
||||
innerref, onChange, editable,
|
||||
height='10rem',
|
||||
...props
|
||||
}: RSInputProps) {
|
||||
const { darkMode } = useConceptTheme();
|
||||
|
||||
const cursor = useMemo(() => editable ? 'cursor-text': 'cursor-default', [editable]);
|
||||
const lightTheme: Extension = useMemo(
|
||||
() => createTheme({
|
||||
theme: 'light',
|
||||
settings: {
|
||||
fontFamily: 'inherit',
|
||||
background: editable ? '#ffffff' : '#f0f2f7',
|
||||
foreground: '#000000',
|
||||
selection: '#036dd626',
|
||||
selectionMatch: '#036dd626',
|
||||
caret: '#5d00ff',
|
||||
},
|
||||
styles: [
|
||||
// { tag: t.comment, color: '#787b8099' },
|
||||
// { tag: t.variableName, color: '#0080ff' },
|
||||
// { tag: [t.string, t.special(t.brace)], color: '#5c6166' },
|
||||
// { tag: t.definition(t.typeName), color: '#5c6166' },
|
||||
]
|
||||
}), [editable]);
|
||||
|
||||
const darkTheme: Extension = useMemo(
|
||||
() => createTheme({
|
||||
theme: 'dark',
|
||||
settings: {
|
||||
fontFamily: 'inherit',
|
||||
background: editable ? '#070b12' : '#374151',
|
||||
foreground: '#e4e4e7',
|
||||
selection: '#ffae00b0',
|
||||
selectionMatch: '#ffae00b0',
|
||||
caret: '#ffaa00'
|
||||
},
|
||||
styles: [
|
||||
// { tag: t.comment, color: '#787b8099' },
|
||||
// { tag: t.variableName, color: '#0080ff' },
|
||||
// { tag: [t.string, t.special(t.brace)], color: '#5c6166' },
|
||||
// { tag: t.definition(t.typeName), color: '#5c6166' },
|
||||
]
|
||||
}), [editable]);
|
||||
|
||||
return (
|
||||
<div className={`w-full h-[${height}]`}>
|
||||
<div className={`w-full h-[${height}] ${cursor}`}>
|
||||
<CodeMirror
|
||||
ref={innerref}
|
||||
basicSetup={editorSetup}
|
||||
extensions={editorExtensions}
|
||||
editable={!disabled}
|
||||
height={height}
|
||||
indentWithTab={false}
|
||||
theme={darkMode ? darkTheme : lightTheme}
|
||||
onChange={(value) => onChange(value)}
|
||||
onChange={value => onChange(value)}
|
||||
editable={editable}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
// Formatted text editing helpers
|
||||
|
||||
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
||||
|
||||
import { TokenID } from '../../../utils/enums';
|
||||
|
||||
export function getSymbolSubstitute(input: string): string | undefined {
|
||||
|
@ -34,65 +36,31 @@ export function getSymbolSubstitute(input: string): string | undefined {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
export interface IManagedText {
|
||||
value: string
|
||||
selStart: number
|
||||
selEnd: number
|
||||
}
|
||||
|
||||
// Note: Wrapper class for textareafield.
|
||||
// WARNING! Manipulations on value do not support UNDO browser
|
||||
// WARNING! No checks for selection out of text boundaries
|
||||
export class TextWrapper implements IManagedText {
|
||||
value: string
|
||||
selStart: number
|
||||
selEnd: number
|
||||
object: HTMLTextAreaElement
|
||||
export class TextWrapper {
|
||||
ref: Required<ReactCodeMirrorRef>
|
||||
|
||||
constructor(element: HTMLTextAreaElement) {
|
||||
this.object = element;
|
||||
this.value = this.object.value;
|
||||
this.selStart = this.object.selectionStart;
|
||||
this.selEnd = this.object.selectionEnd;
|
||||
}
|
||||
|
||||
focus() {
|
||||
this.object.focus();
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.value = this.object.value;
|
||||
this.selStart = this.object.selectionStart;
|
||||
this.selEnd = this.object.selectionEnd;
|
||||
}
|
||||
|
||||
finalize() {
|
||||
this.object.value = this.value;
|
||||
this.object.selectionStart = this.selStart;
|
||||
this.object.selectionEnd = this.selEnd;
|
||||
constructor(object: Required<ReactCodeMirrorRef>) {
|
||||
this.ref = object;
|
||||
}
|
||||
|
||||
replaceWith(data: string) {
|
||||
this.value = this.value.substring(0, this.selStart) + data + this.value.substring(this.selEnd, this.value.length);
|
||||
this.selEnd += data.length - this.selEnd + this.selStart;
|
||||
this.selStart = this.selEnd;
|
||||
this.ref.view.dispatch(this.ref.view.state.replaceSelection(data));
|
||||
}
|
||||
|
||||
envelopeWith(left: string, right: string) {
|
||||
this.value = this.value.substring(0, this.selStart) + left +
|
||||
this.value.substring(this.selStart, this.selEnd) + right +
|
||||
this.value.substring(this.selEnd, this.value.length);
|
||||
this.selEnd += left.length + right.length;
|
||||
}
|
||||
|
||||
moveSel(shift: number) {
|
||||
this.selStart += shift;
|
||||
this.selEnd += shift;
|
||||
}
|
||||
|
||||
setSel(start: number, end: number) {
|
||||
this.selStart = start;
|
||||
this.selEnd = end;
|
||||
this.ref.view.dispatch({
|
||||
changes: [
|
||||
{from: this.ref.view.state.selection.main.from, insert: left},
|
||||
{from: this.ref.view.state.selection.main.to, insert: right}
|
||||
],
|
||||
selection: {
|
||||
anchor: this.ref.view.state.selection.main.from,
|
||||
head: this.ref.view.state.selection.main.to + left.length + right.length
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
insertChar(key: string) {
|
||||
|
@ -114,18 +82,26 @@ export class TextWrapper implements IManagedText {
|
|||
|
||||
case TokenID.PUNC_PL: {
|
||||
this.envelopeWith('(', ')');
|
||||
this.selEnd = this.selStart + 1;
|
||||
this.selStart = this.selEnd;
|
||||
this.ref.view.dispatch({
|
||||
selection: {
|
||||
anchor: this.ref.view.state.selection.main.from + 1,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
case TokenID.PUNC_SL: {
|
||||
this.envelopeWith('[', ']');
|
||||
this.selEnd = this.selStart + 1;
|
||||
this.selStart = this.selEnd;
|
||||
this.ref.view.dispatch({
|
||||
selection: {
|
||||
anchor: this.ref.view.state.selection.main.from + 1,
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
case TokenID.BOOLEAN: {
|
||||
if (this.selEnd !== this.selStart && this.value[this.selStart] === 'ℬ') {
|
||||
const selStart = this.ref.view.state.selection.main.from;
|
||||
if (selStart !== this.ref.view.state.selection.main.to &&
|
||||
this.ref.view.state.sliceDoc(selStart, selStart + 1) === 'ℬ') {
|
||||
this.envelopeWith('ℬ', '');
|
||||
} else {
|
||||
this.envelopeWith('ℬ(', ')');
|
||||
|
|
Loading…
Reference in New Issue
Block a user