Implement click navigation for RSInput
This commit is contained in:
parent
8be4acbf43
commit
03b02ed613
2
TODO.txt
2
TODO.txt
|
@ -4,8 +4,6 @@ For more specific TODOs see comments in code
|
||||||
[Functionality - PROGRESS]
|
[Functionality - PROGRESS]
|
||||||
- Operational synthesis schema as LibraryItem ?
|
- Operational synthesis schema as LibraryItem ?
|
||||||
|
|
||||||
- Clickable IDs in RSEditor tooltips
|
|
||||||
|
|
||||||
- Library organization, search and exploration. Consider new user experience
|
- Library organization, search and exploration. Consider new user experience
|
||||||
- Private projects and permissions. Consider cooperative editing
|
- Private projects and permissions. Consider cooperative editing
|
||||||
|
|
||||||
|
|
|
@ -10,12 +10,13 @@ import { forwardRef, useCallback, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import Label from '@/components/ui/Label';
|
import Label from '@/components/ui/Label';
|
||||||
import { useConceptOptions } from '@/context/OptionsContext';
|
import { useConceptOptions } from '@/context/OptionsContext';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
|
||||||
import { getFontClassName } from '@/models/miscellaneousAPI';
|
import { getFontClassName } from '@/models/miscellaneousAPI';
|
||||||
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||||
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
|
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
|
||||||
import { extractGlobals } from '@/models/rslangAPI';
|
import { extractGlobals } from '@/models/rslangAPI';
|
||||||
|
|
||||||
import { ccBracketMatching } from './bracketMatching';
|
import { ccBracketMatching } from './bracketMatching';
|
||||||
|
import { rsNavigation } from './clickNavigation';
|
||||||
import { RSLanguage } from './rslang';
|
import { RSLanguage } from './rslang';
|
||||||
import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
|
import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
|
||||||
import { rsHoverTooltip } from './tooltip';
|
import { rsHoverTooltip } from './tooltip';
|
||||||
|
@ -39,6 +40,8 @@ interface RSInputProps
|
||||||
noTooltip?: boolean;
|
noTooltip?: boolean;
|
||||||
onChange?: (newValue: string) => void;
|
onChange?: (newValue: string) => void;
|
||||||
onAnalyze?: () => void;
|
onAnalyze?: () => void;
|
||||||
|
schema?: IRSForm;
|
||||||
|
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
|
@ -49,6 +52,9 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
disabled,
|
disabled,
|
||||||
noTooltip,
|
noTooltip,
|
||||||
|
|
||||||
|
schema,
|
||||||
|
onOpenEdit,
|
||||||
|
|
||||||
className,
|
className,
|
||||||
style,
|
style,
|
||||||
|
|
||||||
|
@ -59,7 +65,6 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const { darkMode, colors, mathFont } = useConceptOptions();
|
const { darkMode, colors, mathFont } = useConceptOptions();
|
||||||
const { schema } = useRSForm();
|
|
||||||
|
|
||||||
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
||||||
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
||||||
|
@ -77,7 +82,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
caret: colors.fgDefault
|
caret: colors.fgDefault
|
||||||
},
|
},
|
||||||
styles: [
|
styles: [
|
||||||
{ tag: tags.name, color: colors.fgPurple, cursor: 'default' }, // GlobalID
|
{ tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : 'text' }, // GlobalID
|
||||||
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID
|
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID
|
||||||
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
|
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
|
||||||
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
|
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
|
||||||
|
@ -87,7 +92,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets)
|
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets)
|
||||||
]
|
]
|
||||||
}),
|
}),
|
||||||
[disabled, colors, darkMode]
|
[disabled, colors, darkMode, schema]
|
||||||
);
|
);
|
||||||
|
|
||||||
const editorExtensions = useMemo(
|
const editorExtensions = useMemo(
|
||||||
|
@ -95,9 +100,10 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||||
EditorView.lineWrapping,
|
EditorView.lineWrapping,
|
||||||
RSLanguage,
|
RSLanguage,
|
||||||
ccBracketMatching(darkMode),
|
ccBracketMatching(darkMode),
|
||||||
|
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
|
||||||
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)])
|
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)])
|
||||||
],
|
],
|
||||||
[darkMode, schema, noTooltip]
|
[darkMode, schema, noTooltip, onOpenEdit]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleInput = useCallback(
|
const handleInput = useCallback(
|
||||||
|
|
38
rsconcept/frontend/src/components/RSInput/clickNavigation.ts
Normal file
38
rsconcept/frontend/src/components/RSInput/clickNavigation.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import { Extension } from '@codemirror/state';
|
||||||
|
import { EditorView } from '@uiw/react-codemirror';
|
||||||
|
|
||||||
|
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||||
|
import { findAliasAt } from '@/utils/codemirror';
|
||||||
|
|
||||||
|
const globalsNavigation = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void) => {
|
||||||
|
return EditorView.domEventHandlers({
|
||||||
|
click: (event: MouseEvent, view: EditorView) => {
|
||||||
|
if (!event.ctrlKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pos = view.posAtCoords({ x: event.clientX, y: event.clientY });
|
||||||
|
if (!pos) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { alias } = findAliasAt(pos, view.state);
|
||||||
|
if (!alias) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cst = schema.cstByAlias.get(alias);
|
||||||
|
if (!cst) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
onOpenEdit(cst.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export function rsNavigation(schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void): Extension {
|
||||||
|
return [globalsNavigation(schema, onOpenEdit)];
|
||||||
|
}
|
|
@ -1,30 +1,10 @@
|
||||||
import { syntaxTree } from '@codemirror/language';
|
|
||||||
import { Extension } from '@codemirror/state';
|
import { Extension } from '@codemirror/state';
|
||||||
import { hoverTooltip } from '@codemirror/view';
|
import { hoverTooltip } from '@codemirror/view';
|
||||||
import { EditorState } from '@uiw/react-codemirror';
|
|
||||||
|
|
||||||
import { IRSForm } from '@/models/rsform';
|
import { IRSForm } from '@/models/rsform';
|
||||||
import { findEnvelopingNodes } from '@/utils/codemirror';
|
import { findAliasAt } from '@/utils/codemirror';
|
||||||
import { domTooltipConstituenta } from '@/utils/codemirror';
|
import { domTooltipConstituenta } from '@/utils/codemirror';
|
||||||
|
|
||||||
import { GlobalTokens } from './rslang';
|
|
||||||
|
|
||||||
function findAliasAt(pos: number, state: EditorState) {
|
|
||||||
const { from: lineStart, to: lineEnd, text } = state.doc.lineAt(pos);
|
|
||||||
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), GlobalTokens);
|
|
||||||
let alias = '';
|
|
||||||
let start = 0;
|
|
||||||
let end = 0;
|
|
||||||
nodes.forEach(node => {
|
|
||||||
if (node.to <= lineEnd && node.from >= lineStart) {
|
|
||||||
alias = text.slice(node.from - lineStart, node.to - lineStart);
|
|
||||||
start = node.from;
|
|
||||||
end = node.to;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return { alias, start, end };
|
|
||||||
}
|
|
||||||
|
|
||||||
const globalsHoverTooltip = (schema: IRSForm) => {
|
const globalsHoverTooltip = (schema: IRSForm) => {
|
||||||
return hoverTooltip((view, pos) => {
|
return hoverTooltip((view, pos) => {
|
||||||
const { alias, start, end } = findAliasAt(pos, view.state);
|
const { alias, start, end } = findAliasAt(pos, view.state);
|
||||||
|
|
|
@ -95,6 +95,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
||||||
}
|
}
|
||||||
value={state.definition_formal}
|
value={state.definition_formal}
|
||||||
onChange={value => partialUpdate({ definition_formal: value })}
|
onChange={value => partialUpdate({ definition_formal: value })}
|
||||||
|
schema={schema}
|
||||||
/>
|
/>
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
<AnimateFade key='dlg_cst_definition' hideContent={!state.definition_raw && isElementary}>
|
<AnimateFade key='dlg_cst_definition' hideContent={!state.definition_raw && isElementary}>
|
||||||
|
|
|
@ -113,6 +113,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
||||||
setIsModified={setIsModified}
|
setIsModified={setIsModified}
|
||||||
onEditTerm={controller.editTermForms}
|
onEditTerm={controller.editTermForms}
|
||||||
onRename={controller.renameCst}
|
onRename={controller.renameCst}
|
||||||
|
onOpenEdit={onOpenEdit}
|
||||||
/>
|
/>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showList ? (
|
{showList ? (
|
||||||
|
|
|
@ -11,7 +11,7 @@ import SubmitButton from '@/components/ui/SubmitButton';
|
||||||
import TextArea from '@/components/ui/TextArea';
|
import TextArea from '@/components/ui/TextArea';
|
||||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||||
import { useRSForm } from '@/context/RSFormContext';
|
import { useRSForm } from '@/context/RSFormContext';
|
||||||
import { CstType, IConstituenta, ICstUpdateData } from '@/models/rsform';
|
import { ConstituentaID, CstType, IConstituenta, ICstUpdateData } from '@/models/rsform';
|
||||||
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
|
import { isBaseSet, isBasicConcept, isFunctional } from '@/models/rsformAPI';
|
||||||
import { information, labelCstTypification } from '@/utils/labels';
|
import { information, labelCstTypification } from '@/utils/labels';
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ interface FormConstituentaProps {
|
||||||
|
|
||||||
onRename: () => void;
|
onRename: () => void;
|
||||||
onEditTerm: () => void;
|
onEditTerm: () => void;
|
||||||
|
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function FormConstituenta({
|
function FormConstituenta({
|
||||||
|
@ -47,7 +48,8 @@ function FormConstituenta({
|
||||||
|
|
||||||
toggleReset,
|
toggleReset,
|
||||||
onRename,
|
onRename,
|
||||||
onEditTerm
|
onEditTerm,
|
||||||
|
onOpenEdit
|
||||||
}: FormConstituentaProps) {
|
}: FormConstituentaProps) {
|
||||||
const { schema, cstUpdate, processing } = useRSForm();
|
const { schema, cstUpdate, processing } = useRSForm();
|
||||||
|
|
||||||
|
@ -183,6 +185,7 @@ function FormConstituenta({
|
||||||
toggleReset={toggleReset}
|
toggleReset={toggleReset}
|
||||||
onChange={newValue => setExpression(newValue)}
|
onChange={newValue => setExpression(newValue)}
|
||||||
setTypification={setTypification}
|
setTypification={setTypification}
|
||||||
|
onOpenEdit={onOpenEdit}
|
||||||
/>
|
/>
|
||||||
</AnimateFade>
|
</AnimateFade>
|
||||||
<AnimateFade key='cst_definition_fade' hideContent={!!state && !state?.definition_raw && isElementary}>
|
<AnimateFade key='cst_definition_fade' hideContent={!!state && !state?.definition_raw && isElementary}>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import DlgShowAST from '@/dialogs/DlgShowAST';
|
||||||
import useCheckExpression from '@/hooks/useCheckExpression';
|
import useCheckExpression from '@/hooks/useCheckExpression';
|
||||||
import useLocalStorage from '@/hooks/useLocalStorage';
|
import useLocalStorage from '@/hooks/useLocalStorage';
|
||||||
import { HelpTopic } from '@/models/miscellaneous';
|
import { HelpTopic } from '@/models/miscellaneous';
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
||||||
import { getDefinitionPrefix } from '@/models/rsformAPI';
|
import { getDefinitionPrefix } from '@/models/rsformAPI';
|
||||||
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
|
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
|
||||||
import { TokenID } from '@/models/rslang';
|
import { TokenID } from '@/models/rslang';
|
||||||
|
@ -38,6 +38,7 @@ interface EditorRSExpressionProps {
|
||||||
|
|
||||||
setTypification: (typification: string) => void;
|
setTypification: (typification: string) => void;
|
||||||
onChange: (newValue: string) => void;
|
onChange: (newValue: string) => void;
|
||||||
|
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorRSExpression({
|
function EditorRSExpression({
|
||||||
|
@ -47,6 +48,7 @@ function EditorRSExpression({
|
||||||
toggleReset,
|
toggleReset,
|
||||||
setTypification,
|
setTypification,
|
||||||
onChange,
|
onChange,
|
||||||
|
onOpenEdit,
|
||||||
...restProps
|
...restProps
|
||||||
}: EditorRSExpressionProps) {
|
}: EditorRSExpressionProps) {
|
||||||
const model = useRSForm();
|
const model = useRSForm();
|
||||||
|
@ -185,6 +187,8 @@ function EditorRSExpression({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
onAnalyze={handleCheckExpression}
|
onAnalyze={handleCheckExpression}
|
||||||
|
schema={model.schema}
|
||||||
|
onOpenEdit={onOpenEdit}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ function ConstituentsTable({ items, activeCst, onOpenEdit, maxHeight, denseThres
|
||||||
if (element) {
|
if (element) {
|
||||||
element.scrollIntoView({
|
element.scrollIntoView({
|
||||||
behavior: 'smooth',
|
behavior: 'smooth',
|
||||||
block: 'nearest',
|
block: 'center',
|
||||||
inline: 'end'
|
inline: 'end'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,9 +3,10 @@
|
||||||
*/
|
*/
|
||||||
import { syntaxTree } from '@codemirror/language';
|
import { syntaxTree } from '@codemirror/language';
|
||||||
import { NodeType, Tree, TreeCursor } from '@lezer/common';
|
import { NodeType, Tree, TreeCursor } from '@lezer/common';
|
||||||
import { ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
import { EditorState, ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
|
||||||
|
import { GlobalTokens } from '@/components/RSInput/rslang';
|
||||||
import { IEntityReference, ISyntacticReference } from '@/models/language';
|
import { IEntityReference, ISyntacticReference } from '@/models/language';
|
||||||
import { parseGrammemes } from '@/models/languageAPI';
|
import { parseGrammemes } from '@/models/languageAPI';
|
||||||
import { IConstituenta } from '@/models/rsform';
|
import { IConstituenta } from '@/models/rsform';
|
||||||
|
@ -122,6 +123,25 @@ export function findContainedNodes(start: number, finish: number, tree: Tree, fi
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves globalID from position in Editor.
|
||||||
|
*/
|
||||||
|
export function findAliasAt(pos: number, state: EditorState) {
|
||||||
|
const { from: lineStart, to: lineEnd, text } = state.doc.lineAt(pos);
|
||||||
|
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), GlobalTokens);
|
||||||
|
let alias = '';
|
||||||
|
let start = 0;
|
||||||
|
let end = 0;
|
||||||
|
nodes.forEach(node => {
|
||||||
|
if (node.to <= lineEnd && node.from >= lineStart) {
|
||||||
|
alias = text.slice(node.from - lineStart, node.to - lineStart);
|
||||||
|
start = node.from;
|
||||||
|
end = node.to;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return { alias, start, end };
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create DOM tooltip for {@link Constituenta}.
|
* Create DOM tooltip for {@link Constituenta}.
|
||||||
*/
|
*/
|
||||||
|
@ -137,7 +157,11 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
|
||||||
'text-sm font-main'
|
'text-sm font-main'
|
||||||
);
|
);
|
||||||
|
|
||||||
if (cst) {
|
if (!cst) {
|
||||||
|
const text = document.createElement('p');
|
||||||
|
text.innerText = 'Конституента не определена';
|
||||||
|
dom.appendChild(text);
|
||||||
|
} else {
|
||||||
const alias = document.createElement('p');
|
const alias = document.createElement('p');
|
||||||
alias.innerHTML = `<b>${cst.alias}:</b> ${labelCstTypification(cst)}`;
|
alias.innerHTML = `<b>${cst.alias}:</b> ${labelCstTypification(cst)}`;
|
||||||
dom.appendChild(alias);
|
dom.appendChild(alias);
|
||||||
|
@ -181,10 +205,11 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
|
||||||
children.innerHTML = `<b>Порождает:</b> ${cst.children_alias.join(', ')}`;
|
children.innerHTML = `<b>Порождает:</b> ${cst.children_alias.join(', ')}`;
|
||||||
dom.appendChild(children);
|
dom.appendChild(children);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
const text = document.createElement('p');
|
const clickTip = document.createElement('p');
|
||||||
text.innerText = 'Конституента не определена';
|
clickTip.className = 'w-full text-center text-xs mt-2';
|
||||||
dom.appendChild(text);
|
clickTip.innerText = 'Ctrl + клик для перехода';
|
||||||
|
dom.appendChild(clickTip);
|
||||||
}
|
}
|
||||||
return { dom: dom };
|
return { dom: dom };
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user