mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 04:50:36 +03:00
Implement click navigation for RSInput
This commit is contained in:
parent
867e60581b
commit
568886d1b7
2
TODO.txt
2
TODO.txt
|
@ -4,8 +4,6 @@ For more specific TODOs see comments in code
|
|||
[Functionality - PROGRESS]
|
||||
- Operational synthesis schema as LibraryItem ?
|
||||
|
||||
- Clickable IDs in RSEditor tooltips
|
||||
|
||||
- Library organization, search and exploration. Consider new user experience
|
||||
- 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 { useConceptOptions } from '@/context/OptionsContext';
|
||||
import { useRSForm } from '@/context/RSFormContext';
|
||||
import { getFontClassName } from '@/models/miscellaneousAPI';
|
||||
import { ConstituentaID, IRSForm } from '@/models/rsform';
|
||||
import { generateAlias, getCstTypePrefix, guessCstType } from '@/models/rsformAPI';
|
||||
import { extractGlobals } from '@/models/rslangAPI';
|
||||
|
||||
import { ccBracketMatching } from './bracketMatching';
|
||||
import { rsNavigation } from './clickNavigation';
|
||||
import { RSLanguage } from './rslang';
|
||||
import { getSymbolSubstitute, RSTextWrapper } from './textEditing';
|
||||
import { rsHoverTooltip } from './tooltip';
|
||||
|
@ -39,6 +40,8 @@ interface RSInputProps
|
|||
noTooltip?: boolean;
|
||||
onChange?: (newValue: string) => void;
|
||||
onAnalyze?: () => void;
|
||||
schema?: IRSForm;
|
||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
||||
|
@ -49,6 +52,9 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
|||
disabled,
|
||||
noTooltip,
|
||||
|
||||
schema,
|
||||
onOpenEdit,
|
||||
|
||||
className,
|
||||
style,
|
||||
|
||||
|
@ -59,7 +65,6 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
|||
ref
|
||||
) => {
|
||||
const { darkMode, colors, mathFont } = useConceptOptions();
|
||||
const { schema } = useRSForm();
|
||||
|
||||
const internalRef = useRef<ReactCodeMirrorRef>(null);
|
||||
const thisRef = useMemo(() => (!ref || typeof ref === 'function' ? internalRef : ref), [internalRef, ref]);
|
||||
|
@ -77,7 +82,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
|||
caret: colors.fgDefault
|
||||
},
|
||||
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.propertyName, color: colors.fgTeal }, // Radical
|
||||
{ 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)
|
||||
]
|
||||
}),
|
||||
[disabled, colors, darkMode]
|
||||
[disabled, colors, darkMode, schema]
|
||||
);
|
||||
|
||||
const editorExtensions = useMemo(
|
||||
|
@ -95,9 +100,10 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
|
|||
EditorView.lineWrapping,
|
||||
RSLanguage,
|
||||
ccBracketMatching(darkMode),
|
||||
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
|
||||
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)])
|
||||
],
|
||||
[darkMode, schema, noTooltip]
|
||||
[darkMode, schema, noTooltip, onOpenEdit]
|
||||
);
|
||||
|
||||
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 { hoverTooltip } from '@codemirror/view';
|
||||
import { EditorState } from '@uiw/react-codemirror';
|
||||
|
||||
import { IRSForm } from '@/models/rsform';
|
||||
import { findEnvelopingNodes } from '@/utils/codemirror';
|
||||
import { findAliasAt } 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) => {
|
||||
return hoverTooltip((view, pos) => {
|
||||
const { alias, start, end } = findAliasAt(pos, view.state);
|
||||
|
|
|
@ -95,6 +95,7 @@ function FormCreateCst({ schema, state, partialUpdate, setValidated }: FormCreat
|
|||
}
|
||||
value={state.definition_formal}
|
||||
onChange={value => partialUpdate({ definition_formal: value })}
|
||||
schema={schema}
|
||||
/>
|
||||
</AnimateFade>
|
||||
<AnimateFade key='dlg_cst_definition' hideContent={!state.definition_raw && isElementary}>
|
||||
|
|
|
@ -113,6 +113,7 @@ function EditorConstituenta({ activeCst, isModified, setIsModified, onOpenEdit }
|
|||
setIsModified={setIsModified}
|
||||
onEditTerm={controller.editTermForms}
|
||||
onRename={controller.renameCst}
|
||||
onOpenEdit={onOpenEdit}
|
||||
/>
|
||||
<AnimatePresence>
|
||||
{showList ? (
|
||||
|
|
|
@ -11,7 +11,7 @@ import SubmitButton from '@/components/ui/SubmitButton';
|
|||
import TextArea from '@/components/ui/TextArea';
|
||||
import AnimateFade from '@/components/wrap/AnimateFade';
|
||||
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 { information, labelCstTypification } from '@/utils/labels';
|
||||
|
||||
|
@ -35,6 +35,7 @@ interface FormConstituentaProps {
|
|||
|
||||
onRename: () => void;
|
||||
onEditTerm: () => void;
|
||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function FormConstituenta({
|
||||
|
@ -47,7 +48,8 @@ function FormConstituenta({
|
|||
|
||||
toggleReset,
|
||||
onRename,
|
||||
onEditTerm
|
||||
onEditTerm,
|
||||
onOpenEdit
|
||||
}: FormConstituentaProps) {
|
||||
const { schema, cstUpdate, processing } = useRSForm();
|
||||
|
||||
|
@ -183,6 +185,7 @@ function FormConstituenta({
|
|||
toggleReset={toggleReset}
|
||||
onChange={newValue => setExpression(newValue)}
|
||||
setTypification={setTypification}
|
||||
onOpenEdit={onOpenEdit}
|
||||
/>
|
||||
</AnimateFade>
|
||||
<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 useLocalStorage from '@/hooks/useLocalStorage';
|
||||
import { HelpTopic } from '@/models/miscellaneous';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
import { ConstituentaID, IConstituenta } from '@/models/rsform';
|
||||
import { getDefinitionPrefix } from '@/models/rsformAPI';
|
||||
import { IExpressionParse, IRSErrorDescription, SyntaxTree } from '@/models/rslang';
|
||||
import { TokenID } from '@/models/rslang';
|
||||
|
@ -38,6 +38,7 @@ interface EditorRSExpressionProps {
|
|||
|
||||
setTypification: (typification: string) => void;
|
||||
onChange: (newValue: string) => void;
|
||||
onOpenEdit?: (cstID: ConstituentaID) => void;
|
||||
}
|
||||
|
||||
function EditorRSExpression({
|
||||
|
@ -47,6 +48,7 @@ function EditorRSExpression({
|
|||
toggleReset,
|
||||
setTypification,
|
||||
onChange,
|
||||
onOpenEdit,
|
||||
...restProps
|
||||
}: EditorRSExpressionProps) {
|
||||
const model = useRSForm();
|
||||
|
@ -185,6 +187,8 @@ function EditorRSExpression({
|
|||
disabled={disabled}
|
||||
onChange={handleChange}
|
||||
onAnalyze={handleCheckExpression}
|
||||
schema={model.schema}
|
||||
onOpenEdit={onOpenEdit}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ function ConstituentsTable({ items, activeCst, onOpenEdit, maxHeight, denseThres
|
|||
if (element) {
|
||||
element.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'nearest',
|
||||
block: 'center',
|
||||
inline: 'end'
|
||||
});
|
||||
}
|
||||
|
|
|
@ -3,9 +3,10 @@
|
|||
*/
|
||||
import { syntaxTree } from '@codemirror/language';
|
||||
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 { GlobalTokens } from '@/components/RSInput/rslang';
|
||||
import { IEntityReference, ISyntacticReference } from '@/models/language';
|
||||
import { parseGrammemes } from '@/models/languageAPI';
|
||||
import { IConstituenta } from '@/models/rsform';
|
||||
|
@ -122,6 +123,25 @@ export function findContainedNodes(start: number, finish: number, tree: Tree, fi
|
|||
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}.
|
||||
*/
|
||||
|
@ -137,7 +157,11 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
|
|||
'text-sm font-main'
|
||||
);
|
||||
|
||||
if (cst) {
|
||||
if (!cst) {
|
||||
const text = document.createElement('p');
|
||||
text.innerText = 'Конституента не определена';
|
||||
dom.appendChild(text);
|
||||
} else {
|
||||
const alias = document.createElement('p');
|
||||
alias.innerHTML = `<b>${cst.alias}:</b> ${labelCstTypification(cst)}`;
|
||||
dom.appendChild(alias);
|
||||
|
@ -181,10 +205,11 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
|
|||
children.innerHTML = `<b>Порождает:</b> ${cst.children_alias.join(', ')}`;
|
||||
dom.appendChild(children);
|
||||
}
|
||||
} else {
|
||||
const text = document.createElement('p');
|
||||
text.innerText = 'Конституента не определена';
|
||||
dom.appendChild(text);
|
||||
|
||||
const clickTip = document.createElement('p');
|
||||
clickTip.className = 'w-full text-center text-xs mt-2';
|
||||
clickTip.innerText = 'Ctrl + клик для перехода';
|
||||
dom.appendChild(clickTip);
|
||||
}
|
||||
return { dom: dom };
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user