Improve reference editing

This commit is contained in:
IRBorisov 2023-09-30 17:16:20 +03:00
parent 41bb83b784
commit f54254bb56
6 changed files with 122 additions and 50 deletions

View File

@ -4,7 +4,7 @@ function HelpTerminologyControl() {
<div> <div>
<h1>Терминологизация: Контроль терминологии</h1> <h1>Терминологизация: Контроль терминологии</h1>
<p>Портал позволяет контролировать употребление терминов, привязанных к сущностям в концептуальных схемах.</p> <p>Портал позволяет контролировать употребление терминов, привязанных к сущностям в концептуальных схемах.</p>
<p>Для этого используется механизм текстовых отсылок: <i>использование термина</i> и <i>синтаксическая связь.</i></p> <p>Для этого используется механизм текстовых отсылок: <i>использование термина</i> и <i>связывание слов.</i></p>
<p>При отсылке к термину указывается параметры словоформы так, обеспечивающие корректное согласование слов.</p> <p>При отсылке к термину указывается параметры словоформы так, обеспечивающие корректное согласование слов.</p>
<p><b>Граммема</b> - минимальная единица грамматической информаци, например род, число, падеж.</p> <p><b>Граммема</b> - минимальная единица грамматической информаци, например род, число, падеж.</p>
<p><b>Словоформа</b> - грамматическая форма словосочетания, которая может меняться в зависимости от его грамматических характеристик.</p> <p><b>Словоформа</b> - грамматическая форма словосочетания, которая может меняться в зависимости от его грамматических характеристик.</p>

View File

@ -8,33 +8,36 @@ import { IConstituenta, matchConstituenta } from '../../models/rsform';
import ConstituentaTooltip from '../../pages/RSFormPage/elements/ConstituentaTooltip'; import ConstituentaTooltip from '../../pages/RSFormPage/elements/ConstituentaTooltip';
import { colorfgCstStatus } from '../../utils/color'; import { colorfgCstStatus } from '../../utils/color';
import { prefixes } from '../../utils/constants'; import { prefixes } from '../../utils/constants';
import { labelReferenceType } from '../../utils/labels'; import { compareGrammemeOptions, IGrammemeOption, PremadeWordForms, SelectorGrammems } from '../../utils/selectors';
import { compareGrammemeOptions, IGrammemeOption, PremadeWordForms, SelectorGrammems, SelectorReferenceType } from '../../utils/selectors';
import ConceptTooltip from '../Common/ConceptTooltip'; import ConceptTooltip from '../Common/ConceptTooltip';
import Label from '../Common/Label'; import Label from '../Common/Label';
import Modal from '../Common/Modal'; import Modal from '../Common/Modal';
import SelectMulti from '../Common/SelectMulti'; import SelectMulti from '../Common/SelectMulti';
import SelectSingle from '../Common/SelectSingle';
import TextInput from '../Common/TextInput'; import TextInput from '../Common/TextInput';
import DataTable, { IConditionalStyle } from '../DataTable'; import DataTable, { IConditionalStyle } from '../DataTable';
import HelpTerminologyControl from '../Help/HelpTerminologyControl'; import HelpTerminologyControl from '../Help/HelpTerminologyControl';
import { HelpIcon } from '../Icons'; import { HelpIcon } from '../Icons';
import TermformButton from './TermformButton'; import ReferenceTypeButton from './ReferenceTypeButton';
import WordformButton from './WordformButton';
export interface IReferenceInputState {
type: ReferenceType
refRaw?: string
text?: string
mainRefs: string[]
basePosition: number
}
interface DlgEditReferenceProps { interface DlgEditReferenceProps {
hideWindow: () => void hideWindow: () => void
items: IConstituenta[] items: IConstituenta[]
initial: IReferenceInputState
initialType: ReferenceType
initialRef?: string
initialText?: string
onSave: (newRef: string) => void onSave: (newRef: string) => void
} }
const constituentaHelper = createColumnHelper<IConstituenta>(); const constituentaHelper = createColumnHelper<IConstituenta>();
function DlgEditReference({ hideWindow, items, initialRef, initialText, initialType, onSave }: DlgEditReferenceProps) { function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferenceProps) {
const { colors } = useConceptTheme(); const { colors } = useConceptTheme();
const [type, setType] = useState<ReferenceType>(ReferenceType.ENTITY); const [type, setType] = useState<ReferenceType>(ReferenceType.ENTITY);
@ -49,6 +52,16 @@ function DlgEditReference({ hideWindow, items, initialRef, initialText, initialT
const [selectedGrams, setSelectedGrams] = useState<IGrammemeOption[]>([]); const [selectedGrams, setSelectedGrams] = useState<IGrammemeOption[]>([]);
const [gramOptions, setGramOptions] = useState<IGrammemeOption[]>([]); const [gramOptions, setGramOptions] = useState<IGrammemeOption[]>([]);
const mainLink = useMemo(
() => {
const position = offset > 0 ? initial.basePosition + (offset - 1) : initial.basePosition + offset;
if (offset === 0 || position < 0 || position >= initial.mainRefs.length) {
return 'Некорректное значение смещения';
} else {
return initial.mainRefs[position];
}
}, [initial, offset]);
const isValid = useMemo( const isValid = useMemo(
() => { () => {
if (type === ReferenceType.ENTITY) { if (type === ReferenceType.ENTITY) {
@ -73,23 +86,23 @@ function DlgEditReference({ hideWindow, items, initialRef, initialText, initialT
// Initialization // Initialization
useLayoutEffect( useLayoutEffect(
() => { () => {
setType(initialType); setType(initial.type);
if (initialRef) { if (initial.refRaw) {
if (initialType === ReferenceType.ENTITY) { if (initial.type === ReferenceType.ENTITY) {
const ref = parseEntityReference(initialRef); const ref = parseEntityReference(initial.refRaw);
setAlias(ref.entity); setAlias(ref.entity);
const grams = parseGrammemes(ref.form); const grams = parseGrammemes(ref.form);
setSelectedGrams(SelectorGrammems.filter(data => grams.includes(data.value))); setSelectedGrams(SelectorGrammems.filter(data => grams.includes(data.value)));
} else if (initialType === ReferenceType.SYNTACTIC) { } else if (initial.type === ReferenceType.SYNTACTIC) {
const ref = parseSyntacticReference(initialRef); const ref = parseSyntacticReference(initial.refRaw);
setOffset(ref.offset); setOffset(ref.offset);
setNominal(ref.nominal); setNominal(ref.nominal);
} }
} else if (initialText) { } else if (initial.text) {
setNominal(initialText ?? ''); setNominal(initial.text ?? '');
setFilter(initialText); setFilter(initial.text);
} }
}, [initialRef, initialText, initialType, items]); }, [initial, items]);
// Filter constituents // Filter constituents
useEffect( useEffect(
@ -140,7 +153,7 @@ function DlgEditReference({ hideWindow, items, initialRef, initialText, initialT
<div className='flex flex-start'> <div className='flex flex-start'>
{PremadeWordForms.slice(0, 6).map( {PremadeWordForms.slice(0, 6).map(
(data, index) => (data, index) =>
<TermformButton id={`${prefixes.wordform_list}${index}`} <WordformButton id={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams} text={data.text} example={data.example} grams={data.grams}
isSelected={data.grams.every(gram => selectedGrams.find(item => item.value as Grammeme === gram))} isSelected={data.grams.every(gram => selectedGrams.find(item => item.value as Grammeme === gram))}
onSelectGrams={handleSelectGrams} onSelectGrams={handleSelectGrams}
@ -151,7 +164,7 @@ function DlgEditReference({ hideWindow, items, initialRef, initialText, initialT
<div className='flex flex-start'> <div className='flex flex-start'>
{PremadeWordForms.slice(6, 12).map( {PremadeWordForms.slice(6, 12).map(
(data, index) => (data, index) =>
<TermformButton id={`${prefixes.wordform_list}${index}`} <WordformButton id={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams} text={data.text} example={data.example} grams={data.grams}
isSelected={data.grams.every(gram => selectedGrams.find(item => item.value as Grammeme === gram))} isSelected={data.grams.every(gram => selectedGrams.find(item => item.value as Grammeme === gram))}
onSelectGrams={handleSelectGrams} onSelectGrams={handleSelectGrams}
@ -216,15 +229,17 @@ function DlgEditReference({ hideWindow, items, initialRef, initialText, initialT
canSubmit={isValid} canSubmit={isValid}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
<div className='min-w-[40rem] flex flex-col gap-4 mb-4 mt-2'> <div className='min-w-[40rem] flex flex-col gap-4 mb-4 mt-2 min-h-[34rem]'>
<div className='flex items-center self-center flex-start'> <div className='flex items-center self-center flex-start'>
<SelectSingle <ReferenceTypeButton
className='z-modal-top min-w-[20rem] w-fit' type={ReferenceType.ENTITY}
options={SelectorReferenceType} onSelect={setType}
isSearchable={false} isSelected={type === ReferenceType.ENTITY}
placeholder='Тип ссылки' />
value={{ value: type, label: labelReferenceType(type) }} <ReferenceTypeButton
onChange={data => setType(data?.value ?? ReferenceType.ENTITY)} type={ReferenceType.SYNTACTIC}
onSelect={setType}
isSelected={type === ReferenceType.SYNTACTIC}
/> />
<div id='terminology-help' className='px-1 py-1'> <div id='terminology-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} /> <HelpIcon color='text-primary' size={5} />
@ -238,20 +253,31 @@ function DlgEditReference({ hideWindow, items, initialRef, initialText, initialT
</ConceptTooltip> </ConceptTooltip>
</div> </div>
{type === ReferenceType.SYNTACTIC && {type === ReferenceType.SYNTACTIC &&
<div className='flex gap-4 flex-start'> <div className='flex flex-col gap-2'>
<TextInput id='offset' type='number' <div className='flex flex-start'>
label='Смещение' <TextInput id='offset' type='number'
dimensions='max-w-[10rem]' label='Смещение'
singleRow dimensions='max-w-[10rem]'
value={offset} singleRow
onChange={event => setOffset(event.target.valueAsNumber)} value={offset}
/> onChange={event => setOffset(event.target.valueAsNumber)}
/>
<div className='self-center text-sm font-semibold whitespace-nowrap ml-2'>
Основная ссылка:
</div>
<TextInput
singleRow
disabled
noBorder
value={mainLink}
dimensions='w-full text-sm'
/>
</div>
<TextInput id='nominal' type='text' <TextInput id='nominal' type='text'
dimensions='w-full' dimensions='w-full'
label='Начальная форма' label='Начальная форма'
placeholder='зависимое слово в начальной форме' placeholder='зависимое слово в начальной форме'
spellCheck spellCheck
singleRow
value={nominal} value={nominal}
onChange={event => setNominal(event.target.value)} onChange={event => setNominal(event.target.value)}
/> />

View File

@ -0,0 +1,23 @@
import { ReferenceType } from '../../models/language';
import { labelReferenceType } from '../../utils/labels';
interface ReferenceTypeButtonProps {
id?: string
type: ReferenceType
isSelected?: boolean
onSelect: (type: ReferenceType) => void
}
function ReferenceTypeButton({ type, isSelected, onSelect, ...props }: ReferenceTypeButtonProps) {
return (
<button type='button' tabIndex={-1}
onClick={() => onSelect(type)}
className={`min-w-[12rem] px-2 py-1 border font-semibold rounded-none cursor-pointer clr-btn-clear clr-hover ${isSelected ? 'clr-selected': ''}`}
{...props}
>
{labelReferenceType(type)}
</button>
);
}
export default ReferenceTypeButton;

View File

@ -1,6 +1,6 @@
import { Grammeme } from '../../models/language'; import { Grammeme } from '../../models/language';
interface TermformButtonProps { interface WordformButtonProps {
id?: string id?: string
text: string text: string
example: string example: string
@ -9,7 +9,7 @@ interface TermformButtonProps {
onSelectGrams: (grams: Grammeme[]) => void onSelectGrams: (grams: Grammeme[]) => void
} }
function TermformButton({ text, example, grams, onSelectGrams, isSelected, ...props }: TermformButtonProps) { function WordformButton({ text, example, grams, onSelectGrams, isSelected, ...props }: WordformButtonProps) {
return ( return (
<button <button
type='button' type='button'
@ -24,4 +24,4 @@ function TermformButton({ text, example, grams, onSelectGrams, isSelected, ...pr
); );
} }
export default TermformButton; export default WordformButton;

View File

@ -63,9 +63,9 @@ extends Pick<ReactCodeMirrorProps,
} }
function RefsInput({ function RefsInput({
id, label, innerref, onChange, editable, items, id, label, innerref, editable, items,
initialValue, value, resolved, initialValue, value, resolved,
onFocus, onBlur, onFocus, onBlur, onChange,
...props ...props
}: RefsInputInputProps) { }: RefsInputInputProps) {
const { darkMode, colors } = useConceptTheme(); const { darkMode, colors } = useConceptTheme();
@ -80,6 +80,8 @@ function RefsInput({
const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY); const [currentType, setCurrentType] = useState<ReferenceType>(ReferenceType.ENTITY);
const [refText, setRefText] = useState(''); const [refText, setRefText] = useState('');
const [hintText, setHintText] = useState(''); const [hintText, setHintText] = useState('');
const [basePosition, setBasePosition] = useState(0);
const [mainRefs, setMainRefs] = useState<string[]>([]);
const internalRef = useRef<ReactCodeMirrorRef>(null); const internalRef = useRef<ReactCodeMirrorRef>(null);
const thisRef = useMemo( const thisRef = useMemo(
@ -152,6 +154,12 @@ function RefsInput({
setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC); setCurrentType(nodes[0].type.id === RefEntity ? ReferenceType.ENTITY : ReferenceType.SYNTACTIC);
setRefText(wrap.getSelectionText()); setRefText(wrap.getSelectionText());
} }
const selection = wrap.getSelection();
const mainNodes = wrap.getAllNodes([RefEntity]).filter(node => node.from >= selection.to || node.to <= selection.from);
setMainRefs(mainNodes.map(node => wrap.getText(node.from, node.to)));
setBasePosition(mainNodes.filter(node => node.to <= selection.from).length);
setShowEditor(true); setShowEditor(true);
} }
}, [thisRef, resolveText, value]); }, [thisRef, resolveText, value]);
@ -172,9 +180,13 @@ function RefsInput({
<DlgEditReference <DlgEditReference
hideWindow={() => setShowEditor(false)} hideWindow={() => setShowEditor(false)}
items={items ?? []} items={items ?? []}
initialType={currentType} initial={{
initialRef={refText} type: currentType,
initialText={hintText} refRaw: refText,
text: hintText,
basePosition: basePosition,
mainRefs: mainRefs
}}
onSave={handleInputReference} onSave={handleInputReference}
/> />
} }

View File

@ -240,6 +240,10 @@ export class CodeMirrorWrapper {
this.ref = object; this.ref = object;
} }
getText(from: number, to: number): string {
return this.ref.view.state.doc.sliceString(from, to);
}
getSelection(): SelectionRange { getSelection(): SelectionRange {
return this.ref.view.state.selection.main; return this.ref.view.state.selection.main;
} }
@ -284,7 +288,7 @@ export class CodeMirrorWrapper {
} }
/** /**
* Access list of SyntaxNodes contained in current selection * Access list of SyntaxNodes contained in current selection.
*/ */
getContainedNodes(tokenFilter?: number[]): SyntaxNode[] { getContainedNodes(tokenFilter?: number[]): SyntaxNode[] {
const selection = this.getSelection(); const selection = this.getSelection();
@ -292,13 +296,20 @@ export class CodeMirrorWrapper {
} }
/** /**
* Access list of SyntaxNodes enveloping current selection * Access list of SyntaxNodes enveloping current selection.
*/ */
getEnvelopingNodes(tokenFilter?: number[]): SyntaxNode[] { getEnvelopingNodes(tokenFilter?: number[]): SyntaxNode[] {
const selection = this.getSelection(); const selection = this.getSelection();
return findEnvelopingNodes(selection.from, selection.to, syntaxTree(this.ref.view.state), tokenFilter); return findEnvelopingNodes(selection.from, selection.to, syntaxTree(this.ref.view.state), tokenFilter);
} }
/**
* Access list of SyntaxNodes contained in documents.
*/
getAllNodes(tokenFilter?: number[]): SyntaxNode[] {
return findContainedNodes(0, this.ref.view.state.doc.length, syntaxTree(this.ref.view.state), tokenFilter);
}
/** /**
* Enlarges selection to nearest spaces. * Enlarges selection to nearest spaces.
* *