Add click navigation for references
Some checks are pending
Frontend CI / build (18.x) (push) Waiting to run

This commit is contained in:
IRBorisov 2024-06-19 12:13:05 +03:00
parent 3b13a27868
commit e81e53e7d5
9 changed files with 165 additions and 68 deletions

View File

@ -82,7 +82,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
caret: colors.fgDefault
},
styles: [
{ tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : 'text' }, // GlobalID
{ tag: tags.name, color: colors.fgPurple, cursor: schema ? 'default' : cursor }, // GlobalID
{ tag: tags.variableName, color: colors.fgGreen }, // LocalID
{ tag: tags.propertyName, color: colors.fgTeal }, // Radical
{ tag: tags.keyword, color: colors.fgBlue }, // keywords
@ -92,7 +92,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
{ tag: tags.brace, color: colors.fgPurple, fontWeight: '600' } // braces (curly brackets)
]
}),
[disabled, colors, darkMode, schema]
[disabled, colors, darkMode, schema, cursor]
);
const editorExtensions = useMemo(
@ -101,7 +101,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
RSLanguage,
ccBracketMatching(darkMode),
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)])
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema, onOpenEdit !== undefined)])
],
[darkMode, schema, noTooltip, onOpenEdit]
);

View File

@ -4,7 +4,7 @@ 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) => {
const navigationProducer = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void) => {
return EditorView.domEventHandlers({
click: (event: MouseEvent, view: EditorView) => {
if (!event.ctrlKey) {
@ -34,5 +34,5 @@ const globalsNavigation = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID)
};
export function rsNavigation(schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void): Extension {
return [globalsNavigation(schema, onOpenEdit)];
return [navigationProducer(schema, onOpenEdit)];
}

View File

@ -5,7 +5,7 @@ import { IRSForm } from '@/models/rsform';
import { findAliasAt } from '@/utils/codemirror';
import { domTooltipConstituenta } from '@/utils/codemirror';
const globalsHoverTooltip = (schema: IRSForm) => {
const tooltipProducer = (schema: IRSForm, canClick?: boolean) => {
return hoverTooltip((view, pos) => {
const { alias, start, end } = findAliasAt(pos, view.state);
if (!alias) {
@ -16,11 +16,11 @@ const globalsHoverTooltip = (schema: IRSForm) => {
pos: start,
end: end,
above: false,
create: () => domTooltipConstituenta(cst)
create: () => domTooltipConstituenta(cst, canClick)
};
});
};
export function rsHoverTooltip(schema: IRSForm): Extension {
return [globalsHoverTooltip(schema)];
export function rsHoverTooltip(schema: IRSForm, canClick?: boolean): Extension {
return [tooltipProducer(schema, canClick)];
}

View File

@ -13,10 +13,11 @@ import Label from '@/components/ui/Label';
import { useConceptOptions } from '@/context/OptionsContext';
import DlgEditReference from '@/dialogs/DlgEditReference';
import { ReferenceType } from '@/models/language';
import { IRSForm } from '@/models/rsform';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { CodeMirrorWrapper } from '@/utils/codemirror';
import { PARAMETER } from '@/utils/constants';
import { refsNavigation } from './clickNavigation';
import { NaturalLanguage, ReferenceTokens } from './parse';
import { RefEntity } from './parse/parser.terms';
import { refsHoverTooltip } from './tooltip';
@ -65,6 +66,7 @@ interface RefsInputInputProps
label?: string;
onChange?: (newValue: string) => void;
schema?: IRSForm;
onOpenEdit?: (cstID: ConstituentaID) => void;
disabled?: boolean;
initialValue?: string;
@ -73,7 +75,23 @@ interface RefsInputInputProps
}
const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
({ id, label, disabled, schema, initialValue, value, resolved, onFocus, onBlur, onChange, ...restProps }, ref) => {
(
{
id, // prettier: split-lines
label,
disabled,
schema,
onOpenEdit,
initialValue,
value,
resolved,
onFocus,
onBlur,
onChange,
...restProps
},
ref
) => {
const { darkMode, colors } = useConceptOptions();
const [isFocused, setIsFocused] = useState(false);
@ -114,9 +132,10 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
EditorView.lineWrapping,
EditorView.contentAttributes.of({ spellcheck: 'true' }),
NaturalLanguage,
...(schema ? [refsHoverTooltip(schema, colors)] : [])
...(!schema || !onOpenEdit ? [] : [refsNavigation(schema, onOpenEdit)]),
...(schema ? [refsHoverTooltip(schema, colors, onOpenEdit !== undefined)] : [])
],
[schema, colors]
[schema, colors, onOpenEdit]
);
function handleChange(newValue: string) {

View File

@ -0,0 +1,38 @@
import { Extension } from '@codemirror/state';
import { EditorView } from '@uiw/react-codemirror';
import { ConstituentaID, IRSForm } from '@/models/rsform';
import { findReferenceAt } from '@/utils/codemirror';
const navigationProducer = (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 parse = findReferenceAt(pos, view.state);
if (!parse || !('entity' in parse.ref)) {
return;
}
const cst = schema.cstByAlias.get(parse.ref.entity);
if (!cst) {
return;
}
event.preventDefault();
event.stopPropagation();
onOpenEdit(cst.id);
}
});
};
export function refsNavigation(schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void): Extension {
return [navigationProducer(schema, onOpenEdit)];
}

View File

@ -2,65 +2,58 @@ import { syntaxTree } from '@codemirror/language';
import { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view';
import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI';
import { IEntityReference, ISyntacticReference } from '@/models/language';
import { IRSForm } from '@/models/rsform';
import { IColorTheme } from '@/styling/color';
import {
domTooltipEntityReference,
domTooltipSyntacticReference,
findContainedNodes,
findEnvelopingNodes
findReferenceAt
} from '@/utils/codemirror';
import { ReferenceTokens } from './parse';
import { RefEntity, RefSyntactic } from './parse/parser.terms';
import { RefEntity } from './parse/parser.terms';
export const globalsHoverTooltip = (schema: IRSForm, colors: IColorTheme) => {
export const tooltipProducer = (schema: IRSForm, colors: IColorTheme, canClick?: boolean) => {
return hoverTooltip((view, pos) => {
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens);
if (nodes.length !== 1) {
const parse = findReferenceAt(pos, view.state);
if (!parse) {
return null;
}
const start = nodes[0].from;
const end = nodes[0].to;
const text = view.state.doc.sliceString(start, end);
if (nodes[0].type.id === RefEntity) {
const ref = parseEntityReference(text);
const cst = schema.cstByAlias.get(ref.entity);
if ('entity' in parse.ref) {
const cst = schema.cstByAlias.get(parse.ref.entity);
return {
pos: start,
end: end,
pos: parse.start,
end: parse.end,
above: false,
create: () => domTooltipEntityReference(ref, cst, colors)
create: () => domTooltipEntityReference(parse.ref as IEntityReference, cst, colors, canClick)
};
} else if (nodes[0].type.id === RefSyntactic) {
const ref = parseSyntacticReference(text);
} else {
let masterText: string | undefined = undefined;
if (ref.offset > 0) {
const entities = findContainedNodes(end, view.state.doc.length, syntaxTree(view.state), [RefEntity]);
if (ref.offset <= entities.length) {
const master = entities[ref.offset - 1];
if (parse.ref.offset > 0) {
const entities = findContainedNodes(parse.end, view.state.doc.length, syntaxTree(view.state), [RefEntity]);
if (parse.ref.offset <= entities.length) {
const master = entities[parse.ref.offset - 1];
masterText = view.state.doc.sliceString(master.from, master.to);
}
} else {
const entities = findContainedNodes(0, start, syntaxTree(view.state), [RefEntity]);
if (-ref.offset <= entities.length) {
const master = entities[-ref.offset - 1];
const entities = findContainedNodes(0, parse.start, syntaxTree(view.state), [RefEntity]);
if (-parse.ref.offset <= entities.length) {
const master = entities[-parse.ref.offset - 1];
masterText = view.state.doc.sliceString(master.from, master.to);
}
}
return {
pos: start,
end: end,
pos: parse.start,
end: parse.end,
above: false,
create: () => domTooltipSyntacticReference(ref, masterText)
create: () => domTooltipSyntacticReference(parse.ref as ISyntacticReference, masterText, canClick)
};
} else {
return null;
}
});
};
export function refsHoverTooltip(schema: IRSForm, colors: IColorTheme): Extension {
return [globalsHoverTooltip(schema, colors)];
export function refsHoverTooltip(schema: IRSForm, colors: IColorTheme, canClick?: boolean): Extension {
return [tooltipProducer(schema, colors, canClick)];
}

View File

@ -24,7 +24,7 @@ interface TemplateTabProps {
function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
const { templates, retrieveTemplate } = useLibrary();
const [category, setCategory] = useState<IRSForm | undefined>(undefined);
const [templateSchema, setTemplateSchema] = useState<IRSForm | undefined>(undefined);
const [filteredData, setFilteredData] = useState<IConstituenta[]>([]);
@ -48,16 +48,16 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
);
const categorySelector = useMemo((): { value: number; label: string }[] => {
if (!category) {
if (!templateSchema) {
return [];
}
return category.items
return templateSchema.items
.filter(cst => cst.cst_type === CATEGORY_CST_TYPE)
.map(cst => ({
value: cst.id,
label: cst.term_raw
}));
}, [category]);
}, [templateSchema]);
useEffect(() => {
if (templates.length > 0 && !state.templateID) {
@ -67,22 +67,22 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
useEffect(() => {
if (!state.templateID) {
setCategory(undefined);
setTemplateSchema(undefined);
} else {
retrieveTemplate(state.templateID, setCategory);
retrieveTemplate(state.templateID, setTemplateSchema);
}
}, [state.templateID, retrieveTemplate]);
useEffect(() => {
if (!category) {
if (!templateSchema) {
return;
}
let data = category.items;
let data = templateSchema.items;
if (state.filterCategory) {
data = applyFilterCategory(state.filterCategory, category);
data = applyFilterCategory(state.filterCategory, templateSchema);
}
setFilteredData(data);
}, [state.filterCategory, category]);
}, [state.filterCategory, templateSchema]);
return (
<AnimateFade>
@ -93,14 +93,16 @@ function TemplateTab({ state, partialUpdate }: TemplateTabProps) {
className='flex-grow border-none'
options={categorySelector}
value={
state.filterCategory && category
state.filterCategory && templateSchema
? {
value: state.filterCategory.id,
label: state.filterCategory.term_raw
}
: null
}
onChange={data => partialUpdate({ filterCategory: data ? category?.cstByID.get(data?.value) : undefined })}
onChange={data =>
partialUpdate({ filterCategory: data ? templateSchema?.cstByID.get(data?.value) : undefined })
}
isClearable
/>
<SelectSingle

View File

@ -146,6 +146,7 @@ function FormConstituenta({
maxHeight='8rem'
placeholder='Обозначение, используемое в текстовых определениях'
schema={schema}
onOpenEdit={onOpenEdit}
value={term}
initialValue={state?.term_raw ?? ''}
resolved={state?.term_resolved ?? ''}
@ -196,6 +197,7 @@ function FormConstituenta({
minHeight='3.75rem'
maxHeight='8rem'
schema={schema}
onOpenEdit={onOpenEdit}
value={textDefinition}
initialValue={state?.definition_raw ?? ''}
resolved={state?.definition_resolved ?? ''}

View File

@ -6,9 +6,11 @@ import { NodeType, Tree, TreeCursor } from '@lezer/common';
import { EditorState, ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
import clsx from 'clsx';
import { ReferenceTokens } from '@/components/RefsInput/parse';
import { RefEntity } from '@/components/RefsInput/parse/parser.terms';
import { GlobalTokens } from '@/components/RSInput/rslang';
import { IEntityReference, ISyntacticReference } from '@/models/language';
import { parseGrammemes } from '@/models/languageAPI';
import { parseEntityReference, parseGrammemes, parseSyntacticReference } from '@/models/languageAPI';
import { IConstituenta } from '@/models/rsform';
import { isBasicConcept } from '@/models/rsformAPI';
@ -142,13 +144,30 @@ export function findAliasAt(pos: number, state: EditorState) {
return { alias, start, end };
}
/**
* Retrieves reference from position in Editor.
*/
export function findReferenceAt(pos: number, state: EditorState) {
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(state), ReferenceTokens);
if (nodes.length !== 1) {
return undefined;
}
const start = nodes[0].from;
const end = nodes[0].to;
const text = state.doc.sliceString(start, end);
if (nodes[0].type.id === RefEntity) {
return { ref: parseEntityReference(text), start, end };
} else {
return { ref: parseSyntacticReference(text), start, end };
}
}
/**
* Create DOM tooltip for {@link Constituenta}.
*/
export function domTooltipConstituenta(cst?: IConstituenta) {
export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean) {
const dom = document.createElement('div');
dom.className = clsx(
'z-modalTooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2',
@ -206,10 +225,12 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
dom.appendChild(children);
}
const clickTip = document.createElement('p');
clickTip.className = 'w-full text-center text-xs mt-2';
clickTip.innerText = 'Ctrl + клик для перехода';
dom.appendChild(clickTip);
if (canClick) {
const clickTip = document.createElement('p');
clickTip.className = 'w-full text-center text-xs mt-2';
clickTip.innerText = 'Ctrl + клик для перехода';
dom.appendChild(clickTip);
}
}
return { dom: dom };
}
@ -217,10 +238,14 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
/**
* Create DOM tooltip for {@link IEntityReference}.
*/
export function domTooltipEntityReference(ref: IEntityReference, cst: IConstituenta | undefined, colors: IColorTheme) {
export function domTooltipEntityReference(
ref: IEntityReference,
cst: IConstituenta | undefined,
colors: IColorTheme,
canClick?: boolean
) {
const dom = document.createElement('div');
dom.className = clsx(
'z-tooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2 flex flex-col',
@ -258,16 +283,27 @@ export function domTooltipEntityReference(ref: IEntityReference, cst: IConstitue
grams.appendChild(gram);
});
dom.appendChild(grams);
if (canClick) {
const clickTip = document.createElement('p');
clickTip.className = 'w-full text-center text-xs mt-2';
clickTip.innerHTML = 'Ctrl + клик для перехода</br>Ctrl + пробел для редактирования';
dom.appendChild(clickTip);
}
return { dom: dom };
}
/**
* Create DOM tooltip for {@link ISyntacticReference}.
*/
export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef: string | undefined) {
export function domTooltipSyntacticReference(
ref: ISyntacticReference,
masterRef: string | undefined,
canClick?: boolean
) {
const dom = document.createElement('div');
dom.className = clsx(
'z-tooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense',
'p-2 flex flex-col',
@ -293,6 +329,13 @@ export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef
nominal.innerHTML = `<b>Начальная форма:</b> ${ref.nominal}`;
dom.appendChild(nominal);
if (canClick) {
const clickTip = document.createElement('p');
clickTip.className = 'w-full text-center text-xs mt-2';
clickTip.innerHTML = 'Ctrl + пробел для редактирования';
dom.appendChild(clickTip);
}
return { dom: dom };
}