Skip to content

Commit

Permalink
add the ability to edit cell data
Browse files Browse the repository at this point in the history
  • Loading branch information
Amelia Wattenberger committed Jan 19, 2022
1 parent 45cbe1c commit 6d6225f
Show file tree
Hide file tree
Showing 8 changed files with 562 additions and 134 deletions.
15 changes: 12 additions & 3 deletions example/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ import debounce from 'lodash/debounce';
import exampleData from './data';

const App = () => {
const [data, setData] = React.useState([]);
const [data, setData] = React.useState<any[]>([]);
const [isLoading, setIsLoading] = React.useState(true);
const [dataUrl, setDataUrl] = React.useState('');
const [dataUrl, setDataUrl] = React.useState('https://raw.githubusercontent.com/the-pudding/data/master/boybands/boys.csv');
const [overrideDataUrl, setOverrideDataUrl] = React.useState('');
const [localOverrideDataUrl, setLocalOverrideDataUrl] = React.useState('');

Expand Down Expand Up @@ -91,7 +91,16 @@ const App = () => {
)}
</div>

<div style={{ flex: '1 1 0%' }}>{!isLoading && <Grid data={data} />}</div>
<div style={{ flex: '1 1 0%' }}>{!isLoading && (
<Grid
data={data}
isEditable
onEdit={(newData: any[]) => {
console.log(newData)
setData(newData);
}}
/>
)}</div>
</div>
);
};
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
"react-is": "^17.0.1",
"rollup-plugin-postcss": "^4.0.0",
"size-limit": "^4.10.1",
"tailwindcss": "^2.0.3",
"tailwindcss": "^3.0.15",
"tsdx": "^0.14.1",
"tslib": "^2.1.0",
"typescript": "^4.3.5"
Expand Down
138 changes: 82 additions & 56 deletions src/components/cell.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import React, { useEffect } from 'react';
import { areEqual } from 'react-window';
import tw, { TwStyle } from 'twin.macro';
import anchorme from 'anchorme';
import { cellTypeMap } from '../store';
import { DashIcon, DiffModifiedIcon, PlusIcon } from '@primer/octicons-react';
import DOMPurify from 'dompurify';
import { EditableCell } from './editable-cell';

interface CellProps {
type: string;
Expand All @@ -19,9 +20,13 @@ interface CellProps {
isFirstColumn?: boolean;
hasStatusIndicator?: boolean;
background?: string;
isEditable: boolean;
onCellChange?: (value: any) => void;
isFocused: boolean;
onFocusChange: (value: [number, number] | null) => void;
onMouseEnter?: Function;
}
export const Cell = React.memo(function(props: CellProps) {
export const Cell = React.memo(function (props: CellProps) {
const {
type,
value,
Expand All @@ -32,36 +37,45 @@ export const Cell = React.memo(function(props: CellProps) {
isFirstColumn,
isNearRightEdge,
isNearBottomEdge,
isEditable,
onCellChange,
isFocused,
onFocusChange,
background,
style = {},
onMouseEnter = () => {},
onMouseEnter = () => { },
} = props;

// @ts-ignore
const cellInfo = cellTypeMap[type];
if (!cellInfo) return null;

const { cell: CellComponent } = cellInfo;
const { cell: CellComponent } = cellInfo || {}

const displayValue = formattedValue || value;
const isLongValue = (displayValue || '').length > 23;
const stringWithLinks = displayValue
? React.useMemo(
() =>
DOMPurify.sanitize(
anchorme({
input: displayValue + '',
options: {
attributes: {
target: '_blank',
rel: 'noopener',
},
},
})
),
[value]
const stringWithLinks = React.useMemo(
() => displayValue ? (
DOMPurify.sanitize(
anchorme({
input: displayValue + '',
options: {
attributes: {
target: '_blank',
rel: 'noopener',
},
},
})
)
: '';
) : "",
[value]
)

useEffect(() => {
if (!isFocused) return
onMouseEnter()
}, [isFocused])

if (!cellInfo) return null;

const StatusIcon =
isFirstColumn &&
Expand All @@ -86,54 +100,66 @@ export const Cell = React.memo(function(props: CellProps) {
<div
className="cell group"
css={[
tw`flex flex-none items-center px-4 border-b border-r`,

typeof value === 'undefined' ||
(Number.isNaN(value) && tw`text-gray-300`),
tw`flex border-b border-r`,
status === 'new' && tw`border-green-200`,
status === 'old' && tw`border-pink-200`,
status === 'modified' && tw`border-yellow-200`,
!status && tw`border-gray-200`,
!status && tw`border-gray-200`
]}
onMouseEnter={() => onMouseEnter()}
style={{
...style,
background: background || '#fff',
}}
>
{isFirstColumn && (
<div css={[tw`w-6 flex-none`, statusColor]}>
{StatusIcon && <StatusIcon />}
</div>
)}

<CellComponent
value={value}
formattedValue={stringWithLinks}
rawValue={rawValue}
categoryColor={categoryColor}
/>

{isLongValue && (
}}>
<EditableCell
type={type}
value={rawValue}
isEditable={isEditable}
onChange={onCellChange}
isFocused={isFocused}
onFocusChange={onFocusChange}>
<div
className="cell__long-value"
css={[
tw` absolute p-4 py-2 bg-white opacity-0 group-hover:opacity-100 z-30 border border-gray-200 shadow-md pointer-events-none`,
isNearBottomEdge ? tw`bottom-0` : tw`top-0`,
isNearRightEdge ? tw`right-0` : tw`left-0`,
tw`w-full h-full flex flex-none items-center px-4`,
typeof value === 'undefined' ||
(Number.isNaN(value) && tw`text-gray-300`),
]}
style={{
width: 'max-content',
maxWidth: '27em',
}}
title={rawValue}
onMouseEnter={() => onMouseEnter()}
>
<div
tw="line-clamp-9"
dangerouslySetInnerHTML={{ __html: stringWithLinks }}
{isFirstColumn && (
<div css={[tw`w-6 flex-none`, statusColor]}>
{StatusIcon && <StatusIcon />}
</div>
)}

<CellComponent
value={value}
formattedValue={stringWithLinks}
rawValue={rawValue}
categoryColor={categoryColor}
/>

{isLongValue && (
<div
className="cell__long-value"
css={[
tw` absolute p-4 py-2 bg-white opacity-0 group-hover:opacity-100 z-30 border border-gray-200 shadow-md pointer-events-none`,
isNearBottomEdge ? tw`bottom-0` : tw`top-0`,
isNearRightEdge ? tw`right-0` : tw`left-0`,
]}
style={{
width: 'max-content',
maxWidth: '27em',
}}
title={rawValue}
>
<div
tw="line-clamp-9"
dangerouslySetInnerHTML={{ __html: stringWithLinks }}
/>
</div>
)}
</div>
)}
</EditableCell>
</div>
);
}, areEqual);
115 changes: 115 additions & 0 deletions src/components/editable-cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import React, { useEffect, useRef } from 'react';
import { areEqual } from 'react-window';
import tw from 'twin.macro';

interface EditableCellProps {
type: string;
value: any;
isEditable: boolean;
onChange?: (value: any) => void;
isFocused: boolean;
onFocusChange?: (value: [number, number] | null) => void;
children: any;
}
export const EditableCell = React.memo(function (props: EditableCellProps) {
const {
// type,
value,
isEditable,
onChange,
isFocused,
onFocusChange,
children,
} = props;

const [isEditing, setIsEditing] = React.useState(false);
const [editedValue, setEditedValue] = React.useState(value);
const cellElement = useRef<HTMLDivElement>(null);

useEffect(() => {
setEditedValue(value);
}, [value]);

const onSubmit = () => {
onFocusChange?.([1, 0]);
if (onChange) onChange(editedValue);
}

useEffect(() => {
if (!isFocused) {
setIsEditing(false);
setEditedValue(value);
return
}

const onKeyDown = (e: KeyboardEvent) => {
let diff = cellDiffs[e.key]
if (diff) {
if (e.metaKey) {
// scroll to top/bottom
diff = diff.map(d => d ? Infinity * d : d) as [number, number]
}
onFocusChange?.(diff)
e.stopPropagation()
e.preventDefault()
} else if (e.key === 'Enter') {
setIsEditing(true);
} else if (e.key === 'Escape') {
onFocusChange?.(null)
}
}
window.addEventListener('keydown', onKeyDown);
return () => {
window.removeEventListener('keydown', onKeyDown);
}
}, [isFocused])

if (!isEditable) return children

return (
<div
ref={cellElement}
css={[
tw`w-full h-full flex items-center cursor-cell border-[3px] border-transparent`,
isFocused && tw`border-indigo-500`,
]}
onClick={() => onFocusChange?.([0, 0])}
onDoubleClick={() => setIsEditing(true)}
>
{isEditing ? (
<input
type="text"
autoFocus
onFocus={e => {
e.target.select();
}}
css={[
tw`w-full h-full py-2 px-4 font-mono text-sm focus:outline-none bg-transparent`,
]}
value={editedValue}
onChange={e => setEditedValue(e.target.value)}
onKeyDown={e => {
if (e.key === 'Enter') {
onSubmit()
} else if (e.key === 'Escape') {
onFocusChange?.(null)
}
if (cellDiffs[e.key]) {
e.stopPropagation()
}
}}
onBlur={onSubmit}
/>
) : (
children
)}
</div>
);
}, areEqual);

const cellDiffs = {
"ArrowUp": [-1, 0],
"ArrowDown": [1, 0],
"ArrowLeft": [0, -1],
"ArrowRight": [0, 1],
} as Record<string, [number, number]>
Loading

0 comments on commit 6d6225f

Please sign in to comment.