Skip to content

Commit

Permalink
refactor: improve editable input field component
Browse files Browse the repository at this point in the history
  • Loading branch information
hamed-musallam committed Oct 28, 2024
1 parent 55d358f commit 7093168
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 92 deletions.
247 changes: 168 additions & 79 deletions src/component/elements/EditableColumn.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { CSSProperties, ChangeEvent, KeyboardEvent } from 'react';
import { Button } from '@blueprintjs/core';
import styled from '@emotion/styled';
import type { CSSProperties, KeyboardEvent } from 'react';
import {
forwardRef,
useCallback,
Expand All @@ -7,8 +9,41 @@ import {
useState,
} from 'react';

import type { InputProps } from './Input.js';
import Input from './Input.js';
import { Input2 } from './Input2.js';
import { NumberInput2 } from './NumberInput2.js';

interface OverflowProps {
textOverflowEllipses: boolean;
}

const Text = styled.span<OverflowProps>`
display: table-cell;
vertical-align: middle;
width: 100%;
height: 100%;
${({ textOverflowEllipses }) =>
textOverflowEllipses &&
`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
`}
`;
const Container = styled.span<OverflowProps>`
display: table;
width: 100%;
min-height: 22px;
height: 100%;
${({ textOverflowEllipses }) =>
textOverflowEllipses &&
`
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-flex;
align-items:end;
`}
`;

function extractNumber(val: string | number, type: string) {
if (type === 'number' && typeof val !== 'number') {
Expand All @@ -18,37 +53,45 @@ function extractNumber(val: string | number, type: string) {
return val;
}

interface EditableColumnProps
extends Omit<InputProps, 'style' | 'value' | 'type'> {
function handleMousedown(event) {
event.stopPropagation();
}

const style: CSSProperties = { minWidth: 60 };
const className = 'editable-column';

interface BaseEditableColumnProps {
type: 'number' | 'text';
value: number | string;
validate?: (value?: string | number) => boolean;
}

interface EditableColumnProps extends BaseEditableColumnProps {
onSave?: (element: KeyboardEvent<HTMLInputElement>) => void;
onEditStart?: (element: boolean) => void;
type?: 'number' | 'text';
editStatus?: boolean;
value: string | number;
style?: CSSProperties;
validate?: (value?: string | number) => boolean;
textOverFlowEllipses?: boolean;
textOverflowEllipses?: boolean;
clickType?: 'single' | 'double';
}

const EditableColumn = forwardRef(function EditableColumn(
export const EditableColumn = forwardRef(function EditableColumn(
props: EditableColumnProps,
ref: any,
) {
const {
onSave = () => null,
onSave,
value,
type = 'text',
type,
style,
onEditStart = () => null,
onEditStart,
editStatus = false,
validate = () => true,
textOverFlowEllipses = false,
...InputProps
validate,
textOverflowEllipses = false,
clickType = 'single',
} = props;

const [enabled, enableEdit] = useState<boolean | undefined>();
const [isValid, setValid] = useState<boolean>(true);
const [val, setVal] = useState(extractNumber(value, type));
useEffect(() => {
enableEdit(editStatus);
}, [editStatus]);
Expand All @@ -71,85 +114,131 @@ const EditableColumn = forwardRef(function EditableColumn(

function startEditHandler() {
globalThis.addEventListener('mousedown', mouseClickCallback);
onEditStart(true);
onEditStart?.(true);
enableEdit(true);
}

function saveHandler(event: KeyboardEvent<HTMLInputElement>) {
const valid = validate(val);
setValid(valid);
// when press Enter or Tab
if (valid && ['Enter', 'Tab'].includes(event.key)) {
onSave(event);
enableEdit(false);
globalThis.removeEventListener('mousedown', mouseClickCallback);
}
// close edit mode if press Enter, Tab or Escape
if (['Escape'].includes(event.key)) {
enableEdit(false);
globalThis.removeEventListener('mousedown', mouseClickCallback);
}
function onConfirm(event: KeyboardEvent<HTMLInputElement>) {
onSave?.(event);
enableEdit(false);
globalThis.removeEventListener('mousedown', mouseClickCallback);
}

function onCancel() {
enableEdit(false);
globalThis.removeEventListener('mousedown', mouseClickCallback);
}

function handleChange(e: ChangeEvent<HTMLInputElement>) {
setVal(e.target.value);
let clickHandler = {};

if (clickType === 'single' && !enabled) {
clickHandler = { onClick: startEditHandler };
}

if (clickType === 'double' && !enabled) {
clickHandler = { onDoubleClick: startEditHandler };
}

return (
<div
style={{
display: 'table',
width: '100%',
height: '100%',
...(textOverFlowEllipses
? { whiteSpace: 'nowrap', overflow: 'hidden', display: 'inline-flex' }
: {}),
...style,
}}
<Container
style={style}
textOverflowEllipses={textOverflowEllipses}
className="editable-column-input"
onDoubleClick={startEditHandler}
{...clickHandler}
>
{!enabled && (
<span
style={{
display: 'table-cell',
verticalAlign: 'middle',
width: '100%',
...(textOverFlowEllipses
? { textOverflow: 'ellipsis', overflow: 'hidden' }
: {}),
}}
>
{value} &nbsp;
</span>
<Text textOverflowEllipses={textOverflowEllipses}>
{value ?? '&nbsp;'}
</Text>
)}
{enabled && (
<div style={{ display: 'table-cell', verticalAlign: 'middle' }}>
<Input
style={{
inputWrapper: {
...(!isValid && { borderColor: 'red' }),
width: '100%',
},
input: {
padding: '5px',
width: '100%',
minWidth: '60px',
},
}}
autoSelect
className="editable-column"
value={val}
<EditFiled
value={value}
type={type}
onChange={handleChange}
onKeyDown={saveHandler}
onMouseDown={(e) => e.stopPropagation()}
{...InputProps}
onConfirm={onConfirm}
onCancel={onCancel}
validate={validate}
/>
</div>
)}
</div>
</Container>
);
});

export default EditableColumn;
interface EditFiledProps extends BaseEditableColumnProps {
onConfirm: (event: KeyboardEvent<HTMLInputElement>) => void;
onCancel: (event?: KeyboardEvent<HTMLInputElement>) => void;
}

function EditFiled(props: EditFiledProps) {
const { value: externalValue, type, onConfirm, onCancel, validate } = props;

const [isValid, setValid] = useState<boolean>(true);
const [value, setVal] = useState(extractNumber(externalValue, type));

function handleKeydown(event: KeyboardEvent<HTMLInputElement>) {
const valid = typeof validate === 'function' ? validate(value) : true;
setValid(valid);
// when press Enter or Tab
if (valid && ['Enter', 'Tab'].includes(event.key)) {
onConfirm(event);
}
// close edit mode if press Enter, Tab or Escape
if (['Escape'].includes(event.key)) {
onCancel(event);
}
}

function handleChange(value: string | number) {
setVal(value);
}

const intent = !isValid ? 'danger' : 'none';

const rightElement = (
<Button
minimal
icon="cross"
onMouseDown={handleMousedown}
onClick={() => onCancel()}
/>
);

if (type === 'number') {
return (
<NumberInput2
intent={intent}
style={style}
autoSelect
className={className}
value={value}
onValueChange={(valueAsNumber, valueString) =>
handleChange(valueString ?? Number(valueString))
}
onKeyDown={handleKeydown}
onMouseDown={handleMousedown}
small
fill
buttonPosition="none"
stepSize={1}
rightElement={rightElement}
/>
);
}

return (
<Input2
intent={intent}
style={style}
autoSelect
className={className}
value={value as string}
onChange={(value) => handleChange(value)}
onKeyDown={handleKeydown}
onMouseDown={handleMousedown}
small
rightElement={rightElement}
/>
);
}
2 changes: 1 addition & 1 deletion src/component/panels/IntegralsPanel/IntegralTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { FaRegTrashAlt } from 'react-icons/fa';
import { SIGNAL_KINDS } from '../../../data/constants/signalsKinds.js';
import { checkIntegralKind } from '../../../data/data1d/Spectrum1D/index.js';
import { useDispatch } from '../../context/DispatchContext.js';
import EditableColumn from '../../elements/EditableColumn.js';
import { EditableColumn } from '../../elements/EditableColumn.js';
import ReactTable from '../../elements/ReactTable/ReactTable.js';
import type { CustomColumn } from '../../elements/ReactTable/utility/addCustomColumn.js';
import addCustomColumn, {
Expand Down
5 changes: 3 additions & 2 deletions src/component/panels/MoleculesPanel/MoleculeHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
extractNumber,
} from '../../../data/molecules/MoleculeManager.js';
import { useDispatch } from '../../context/DispatchContext.js';
import EditableColumn from '../../elements/EditableColumn.js';
import { EditableColumn } from '../../elements/EditableColumn.js';
import Label from '../../elements/Label.js';

interface MoleculeHeaderProps {
Expand Down Expand Up @@ -61,7 +61,8 @@ export default function MoleculeHeader(props: MoleculeHeaderProps) {
style={styles.labelInput}
validate={validateLabel}
onSave={(event) => saveLabelHandler(currentMolecule.id, event)}
textOverFlowEllipses
textOverflowEllipses
type="text"
/>
</Label>
<span>
Expand Down
2 changes: 1 addition & 1 deletion src/component/panels/PeaksPanel/PeaksTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { memo, useCallback, useMemo, useState } from 'react';
import { FaEdit, FaRegTrashAlt } from 'react-icons/fa';

import { useDispatch } from '../../context/DispatchContext.js';
import EditableColumn from '../../elements/EditableColumn.js';
import { EditableColumn } from '../../elements/EditableColumn.js';
import ReactTable from '../../elements/ReactTable/ReactTable.js';
import type { ControlCustomColumn } from '../../elements/ReactTable/utility/addCustomColumn.js';
import addCustomColumn, {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useDispatch } from '../../../context/DispatchContext.js';
import EditableColumn from '../../../elements/EditableColumn.js';
import { EditableColumn } from '../../../elements/EditableColumn.js';
import type { OnHoverEvent, RowSpanTags } from '../RangesTableRow.js';
import type { RangeData } from '../hooks/useMapRanges.js';

Expand Down Expand Up @@ -29,6 +29,7 @@ export function RangeAssignmentColumn({
value={row?.assignment || ''}
onSave={saveHandler}
style={{ padding: '0.1rem 0.4rem' }}
type="text"
/>
</td>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { checkRangeKind } from '../../../../data/utilities/RangeUtilities.js';
import { useDispatch } from '../../../context/DispatchContext.js';
import EditableColumn from '../../../elements/EditableColumn.js';
import { EditableColumn } from '../../../elements/EditableColumn.js';
import { formatNumber } from '../../../utility/formatNumber.js';
import type { RangeColumnProps } from '../RangesTableRow.js';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { checkMultiplicity } from 'nmr-processing';

import { useDispatch } from '../../../context/DispatchContext.js';
import EditableColumn from '../../../elements/EditableColumn.js';
import { EditableColumn } from '../../../elements/EditableColumn.js';
import { formatNumber } from '../../../utility/formatNumber.js';
import type { RangeColumnProps } from '../RangesTableRow.js';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { useAlert } from '../../../elements/Alert.js';
import type { ContextMenuItem } from '../../../elements/ContextMenuBluePrint.js';
import { ContextMenu } from '../../../elements/ContextMenuBluePrint.js';
import { useDialog } from '../../../elements/DialogManager.js';
import EditableColumn from '../../../elements/EditableColumn.js';
import { EditableColumn } from '../../../elements/EditableColumn.js';
import { useHighlight } from '../../../highlight/index.js';
import { convertValuesString } from '../utilities/Utilities.js';
import useInView from '../utilities/useInView.js';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get.js';

import { useDispatch } from '../../../context/DispatchContext.js';
import EditableColumn from '../../../elements/EditableColumn.js';
import { EditableColumn } from '../../../elements/EditableColumn.js';
import { usePanelPreferences } from '../../../hooks/usePanelPreferences.js';
import { formatNumber } from '../../../utility/formatNumber.js';
import type { ZoneData } from '../hooks/useMapZones.js';
Expand Down
Loading

0 comments on commit 7093168

Please sign in to comment.