Replace RS editor and fix colors in dark mode

This commit is contained in:
IRBorisov 2023-08-12 01:03:06 +03:00
parent 83e08ca7fc
commit 2bab898032
7 changed files with 124 additions and 153 deletions

View File

@ -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: {

View File

@ -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 {

View File

@ -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>

View File

@ -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]'>

View File

@ -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>

View File

@ -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>

View File

@ -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('(', ')');