mirror of
https://github.com/IRBorisov/ConceptPortal.git
synced 2025-06-26 13:00:39 +03:00
Add click navigation for references
Some checks are pending
Frontend CI / build (18.x) (push) Waiting to run
Some checks are pending
Frontend CI / build (18.x) (push) Waiting to run
This commit is contained in:
parent
3b13a27868
commit
e81e53e7d5
|
@ -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]
|
||||
);
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)];
|
||||
}
|
|
@ -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)];
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 ?? ''}
|
||||
|
|
|
@ -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 };
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user