mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 10:53:51 +00:00
v2 styling (#263)
* feat: implement progress bar on running event * refactor: use event action hook * refactor: align backend data * style: v2 style tweaks * style: v2 style tweaks + ts <Info> * fix: fix delay increment calls * style: v2 style tweaks + ts <Playback> * style: v2 style tweaks + ts <EventEditor> * style: v2 style tweaks + ts <MessageControl> * fix: naming issue with message control socket endpoint * chore: upgrade style dependencies * style: v2 style tweaks + ts <MenuBar> * style: v2 style tweaks + ts <RundownMenu> * style: v2 style tweaks + ts <Playback> * fix: add selected and next information to EventTimer * style: v2 style tweaks + ts <EventBlock> * style: v2 style tweaks + ts <DelayBlock> * style: v2 style tweaks + ts <BlockBlock> * style: v2 style tweaks + ts <QuickEntry> * refactor: extract <TextInput> logic * chore: upgrade build dependencies * chore: folder structure refactor * fix: issue with dependency path in electron * chore: update version in demo db
This commit is contained in:
@@ -1,27 +0,0 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Textarea } from '@chakra-ui/react';
|
||||
import autosize from 'autosize/dist/autosize';
|
||||
|
||||
export const AutoTextArea = (props) => {
|
||||
const ref = useRef();
|
||||
|
||||
useEffect(() => {
|
||||
const node = ref.current;
|
||||
autosize(ref.current);
|
||||
return () => {
|
||||
autosize.destroy(node);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
overflow='hidden'
|
||||
w='100%'
|
||||
resize='none'
|
||||
ref={ref}
|
||||
transition='height none'
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,106 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Input, Textarea } from '@chakra-ui/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default function TextInput(props) {
|
||||
const { isTextArea, size = 'sm', field, initialText = '', submitHandler } = props;
|
||||
const inputRef = useRef(null);
|
||||
const [text, setText] = useState(initialText);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof initialText === 'undefined') {
|
||||
setText('');
|
||||
} else {
|
||||
setText(initialText);
|
||||
}
|
||||
}, [initialText]);
|
||||
|
||||
/**
|
||||
* @description Handles Input value change
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(newValue) => {
|
||||
if (newValue !== text) {
|
||||
setText(newValue);
|
||||
}
|
||||
},
|
||||
[text]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles submit events
|
||||
* @param {string} valueToSubmit
|
||||
*/
|
||||
const handleSubmit = useCallback(
|
||||
(valueToSubmit) => {
|
||||
// No need to update if it hasn't changed
|
||||
if (valueToSubmit === initialText) {
|
||||
return;
|
||||
}
|
||||
const cleanVal = valueToSubmit.trim();
|
||||
submitHandler(field, cleanVal);
|
||||
|
||||
if (cleanVal !== valueToSubmit) {
|
||||
setText(cleanVal);
|
||||
}
|
||||
},
|
||||
[field, initialText, submitHandler]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Resets input value to given
|
||||
*/
|
||||
const resetValue = useCallback(async () => {
|
||||
setText(initialText);
|
||||
},[initialText])
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const keyHandler = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Escape') {
|
||||
resetValue();
|
||||
} else if (event.key === 'Enter') {
|
||||
if (!isTextArea) {
|
||||
handleSubmit(text);
|
||||
}
|
||||
}
|
||||
},
|
||||
[resetValue, isTextArea, handleSubmit, text]
|
||||
);
|
||||
|
||||
return isTextArea ? (
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
variant='filled'
|
||||
value={text}
|
||||
onChange={(event) => handleChange(event.target.value)}
|
||||
onBlur={(event) => handleSubmit(event.target.value)}
|
||||
onKeyDown={(event) => keyHandler(event)}
|
||||
data-testid='input-textarea'
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
variant='filled'
|
||||
value={text}
|
||||
onChange={(event) => handleChange(event.target.value)}
|
||||
onBlur={(event) => handleSubmit(event.target.value)}
|
||||
onKeyDown={(event) => keyHandler(event)}
|
||||
data-testid='input-textfield'
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
TextInput.propTypes = {
|
||||
isTextArea: PropTypes.bool,
|
||||
size: PropTypes.string,
|
||||
field: PropTypes.string.isRequired,
|
||||
initialText: PropTypes.string,
|
||||
submitHandler: PropTypes.func,
|
||||
};
|
||||
@@ -1,197 +0,0 @@
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||
import { LoggingContext } from 'common/context/LoggingContext';
|
||||
import { forgivingStringToMillis } from 'common/utils/dateConfig';
|
||||
import { stringFromMillis } from 'common/utils/time';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayFast } from '../../../ontimeConfig';
|
||||
|
||||
import style from './TimeInput.module.scss';
|
||||
|
||||
export default function TimeInput(props) {
|
||||
const { name, submitHandler, time = 0, delay, placeholder, validationHandler, previousEnd } = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const inputRef = useRef(null);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
/**
|
||||
* @description Resets input value to given
|
||||
*/
|
||||
const resetValue = useCallback(() => {
|
||||
// Todo: check if change is necessary
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [delay, emitError, time]);
|
||||
|
||||
/**
|
||||
* @description Selects input text on focus
|
||||
*/
|
||||
const handleFocus = useCallback(() => {
|
||||
inputRef.current.select();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description Submit handler
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleSubmit = useCallback(
|
||||
(newValue) => {
|
||||
// Check if there is anything there
|
||||
if (newValue === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let newValMillis = 0;
|
||||
|
||||
// check for known aliases
|
||||
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
}
|
||||
} else if (
|
||||
newValue.startsWith('+') ||
|
||||
newValue.startsWith('p+') ||
|
||||
newValue.startsWith('p +')
|
||||
) {
|
||||
// string to pass should add to the end before
|
||||
const val = newValue.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(newValue);
|
||||
}
|
||||
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// check if time is different from before
|
||||
if (newValMillis === originalMillis) return false;
|
||||
|
||||
// validate with parent
|
||||
if (!validationHandler(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
submitHandler(name, newValMillis);
|
||||
|
||||
return true;
|
||||
},
|
||||
[delay, name, previousEnd, submitHandler, time, validationHandler]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Prepare time fields
|
||||
* @param {string} value string to be parsed
|
||||
*/
|
||||
const validateAndSubmit = useCallback(
|
||||
(newValue) => {
|
||||
const success = handleSubmit(newValue);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(newValue);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
resetValue();
|
||||
}
|
||||
},
|
||||
[delay, handleSubmit, resetValue]
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = useCallback(
|
||||
(event) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current.blur();
|
||||
validateAndSubmit(event.target.value);
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit(event.target.value);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
inputRef.current.blur();
|
||||
resetValue();
|
||||
}
|
||||
},
|
||||
[resetValue, validateAndSubmit]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
resetValue();
|
||||
}, [emitError, resetValue, time]);
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
/*
|
||||
<IconButton
|
||||
size='sm'
|
||||
icon="s"
|
||||
aria-label='automate'
|
||||
colorScheme='white'
|
||||
style={{ borderRadius: '2px', width: 'min-content' }}
|
||||
tabIndex={-1}
|
||||
variant='ghost'
|
||||
/>
|
||||
*/
|
||||
|
||||
const buttonInitial = () => {
|
||||
if (name === 'timeStart') return 'S';
|
||||
if (name === 'timeEnd') return 'E';
|
||||
if (name === 'duration') return 'D';
|
||||
}
|
||||
|
||||
const buttonTooltip = () => {
|
||||
if (name === 'timeStart') return 'Start';
|
||||
if (name === 'timeEnd') return 'End';
|
||||
if (name === 'duration') return 'Duration';
|
||||
}
|
||||
|
||||
return (
|
||||
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
|
||||
<InputLeftElement width='fit-content'>
|
||||
<Tooltip label={buttonTooltip()} openDelay={tooltipDelayFast}>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='filled'
|
||||
tabIndex={-1}
|
||||
backgroundColor='#303030'
|
||||
color='#fffffa'
|
||||
borderRadius='2px 0 0 2px'
|
||||
border={isDelayed ? "1px solid #d69e2e55" : "1px solid transparent"}
|
||||
>
|
||||
{buttonInitial()}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-testid='time-input'
|
||||
className={style.inputField}
|
||||
type='text'
|
||||
placeholder={placeholder}
|
||||
variant='filled'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={resetValue}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
TimeInput.propTypes = {
|
||||
name: PropTypes.string,
|
||||
submitHandler: PropTypes.func,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
placeholder: PropTypes.string,
|
||||
validationHandler: PropTypes.func,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -1,47 +0,0 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
@mixin input-field {
|
||||
border: 1px solid transparent;
|
||||
background-color: $input-bg;
|
||||
font-size: 1em;
|
||||
letter-spacing: 1px;
|
||||
|
||||
&:hover {
|
||||
background-color: $input-hover-bg;
|
||||
}
|
||||
}
|
||||
|
||||
.timeInput {
|
||||
width: fit-content !important;
|
||||
|
||||
.inputField {
|
||||
@include input-field;
|
||||
width: 7.5em;
|
||||
padding: 0 0 0 2.6em;
|
||||
}
|
||||
|
||||
&.delayed {
|
||||
.inputField {
|
||||
border: $input-delayed-border;
|
||||
background-color: $input-bg-delayed;
|
||||
|
||||
&:hover {
|
||||
background-color: $input-hover-bg-delayed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.delayInput {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.inputField {
|
||||
@include input-field;
|
||||
}
|
||||
|
||||
.label {
|
||||
padding-left: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Textarea, TextareaProps } from '@chakra-ui/react';
|
||||
// @ts-expect-error no types from library
|
||||
import autosize from 'autosize/dist/autosize';
|
||||
|
||||
interface AutoTextAreaProps extends TextareaProps {
|
||||
isDark?: boolean;
|
||||
}
|
||||
|
||||
export const AutoTextArea = (props: AutoTextAreaProps) => {
|
||||
const { isDark, ...rest } = props;
|
||||
const ref = useRef<HTMLTextAreaElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const node = ref.current;
|
||||
autosize(ref.current);
|
||||
return () => {
|
||||
autosize.destroy(node);
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
<Textarea
|
||||
overflow='hidden'
|
||||
w='100%'
|
||||
resize='none'
|
||||
ref={ref}
|
||||
transition='height none'
|
||||
variant={isDark ? 'ontime-filled' : 'ontime-filled-onlight'}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
};
|
||||
-2
@@ -1,5 +1,3 @@
|
||||
@use '../../../theme/main' as *;
|
||||
|
||||
input[type="color"] {
|
||||
appearance: none;
|
||||
cursor: pointer;
|
||||
+1
-1
@@ -12,7 +12,7 @@ export default function ColourInput(props: ColourInputProps) {
|
||||
return (
|
||||
<Input
|
||||
size='sm'
|
||||
variant='filled'
|
||||
variant='ontime-filled'
|
||||
className={style.colourInput}
|
||||
type='color'
|
||||
value={value}
|
||||
@@ -0,0 +1,9 @@
|
||||
.delayInput {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.inputField {
|
||||
text-align: center;
|
||||
}
|
||||
+30
-28
@@ -3,24 +3,25 @@ import { Input } from '@chakra-ui/react';
|
||||
import { clamp } from 'common/utils/math';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import style from './TimeInput.module.scss';
|
||||
import style from './DelayInput.module.scss';
|
||||
|
||||
const inputProps = {
|
||||
const inputStyleProps = {
|
||||
width: 20,
|
||||
backgroundColor: 'rgba(255,255,255,0.13)',
|
||||
color: '#fff',
|
||||
border: '1px solid #ecc94b55',
|
||||
variant: 'filled',
|
||||
borderRadius: '3px',
|
||||
placeholder: '-',
|
||||
textAlign: 'center',
|
||||
size: 'sm',
|
||||
color: '#E69056',
|
||||
variant: 'ontime-filled',
|
||||
};
|
||||
|
||||
export default function DelayInput(props) {
|
||||
const { submitHandler, value } = props;
|
||||
interface DelayInputProps {
|
||||
submitHandler: (value: number) => void;
|
||||
value?: number;
|
||||
}
|
||||
|
||||
export default function DelayInput(props: DelayInputProps) {
|
||||
const { submitHandler, value = 0 } = props;
|
||||
const [_value, setValue] = useState(value);
|
||||
const inputRef = useRef(null);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (value == null) return;
|
||||
@@ -32,7 +33,8 @@ export default function DelayInput(props) {
|
||||
* @param {string} value string to be parsed
|
||||
*/
|
||||
const validate = useCallback(
|
||||
(newValue) => {
|
||||
(newValue?: string) => {
|
||||
console.log('debug', newValue, typeof newValue);
|
||||
if (newValue === '') setValue(0);
|
||||
const delayValue = clamp(Number(newValue), -60, 60);
|
||||
|
||||
@@ -41,42 +43,42 @@ export default function DelayInput(props) {
|
||||
|
||||
submitHandler(delayValue);
|
||||
},
|
||||
[submitHandler, value]
|
||||
[submitHandler, value],
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = useCallback((event) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current.blur();
|
||||
validate(event.target.value);
|
||||
} else if (event.key === 'Escape') {
|
||||
inputRef.current.blur();
|
||||
const onKeyDownHandler = useCallback((key: string) => {
|
||||
if (key === 'Enter') {
|
||||
inputRef.current?.blur();
|
||||
validate(inputRef.current?.value);
|
||||
} else if (key === 'Escape') {
|
||||
inputRef.current?.blur();
|
||||
setValue(value);
|
||||
}
|
||||
}, [validate, value]);
|
||||
|
||||
const labelText = `${Math.abs(value) > 1 ? 'minutes' : 'minute'} ${
|
||||
value >= 0 ? 'delayed' : 'ahead'
|
||||
const labelText = `${Math.abs(value) !== 1 ? 'minutes' : 'minute'} ${
|
||||
value !== undefined && value >= 0 ? 'delayed' : 'ahead'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<div className={style.delayInput}>
|
||||
<label className={style.delayInput}>
|
||||
<Input
|
||||
{...inputStyleProps}
|
||||
ref={inputRef}
|
||||
data-testid='delay-input'
|
||||
className={style.inputField}
|
||||
{...inputProps}
|
||||
value={_value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={() => setValue(value)}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
onChange={(event) => setValue(Number(event.target.value))}
|
||||
onBlur={(event) => validate(event.target.value)}
|
||||
onKeyDown={(event) => onKeyDownHandler(event.key)}
|
||||
type='number'
|
||||
/>
|
||||
<span className={style.label}>{labelText}</span>
|
||||
</div>
|
||||
{labelText}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { Input, Textarea } from '@chakra-ui/react';
|
||||
|
||||
import { Size } from '../../../models/UtilTypes';
|
||||
|
||||
import useReactiveTextInput from './useReactiveTextInput';
|
||||
|
||||
interface TextInputProps {
|
||||
isTextArea?: boolean;
|
||||
isFullHeight?: boolean;
|
||||
size?: Size;
|
||||
field: string;
|
||||
initialText?: string;
|
||||
submitHandler: (field: string, newValue: string) => void;
|
||||
}
|
||||
|
||||
export default function TextInput(props: TextInputProps) {
|
||||
const { isTextArea, isFullHeight, size = 'sm', field, initialText = '', submitHandler } = props;
|
||||
const inputRef = useRef(null);
|
||||
|
||||
const submitCallback = useCallback((newValue: string) =>
|
||||
submitHandler(field, newValue)
|
||||
, [field]);
|
||||
|
||||
const textInputProps = useReactiveTextInput(initialText, submitCallback, { submitOnEnter: true });
|
||||
const textAreaProps = useReactiveTextInput(initialText, submitCallback);
|
||||
|
||||
return isTextArea ? (
|
||||
<Textarea
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
variant='ontime-filled'
|
||||
{...textAreaProps}
|
||||
style={{ height: isFullHeight ? '100%' : undefined }}
|
||||
data-testid='input-textarea'
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
ref={inputRef}
|
||||
size={size}
|
||||
variant='ontime-filled'
|
||||
{...textInputProps}
|
||||
data-testid='input-textfield'
|
||||
/>
|
||||
);
|
||||
}
|
||||
+6
-3
@@ -9,7 +9,8 @@ describe('TextInput component', () => {
|
||||
it('renders correctly', () => {
|
||||
const testField = 'test';
|
||||
const testText = 'Test 123';
|
||||
render(<TextInput field={testField} initialText={testText} />);
|
||||
const submitHandler = vi.fn();
|
||||
render(<TextInput field={testField} initialText={testText} submitHandler={submitHandler} />);
|
||||
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
@@ -19,7 +20,9 @@ describe('TextInput component', () => {
|
||||
it('Handles renders as textarea', () => {
|
||||
const testField = 'test';
|
||||
const testText = 'Test 123';
|
||||
render(<TextInput field={testField} initialText={testText} isTextArea />);
|
||||
const submitHandler = vi.fn();
|
||||
render(<TextInput field={testField} initialText={testText} isTextArea
|
||||
submitHandler={submitHandler} />);
|
||||
|
||||
const input = screen.getByTestId('input-textarea');
|
||||
expect(input).toBeInTheDocument();
|
||||
@@ -87,7 +90,7 @@ describe('TextInput component', () => {
|
||||
it('handles undefined value', () => {
|
||||
const testField = 'test';
|
||||
const expected = '';
|
||||
render(<TextInput field={testField} />);
|
||||
render(<TextInput field={testField} submitHandler={vi.fn()} />);
|
||||
const input = screen.getByTestId('input-textfield');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toHaveValue(expected);
|
||||
@@ -0,0 +1,88 @@
|
||||
import { ChangeEvent, useCallback, useEffect, useState } from 'react';
|
||||
|
||||
interface UseReactiveTextInputReturn {
|
||||
value: string;
|
||||
onChange: (event: ChangeEvent) => void;
|
||||
onBlur: (event: ChangeEvent) => void;
|
||||
onKeyDown: (event: KeyboardEvent) => void;
|
||||
}
|
||||
|
||||
export default function useReactiveTextInput(
|
||||
initialText: string,
|
||||
submitCallback: (newValue: string) => void,
|
||||
options?: {
|
||||
submitOnEnter?: boolean;
|
||||
},
|
||||
): UseReactiveTextInputReturn {
|
||||
const [text, setText] = useState(initialText);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof initialText === 'undefined') {
|
||||
setText('');
|
||||
} else {
|
||||
setText(initialText);
|
||||
}
|
||||
}, [initialText]);
|
||||
|
||||
/**
|
||||
* @description Handles Input value change
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleChange = useCallback(
|
||||
(newValue: string) => {
|
||||
if (newValue !== text) {
|
||||
setText(newValue);
|
||||
}
|
||||
},
|
||||
[text],
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Handles submit events
|
||||
* @param {string} valueToSubmit
|
||||
*/
|
||||
const handleSubmit = useCallback(
|
||||
(valueToSubmit: string) => {
|
||||
// No need to update if it hasn't changed
|
||||
if (valueToSubmit === initialText) {
|
||||
return;
|
||||
}
|
||||
const cleanVal = valueToSubmit.trim();
|
||||
submitCallback(cleanVal);
|
||||
|
||||
if (cleanVal !== valueToSubmit) {
|
||||
setText(cleanVal);
|
||||
}
|
||||
},
|
||||
[initialText, submitCallback],
|
||||
);
|
||||
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {string} key
|
||||
*/
|
||||
const keyHandler = useCallback(
|
||||
(key: string) => {
|
||||
switch (key) {
|
||||
case 'Escape':
|
||||
setText(initialText);
|
||||
break;
|
||||
case 'Enter':
|
||||
if (options?.submitOnEnter) {
|
||||
handleSubmit(text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
[initialText, handleSubmit, text],
|
||||
);
|
||||
|
||||
return {
|
||||
value: text,
|
||||
onChange: (event) => handleChange((event.target as HTMLInputElement).value),
|
||||
onBlur: (event) => handleSubmit((event.target as HTMLInputElement).value),
|
||||
onKeyDown: (event) => keyHandler(event.key),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
import { useCallback, useContext, useEffect, useRef, useState } from 'react';
|
||||
import { Button, Input, InputGroup, InputLeftElement, Tooltip } from '@chakra-ui/react';
|
||||
import { LoggingContext } from 'common/context/LoggingContext';
|
||||
import { forgivingStringToMillis } from 'common/utils/dateConfig';
|
||||
import { stringFromMillis } from 'common/utils/time';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { tooltipDelayFast } from '../../../../ontimeConfig';
|
||||
|
||||
import style from './TimeInput.module.scss';
|
||||
|
||||
export default function TimeInput(props) {
|
||||
const {
|
||||
name, submitHandler, time = 0, delay, placeholder, validationHandler, previousEnd,
|
||||
} = props;
|
||||
const { emitError } = useContext(LoggingContext);
|
||||
const inputRef = useRef(null);
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
/**
|
||||
* @description Resets input value to given
|
||||
*/
|
||||
const resetValue = useCallback(() => {
|
||||
// Todo: check if change is necessary
|
||||
try {
|
||||
setValue(stringFromMillis(time + delay));
|
||||
} catch (error) {
|
||||
emitError(`Unable to parse date: ${error.text}`);
|
||||
}
|
||||
}, [delay, emitError, time]);
|
||||
|
||||
/**
|
||||
* @description Selects input text on focus
|
||||
*/
|
||||
const handleFocus = useCallback(() => {
|
||||
inputRef.current.select();
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* @description Submit handler
|
||||
* @param {string} newValue
|
||||
*/
|
||||
const handleSubmit = useCallback((newValue) => {
|
||||
// Check if there is anything there
|
||||
if (newValue === '') {
|
||||
return false;
|
||||
}
|
||||
|
||||
let newValMillis = 0;
|
||||
|
||||
// check for known aliases
|
||||
if (newValue === 'p' || newValue === 'prev' || newValue === 'previous') {
|
||||
// string to pass should be the time of the end before
|
||||
if (previousEnd != null) {
|
||||
newValMillis = previousEnd;
|
||||
}
|
||||
} else if (newValue.startsWith('+') || newValue.startsWith('p+') || newValue.startsWith('p +')) {
|
||||
// string to pass should add to the end before
|
||||
const val = newValue.substring(1);
|
||||
newValMillis = previousEnd + forgivingStringToMillis(val);
|
||||
} else {
|
||||
// convert entered value to milliseconds
|
||||
newValMillis = forgivingStringToMillis(newValue);
|
||||
}
|
||||
|
||||
// Time now and time submittedVal
|
||||
const originalMillis = time + delay;
|
||||
|
||||
// check if time is different from before
|
||||
if (newValMillis === originalMillis) return false;
|
||||
|
||||
// validate with parent
|
||||
if (!validationHandler(name, newValMillis)) return false;
|
||||
|
||||
// update entry
|
||||
submitHandler(name, newValMillis);
|
||||
|
||||
return true;
|
||||
}, [delay, name, previousEnd, submitHandler, time, validationHandler]);
|
||||
|
||||
/**
|
||||
* @description Prepare time fields
|
||||
* @param {string} value string to be parsed
|
||||
*/
|
||||
const validateAndSubmit = useCallback((newValue) => {
|
||||
const success = handleSubmit(newValue);
|
||||
if (success) {
|
||||
const ms = forgivingStringToMillis(newValue);
|
||||
setValue(stringFromMillis(ms + delay));
|
||||
} else {
|
||||
resetValue();
|
||||
}
|
||||
}, [delay, handleSubmit, resetValue]);
|
||||
|
||||
/**
|
||||
* @description Handles common keys for submit and cancel
|
||||
* @param {KeyboardEvent} event
|
||||
*/
|
||||
const onKeyDownHandler = useCallback((event) => {
|
||||
if (event.key === 'Enter') {
|
||||
inputRef.current.blur();
|
||||
validateAndSubmit(event.target.value);
|
||||
} else if (event.key === 'Tab') {
|
||||
validateAndSubmit(event.target.value);
|
||||
}
|
||||
if (event.key === 'Escape') {
|
||||
inputRef.current.blur();
|
||||
resetValue();
|
||||
}
|
||||
}, [resetValue, validateAndSubmit]);
|
||||
|
||||
useEffect(() => {
|
||||
if (time == null) return;
|
||||
resetValue();
|
||||
}, [emitError, resetValue, time]);
|
||||
|
||||
const isDelayed = delay != null && delay !== 0;
|
||||
|
||||
const ButtonInitial = () => {
|
||||
if (name === 'timeStart') return 'S';
|
||||
if (name === 'timeEnd') return 'E';
|
||||
if (name === 'duration') return 'D';
|
||||
};
|
||||
|
||||
const ButtonTooltip = () => {
|
||||
if (name === 'timeStart') return 'Start';
|
||||
if (name === 'timeEnd') return 'End';
|
||||
if (name === 'duration') return 'Duration';
|
||||
};
|
||||
|
||||
return (
|
||||
<InputGroup size='sm' className={`${style.timeInput} ${isDelayed ? style.delayed : ''}`}>
|
||||
<InputLeftElement width='fit-content'>
|
||||
<Tooltip label={<ButtonTooltip />} openDelay={tooltipDelayFast} variant='ontime-ondark'>
|
||||
<Button
|
||||
size='sm'
|
||||
variant='ontime-subtle-white'
|
||||
className={`${style.inputButton} ${isDelayed ? style.delayed : ''}`}
|
||||
tabIndex={-1}
|
||||
border={isDelayed ? '1px solid #E69056' : '1px solid transparent'}
|
||||
borderRight='1px solid transparent'
|
||||
borderRadius='2px 0 0 2px'
|
||||
>
|
||||
<ButtonInitial />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</InputLeftElement>
|
||||
<Input
|
||||
ref={inputRef}
|
||||
data-testid='time-input'
|
||||
className={style.inputField}
|
||||
type='text'
|
||||
placeholder={placeholder}
|
||||
variant='ontime-filled'
|
||||
onFocus={handleFocus}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
onBlur={resetValue}
|
||||
onKeyDown={onKeyDownHandler}
|
||||
value={value}
|
||||
maxLength={8}
|
||||
/>
|
||||
</InputGroup>
|
||||
);
|
||||
}
|
||||
|
||||
TimeInput.propTypes = {
|
||||
name: PropTypes.string,
|
||||
submitHandler: PropTypes.func,
|
||||
time: PropTypes.number,
|
||||
delay: PropTypes.number,
|
||||
placeholder: PropTypes.string,
|
||||
validationHandler: PropTypes.func,
|
||||
previousEnd: PropTypes.number,
|
||||
};
|
||||
@@ -0,0 +1,19 @@
|
||||
$input-font-size: 15px;
|
||||
$input-delayed-border-color: #E69056;
|
||||
|
||||
.timeInput {
|
||||
width: fit-content !important;
|
||||
|
||||
.inputField {
|
||||
font-size: $input-font-size;
|
||||
letter-spacing: 1px;
|
||||
width: 7.5em;
|
||||
padding: 0 0 0 2.6em;
|
||||
}
|
||||
|
||||
&.delayed {
|
||||
.inputField {
|
||||
border: 1px solid $input-delayed-border-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user