Refactor Tabs UI components

This commit is contained in:
IRBorisov 2023-12-01 22:50:43 +03:00
parent b118c64b9b
commit 6999e086d5
10 changed files with 297 additions and 242 deletions

View File

@ -11,7 +11,7 @@ extends Omit<TabProps, 'title' | 'children'> {
function ConceptTab({ label, tooltip, className, ...otherProps }: ConceptTabProps) {
return (
<Tab
className={`px-2 py-1 h-full flex justify-center text-sm hover:cursor-pointer clr-tab whitespace-nowrap min-w-[6rem] ${className}`}
className={`px-2 py-1 h-full min-w-[6rem] flex justify-center text-sm hover:cursor-pointer clr-tab whitespace-nowrap small-caps select-none font-semibold ${className}`}
title={tooltip}
{...otherProps}
>

View File

@ -9,7 +9,7 @@ extends Omit<React.DetailedHTMLProps<LabelHTMLAttributes<HTMLLabelElement>, HTML
function Label({ text, tooltip, className, ...restProps }: LabelProps) {
return (
<label
className={`text-sm font-semibold ${className}`}
className={`text-sm font-semibold ${className} whitespace-nowrap`}
title={tooltip}
{...restProps}
>

View File

@ -45,7 +45,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
term_forms: []
});
const [ activeTab, setActiveTab ] = useState(TabID.TEMPLATE);
const [activeTab, setActiveTab] = useState(TabID.TEMPLATE);
const handleSubmit = () => onCreate(constituenta);
@ -123,7 +123,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
onSelect={setActiveTab}
>
<div className='flex gap-1 pl-6 mb-3'>
<TabList className='flex items-start font-semibold text-center border select-none clr-controls small-caps'>
<TabList className='flex border'>
<ConceptTab
label='Шаблон'
tooltip='Выбор шаблона выражения'
@ -147,7 +147,7 @@ function DlgConstituentaTemplate({ hideWindow, schema, onCreate, insertAfter }:
<ConceptTooltip
anchorSelect='#templates-help'
className='max-w-[30rem] z-modal-tooltip'
offset={4}
offset={10}
>
<HelpRSTemplates />
</ConceptTooltip>

View File

@ -1,22 +1,16 @@
import { useCallback, useEffect, useLayoutEffect, useMemo, useState } from 'react';
import { useState } from 'react';
import { TabList, TabPanel, Tabs } from 'react-tabs';
import ConceptTab from '../../components/Common/ConceptTab';
import ConceptTooltip from '../../components/Common/ConceptTooltip';
import Label from '../../components/Common/Label';
import Modal from '../../components/Common/Modal';
import SelectMulti from '../../components/Common/SelectMulti';
import TextInput from '../../components/Common/TextInput';
import HelpTerminologyControl from '../../components/Help/HelpTerminologyControl';
import { HelpIcon } from '../../components/Icons';
import ConstituentaPicker from '../../components/Shared/ConstituentaPicker';
import { Grammeme, ReferenceType } from '../../models/language';
import { getCompatibleGrams, parseEntityReference, parseGrammemes, parseSyntacticReference } from '../../models/languageAPI';
import { CstMatchMode } from '../../models/miscelanious';
import { ReferenceType } from '../../models/language';
import { IConstituenta } from '../../models/rsform';
import { matchConstituenta } from '../../models/rsformAPI';
import { prefixes } from '../../utils/constants';
import { compareGrammemeOptions, IGrammemeOption, PremadeWordForms, SelectorGrammems } from '../../utils/selectors';
import ReferenceTypeButton from './ReferenceTypeButton';
import WordformButton from './WordformButton';
import { labelReferenceType } from '../../utils/labels';
import EntityTab from './EntityTab';
import SyntacticTab from './SyntacticTab';
export interface IReferenceInputState {
type: ReferenceType
@ -33,127 +27,18 @@ interface DlgEditReferenceProps {
onSave: (newRef: string) => void
}
export enum TabID {
ENTITY = 0,
SYNTACTIC = 1
}
function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferenceProps) {
const [type, setType] = useState<ReferenceType>(ReferenceType.ENTITY);
const [activeTab, setActiveTab] = useState(initial.type === ReferenceType.ENTITY ? TabID.ENTITY : TabID.SYNTACTIC);
const [nominal, setNominal] = useState('');
const [offset, setOffset] = useState(1);
const [reference, setReference] = useState('');
const [isValid, setIsValid] = useState(false);
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
const [alias, setAlias] = useState('');
const [term, setTerm] = useState('');
const [selectedGrams, setSelectedGrams] = 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(
() => {
if (type === ReferenceType.ENTITY) {
return alias !== '' && selectedGrams.length > 0;
} else if (type === ReferenceType.SYNTACTIC) {
return nominal !== '' && offset !== 0;
} else {
return false;
}
}, [type, alias, selectedGrams, nominal, offset]);
function produceReference(): string {
if (type === ReferenceType.ENTITY) {
return `@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`;
} else if (type === ReferenceType.SYNTACTIC) {
return `@{${offset}|${nominal}}`;
} else {
return '';
}
}
// Initialization
useLayoutEffect(
() => {
setType(initial.type);
if (initial.refRaw) {
if (initial.type === ReferenceType.ENTITY) {
const ref = parseEntityReference(initial.refRaw);
setAlias(ref.entity);
const grams = parseGrammemes(ref.form);
setSelectedGrams(SelectorGrammems.filter(data => grams.includes(data.value)));
} else if (initial.type === ReferenceType.SYNTACTIC) {
const ref = parseSyntacticReference(initial.refRaw);
setOffset(ref.offset);
setNominal(ref.nominal);
}
} else if (initial.text) {
setNominal(initial.text ?? '');
}
}, [initial, items]);
// Filter grammemes when input changes
useEffect(
() => {
const compatible = getCompatibleGrams(
selectedGrams
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
.map(data => data.value as Grammeme)
);
setGramOptions(SelectorGrammems.filter(({value}) => compatible.includes(value as Grammeme)));
}, [selectedGrams]);
// Update term when alias changes
useEffect(
() => {
const cst = items.find(item => item.alias === alias)
setTerm(cst?.term_resolved ?? '')
}, [alias, term, items]);
const handleSubmit = () => onSave(produceReference());
function handleSelectConstituenta(cst: IConstituenta) {
setAlias(cst.alias);
setSelectedCst(cst);
}
const handleSelectGrams = useCallback(
(grams: Grammeme[]) => {
setSelectedGrams(SelectorGrammems.filter(({value}) => grams.includes(value as Grammeme)));
}, []);
const FormButtons = useMemo(() => {
return (
<div className='flex flex-col items-center w-full text-sm'>
<div className='flex flex-start'>
{PremadeWordForms.slice(0, 6).map(
(data, index) =>
<WordformButton key={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams}
isSelected={data.grams.every(gram => selectedGrams.find(item => item.value as Grammeme === gram))}
onSelectGrams={handleSelectGrams}
/>
)}
</div>
<div className='flex flex-start'>
{PremadeWordForms.slice(6, 12).map(
(data, index) =>
<WordformButton key={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams}
isSelected={data.grams.every(gram => selectedGrams.find(item => item.value as Grammeme === gram))}
onSelectGrams={handleSelectGrams}
/>
)}
</div>
</div>);
}, [handleSelectGrams, selectedGrams]);
const handleSubmit = () => onSave(reference);
return (
<Modal
@ -164,99 +49,57 @@ function DlgEditReference({ hideWindow, items, initial, onSave }: DlgEditReferen
onSubmit={handleSubmit}
>
<div className='min-w-[40rem] max-w-[40rem] flex flex-col gap-3 mb-2 min-h-[34rem]'>
<div className='flex items-center self-center flex-start'>
<ReferenceTypeButton
type={ReferenceType.ENTITY}
onSelect={setType}
isSelected={type === ReferenceType.ENTITY}
/>
<ReferenceTypeButton
type={ReferenceType.SYNTACTIC}
onSelect={setType}
isSelected={type === ReferenceType.SYNTACTIC}
/>
<Tabs defaultFocus
className='flex flex-col items-center'
selectedTabClassName='clr-selected'
selectedIndex={activeTab}
onSelect={setActiveTab}
>
<div className='flex gap-1 pl-6 mb-3'>
<TabList className='flex border'>
<ConceptTab
label={labelReferenceType(ReferenceType.ENTITY)}
tooltip='Отсылка на термин в заданной словоформе'
className='w-[12rem] border-r-2'
/>
<ConceptTab
label={labelReferenceType(ReferenceType.SYNTACTIC)}
tooltip='Установление синтаксической связи с отсылкой на термин'
className='w-[12rem]'
/>
</TabList>
<div id='terminology-help' className='px-1 py-1'>
<HelpIcon color='text-primary' size={5} />
</div>
<ConceptTooltip
anchorSelect='#terminology-help'
className='max-w-[30rem] z-modal-tooltip'
offset={4}
offset={10}
>
<HelpTerminologyControl />
</ConceptTooltip>
</div>
{type === ReferenceType.SYNTACTIC ?
<div className='flex flex-col gap-2'>
<div className='flex flex-start'>
<TextInput type='number' dense
label='Смещение'
dimensions='max-w-[10rem]'
value={offset}
onChange={event => setOffset(event.target.valueAsNumber)}
/>
<div className='self-center ml-2 text-sm font-semibold whitespace-nowrap'>
Основная ссылка:
</div>
<TextInput disabled dense noBorder
value={mainLink}
dimensions='w-full text-sm'
/>
</div>
<TextInput spellCheck
label='Начальная форма'
placeholder='зависимое слово в начальной форме'
value={nominal}
onChange={event => setNominal(event.target.value)}
<div className='w-full'>
<TabPanel>
<EntityTab
initial={initial}
items={items}
setReference={setReference}
setIsValid={setIsValid}
/>
</div> : null}
{type === ReferenceType.ENTITY ?
<div className='flex flex-col gap-3'>
<ConstituentaPicker
value={selectedCst}
data={items}
onSelectValue={handleSelectConstituenta}
prefixID={prefixes.cst_modal_list}
describeFunc={cst => cst.term_resolved}
matchFunc={(cst, filter) => matchConstituenta(cst, filter, CstMatchMode.TERM)}
prefilterFunc={cst => cst.term_resolved !== ''}
rows={8}
</TabPanel>
<TabPanel>
<SyntacticTab
initial={initial}
setReference={setReference}
setIsValid={setIsValid}
/>
<div className='flex gap-4 flex-start'>
<TextInput dense
label='Отсылаемая конституента'
placeholder='Имя'
dimensions='max-w-[16rem] min-w-[16rem] whitespace-nowrap'
value={alias}
onChange={event => setAlias(event.target.value)}
/>
<div className='flex items-center w-full flex-start'>
<div className='self-center text-sm font-semibold'>
Термин:
</div>
<TextInput disabled dense noBorder
value={term}
tooltip={term}
dimensions='w-full text-sm'
/>
</div>
</div>
{FormButtons}
<div className='flex items-center gap-4 flex-start'>
<Label text='Отсылаемая словоформа'/>
<SelectMulti
placeholder='Выберите граммемы'
className='flex-grow h-full'
menuPlacement='top'
options={gramOptions}
value={selectedGrams}
onChange={newValue => setSelectedGrams([...newValue].sort(compareGrammemeOptions))}
/>
</div>
</div> : null}
</TabPanel>
</div>
</Tabs>
</div>
</Modal>);
}

View File

@ -0,0 +1,123 @@
import { useEffect, useLayoutEffect, useState } from 'react';
import Label from '../../components/Common/Label';
import SelectMulti from '../../components/Common/SelectMulti';
import TextInput from '../../components/Common/TextInput';
import ConstituentaPicker from '../../components/Shared/ConstituentaPicker';
import { Grammeme, ReferenceType } from '../../models/language';
import { getCompatibleGrams, parseEntityReference, parseGrammemes } from '../../models/languageAPI';
import { CstMatchMode } from '../../models/miscelanious';
import { IConstituenta } from '../../models/rsform';
import { matchConstituenta } from '../../models/rsformAPI';
import { prefixes } from '../../utils/constants';
import { compareGrammemeOptions,IGrammemeOption, SelectorGrammems } from '../../utils/selectors';
import { IReferenceInputState } from './DlgEditReference';
import SelectTermform from './SelectTermform';
interface EntityTabProps {
initial: IReferenceInputState
items: IConstituenta[]
setIsValid: React.Dispatch<React.SetStateAction<boolean>>
setReference: React.Dispatch<React.SetStateAction<string>>
}
function EntityTab({ initial, items, setIsValid, setReference }: EntityTabProps) {
const [selectedCst, setSelectedCst] = useState<IConstituenta | undefined>(undefined);
const [alias, setAlias] = useState('');
const [term, setTerm] = useState('');
const [selectedGrams, setSelectedGrams] = useState<IGrammemeOption[]>([]);
const [gramOptions, setGramOptions] = useState<IGrammemeOption[]>([]);
// Initialization
useLayoutEffect(
() => {
if (!!initial.refRaw && initial.type === ReferenceType.ENTITY) {
const ref = parseEntityReference(initial.refRaw);
setAlias(ref.entity);
const grams = parseGrammemes(ref.form);
setSelectedGrams(SelectorGrammems.filter(data => grams.includes(data.value)));
}
}, [initial, items]);
// Produce result
useEffect(
() => {
setIsValid(alias !== '' && selectedGrams.length > 0);
setReference(`@{${alias}|${selectedGrams.map(gram => gram.value).join(',')}}`);
}, [alias, selectedGrams, setIsValid, setReference]);
// Filter grammemes when input changes
useEffect(
() => {
const compatible = getCompatibleGrams(
selectedGrams
.filter(data => Object.values(Grammeme).includes(data.value as Grammeme))
.map(data => data.value as Grammeme)
);
setGramOptions(SelectorGrammems.filter(({value}) => compatible.includes(value as Grammeme)));
}, [selectedGrams]);
// Update term when alias changes
useEffect(
() => {
const cst = items.find(item => item.alias === alias)
setTerm(cst?.term_resolved ?? '')
}, [alias, term, items]);
function handleSelectConstituenta(cst: IConstituenta) {
setAlias(cst.alias);
setSelectedCst(cst);
}
return (
<div className='flex flex-col gap-3'>
<ConstituentaPicker
value={selectedCst}
data={items}
onSelectValue={handleSelectConstituenta}
prefixID={prefixes.cst_modal_list}
describeFunc={cst => cst.term_resolved}
matchFunc={(cst, filter) => matchConstituenta(cst, filter, CstMatchMode.TERM)}
prefilterFunc={cst => cst.term_resolved !== ''}
rows={8}
/>
<div className='flex gap-4 flex-start'>
<TextInput dense
label='Отсылаемая конституента'
placeholder='Имя'
dimensions='max-w-[16rem] min-w-[16rem] whitespace-nowrap'
value={alias}
onChange={event => setAlias(event.target.value)}
/>
<div className='flex items-center w-full flex-start'>
<Label text='Термин' />
<TextInput disabled dense noBorder
value={term}
tooltip={term}
dimensions='w-full text-sm'
/>
</div>
</div>
<SelectTermform
selected={selectedGrams}
setSelected={setSelectedGrams}
/>
<div className='flex items-center gap-4 flex-start'>
<Label text='Отсылаемая словоформа'/>
<SelectMulti
placeholder='Выберите граммемы'
className='flex-grow h-full'
menuPlacement='top'
options={gramOptions}
value={selectedGrams}
onChange={newValue => setSelectedGrams([...newValue].sort(compareGrammemeOptions))}
/>
</div>
</div>);
}
export default EntityTab;

View File

@ -1,23 +0,0 @@
import SwitchButton from '../../components/Common/SwitchButton';
import { ReferenceType } from '../../models/language';
import { labelReferenceType } from '../../utils/labels';
interface ReferenceTypeButtonProps {
type: ReferenceType
isSelected?: boolean
onSelect: (type: ReferenceType) => void
}
function ReferenceTypeButton({ type, isSelected, onSelect }: ReferenceTypeButtonProps) {
return (
<SwitchButton
value={type}
isSelected={isSelected}
onSelect={onSelect}
dimensions='min-w-[12rem] h-fit'
label={labelReferenceType(type)}
/>
);
}
export default ReferenceTypeButton;

View File

@ -0,0 +1,45 @@
import { useCallback } from 'react';
import { Grammeme } from '../../models/language';
import { prefixes } from '../../utils/constants';
import { IGrammemeOption, PremadeWordForms, SelectorGrammems } from '../../utils/selectors';
import WordformButton from './WordformButton';
interface SelectTermformProps {
selected: IGrammemeOption[]
setSelected: React.Dispatch<React.SetStateAction<IGrammemeOption[]>>
}
function SelectTermform({ selected, setSelected }: SelectTermformProps) {
const handleSelect = useCallback(
(grams: Grammeme[]) => {
setSelected(SelectorGrammems.filter(({value}) => grams.includes(value as Grammeme)));
}, [setSelected]);
return (
<div className='flex flex-col items-center w-full text-sm'>
<div className='flex flex-start'>
{PremadeWordForms.slice(0, 6).map(
(data, index) =>
<WordformButton key={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams}
isSelected={data.grams.every(gram => selected.find(item => item.value as Grammeme === gram))}
onSelectGrams={handleSelect}
/>
)}
</div>
<div className='flex flex-start'>
{PremadeWordForms.slice(6, 12).map(
(data, index) =>
<WordformButton key={`${prefixes.wordform_list}${index}`}
text={data.text} example={data.example} grams={data.grams}
isSelected={data.grams.every(gram => selected.find(item => item.value as Grammeme === gram))}
onSelectGrams={handleSelect}
/>
)}
</div>
</div>);
}
export default SelectTermform;

View File

@ -0,0 +1,66 @@
import { useEffect, useLayoutEffect, useMemo, useState } from 'react';
import TextInput from '../../components/Common/TextInput';
import { ReferenceType } from '../../models/language';
import { parseSyntacticReference } from '../../models/languageAPI';
import { IReferenceInputState } from './DlgEditReference';
interface SyntacticTabProps {
initial: IReferenceInputState
setIsValid: React.Dispatch<React.SetStateAction<boolean>>
setReference: React.Dispatch<React.SetStateAction<string>>
}
function SyntacticTab({ initial, setIsValid, setReference }: SyntacticTabProps) {
const [nominal, setNominal] = useState('');
const [offset, setOffset] = useState(1);
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]);
useLayoutEffect(
() => {
if (initial.refRaw && initial.type === ReferenceType.SYNTACTIC) {
const ref = parseSyntacticReference(initial.refRaw);
setOffset(ref.offset);
setNominal(ref.nominal);
} else {
setNominal(initial.text ?? '');
}
}, [initial]);
useEffect(
() => {
setIsValid(nominal !== '' && offset !== 0);
setReference(`@{${offset}|${nominal}}`);
}, [nominal, offset, setIsValid, setReference]);
return (
<div className='flex flex-col gap-2'>
<TextInput type='number' dense
label='Смещение'
dimensions='max-w-[10rem]'
value={offset}
onChange={event => setOffset(event.target.valueAsNumber)}
/>
<TextInput disabled dense noBorder
label='Основная ссылка'
value={mainLink}
/>
<TextInput spellCheck
label='Начальная форма'
placeholder='зависимое слово в начальной форме'
value={nominal}
onChange={event => setNominal(event.target.value)}
/>
</div>);
}
export default SyntacticTab;

View File

@ -193,6 +193,7 @@
}
:is(.clr-controls,
.clr-tab,
.clr-btn-default
) {
background-color: var(--cl-bg-80);

View File

@ -388,7 +388,7 @@ function RSTabs() {
className='flex flex-col w-full'
>
<div className='flex justify-center w-[100vw]'>
<TabList className='flex items-start border-b-2 border-x-2 select-none justify-stretch w-fit clr-controls h-[1.9rem] small-caps font-semibold'>
<TabList className='flex border-b-2 border-x-2 justify-stretch w-fit h-[1.9rem]'>
<RSTabsMenu
onDownload={onDownloadSchema}
onDestroy={onDestroySchema}