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)' disabled: 'rgba(228, 228, 231, 0.54)'
}, },
background: { background: {
default: '#002b36' default: '#002129'
}, },
context: { context: {
background: '#3e014d', background: '#3e014d',
text: 'rgba(228, 228, 231, 0.87)' text: 'rgba(228, 228, 231, 0.87)'
}, },
highlightOnHover: { highlightOnHover: {
default: '#3e014d', default: '#2d0138',
text: 'rgba(228, 228, 231, 1)' text: 'rgba(228, 228, 231, 1)'
}, },
divider: { divider: {
default: '#6b6b6b' default: '#6b6b6b'
}, },
striped: { striped: {
default: '#004859', default: '#003845',
text: 'rgba(228, 228, 231, 1)' text: 'rgba(228, 228, 231, 1)'
}, },
selected: { selected: {

View File

@ -3,7 +3,7 @@
@tailwind utilities; @tailwind utilities;
.rdt_TableCell{ .rdt_TableCell{
font-size: 14px; font-size: 0.875rem;
} }
[data-color-scheme="dark"] { [data-color-scheme="dark"] {
@ -59,7 +59,7 @@
} }
.clr-input { .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 { .clr-footer {

View File

@ -171,8 +171,8 @@ function EditorConstituenta({ activeID, onShowAST, onCreateCst, onOpenEdit, onDe
<p>- столбец "Описание" содержит один из непустых текстовых атрибутов</p> <p>- столбец "Описание" содержит один из непустых текстовых атрибутов</p>
<Divider margins='mt-2' /> <Divider margins='mt-2' />
<h1>Статусы</h1> <h1>Статусы</h1>
{ [... mapStatusInfo.values()].map(info => { { [... mapStatusInfo.values()].map((info, index) => {
return (<p className='py-1'> return (<p className='py-1' key={`status-info-${index}`}>
<span className={`inline-block font-semibold min-w-[4rem] text-center border ${info.color}`}> <span className={`inline-block font-semibold min-w-[4rem] text-center border ${info.color}`}>
{info.text} {info.text}
</span> </span>

View File

@ -4,7 +4,6 @@ import { useCallback, useLayoutEffect, useMemo, useRef, useState } from 'react';
import Button from '../../components/Common/Button'; import Button from '../../components/Common/Button';
import Label from '../../components/Common/Label'; import Label from '../../components/Common/Label';
import { Loader } from '../../components/Common/Loader'; import { Loader } from '../../components/Common/Loader';
import { useAuth } from '../../context/AuthContext';
import { useRSForm } from '../../context/RSFormContext'; import { useRSForm } from '../../context/RSFormContext';
import useCheckExpression from '../../hooks/useCheckExpression'; import useCheckExpression from '../../hooks/useCheckExpression';
import { TokenID } from '../../utils/enums'; import { TokenID } from '../../utils/enums';
@ -33,15 +32,13 @@ interface EditorRSExpressionProps {
} }
function EditorRSExpression({ function EditorRSExpression({
id, activeCst, label, disabled, isActive, placeholder, value, setValue, onShowAST, id, activeCst, label, disabled, isActive, placeholder, value, onShowAST,
toggleEditMode, setTypification, onChange toggleEditMode, setTypification, onChange
}: EditorRSExpressionProps) { }: EditorRSExpressionProps) {
const { user } = useAuth();
const { schema } = useRSForm(); const { schema } = useRSForm();
const [isModified, setIsModified] = useState(false); const [isModified, setIsModified] = useState(false);
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema }); const { parseData, checkExpression, resetParse, loading } = useCheckExpression({ schema });
const expressionCtrl = useRef<HTMLTextAreaElement>(null);
const rsInput = useRef<ReactCodeMirrorRef>(null); const rsInput = useRef<ReactCodeMirrorRef>(null);
useLayoutEffect(() => { useLayoutEffect(() => {
@ -66,11 +63,10 @@ function EditorRSExpression({
const expression = prefix + value; const expression = prefix + value;
checkExpression(expression, parse => { checkExpression(expression, parse => {
if (parse.errors.length > 0) { if (parse.errors.length > 0) {
const errorPosition = parse.errors[0].position - prefix.length onShowError(parse.errors[0]);
expressionCtrl.current!.selectionStart = errorPosition; } else {
expressionCtrl.current!.selectionEnd = errorPosition; rsInput.current?.view?.focus();
} }
expressionCtrl.current!.focus();
setIsModified(false); setIsModified(false);
setTypification(getTypificationLabel({ setTypification(getTypificationLabel({
isValid: parse.parseResult, isValid: parse.parseResult,
@ -82,38 +78,40 @@ function EditorRSExpression({
const onShowError = useCallback( const onShowError = useCallback(
(error: IRSErrorDescription) => { (error: IRSErrorDescription) => {
if (!activeCst || !expressionCtrl.current) { if (!activeCst || !rsInput.current) {
return; 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; rsInput.current?.view?.focus();
expressionCtrl.current.selectionEnd = errorPosition;
expressionCtrl.current.focus();
}, [activeCst]); }, [activeCst]);
const handleEdit = useCallback((id: TokenID, key?: string) => { const handleEdit = useCallback((id: TokenID, key?: string) => {
if (!expressionCtrl.current) { if (!rsInput.current || !rsInput.current.editor || !rsInput.current.state || !rsInput.current.view) {
return; return;
} }
const text = new TextWrapper(expressionCtrl.current); const text = new TextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
if (id === TokenID.ID_LOCAL) { if (id === TokenID.ID_LOCAL) {
text.insertChar(key ?? 'unknown_local'); text.insertChar(key ?? 'unknown_local');
} else { } else {
text.insertToken(id); text.insertToken(id);
} }
text.finalize(); rsInput.current?.view?.focus();
text.focus();
setValue(text.value);
setIsModified(true); setIsModified(true);
}, [setValue]); }, []);
const handleInput = useCallback( const handleInput = useCallback(
(event: React.KeyboardEvent<HTMLTextAreaElement>) => { (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!expressionCtrl.current) { if (!rsInput.current) {
return; return;
} }
const text = new TextWrapper(expressionCtrl.current); const text = new TextWrapper(rsInput.current as Required<ReactCodeMirrorRef>);
// rsInput.current?.state?.selection
if (event.shiftKey && event.key === '*' && !event.altKey) { if (event.shiftKey && event.key === '*' && !event.altKey) {
text.insertToken(TokenID.DECART); text.insertToken(TokenID.DECART);
} else if (event.altKey) { } else if (event.altKey) {
@ -130,10 +128,8 @@ function EditorRSExpression({
return; return;
} }
event.preventDefault(); event.preventDefault();
text.finalize();
setValue(text.value);
setIsModified(true); setIsModified(true);
}, [expressionCtrl, setValue]); }, []);
const EditButtons = useMemo(() => { const EditButtons = useMemo(() => {
return (<div className='flex items-center justify-between w-full'> return (<div className='flex items-center justify-between w-full'>
@ -229,24 +225,15 @@ function EditorRSExpression({
required={false} required={false}
htmlFor={id} htmlFor={id}
/> />
<textarea id={id} ref={expressionCtrl} <RSInput innerref={rsInput}
className='w-full px-3 py-2 mt-2 leading-tight border shadow clr-input' className='mt-2'
rows={6} value={value}
placeholder={placeholder} placeholder={placeholder}
value={value} editable={!disabled}
onChange={event => handleChange(event.target.value)}
onFocus={handleFocusIn}
onKeyDown={handleInput}
disabled={disabled}
spellCheck={false}
/>
{ user?.is_staff &&
<RSInput ref={rsInput}
value={value}
onChange={handleChange} onChange={handleChange}
placeholder={placeholder} onKeyDown={handleInput}
disabled={disabled} onFocus={handleFocusIn}
/> } />
<div className='flex w-full gap-4 py-1 mt-1 justify-stretch'> <div className='flex w-full gap-4 py-1 mt-1 justify-stretch'>
<div className='flex flex-col gap-2'> <div className='flex flex-col gap-2'>
<Button <Button
@ -257,7 +244,7 @@ function EditorRSExpression({
onClick={handleCheckExpression} onClick={handleCheckExpression}
/> />
</div> </div>
{isActive && EditButtons} {isActive && !disabled && EditButtons}
</div> </div>
{ (loading || parseData) && { (loading || parseData) &&
<div className='w-full overflow-y-auto border mt-2 max-h-[14rem] min-h-[7rem]'> <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 ( return (
<div className='px-3 py-2'> <div className='px-3 py-2'>
<p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p> <p>Ошибок: <b>{errorCount}</b> | Предупреждений: <b>{warningsCount}</b></p>
{data.errors.map(error => { {data.errors.map((error, index) => {
return ( 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 className='mr-1 font-semibold underline'>{error.isCritical ? 'Ошибка' : 'Предупреждение'} {getRSErrorPrefix(error)}:</span>
<span> {getRSErrorMessage(error)}</span> <span> {getRSErrorMessage(error)}</span>
</p> </p>

View File

@ -1,43 +1,12 @@
import { bracketMatching } from '@codemirror/language';
import { Extension } from '@codemirror/state'; import { Extension } from '@codemirror/state';
import { createTheme } from '@uiw/codemirror-themes'; 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 { EditorView } from 'codemirror';
import { Ref } from 'react'; import { Ref, useMemo } from 'react';
import { useConceptTheme } from '../../../context/ThemeContext'; 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 = { const editorSetup: BasicSetupOptions = {
highlightSpecialChars: true, highlightSpecialChars: true,
history: true, history: true,
@ -52,7 +21,7 @@ const editorSetup: BasicSetupOptions = {
dropCursor: false, dropCursor: false,
allowMultipleSelections: false, allowMultipleSelections: false,
indentOnInput: false, indentOnInput: false,
bracketMatching: true, bracketMatching: false,
closeBrackets: false, closeBrackets: false,
autocompletion: false, autocompletion: false,
rectangularSelection: false, rectangularSelection: false,
@ -67,35 +36,74 @@ const editorSetup: BasicSetupOptions = {
}; };
const editorExtensions = [ const editorExtensions = [
EditorView.lineWrapping EditorView.lineWrapping,
bracketMatching()
]; ];
interface RSInputProps { interface RSInputProps
ref?: Ref<ReactCodeMirrorRef> extends Omit<ReactCodeMirrorProps, 'onChange'> {
value?: string innerref?: Ref<ReactCodeMirrorRef> | undefined
disabled?: boolean
height?: string
placeholder?: string
onChange: (newValue: string) => void onChange: (newValue: string) => void
onKeyDown: (event: React.KeyboardEvent<HTMLDivElement>) => void
} }
function RSInput({ function RSInput({
disabled, onChange, innerref, onChange, editable,
height='10rem', height='10rem',
...props ...props
}: RSInputProps) { }: RSInputProps) {
const { darkMode } = useConceptTheme(); 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 ( return (
<div className={`w-full h-[${height}]`}> <div className={`w-full h-[${height}] ${cursor}`}>
<CodeMirror <CodeMirror
ref={innerref}
basicSetup={editorSetup} basicSetup={editorSetup}
extensions={editorExtensions} extensions={editorExtensions}
editable={!disabled}
height={height} height={height}
indentWithTab={false} indentWithTab={false}
theme={darkMode ? darkTheme : lightTheme} theme={darkMode ? darkTheme : lightTheme}
onChange={(value) => onChange(value)} onChange={value => onChange(value)}
editable={editable}
{...props} {...props}
/> />
</div> </div>

View File

@ -1,5 +1,7 @@
// Formatted text editing helpers // Formatted text editing helpers
import { ReactCodeMirrorRef } from '@uiw/react-codemirror';
import { TokenID } from '../../../utils/enums'; import { TokenID } from '../../../utils/enums';
export function getSymbolSubstitute(input: string): string | undefined { export function getSymbolSubstitute(input: string): string | undefined {
@ -34,65 +36,31 @@ export function getSymbolSubstitute(input: string): string | undefined {
return undefined; return undefined;
} }
export interface IManagedText {
value: string
selStart: number
selEnd: number
}
// Note: Wrapper class for textareafield. // Note: Wrapper class for textareafield.
// WARNING! Manipulations on value do not support UNDO browser // WARNING! Manipulations on value do not support UNDO browser
// WARNING! No checks for selection out of text boundaries // WARNING! No checks for selection out of text boundaries
export class TextWrapper implements IManagedText { export class TextWrapper {
value: string ref: Required<ReactCodeMirrorRef>
selStart: number
selEnd: number
object: HTMLTextAreaElement
constructor(element: HTMLTextAreaElement) { constructor(object: Required<ReactCodeMirrorRef>) {
this.object = element; this.ref = object;
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;
} }
replaceWith(data: string) { replaceWith(data: string) {
this.value = this.value.substring(0, this.selStart) + data + this.value.substring(this.selEnd, this.value.length); this.ref.view.dispatch(this.ref.view.state.replaceSelection(data));
this.selEnd += data.length - this.selEnd + this.selStart;
this.selStart = this.selEnd;
} }
envelopeWith(left: string, right: string) { envelopeWith(left: string, right: string) {
this.value = this.value.substring(0, this.selStart) + left + this.ref.view.dispatch({
this.value.substring(this.selStart, this.selEnd) + right + changes: [
this.value.substring(this.selEnd, this.value.length); {from: this.ref.view.state.selection.main.from, insert: left},
this.selEnd += left.length + right.length; {from: this.ref.view.state.selection.main.to, insert: right}
} ],
selection: {
moveSel(shift: number) { anchor: this.ref.view.state.selection.main.from,
this.selStart += shift; head: this.ref.view.state.selection.main.to + left.length + right.length
this.selEnd += shift; }
} });
setSel(start: number, end: number) {
this.selStart = start;
this.selEnd = end;
} }
insertChar(key: string) { insertChar(key: string) {
@ -114,18 +82,26 @@ export class TextWrapper implements IManagedText {
case TokenID.PUNC_PL: { case TokenID.PUNC_PL: {
this.envelopeWith('(', ')'); this.envelopeWith('(', ')');
this.selEnd = this.selStart + 1; this.ref.view.dispatch({
this.selStart = this.selEnd; selection: {
anchor: this.ref.view.state.selection.main.from + 1,
}
});
return true; return true;
} }
case TokenID.PUNC_SL: { case TokenID.PUNC_SL: {
this.envelopeWith('[', ']'); this.envelopeWith('[', ']');
this.selEnd = this.selStart + 1; this.ref.view.dispatch({
this.selStart = this.selEnd; selection: {
anchor: this.ref.view.state.selection.main.from + 1,
}
});
return true; return true;
} }
case TokenID.BOOLEAN: { 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('', ''); this.envelopeWith('', '');
} else { } else {
this.envelopeWith('(', ')'); this.envelopeWith('(', ')');