Refactor UI styling and implement text editing buttons

This commit is contained in:
IRBorisov 2023-07-21 18:44:14 +03:00
parent 53868d48d7
commit 5bcadaf58b
19 changed files with 406 additions and 210 deletions

View File

@ -16,7 +16,7 @@ import ToasterThemed from './components/ToasterThemed';
function App() {
return (
<div className='antialiased bg-gray-50 dark:bg-gray-800'>
<div className='antialiased clr-app'>
<Navigation />
<ToasterThemed
className='mt-[4rem] text-sm'

View File

@ -8,20 +8,18 @@ interface ButtonProps {
disabled?: boolean
dense?: boolean
loading?: boolean
colorClass?: string
borderClass?: string
onClick?: MouseEventHandler<HTMLButtonElement> | undefined
}
function Button({id, text, icon, tooltip,
dense, disabled,
colorClass, borderClass='border rounded',
loading, onClick
borderClass='border rounded',
loading, onClick,
...props
}: ButtonProps) {
const padding = dense ? 'px-1' : 'px-3 py-2'
const cursor = 'disabled:cursor-not-allowed ' + (loading ? 'cursor-progress ': 'cursor-pointer ')
const baseColor = 'dark:disabled:text-zinc-400 disabled:text-gray-400 bg-gray-100 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400'
const color = baseColor + ' ' + (colorClass || 'text-gray-500 dark:text-zinc-200')
return (
<button id={id}
type='button'
@ -29,8 +27,9 @@ function Button({id, text, icon, tooltip,
onClick={onClick}
title={tooltip}
className={padding + ' ' + borderClass + ' ' +
'inline-flex items-center gap-2 align-middle justify-center w-fit h-fit ' + cursor + color
'inline-flex items-center gap-2 align-middle justify-center w-fit h-fit clr-btn-default ' + cursor
}
{...props}
>
{icon && <span>{icon}</span>}
{text && <span className={'font-semibold'}>{text}</span>}

View File

@ -14,7 +14,7 @@ function Checkbox({id, required, disabled, label, widthClass='w-full', value, on
return (
<div className={'flex gap-2 [&:not(:first-child)]:mt-3 ' + widthClass}>
<input id={id} type='checkbox'
className='relative cursor-pointer peer w-4 h-4 shrink-0 mt-0.5 bg-white border rounded-sm appearance-none dark:bg-gray-900 checked:bg-blue-700 dark:checked:bg-orange-500'
className='relative cursor-pointer peer w-4 h-4 shrink-0 mt-0.5 border rounded-sm appearance-none clr-checkbox'
required={required}
disabled={disabled}
checked={value}

View File

@ -5,7 +5,7 @@ function ConceptTab({children, className, ...otherProps} : TabProps) {
return (
<Tab
className={
'px-2 py-1 text-sm text-gray-600 hover:cursor-pointer dark:text-zinc-200 hover:bg-gray-300 dark:hover:bg-gray-400'
'px-2 py-1 text-sm hover:cursor-pointer clr-tab'
+ ' ' + className
}
{...otherProps}

View File

@ -7,7 +7,7 @@ interface SubmitButtonProps {
function SubmitButton({text='ОК', disabled, loading=false}: SubmitButtonProps) {
return (
<button type='submit'
className={`px-4 py-2 font-bold text-white disabled:cursor-not-allowed rounded clr-btn-primary ${loading ? ' cursor-progress': ''}`}
className={`px-4 py-2 font-bold disabled:cursor-not-allowed rounded clr-btn-primary ${loading ? ' cursor-progress': ''}`}
disabled={disabled}
>
{text}

View File

@ -3,9 +3,9 @@ import Button from './Common/Button';
function ErrorFallback({ error, resetErrorBoundary }: FallbackProps) {
return (
<div className='flex flex-col items-center antialiased bg-gray-50 dark:bg-gray-800' role='alert'>
<div className='flex flex-col items-center antialiased clr-app' role='alert'>
<h1 className='text-lg font-semibold'>Something went wrong!</h1>
<pre className='text-red-400'>Error message: {error.message}</pre>
<pre className='text-red'>Error message: {error.message}</pre>
<Button onClick={resetErrorBoundary} text='Try again' />
</div>
);

View File

@ -3,7 +3,7 @@ import { urls } from '../utils/constants';
function Footer() {
return (
<footer className='z-50 px-4 pt-2 pb-4 text-gray-600 bg-white border-t-2 border-gray-400 dark:bg-gray-700 dark:border-gray-300 dark:text-gray-300'>
<footer className='z-50 px-4 pt-2 pb-4 t border-t-2 clr-footer'>
<div className='flex items-stretch justify-center w-full mx-auto'>
<div className='px-4 underline'>
<Link to='manuals' tabIndex={-1}>Справка</Link> <br/>

View File

@ -3,22 +3,23 @@
interface IconSVGProps {
viewbox: string
size?: number
color?: string
props?: React.SVGProps<SVGSVGElement>
children: React.ReactNode
}
export interface IconProps {
size?: number
color?: string
}
function IconSVG({viewbox, size=6, props, children} : IconSVGProps) {
function IconSVG({viewbox, size=6, color, props, children} : IconSVGProps) {
const width = `${size*1/4}rem`
const sizeClass = `w-[${width}] h-[${width}]`;
return (
<svg
width={width}
height={width}
className={sizeClass}
className={`w-[${width}] h-[${width}] ${color}`}
fill='currentColor'
viewBox={viewbox}
{...props}
@ -28,163 +29,163 @@ function IconSVG({viewbox, size=6, props, children} : IconSVGProps) {
);
}
export function MagnifyingGlassIcon({size}: IconProps) {
export function MagnifyingGlassIcon({size, ...props}: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size || 5}>
<IconSVG viewbox='0 0 20 20' size={size || 5} {...props} >
<path d='M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z'/>
</IconSVG>
);
}
export function BellIcon({size}: IconProps) {
export function BellIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M10 2a6 6 0 00-6 6v3.586l-.707.707A1 1 0 004 14h12a1 1 0 00.707-1.707L16 11.586V8a6 6 0 00-6-6zM10 18a3 3 0 01-3-3h6a3 3 0 01-3 3z' />
</IconSVG>
);
}
export function EyeIcon({size}: IconProps) {
export function EyeIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 1024 1024' size={size}>
<IconSVG viewbox='0 0 1024 1024' {...props}>
<path d='M942.2 486.2C847.4 286.5 704.1 186 512 186c-192.2 0-335.4 100.5-430.2 300.3a60.3 60.3 0 000 51.5C176.6 737.5 319.9 838 512 838c192.2 0 335.4-100.5 430.2-300.3 7.7-16.2 7.7-35 0-51.5zM512 766c-161.3 0-279.4-81.8-362.7-254C232.6 339.8 350.7 258 512 258c161.3 0 279.4 81.8 362.7 254C791.5 684.2 673.4 766 512 766zm-4-430c-97.2 0-176 78.8-176 176s78.8 176 176 176 176-78.8 176-176-78.8-176-176-176zm0 288c-61.9 0-112-50.1-112-112s50.1-112 112-112 112 50.1 112 112-50.1 112-112 112z' />
</IconSVG>
);
}
export function EyeOffIcon({size}: IconProps) {
export function EyeOffIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 1024 1024' size={size}>
<IconSVG viewbox='0 0 1024 1024' {...props}>
<path d='M942.2 486.2Q889.47 375.11 816.7 305l-50.88 50.88C807.31 395.53 843.45 447.4 874.7 512 791.5 684.2 673.4 766 512 766q-72.67 0-133.87-22.38L323 798.75Q408 838 512 838q288.3 0 430.2-300.3a60.29 60.29 0 000-51.5zm-63.57-320.64L836 122.88a8 8 0 00-11.32 0L715.31 232.2Q624.86 186 512 186q-288.3 0-430.2 300.3a60.3 60.3 0 000 51.5q56.69 119.4 136.5 191.41L112.48 835a8 8 0 000 11.31L155.17 889a8 8 0 0011.31 0l712.15-712.12a8 8 0 000-11.32zM149.3 512C232.6 339.8 350.7 258 512 258c54.54 0 104.13 9.36 149.12 28.39l-70.3 70.3a176 176 0 00-238.13 238.13l-83.42 83.42C223.1 637.49 183.3 582.28 149.3 512zm246.7 0a112.11 112.11 0 01146.2-106.69L401.31 546.2A112 112 0 01396 512z'/>
<path d='M508 624c-3.46 0-6.87-.16-10.25-.47l-52.82 52.82a176.09 176.09 0 00227.42-227.42l-52.82 52.82c.31 3.38.47 6.79.47 10.25a111.94 111.94 0 01-112 112z' />
</IconSVG>
);
}
export function PenIcon({size}: IconProps) {
export function PenIcon(props: IconProps) {
return (
<IconSVG viewbox='-3 -3 21 21' size={size}>
<IconSVG viewbox='-3 -3 21 21' {...props}>
<path d='M15.502 1.94a.5.5 0 010 .706L14.459 3.69l-2-2L13.502.646a.5.5 0 01.707 0l1.293 1.293zm-1.75 2.456l-2-2L4.939 9.21a.5.5 0 00-.121.196l-.805 2.414a.25.25 0 00.316.316l2.414-.805a.5.5 0 00.196-.12l6.813-6.814z' />
<path d='M1 13.5A1.5 1.5 0 002.5 15h11a1.5 1.5 0 001.5-1.5v-6a.5.5 0 00-1 0v6a.5.5 0 01-.5.5h-11a.5.5 0 01-.5-.5v-11a.5.5 0 01.5-.5H9a.5.5 0 000-1H2.5A1.5 1.5 0 001 2.5v11z' />
</IconSVG>
);
}
export function SquaresIcon({size}: IconProps) {
export function SquaresIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M5 3a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2V5a2 2 0 00-2-2H5zM5 11a2 2 0 00-2 2v2a2 2 0 002 2h2a2 2 0 002-2v-2a2 2 0 00-2-2H5zM11 5a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V5zM11 13a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z' />
</IconSVG>
);
}
export function GroupIcon({size}: IconProps) {
export function GroupIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M13 6a3 3 0 11-6 0 3 3 0 016 0zM18 8a2 2 0 11-4 0 2 2 0 014 0zM14 15a4 4 0 00-8 0v3h8v-3zM6 8a2 2 0 11-4 0 2 2 0 014 0zM16 18v-3a5.972 5.972 0 00-.75-2.906A3.005 3.005 0 0119 15v3h-3zM4.75 12.094A5.973 5.973 0 004 15v3H1v-3a3 3 0 013.75-2.906z' />
</IconSVG>
);
}
export function FrameIcon({size}: IconProps) {
export function FrameIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M5 3a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2V5a2 2 0 00-2-2H5zm0 2h10v7h-2l-1 2H8l-1-2H5V5z' />
</IconSVG>
);
}
export function AsteriskIcon({size}: IconProps) {
export function AsteriskIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z' />
</IconSVG>
);
}
export function MenuIcon({size}: IconProps) {
export function MenuIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M4 6h16v2H4zm0 5h16v2H4zm0 5h16v2H4z' />
</IconSVG>
);
}
export function ShareIcon({size}: IconProps) {
export function ShareIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M5.5 15a3.51 3.51 0 002.36-.93l6.26 3.58a3.06 3.06 0 00-.12.85 3.53 3.53 0 101.14-2.57l-6.26-3.58a2.74 2.74 0 00.12-.76l6.15-3.52A3.49 3.49 0 1014 5.5a3.35 3.35 0 00.12.85L8.43 9.6A3.5 3.5 0 105.5 15zm12 2a1.5 1.5 0 11-1.5 1.5 1.5 1.5 0 011.5-1.5zm0-13A1.5 1.5 0 1116 5.5 1.5 1.5 0 0117.5 4zm-12 6A1.5 1.5 0 114 11.5 1.5 1.5 0 015.5 10z' />
</IconSVG>
);
}
export function FilterCogIcon({size}: IconProps) {
export function FilterCogIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M22.77 19.32l-1.07-.82c.02-.17.04-.33.04-.5s-.01-.33-.04-.5l1.06-.82a.26.26 0 00.06-.32l-1-1.73c-.06-.13-.19-.13-.32-.13l-1.23.5c-.27-.18-.54-.35-.85-.47l-.19-1.32A.236.236 0 0019 13h-2a.26.26 0 00-.26.21l-.19 1.32c-.3.13-.59.29-.85.47l-1.24-.5c-.11 0-.24 0-.31.13l-1 1.73c-.06.11-.04.24.06.32l1.06.82a4.193 4.193 0 000 1l-1.06.82a.26.26 0 00-.06.32l1 1.73c.06.13.19.13.31.13l1.24-.5c.26.18.54.35.85.47l.19 1.32c.02.12.12.21.26.21h2c.11 0 .22-.09.24-.21l.19-1.32c.3-.13.57-.29.84-.47l1.23.5c.13 0 .26 0 .33-.13l1-1.73a.26.26 0 00-.06-.32M18 19.5c-.84 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5M3 3c-.22 0-.43.08-.62.22a1 1 0 00-.17 1.4L7.97 12H8v5.87c-.04.29.06.6.29.83l2.01 2.01c.35.35.89.37 1.28.09-.38-.89-.58-1.84-.58-2.8 0-1.27.35-2.5 1-3.6V12h.03l5.76-7.38a1 1 0 00-.17-1.4c-.19-.14-.4-.22-.62-.22H3z' />
</IconSVG>
);
}
export function FilterIcon({size}: IconProps) {
export function FilterIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M22 3H2l8 9.46V19l4 2v-8.54L22 3z' />
</IconSVG>
);
}
export function SortIcon({size}: IconProps) {
export function SortIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M8 16H4l6 6V2H8zm6-11v17h2V8h4l-6-6z' />
</IconSVG>
);
}
export function BookmarkIcon({size}: IconProps) {
export function BookmarkIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M5 2a2 2 0 00-2 2v14l3.5-2 3.5 2 3.5-2 3.5 2V4a2 2 0 00-2-2H5zm2.5 3a1.5 1.5 0 100 3 1.5 1.5 0 000-3zm6.207.293a1 1 0 00-1.414 0l-6 6a1 1 0 101.414 1.414l6-6a1 1 0 000-1.414zM12.5 10a1.5 1.5 0 100 3 1.5 1.5 0 000-3z' />
</IconSVG>
);
}
export function UserIcon({size}: IconProps) {
export function UserIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 512 512' size={size}>
<IconSVG viewbox='0 0 512 512' {...props}>
<path d='M399 384.2c-22.1-38.4-63.6-64.2-111-64.2h-64c-47.4 0-88.9 25.8-111 64.2 35.2 39.2 86.2 63.8 143 63.8s107.8-24.7 143-63.8zM512 256c0 141.4-114.6 256-256 256S0 397.4 0 256 114.6 0 256 0s256 114.6 256 256zm-256 16c39.8 0 72-32.2 72-72s-32.2-72-72-72-72 32.2-72 72 32.2 72 72 72z' />
</IconSVG>
);
}
export function EducationIcon({size}: IconProps) {
export function EducationIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M3.33 8L10 12l10-6-10-6L0 6h10v2H3.33zM0 8v8l2-2.22V9.2L0 8zm10 12l-5-3-2-1.2v-6l7 4.2 7-4.2v6L10 20z' />
</IconSVG>
);
}
export function DarkThemeIcon({size}: IconProps) {
export function DarkThemeIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z' />
</IconSVG>
);
}
export function LightThemeIcon({size}: IconProps) {
export function LightThemeIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 20 20' size={size}>
<IconSVG viewbox='0 0 20 20' {...props}>
<path d='M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z' />
</IconSVG>
);
}
export function LibraryIcon({size}: IconProps) {
export function LibraryIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 512 512' size={size}>
<IconSVG viewbox='0 0 512 512' {...props}>
<path d='M64 480H48a32 32 0 01-32-32V112a32 32 0 0132-32h16a32 32 0 0132 32v336a32 32 0 01-32 32zM240 176a32 32 0 00-32-32h-64a32 32 0 00-32 32v28a4 4 0 004 4h120a4 4 0 004-4zM112 448a32 32 0 0032 32h64a32 32 0 0032-32v-30a2 2 0 00-2-2H114a2 2 0 00-2 2z' />
<path d='M114 240 H238 A2 2 0 0 1 240 242 V382 A2 2 0 0 1 238 384 H114 A2 2 0 0 1 112 382 V242 A2 2 0 0 1 114 240 z' />
<path d='M320 480h-32a32 32 0 01-32-32V64a32 32 0 0132-32h32a32 32 0 0132 32v384a32 32 0 01-32 32zM495.89 445.45l-32.23-340c-1.48-15.65-16.94-27-34.53-25.31l-31.85 3c-17.59 1.67-30.65 15.71-29.17 31.36l32.23 340c1.48 15.65 16.94 27 34.53 25.31l31.85-3c17.59-1.67 30.65-15.71 29.17-31.36z' />
@ -192,76 +193,84 @@ export function LibraryIcon({size}: IconProps) {
);
}
export function PlusIcon({size}: IconProps) {
export function PlusIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 1024 1024' size={size}>
<IconSVG viewbox='0 0 1024 1024' {...props}>
<path d='M880 112H144c-17.7 0-32 14.3-32 32v736c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V144c0-17.7-14.3-32-32-32zM704 536c0 4.4-3.6 8-8 8H544v152c0 4.4-3.6 8-8 8h-48c-4.4 0-8-3.6-8-8V544H328c-4.4 0-8-3.6-8-8v-48c0-4.4 3.6-8 8-8h152V328c0-4.4 3.6-8 8-8h48c4.4 0 8 3.6 8 8v152h152c4.4 0 8 3.6 8 8v48z' />
</IconSVG>
);
}
export function SmallPlusIcon({size}: IconProps) {
export function SmallPlusIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12zm10-8a8 8 0 100 16 8 8 0 000-16z'/>
<path d='M13 7a1 1 0 10-2 0v4H7a1 1 0 100 2h4v4a1 1 0 102 0v-4h4a1 1 0 100-2h-4V7z'/>
</IconSVG>
);
}
export function UploadIcon({size}: IconProps) {
export function UploadIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M11 15h2V9h3l-4-5-4 5h3z'/>
<path d='M20 18H4v-7H2v7c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2v-7h-2v7z'/>
</IconSVG>
);
}
export function DownloadIcon({size}: IconProps) {
export function DownloadIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 16l4-5h-3V4h-2v7H8z'/>
<path d='M20 18H4v-7H2v7c0 1.103.897 2 2 2h16c1.103 0 2-.897 2-2v-7h-2v7z'/>
</IconSVG>
);
}
export function CrownIcon({size}: IconProps) {
export function CrownIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M5 16L3 5l5.5 5L12 4l3.5 6L21 5l-2 11H5m14 3c0 .6-.4 1-1 1H6c-.6 0-1-.4-1-1v-1h14v1z' />
</IconSVG>
);
}
export function ArrowUpIcon({size}: IconProps) {
export function ArrowUpIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 16 16' size={size}>
<IconSVG viewbox='0 0 16 16' {...props}>
<path d='M8 12a.5.5 0 00.5-.5V5.707l2.146 2.147a.5.5 0 00.708-.708l-3-3a.5.5 0 00-.708 0l-3 3a.5.5 0 10.708.708L7.5 5.707V11.5a.5.5 0 00.5.5z' />
</IconSVG>
);
}
export function ArrowDownIcon({size}: IconProps) {
export function ArrowDownIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 16 16' size={size}>
<IconSVG viewbox='0 0 16 16' {...props}>
<path d='M8 4a.5.5 0 01.5.5v5.793l2.146-2.147a.5.5 0 01.708.708l-3 3a.5.5 0 01-.708 0l-3-3a.5.5 0 11.708-.708L7.5 10.293V4.5A.5.5 0 018 4z' />
</IconSVG>
);
}
export function DumpBinIcon({size}: IconProps) {
export function CloneIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 512 512' {...props}>
<path d='M64 464h224c8.8 0 16-7.2 16-16v-64h48v64c0 35.3-28.7 64-64 64H64c-35.35 0-64-28.7-64-64V224c0-35.3 28.65-64 64-64h64v48H64c-8.84 0-16 7.2-16 16v224c0 8.8 7.16 16 16 16zm96-400c0-35.35 28.7-64 64-64h224c35.3 0 64 28.65 64 64v224c0 35.3-28.7 64-64 64H224c-35.3 0-64-28.7-64-64V64zm64 240h224c8.8 0 16-7.2 16-16V64c0-8.84-7.2-16-16-16H224c-8.8 0-16 7.16-16 16v224c0 8.8 7.2 16 16 16z' />
</IconSVG>
);
}
export function DumpBinIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19a2 2 0 002 2h8a2 2 0 002-2V7H6v12z' />
</IconSVG>
);
}
export function ArrowsRotateIcon({size}: IconProps) {
export function ArrowsRotateIcon(props: IconProps) {
return (
<IconSVG viewbox='0 0 24 24' size={size}>
<IconSVG viewbox='0 0 24 24' {...props}>
<path d='M12 6v3l4-4-4-4v3a8 8 0 00-8 8c0 1.57.46 3.03 1.24 4.26L6.7 14.8A5.9 5.9 0 016 12a6 6 0 016-6m6.76 1.74L17.3 9.2c.44.84.7 1.8.7 2.8a6 6 0 01-6 6v-3l-4 4 4 4v-3a8 8 0 008-8c0-1.57-.46-3.03-1.24-4.26z' />
</IconSVG>
);

View File

@ -21,7 +21,7 @@ function Navigation() {
{isActive &&
<button
title='Скрыть навигацию'
className='absolute top-0 right-0 z-[60] border-b-2 border-l-2 clr-nav w-[1.2rem] h-[4rem] '
className='absolute top-0 right-0 z-[60] w-[1.2rem] h-[4rem] border-b-2 border-l-2 clr-nav rounded-none'
onClick={() => setActive(!isActive)}
>
<p>{'>'}</p><p>{'>'}</p>
@ -29,13 +29,13 @@ function Navigation() {
{!isActive &&
<button
title='Показать навигацию'
className='absolute top-0 right-0 z-[60] border-b-2 border-l-2 clr-nav w-[4rem] h-[1.6rem]'
className='absolute top-0 right-0 z-[60] w-[4rem] h-[1.6rem] border-b-2 border-l-2 clr-nav rounded-none'
onClick={() => setActive(!isActive)}
>
{''}
</button>}
{isActive &&
<div className='border-b-2 clr-nav pr-6 pl-2 py-2.5 h-[4rem] flex items-center justify-between '>
<div className='pr-6 pl-2 py-2.5 h-[4rem] flex items-center justify-between border-b-2 clr-nav rounded-none'>
<div className='flex items-start justify-start '>
<Logo title='КонцептПортал' />
<TopSearch placeholder='Поиск схемы...' />

View File

@ -16,15 +16,35 @@
@layer components {
.border {
@apply border-gray-300 rounded dark:border-gray-400
@apply clr-border rounded
}
.dark {
@apply text-zinc-200 bg-gray-900
}
.clr-border {
@apply border-gray-300 dark:border-gray-400
}
.clr-app {
@apply bg-gray-50 dark:bg-gray-800
}
.clr-bg-pop {
@apply bg-gray-100 dark:bg-gray-600
}
.clr-nav {
@apply border-gray-400 dark:border-gray-300 bg-white dark:bg-gray-700 rounded-none
@apply border-gray-400 dark:border-gray-300 bg-white dark:bg-gray-700
}
.clr-footer {
@apply text-gray-600 bg-white border-gray-400 dark:bg-gray-700 dark:border-gray-300 dark:text-gray-300
}
.clr-tab {
@apply text-gray-600 dark:text-zinc-200 hover:bg-gray-300 dark:hover:bg-gray-400
}
.clr-hover {
@ -32,7 +52,20 @@
}
.clr-btn-primary {
@apply bg-blue-400 hover:bg-blue-600 dark:bg-orange-600 dark:hover:bg-orange-400 disabled:bg-gray-400 dark:disabled:bg-gray-400
@apply text-white bg-blue-400 hover:bg-blue-600 dark:bg-orange-600 dark:hover:bg-orange-400 disabled:bg-gray-400 dark:disabled:bg-gray-400
}
.clr-btn-default {
@apply text-gray-500 dark:text-zinc-200 dark:disabled:text-zinc-400 disabled:text-gray-400 bg-gray-100 hover:bg-gray-300 dark:bg-gray-600 dark:hover:bg-gray-400
}
/* Transparent button */
.clr-btn-clear {
@apply hover:bg-gray-300 dark:hover:bg-gray-400
}
.clr-checkbox {
@apply bg-white dark:bg-gray-900 checked:bg-blue-700 dark:checked:bg-orange-500
}
.text-red {

View File

@ -1,6 +1,6 @@
import { useCallback, useEffect, useState } from 'react';
import { useRSForm } from '../../context/RSFormContext';
import { EditMode, IConstituenta } from '../../utils/models';
import { EditMode } from '../../utils/models';
import { toast } from 'react-toastify';
import TextArea from '../../components/Common/TextArea';
import ExpressionEditor from './ExpressionEditor';
@ -89,9 +89,6 @@ function ConstituentEditor() {
</label>
<b className='ml-2'>{alias}</b>
</span>
<div className='mt-2 h-[1rem]'>
<SubmitButton text='Сохранить изменения' disabled={!isEditable} />
</div>
<span>
<label
title='Изменить тип конституенты'
@ -124,6 +121,7 @@ function ConstituentEditor() {
isActive={editMode==='rslang'}
toggleEditMode={() => setEditMode(EditMode.RSLANG)}
onChange={event => setExpression(event.target.value)}
setValue={setExpression}
setTypification={setTypification}
/>
<TextArea id='definition' label='Текстовое определение'
@ -144,7 +142,9 @@ function ConstituentEditor() {
onChange={event => setConvention(event.target.value)}
onFocus={() => setEditMode(EditMode.TEXT)}
/>
<div className='mt-2 w-full flex justify-center'>
<SubmitButton text='Сохранить изменения' disabled={!isEditable} />
</div>
</form>
<ConstituentsSideList expression={expression}/>
</div>

View File

@ -163,43 +163,40 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
return (
<div className='w-full'>
<div className='flex justify-start w-full gap-1 px-2 py-1 border-y'>
<div className='mr-3'>Выбраны <span className='ml-2'><b>{selectedRows.length}</b> из {schema?.stats?.count_all || 0}</span></div>
<div className='flex justify-start w-full gap-1 px-2 py-1 border-y items-center h-[2.2rem]'>
<div className='mr-3 whitespace-nowrap'>Выбраны <span className='ml-2'><b>{selectedRows.length}</b> из {schema?.stats?.count_all || 0}</span></div>
{isEditable && <div className='flex justify-start w-full gap-1'>
<Button
tooltip='Переместить вверх'
icon={<ArrowUpIcon size={5}/>}
disabled={nothingSelected || !isEditable}
icon={<ArrowUpIcon size={6}/>}
disabled={nothingSelected}
dense
onClick={handleMoveUp}
/>
<Button
tooltip='Переместить вниз'
icon={<ArrowDownIcon size={5}/>}
disabled={nothingSelected || !isEditable}
icon={<ArrowDownIcon size={6}/>}
disabled={nothingSelected}
dense
onClick={handleMoveDown}
/>
<Button
colorClass='text-red'
tooltip='Удалить выбранные'
icon={<DumpBinIcon size={5}/>}
disabled={nothingSelected || !isEditable}
icon={<DumpBinIcon color={!nothingSelected ? 'text-red': ''} size={6}/>}
disabled={nothingSelected}
dense
onClick={handleDelete}
/>
<Divider vertical margins='1' />
<Button
tooltip='Переиндексировать имена'
icon={<ArrowsRotateIcon size={5}/>}
disabled={!isEditable}
icon={<ArrowsRotateIcon color='text-primary' size={6}/>}
dense
onClick={handleReindex}
/>
<Button
colorClass='text-green'
tooltip='Новая конституента'
icon={<SmallPlusIcon size={5}/>}
disabled={!isEditable}
icon={<SmallPlusIcon color='text-green' size={6}/>}
dense
onClick={() => handleAddNew()}
/>
@ -209,12 +206,11 @@ function ConstituentsTable({onOpenEdit}: ConstituentsTableProps) {
return <Button
text={`${getCstTypePrefix(type)}`}
tooltip={getCstTypeLabel(type)}
disabled={!isEditable}
dense
onClick={() =>handleAddNew(type)}
/>;
})
}
})}
</div>}
</div>
<DataTableThemed
data={schema!.items!}

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import Button from '../../components/Common/Button';
import Label from '../../components/Common/Label';
import { useRSForm } from '../../context/RSFormContext';
@ -9,8 +9,8 @@ import useCheckExpression from '../../hooks/useCheckExpression';
import ParsingResult from './ParsingResult';
import { Loader } from '../../components/Common/Loader';
import StatusBar from './StatusBar';
import PrettyJson from '../../components/Common/PrettyJSON';
import { AxiosResponse } from 'axios';
import { TextWrapper } from '../../utils/textEditing';
interface ExpressionEditorProps {
id: string
@ -22,15 +22,17 @@ interface ExpressionEditorProps {
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void
toggleEditMode: () => void
setTypification: (typificaiton: string) => void
setValue: (expression: string) => void
}
function ExpressionEditor({
id, label, disabled, isActive, placeholder, value,
id, label, disabled, isActive, placeholder, value, setValue,
toggleEditMode, setTypification, onChange
}: ExpressionEditorProps) {
const { schema, active } = useRSForm();
const [isModified, setIsModified] = useState(false);
const { parseData, checkExpression, resetParse, loading } = useCheckExpression({schema: schema});
const expressionCtrl = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
setIsModified(false);
@ -49,8 +51,16 @@ function ExpressionEditor({
}, [value, checkExpression, active, setTypification]);
const handleEdit = useCallback((id: TokenID) => {
toast.info(`Кнопка ${id}`);
}, []);
if (!expressionCtrl.current) {
toast.error('Нет доступа к полю редактирования формального выражения');
return;
}
let text = new TextWrapper(expressionCtrl.current);
text.insertToken(id);
text.finalize();
text.focus();
setValue(text.value);
}, [setValue]);
const handleChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => {
onChange(event);
@ -119,7 +129,7 @@ function ExpressionEditor({
required={false}
htmlFor={id}
/>
<textarea id={id}
<textarea id={id} ref={expressionCtrl}
className='w-full px-3 py-2 mt-2 leading-tight border shadow dark:bg-gray-800'
rows={6}
placeholder={placeholder}

View File

@ -9,7 +9,6 @@ interface RSEditButtonProps {
function RSEditButton({id, disabled, onInsert}: RSEditButtonProps) {
const data = getRSButtonData(id);
const color = 'hover:bg-gray-300 dark:hover:bg-gray-400 text-gray-800 dark:text-zinc-200'
const width = data.text.length > 3 ? 'w-[4rem]' : 'w-[2rem]';
return (
<button
@ -18,7 +17,7 @@ function RSEditButton({id, disabled, onInsert}: RSEditButtonProps) {
onClick={() => onInsert(id)}
title={data.tooltip}
tabIndex={-1}
className={`px-1 cursor-pointer border rounded-none h-7 ${width} ${color}`}
className={`px-1 cursor-pointer border rounded-none h-7 ${width} clr-btn-clear`}
>
{data.text && <span className='whitespace-nowrap'>{data.text}</span>}
</button>

View File

@ -13,13 +13,16 @@ import { toast } from 'react-toastify';
import fileDownload from 'js-file-download';
import { AxiosResponse } from 'axios';
import { useAuth } from '../../context/AuthContext';
import { claimOwnershipProc, deleteRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
function RSFormCard() {
const navigate = useNavigate();
const intl = useIntl();
const { getUserLabel } = useUsers();
const { schema, update, download, reload, isEditable, isClaimable, processing, destroy, claim } = useRSForm();
const {
schema, update, download, reload,
isEditable, isOwned, isClaimable, processing, destroy, claim
} = useRSForm();
const { user } = useAuth();
const [title, setTitle] = useState('');
@ -52,14 +55,8 @@ function RSFormCard() {
useCallback(() => deleteRSFormProc(destroy, navigate), [destroy, navigate]);
const handleDownload = useCallback(() => {
download((response: AxiosResponse) => {
try {
const fileName = (schema?.alias || 'Schema') + '.trs';
fileDownload(response.data, fileName);
} catch (error: any) {
toast.error(error.message);
}
});
downloadRSFormProc(download, fileName);
}, [download, schema?.alias]);
return (
@ -93,30 +90,26 @@ function RSFormCard() {
<div className='flex justify-end gap-1'>
<Button
tooltip='Поделиться схемой'
icon={<ShareIcon />}
colorClass='text-primary'
icon={<ShareIcon color='text-primary'/>}
onClick={shareCurrentURLProc}
/>
<Button
disabled={processing}
tooltip='Скачать TRS файл'
icon={<DownloadIcon />}
colorClass='text-primary'
icon={<DownloadIcon color='text-primary'/>}
loading={processing}
onClick={handleDownload}
/>
<Button
tooltip={isClaimable ? 'Стать владельцем' : 'Вы уже являетесь владельцем' }
disabled={!isClaimable || processing || !user}
icon={<CrownIcon />}
colorClass='text-green'
icon={<CrownIcon color={isOwned ? '' : 'text-green'}/>}
onClick={() => claimOwnershipProc(claim, reload)}
/>
<Button
tooltip={ isEditable ? 'Удалить схему' : 'Вы не можете редактировать данную схему'}
disabled={!isEditable || processing}
icon={<DumpBinIcon />}
colorClass='text-red'
icon={<DumpBinIcon color={isEditable ? 'text-red': ''} />}
loading={processing}
onClick={handleDelete}
/>

View File

@ -43,31 +43,6 @@ function RSFormTabs() {
}
}, [setActive, schema, setInit]);
// const [ locationKeys, setLocationKeys ] = useState([])
// const history = useHistory()
// useEffect(() => {
// return history.listen(location => {
// if (history.action === 'PUSH') {
// setLocationKeys([ location.key ])
// }
// if (history.action === 'POP') {
// if (locationKeys[1] === location.key) {
// setLocationKeys(([ _, ...keys ]) => keys)
// // Handle forward event
// } else {
// setLocationKeys((keys) => [ location.key, ...keys ])
// // Handle back event
// }
// }
// })
// }, [ locationKeys, ])
useEffect(() => {
const url = new URL(window.location.href);
const tabQuery = url.searchParams.get('tab');
@ -109,10 +84,10 @@ function RSFormTabs() {
defaultFocus={true}
selectedTabClassName='font-bold'
>
<TabList className='flex items-start bg-gray-100 w-fit dark:bg-gray-600'>
<TabList className='flex items-start w-fit clr-bg-pop'>
<TablistTools />
<ConceptTab>Паспорт схемы</ConceptTab>
<ConceptTab className='border-gray-300 border-x-2 dark:border-gray-400 min-w-[10rem] flex justify-between gap-2'>
<ConceptTab className='border-x-2 clr-border min-w-[10rem] flex justify-between gap-2'>
<span>Конституенты</span>
<span>{`${schema.stats?.count_errors} | ${schema.stats?.count_all}`}</span>
</ConceptTab>

View File

@ -1,32 +1,60 @@
import { useCallback } from 'react';
import Button from '../../components/Common/Button';
import Dropdown from '../../components/Common/Dropdown';
import { CrownIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon } from '../../components/Icons';
import { CloneIcon, CrownIcon, DownloadIcon, DumpBinIcon, EyeIcon, EyeOffIcon, MenuIcon, PenIcon, ShareIcon, UploadIcon } from '../../components/Icons';
import { useRSForm } from '../../context/RSFormContext';
import useDropdown from '../../hooks/useDropdown';
import DropdownButton from '../../components/Common/DropdownButton';
import Checkbox from '../../components/Common/Checkbox';
import { useAuth } from '../../context/AuthContext';
import { claimOwnershipProc, deleteRSFormProc } from '../../utils/procedures';
import { claimOwnershipProc, deleteRSFormProc, downloadRSFormProc, shareCurrentURLProc } from '../../utils/procedures';
import { useNavigate } from 'react-router-dom';
import { toast } from 'react-toastify';
function TablistTools() {
const navigate = useNavigate();
const {user} = useAuth();
const {
const { schema,
isOwned, isEditable, isTracking, readonly, forceAdmin,
toggleTracking, toggleForceAdmin, toggleReadonly,
claim, reload, destroy
claim, reload, destroy, download
} = useRSForm();
const schemaMenu = useDropdown();
const editMenu = useDropdown();
const handleClaimOwner = useCallback(() => {
claimOwnershipProc(claim, reload)
}, [claim, reload]);
editMenu.hide();
claimOwnershipProc(claim, reload);
}, [claim, reload, editMenu]);
const handleDelete =
useCallback(() => deleteRSFormProc(destroy, navigate), [destroy, navigate]);
const handleDelete = useCallback(() => {
schemaMenu.hide();
deleteRSFormProc(destroy, navigate);
}, [destroy, navigate, schemaMenu]);
const handleDownload = useCallback(() => {
schemaMenu.hide();
const fileName = (schema?.alias || 'Schema') + '.trs';
downloadRSFormProc(download, fileName);
}, [schemaMenu, download, schema?.alias]);
const handleUpload = useCallback(() => {
// TODO: implement
schemaMenu.hide();
toast.info('Замена содержимого на файл Экстеора');
}, [schemaMenu]);
const handleClone = useCallback(() => {
// TODO: implement
schemaMenu.hide();
toast.info('Клонирование РС-формы');
}, [schemaMenu]);
const handleShare = useCallback(() => {
schemaMenu.hide();
shareCurrentURLProc();
}, [schemaMenu]);
return (
<div className='flex items-center w-fit'>
@ -40,22 +68,43 @@ function TablistTools() {
/>
{ schemaMenu.isActive &&
<Dropdown>
<p>клонировать</p>
<p>поделиться</p>
<DropdownButton disabled={!isEditable} onClick={handleDelete}>
<div className='inline-flex items-center gap-1 justify-normal'>
<span className={isEditable ? 'text-red' : ''}><DumpBinIcon size={4} /></span>
<p>Удалить схему</p>
<DropdownButton onClick={handleShare}>
<div className='inline-flex items-center gap-2 justify-start'>
<ShareIcon color='text-primary' size={4}/>
<p>Поделиться</p>
</div>
</DropdownButton>
<DropdownButton onClick={handleClone}>
<div className='inline-flex items-center gap-2 justify-start'>
<CloneIcon color='text-primary' size={4}/>
<p>Клонировать</p>
</div>
</DropdownButton>
<DropdownButton onClick={handleDownload}>
<div className='inline-flex items-center gap-2 justify-start'>
<DownloadIcon color='text-primary' size={4}/>
<p>Выгрузить файл Экстеор</p>
</div>
</DropdownButton>
<DropdownButton disabled={!isEditable} onClick={handleUpload}>
<div className='inline-flex items-center gap-2 justify-start'>
<UploadIcon color={isEditable ? 'text-red' : ''} size={4}/>
<p>Загрузить из Экстеора</p>
</div>
</DropdownButton>
<DropdownButton disabled={!isEditable} onClick={handleDelete}>
<span className='inline-flex items-center gap-2 justify-start'>
<DumpBinIcon color={isEditable ? 'text-red' : ''} size={4} />
<p>Удалить схему</p>
</span>
</DropdownButton>
</Dropdown>}
</div>
<div ref={editMenu.ref}>
<Button
tooltip={'измнение ' + (isEditable ? 'доступно': 'запрещено')}
colorClass={ isEditable ? 'text-green': 'text-red'}
borderClass=''
icon={<PenIcon size={5}/>}
icon={<PenIcon size={5} color={isEditable ? 'text-green': 'text-red'}/>}
dense
onClick={editMenu.toggle}
/>
@ -82,9 +131,11 @@ function TablistTools() {
<div>
<Button
tooltip={'отслеживание: ' + (isTracking ? 'включено': 'выключено')}
icon={isTracking ? <EyeIcon size={5}/> : <EyeOffIcon size={5}/>}
icon={isTracking ?
<EyeIcon color='text-primary' size={5}/>
: <EyeOffIcon size={5}/>
}
borderClass=''
colorClass={isTracking ? 'text-primary': ''}
dense
onClick={toggleTracking}
/>
@ -94,4 +145,3 @@ function TablistTools() {
}
export default TablistTools

View File

@ -1,5 +1,6 @@
import { toast } from 'react-toastify';
import { BackendCallback } from './backendAPI';
import fileDownload from 'js-file-download';
export function shareCurrentURLProc() {
const url = window.location.href + '&share';
@ -32,3 +33,16 @@ export function deleteRSFormProc(
navigate('/rsforms?filter=personal');
});
}
export function downloadRSFormProc(
download: (callback: BackendCallback) => void,
fileName: string
) {
download((response) => {
try {
fileDownload(response.data, fileName);
} catch (error: any) {
toast.error(error.message);
}
});
}

View File

@ -0,0 +1,118 @@
// Formatted text editing helpers
import { TokenID } from './models'
export interface IManagedText {
value: string
selStart: number
selEnd: number
}
// Note: Wrapper class for textareafield.
// WARNING! Manipulations on value do not support UNDO browser
// WARNING! No checks for selection out of text boundaries
export class TextWrapper implements IManagedText {
value: string
selStart: number
selEnd: number
object: HTMLTextAreaElement
constructor(element: HTMLTextAreaElement) {
this.object = element;
this.value = this.object.value;
this.selStart = this.object.selectionStart;
this.selEnd = this.object.selectionEnd;
}
focus() {
this.object.focus();
}
refresh() {
this.value = this.object.value;
this.selStart = this.object.selectionStart;
this.selEnd = this.object.selectionEnd;
}
finalize() {
this.object.value = this.value;
this.object.selectionStart = this.selStart;
this.object.selectionEnd = this.selEnd;
}
replaceWith(data: string) {
this.value = this.value.substring(0, this.selStart) + data + this.value.substring(this.selEnd, this.value.length);
this.selEnd += data.length - this.selEnd + this.selStart;
this.selStart = this.selEnd;
}
envelopeWith(left: string, right: string) {
this.value = this.value.substring(0, this.selStart) + left +
this.value.substring(this.selStart, this.selEnd) + right +
this.value.substring(this.selEnd, this.value.length);
this.selEnd += left.length + right.length;
}
moveSel(shift: number) {
this.selStart += shift;
this.selEnd += shift;
}
setSel(start: number, end: number) {
this.selStart = start;
this.selEnd = end;
}
insertToken(tokenID: TokenID) {
switch(tokenID) {
case TokenID.NT_DECLARATIVE_EXPR: this.envelopeWith('D{ξ∈X1 | ', '}'); return;
case TokenID.NT_IMPERATIVE_EXPR: this.envelopeWith('I{(σ, γ) | σ:∈X1; γ:=F1[σ]; ', '}'); return;
case TokenID.NT_RECURSIVE_FULL: this.envelopeWith('R{ ξ:=D1 | 1=1 | ', '}'); return;
case TokenID.BIGPR: this.envelopeWith('Pr1(', ')'); return;
case TokenID.SMALLPR: this.envelopeWith('pr1(', ')'); return;
case TokenID.FILTER: this.envelopeWith('Fi1[α](', ')'); return;
case TokenID.REDUCE: this.envelopeWith('red(', ')'); return;
case TokenID.CARD: this.envelopeWith('card(', ')'); return;
case TokenID.BOOL: this.envelopeWith('bool(', ')'); return;
case TokenID.DEBOOL: this.envelopeWith('debool(', ')'); return;
case TokenID.PUNC_PL: this.envelopeWith('(', ')'); return;
case TokenID.PUNC_SL: this.envelopeWith('[', ']'); return;
case TokenID.BOOLEAN: {
if (this.selEnd !== this.selStart && this.value.at(this.selStart) === '') {
this.envelopeWith('', '');
} else {
this.envelopeWith('(', ')');
}
return;
}
case TokenID.DECART: this.replaceWith('×'); return;
case TokenID.FORALL: this.replaceWith('∀'); return;
case TokenID.EXISTS: this.replaceWith('∃'); return;
case TokenID.IN: this.replaceWith('∈'); return;
case TokenID.NOTIN: this.replaceWith('∉'); return;
case TokenID.OR: this.replaceWith(''); return;
case TokenID.AND: this.replaceWith('&'); return;
case TokenID.SUBSET_OR_EQ: this.replaceWith('⊆'); return;
case TokenID.IMPLICATION: this.replaceWith('⇒'); return;
case TokenID.INTERSECTION: this.replaceWith('∩'); return;
case TokenID.UNION: this.replaceWith(''); return;
case TokenID.MINUS: this.replaceWith('\\'); return;
case TokenID.SYMMINUS: this.replaceWith('∆'); return;
case TokenID.LIT_EMPTYSET: this.replaceWith('∅'); return;
case TokenID.LIT_INTSET: this.replaceWith('Z'); return;
case TokenID.SUBSET: this.replaceWith('⊂'); return;
case TokenID.NOTSUBSET: this.replaceWith('⊄'); return;
case TokenID.EQUAL: this.replaceWith('='); return;
case TokenID.NOTEQUAL: this.replaceWith('≠'); return;
case TokenID.NOT: this.replaceWith('¬'); return;
case TokenID.EQUIVALENT: this.replaceWith('⇔'); return;
case TokenID.GREATER_OR_EQ: this.replaceWith('≥'); return;
case TokenID.LESSER_OR_EQ: this.replaceWith('≤'); return;
case TokenID.PUNC_ASSIGN: this.replaceWith(':='); return;
case TokenID.PUNC_ITERATE: this.replaceWith(':∈'); return;
}
}
};