2023-12-15 17:34:50 +03:00
|
|
|
'use client';
|
2023-11-26 02:24:16 +03:00
|
|
|
|
|
|
|
import { Extension } from '@codemirror/state';
|
|
|
|
import { tags } from '@lezer/highlight';
|
|
|
|
import { createTheme } from '@uiw/codemirror-themes';
|
|
|
|
import CodeMirror, { BasicSetupOptions, ReactCodeMirrorProps, ReactCodeMirrorRef } from '@uiw/react-codemirror';
|
2023-12-15 17:34:50 +03:00
|
|
|
import clsx from 'clsx';
|
2023-11-26 02:24:16 +03:00
|
|
|
import { EditorView } from 'codemirror';
|
2023-12-27 18:44:37 +03:00
|
|
|
import { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
|
2023-11-26 02:24:16 +03:00
|
|
|
|
2024-01-04 19:38:12 +03:00
|
|
|
import Label from '@/components/ui/Label';
|
2024-06-26 19:47:31 +03:00
|
|
|
import { useConceptOptions } from '@/context/ConceptOptionsContext';
|
2023-12-13 14:32:57 +03:00
|
|
|
import DlgEditReference from '@/dialogs/DlgEditReference';
|
|
|
|
import { ReferenceType } from '@/models/language';
|
2024-06-19 12:13:05 +03:00
|
|
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
2023-12-13 14:32:57 +03:00
|
|
|
import { CodeMirrorWrapper } from '@/utils/codemirror';
|
2024-04-08 11:47:47 +03:00
|
|
|
import { PARAMETER } from '@/utils/constants';
|
2023-12-13 14:32:57 +03:00
|
|
|
|
2024-06-19 12:13:05 +03:00
|
|
|
import { refsNavigation } from './clickNavigation';
|
2023-11-26 02:24:16 +03:00
|
|
|
import { NaturalLanguage, ReferenceTokens } from './parse';
|
|
|
|
import { RefEntity } from './parse/parser.terms';
|
|
|
|
import { refsHoverTooltip } from './tooltip';
|
|
|
|
|
|
|
|
const editorSetup: BasicSetupOptions = {
|
|
|
|
highlightSpecialChars: false,
|
|
|
|
history: true,
|
2024-01-29 15:27:06 +03:00
|
|
|
drawSelection: true,
|
2023-11-26 02:24:16 +03:00
|
|
|
syntaxHighlighting: false,
|
|
|
|
defaultKeymap: true,
|
|
|
|
historyKeymap: true,
|
|
|
|
|
|
|
|
lineNumbers: false,
|
|
|
|
highlightActiveLineGutter: false,
|
|
|
|
foldGutter: false,
|
|
|
|
dropCursor: true,
|
|
|
|
allowMultipleSelections: false,
|
|
|
|
indentOnInput: false,
|
|
|
|
bracketMatching: false,
|
|
|
|
closeBrackets: false,
|
|
|
|
autocompletion: false,
|
|
|
|
rectangularSelection: false,
|
|
|
|
crosshairCursor: false,
|
|
|
|
highlightActiveLine: false,
|
|
|
|
highlightSelectionMatches: false,
|
|
|
|
closeBracketsKeymap: false,
|
|
|
|
searchKeymap: false,
|
|
|
|
foldKeymap: false,
|
|
|
|
completionKeymap: false,
|
|
|
|
lintKeymap: false
|
|
|
|
};
|
|
|
|
|
2023-12-28 14:04:44 +03:00
|
|
|
interface RefsInputInputProps
|
2024-05-09 12:47:59 +03:00
|
|
|
extends Pick<
|
|
|
|
ReactCodeMirrorProps,
|
|
|
|
| 'id' // prettier: split-lines
|
|
|
|
| 'height'
|
|
|
|
| 'minHeight'
|
|
|
|
| 'maxHeight'
|
|
|
|
| 'value'
|
|
|
|
| 'className'
|
|
|
|
| 'onFocus'
|
|
|
|
| 'onBlur'
|
|
|
|
| 'placeholder'
|
|
|
|
> {
|
2023-12-28 14:04:44 +03:00
|
|
|
label?: string;
|
|
|
|
onChange?: (newValue: string) => void;
|
2024-04-07 15:38:24 +03:00
|
|
|
schema?: IRSForm;
|
2024-06-19 12:13:05 +03:00
|
|
|
onOpenEdit?: (cstID: ConstituentaID) => void;
|
2023-12-28 14:04:44 +03:00
|
|
|
disabled?: boolean;
|
|
|
|
|
|
|
|
initialValue?: string;
|
|
|
|
value?: string;
|
|
|
|
resolved?: string;
|
2023-11-26 02:24:16 +03:00
|
|
|
}
|
|
|
|
|
2023-12-27 18:44:37 +03:00
|
|
|
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
|
2024-06-19 12:13:05 +03:00
|
|
|
(
|
|
|
|
{
|
|
|
|
id, // prettier: split-lines
|
|
|
|
label,
|
|
|
|
disabled,
|
|
|
|
schema,
|
|
|
|
onOpenEdit,
|
|
|
|
initialValue,
|
|
|
|
value,
|
|
|
|
resolved,
|
|
|
|
onFocus,
|
|
|
|
onBlur,
|
|
|
|
onChange,
|
|
|
|
...restProps
|
|
|
|
},
|
|
|
|
ref
|
|
|
|
) => {
|
2024-04-01 19:07:20 +03:00
|
|
|
const { darkMode, colors } = useConceptOptions();
|
2023-12-28 14:04:44 +03:00
|
|
|
|
|
|
|
const [isFocused, setIsFocused] = useState(false);
|
|
|
|
|
|
|
|
const [showEditor, setShowEditor] = useState(false);
|
|
|
|
const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY);
|
|
|
|
const [refText, setRefText] = useState('');
|
|
|
|
const [hintText, setHintText] = useState('');
|
|
|
|
const [basePosition, setBasePosition] = useState(0);
|
|
|
|
const [mainRefs, setMainRefs] = useState<string[]>([]);
|
|
|
|
|
|
|
|
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
|
|
|
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
|
|
|
|
|
|
|
const cursor = useMemo(() => (!disabled ? 'cursor-text' : 'cursor-default'), [disabled]);
|
|
|
|
const customTheme: Extension = useMemo(
|
|
|
|
() =>
|
|
|
|
createTheme({
|
|
|
|
theme: darkMode ? 'dark' : 'light',
|
|
|
|
settings: {
|
|
|
|
fontFamily: 'inherit',
|
|
|
|
background: !disabled ? colors.bgInput : colors.bgDefault,
|
|
|
|
foreground: colors.fgDefault,
|
2024-03-06 21:33:59 +03:00
|
|
|
selection: colors.bgHover,
|
|
|
|
caret: colors.fgDefault
|
2023-12-28 14:04:44 +03:00
|
|
|
},
|
|
|
|
styles: [
|
|
|
|
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // EntityReference
|
|
|
|
{ tag: tags.literal, color: colors.fgTeal, cursor: 'default' }, // SyntacticReference
|
|
|
|
{ tag: tags.comment, color: colors.fgRed } // Error
|
|
|
|
]
|
|
|
|
}),
|
|
|
|
[disabled, colors, darkMode]
|
|
|
|
);
|
|
|
|
|
|
|
|
const editorExtensions = useMemo(
|
2024-04-04 16:34:07 +03:00
|
|
|
() => [
|
|
|
|
EditorView.lineWrapping,
|
|
|
|
EditorView.contentAttributes.of({ spellcheck: 'true' }),
|
|
|
|
NaturalLanguage,
|
2024-06-19 12:13:05 +03:00
|
|
|
...(!schema || !onOpenEdit ? [] : [refsNavigation(schema, onOpenEdit)]),
|
|
|
|
...(schema ? [refsHoverTooltip(schema, colors, onOpenEdit !== undefined)] : [])
|
2024-04-04 16:34:07 +03:00
|
|
|
],
|
2024-06-19 12:13:05 +03:00
|
|
|
[schema, colors, onOpenEdit]
|
2023-12-28 14:04:44 +03:00
|
|
|
);
|
|
|
|
|
|
|
|
function handleChange(newValue: string) {
|
|
|
|
if (onChange) onChange(newValue);
|
2023-11-26 02:24:16 +03:00
|
|
|
}
|
2023-12-28 14:04:44 +03:00
|
|
|
|
|
|
|
function handleFocusIn(event: React.FocusEvent<HTMLDivElement>) {
|
|
|
|
setIsFocused(true);
|
|
|
|
if (onFocus) onFocus(event);
|
2023-11-26 02:24:16 +03:00
|
|
|
}
|
|
|
|
|
2023-12-28 14:04:44 +03:00
|
|
|
function handleFocusOut(event: React.FocusEvent<HTMLDivElement>) {
|
|
|
|
setIsFocused(false);
|
|
|
|
if (onBlur) onBlur(event);
|
2023-11-26 02:24:16 +03:00
|
|
|
}
|
2023-12-28 14:04:44 +03:00
|
|
|
|
|
|
|
const handleInput = useCallback(
|
|
|
|
(event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
|
|
if (!thisRef.current?.view) {
|
|
|
|
event.preventDefault();
|
2024-05-23 13:36:16 +03:00
|
|
|
event.stopPropagation();
|
2023-12-28 14:04:44 +03:00
|
|
|
return;
|
|
|
|
}
|
2024-05-22 12:18:05 +03:00
|
|
|
if ((event.ctrlKey || event.metaKey) && event.code === 'Space') {
|
2024-05-23 13:36:16 +03:00
|
|
|
event.preventDefault();
|
|
|
|
event.stopPropagation();
|
|
|
|
|
2023-12-28 14:04:44 +03:00
|
|
|
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
|
|
|
wrap.fixSelection(ReferenceTokens);
|
|
|
|
const nodes = wrap.getEnvelopingNodes(ReferenceTokens);
|
|
|
|
if (nodes.length !== 1) {
|
|
|
|
setCurrentType(ReferenceType.ENTITY);
|
|
|
|
setRefText('');
|
|
|
|
setHintText(wrap.getSelectionText());
|
|
|
|
} else {
|
|
|
|
setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC);
|
|
|
|
setRefText(wrap.getSelectionText());
|
|
|
|
}
|
|
|
|
|
|
|
|
const selection = wrap.getSelection();
|
|
|
|
const mainNodes = wrap
|
|
|
|
.getAllNodes([RefEntity])
|
|
|
|
.filter(node => node.from >= selection.to || node.to <= selection.from);
|
|
|
|
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
|
|
|
|
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
|
|
|
|
|
|
|
|
setShowEditor(true);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[thisRef]
|
|
|
|
);
|
|
|
|
|
|
|
|
const handleInputReference = useCallback(
|
|
|
|
(referenceText: string) => {
|
|
|
|
if (!thisRef.current?.view) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
thisRef.current.view.focus();
|
|
|
|
const wrap = new CodeMirrorWrapper(thisRef.current as Required<ReactCodeMirrorRef>);
|
|
|
|
wrap.replaceWith(referenceText);
|
|
|
|
},
|
|
|
|
[thisRef]
|
|
|
|
);
|
|
|
|
|
2024-04-08 11:47:47 +03:00
|
|
|
const hideEditReference = useCallback(() => {
|
|
|
|
setShowEditor(false);
|
|
|
|
setTimeout(() => thisRef.current?.view?.focus(), PARAMETER.refreshTimeout);
|
|
|
|
}, [thisRef]);
|
|
|
|
|
2023-12-28 14:04:44 +03:00
|
|
|
return (
|
2024-04-01 21:45:10 +03:00
|
|
|
<div className={clsx('flex flex-col gap-2', cursor)}>
|
2024-12-12 13:19:12 +03:00
|
|
|
{showEditor && schema ? (
|
|
|
|
<DlgEditReference
|
|
|
|
hideWindow={hideEditReference}
|
|
|
|
schema={schema}
|
|
|
|
initial={{
|
|
|
|
type: currentType,
|
|
|
|
refRaw: refText,
|
|
|
|
text: hintText,
|
|
|
|
basePosition: basePosition,
|
|
|
|
mainRefs: mainRefs
|
|
|
|
}}
|
|
|
|
onSave={handleInputReference}
|
|
|
|
/>
|
|
|
|
) : null}
|
2024-04-01 21:45:10 +03:00
|
|
|
<Label text={label} />
|
|
|
|
<CodeMirror
|
|
|
|
id={id}
|
|
|
|
ref={thisRef}
|
|
|
|
basicSetup={editorSetup}
|
|
|
|
theme={customTheme}
|
|
|
|
extensions={editorExtensions}
|
|
|
|
value={isFocused ? value : value !== initialValue || showEditor ? value : resolved}
|
|
|
|
indentWithTab={false}
|
|
|
|
onChange={handleChange}
|
2024-04-08 11:47:47 +03:00
|
|
|
editable={!disabled && !showEditor}
|
2024-04-01 21:45:10 +03:00
|
|
|
onKeyDown={handleInput}
|
|
|
|
onFocus={handleFocusIn}
|
|
|
|
onBlur={handleFocusOut}
|
|
|
|
{...restProps}
|
|
|
|
/>
|
|
|
|
</div>
|
2023-12-28 14:04:44 +03:00
|
|
|
);
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
export default RefsInput;
|