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 caret: colors.fgDefault
}, },
styles: [ 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.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
@ -92,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, schema] [disabled, colors, darkMode, schema, cursor]
); );
const editorExtensions = useMemo( const editorExtensions = useMemo(
@ -101,7 +101,7 @@ const RSInput = forwardRef<ReactCodeMirrorRef, RSInputProps>(
RSLanguage, RSLanguage,
ccBracketMatching(darkMode), ccBracketMatching(darkMode),
...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]), ...(!schema || !onOpenEdit ? [] : [rsNavigation(schema, onOpenEdit)]),
...(noTooltip || !schema ? [] : [rsHoverTooltip(schema)]) ...(noTooltip || !schema ? [] : [rsHoverTooltip(schema, onOpenEdit !== undefined)])
], ],
[darkMode, schema, noTooltip, onOpenEdit] [darkMode, schema, noTooltip, onOpenEdit]
); );

View File

@ -4,7 +4,7 @@ import { EditorView } from '@uiw/react-codemirror';
import { ConstituentaID, IRSForm } from '@/models/rsform'; import { ConstituentaID, IRSForm } from '@/models/rsform';
import { findAliasAt } from '@/utils/codemirror'; import { findAliasAt } from '@/utils/codemirror';
const globalsNavigation = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void) => { const navigationProducer = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void) => {
return EditorView.domEventHandlers({ return EditorView.domEventHandlers({
click: (event: MouseEvent, view: EditorView) => { click: (event: MouseEvent, view: EditorView) => {
if (!event.ctrlKey) { if (!event.ctrlKey) {
@ -34,5 +34,5 @@ const globalsNavigation = (schema: IRSForm, onOpenEdit: (cstID: ConstituentaID)
}; };
export function rsNavigation(schema: IRSForm, onOpenEdit: (cstID: ConstituentaID) => void): Extension { 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 { findAliasAt } from '@/utils/codemirror';
import { domTooltipConstituenta } from '@/utils/codemirror'; import { domTooltipConstituenta } from '@/utils/codemirror';
const globalsHoverTooltip = (schema: IRSForm) => { const tooltipProducer = (schema: IRSForm, canClick?: boolean) => {
return hoverTooltip((view, pos) => { return hoverTooltip((view, pos) => {
const { alias, start, end } = findAliasAt(pos, view.state); const { alias, start, end } = findAliasAt(pos, view.state);
if (!alias) { if (!alias) {
@ -16,11 +16,11 @@ const globalsHoverTooltip = (schema: IRSForm) => {
pos: start, pos: start,
end: end, end: end,
above: false, above: false,
create: () => domTooltipConstituenta(cst) create: () => domTooltipConstituenta(cst, canClick)
}; };
}); });
}; };
export function rsHoverTooltip(schema: IRSForm): Extension { export function rsHoverTooltip(schema: IRSForm, canClick?: boolean): Extension {
return [globalsHoverTooltip(schema)]; return [tooltipProducer(schema, canClick)];
} }

View File

@ -13,10 +13,11 @@ import Label from '@/components/ui/Label';
import { useConceptOptions } from '@/context/OptionsContext'; import { useConceptOptions } from '@/context/OptionsContext';
import DlgEditReference from '@/dialogs/DlgEditReference'; import DlgEditReference from '@/dialogs/DlgEditReference';
import { ReferenceType } from '@/models/language'; import { ReferenceType } from '@/models/language';
import { IRSForm } from '@/models/rsform'; import { ConstituentaID, IRSForm } from '@/models/rsform';
import { CodeMirrorWrapper } from '@/utils/codemirror'; import { CodeMirrorWrapper } from '@/utils/codemirror';
import { PARAMETER } from '@/utils/constants'; import { PARAMETER } from '@/utils/constants';
import { refsNavigation } from './clickNavigation';
import { NaturalLanguage, ReferenceTokens } from './parse'; import { NaturalLanguage, ReferenceTokens } from './parse';
import { RefEntity } from './parse/parser.terms'; import { RefEntity } from './parse/parser.terms';
import { refsHoverTooltip } from './tooltip'; import { refsHoverTooltip } from './tooltip';
@ -65,6 +66,7 @@ interface RefsInputInputProps
label?: string; label?: string;
onChange?: (newValue: string) => void; onChange?: (newValue: string) => void;
schema?: IRSForm; schema?: IRSForm;
onOpenEdit?: (cstID: ConstituentaID) => void;
disabled?: boolean; disabled?: boolean;
initialValue?: string; initialValue?: string;
@ -73,7 +75,23 @@ interface RefsInputInputProps
} }
const RefsInput = forwardRef<ReactCodeMirrorRef, 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 { darkMode, colors } = useConceptOptions();
const [isFocused, setIsFocused] = useState(false); const [isFocused, setIsFocused] = useState(false);
@ -114,9 +132,10 @@ const RefsInput = forwardRef<ReactCodeMirrorRef, RefsInputInputProps>(
EditorView.lineWrapping, EditorView.lineWrapping,
EditorView.contentAttributes.of({ spellcheck: 'true' }), EditorView.contentAttributes.of({ spellcheck: 'true' }),
NaturalLanguage, 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) { 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 { Extension } from '@codemirror/state';
import { hoverTooltip } from '@codemirror/view'; import { hoverTooltip } from '@codemirror/view';
import { parseEntityReference, parseSyntacticReference } from '@/models/languageAPI'; import { IEntityReference, ISyntacticReference } from '@/models/language';
import { IRSForm } from '@/models/rsform'; import { IRSForm } from '@/models/rsform';
import { IColorTheme } from '@/styling/color'; import { IColorTheme } from '@/styling/color';
import { import {
domTooltipEntityReference, domTooltipEntityReference,
domTooltipSyntacticReference, domTooltipSyntacticReference,
findContainedNodes, findContainedNodes,
findEnvelopingNodes findReferenceAt
} from '@/utils/codemirror'; } from '@/utils/codemirror';
import { ReferenceTokens } from './parse'; import { RefEntity } from './parse/parser.terms';
import { RefEntity, RefSyntactic } from './parse/parser.terms';
export const globalsHoverTooltip = (schema: IRSForm, colors: IColorTheme) => { export const tooltipProducer = (schema: IRSForm, colors: IColorTheme, canClick?: boolean) => {
return hoverTooltip((view, pos) => { return hoverTooltip((view, pos) => {
const nodes = findEnvelopingNodes(pos, pos, syntaxTree(view.state), ReferenceTokens); const parse = findReferenceAt(pos, view.state);
if (nodes.length !== 1) { if (!parse) {
return null; return null;
} }
const start = nodes[0].from;
const end = nodes[0].to; if ('entity' in parse.ref) {
const text = view.state.doc.sliceString(start, end); const cst = schema.cstByAlias.get(parse.ref.entity);
if (nodes[0].type.id === RefEntity) {
const ref = parseEntityReference(text);
const cst = schema.cstByAlias.get(ref.entity);
return { return {
pos: start, pos: parse.start,
end: end, end: parse.end,
above: false, above: false,
create: () => domTooltipEntityReference(ref, cst, colors) create: () => domTooltipEntityReference(parse.ref as IEntityReference, cst, colors, canClick)
}; };
} else if (nodes[0].type.id === RefSyntactic) { } else {
const ref = parseSyntacticReference(text);
let masterText: string | undefined = undefined; let masterText: string | undefined = undefined;
if (ref.offset > 0) { if (parse.ref.offset > 0) {
const entities = findContainedNodes(end, view.state.doc.length, syntaxTree(view.state), [RefEntity]); const entities = findContainedNodes(parse.end, view.state.doc.length, syntaxTree(view.state), [RefEntity]);
if (ref.offset <= entities.length) { if (parse.ref.offset <= entities.length) {
const master = entities[ref.offset - 1]; const master = entities[parse.ref.offset - 1];
masterText = view.state.doc.sliceString(master.from, master.to); masterText = view.state.doc.sliceString(master.from, master.to);
} }
} else { } else {
const entities = findContainedNodes(0, start, syntaxTree(view.state), [RefEntity]); const entities = findContainedNodes(0, parse.start, syntaxTree(view.state), [RefEntity]);
if (-ref.offset <= entities.length) { if (-parse.ref.offset <= entities.length) {
const master = entities[-ref.offset - 1]; const master = entities[-parse.ref.offset - 1];
masterText = view.state.doc.sliceString(master.from, master.to); masterText = view.state.doc.sliceString(master.from, master.to);
} }
} }
return { return {
pos: start, pos: parse.start,
end: end, end: parse.end,
above: false, 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 { export function refsHoverTooltip(schema: IRSForm, colors: IColorTheme, canClick?: boolean): Extension {
return [globalsHoverTooltip(schema, colors)]; return [tooltipProducer(schema, colors, canClick)];
} }

View File

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

View File

@ -146,6 +146,7 @@ function FormConstituenta({
maxHeight='8rem' maxHeight='8rem'
placeholder='Обозначение, используемое в текстовых определениях' placeholder='Обозначение, используемое в текстовых определениях'
schema={schema} schema={schema}
onOpenEdit={onOpenEdit}
value={term} value={term}
initialValue={state?.term_raw ?? ''} initialValue={state?.term_raw ?? ''}
resolved={state?.term_resolved ?? ''} resolved={state?.term_resolved ?? ''}
@ -196,6 +197,7 @@ function FormConstituenta({
minHeight='3.75rem' minHeight='3.75rem'
maxHeight='8rem' maxHeight='8rem'
schema={schema} schema={schema}
onOpenEdit={onOpenEdit}
value={textDefinition} value={textDefinition}
initialValue={state?.definition_raw ?? ''} initialValue={state?.definition_raw ?? ''}
resolved={state?.definition_resolved ?? ''} 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 { EditorState, ReactCodeMirrorRef, SelectionRange } from '@uiw/react-codemirror';
import clsx from 'clsx'; 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 { GlobalTokens } from '@/components/RSInput/rslang';
import { IEntityReference, ISyntacticReference } from '@/models/language'; import { IEntityReference, ISyntacticReference } from '@/models/language';
import { parseGrammemes } from '@/models/languageAPI'; import { parseEntityReference, parseGrammemes, parseSyntacticReference } from '@/models/languageAPI';
import { IConstituenta } from '@/models/rsform'; import { IConstituenta } from '@/models/rsform';
import { isBasicConcept } from '@/models/rsformAPI'; import { isBasicConcept } from '@/models/rsformAPI';
@ -142,13 +144,30 @@ export function findAliasAt(pos: number, state: EditorState) {
return { alias, start, end }; 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}. * Create DOM tooltip for {@link Constituenta}.
*/ */
export function domTooltipConstituenta(cst?: IConstituenta) { export function domTooltipConstituenta(cst?: IConstituenta, canClick?: boolean) {
const dom = document.createElement('div'); const dom = document.createElement('div');
dom.className = clsx( dom.className = clsx(
'z-modalTooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]', 'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense', 'dense',
'p-2', 'p-2',
@ -206,21 +225,27 @@ export function domTooltipConstituenta(cst?: IConstituenta) {
dom.appendChild(children); dom.appendChild(children);
} }
if (canClick) {
const clickTip = document.createElement('p'); const clickTip = document.createElement('p');
clickTip.className = 'w-full text-center text-xs mt-2'; clickTip.className = 'w-full text-center text-xs mt-2';
clickTip.innerText = 'Ctrl + клик для перехода'; clickTip.innerText = 'Ctrl + клик для перехода';
dom.appendChild(clickTip); dom.appendChild(clickTip);
} }
}
return { dom: dom }; return { dom: dom };
} }
/** /**
* Create DOM tooltip for {@link IEntityReference}. * 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'); const dom = document.createElement('div');
dom.className = clsx( dom.className = clsx(
'z-tooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]', 'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense', 'dense',
'p-2 flex flex-col', 'p-2 flex flex-col',
@ -258,16 +283,27 @@ export function domTooltipEntityReference(ref: IEntityReference, cst: IConstitue
grams.appendChild(gram); grams.appendChild(gram);
}); });
dom.appendChild(grams); 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 }; return { dom: dom };
} }
/** /**
* Create DOM tooltip for {@link ISyntacticReference}. * 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'); const dom = document.createElement('div');
dom.className = clsx( dom.className = clsx(
'z-tooltip',
'max-h-[25rem] max-w-[25rem] min-w-[10rem]', 'max-h-[25rem] max-w-[25rem] min-w-[10rem]',
'dense', 'dense',
'p-2 flex flex-col', 'p-2 flex flex-col',
@ -293,6 +329,13 @@ export function domTooltipSyntacticReference(ref: ISyntacticReference, masterRef
nominal.innerHTML = `<b>Начальная форма:</b> ${ref.nominal}`; nominal.innerHTML = `<b>Начальная форма:</b> ${ref.nominal}`;
dom.appendChild(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 }; return { dom: dom };
} }