Skip to content

Commit

Permalink
#25 Table editor
Browse files Browse the repository at this point in the history
  • Loading branch information
Polleps committed Nov 15, 2022
1 parent dda6905 commit 46ed308
Show file tree
Hide file tree
Showing 41 changed files with 2,092 additions and 23 deletions.
9 changes: 9 additions & 0 deletions .vscode/StyledTheme.code-snippets
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"StyledTheme": {
"prefix": "theme-interpolation",
"body": [
"${p => p.theme.${1:colors}.$2};"
],
"description": "Insert Theme interpolation in styled-component"
}
}
4 changes: 4 additions & 0 deletions data-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@dnd-kit/core": "^4.0.3",
"@dnd-kit/sortable": "^5.1.0",
"@dnd-kit/utilities": "^3.0.2",
"@radix-ui/react-scroll-area": "^1.0.1",
"@tomic/react": "workspace:*",
"polished": "^4.1.0",
"query-string": "^7.0.0",
Expand All @@ -27,6 +28,8 @@
"react-markdown": "^8.0.3",
"react-router": "^6.0.0",
"react-router-dom": "^6.0.0",
"react-virtualized-auto-sizer": "^1.0.7",
"react-window": "^1.8.7",
"remark-gfm": "^3.0.1",
"styled-components": "^5.3.3",
"yamde": "^1.7.1"
Expand All @@ -36,6 +39,7 @@
"@types/react": "^18.0.10",
"@types/react-dom": "^18.0.5",
"@types/react-router-dom": "^5.0.0",
"@types/react-window": "^1.8.5",
"@types/styled-components": "^5.1.25",
"babel-plugin-styled-components": "^2.0.7",
"csstype": "^3.1.0",
Expand Down
3 changes: 3 additions & 0 deletions data-browser/src/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@ const Content = styled.div<ContentProps>`
display: block;
flex: 1;
overflow-y: auto;
border-top: 1px solid ${p => p.theme.colors.bg2};
border-left: 1px solid ${p => p.theme.colors.bg2};
border-top-left-radius: ${p => p.theme.radius};
`;

/** Persistently shown navigation bar */
Expand Down
57 changes: 57 additions & 0 deletions data-browser/src/components/ScrollArea.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import * as RadixScrollArea from '@radix-ui/react-scroll-area';
import styled from 'styled-components';
import { transparentize } from 'polished';

const SIZE = '0.8rem';

export interface ScrollAreaProps {
className?: string;
}

export const ScrollArea = React.forwardRef<
HTMLDivElement,
React.PropsWithChildren<ScrollAreaProps>
>(({ children, className }, ref): JSX.Element => {
return (
<RadixScrollArea.Root type='scroll'>
<RadixScrollArea.Viewport className={className} ref={ref}>
{children}
</RadixScrollArea.Viewport>
<ScrollBar orientation='horizontal'>
<Thumb />
</ScrollBar>
<ScrollBar orientation='vertical'>
<Thumb />
</ScrollBar>
<RadixScrollArea.Corner />
</RadixScrollArea.Root>
);
});

ScrollArea.displayName = 'ScrollArea';

const ScrollBar = styled(RadixScrollArea.Scrollbar)`
display: flex;
/* ensures no selection */
user-select: none;
/* disable browser handling of all panning and zooming gestures on touch devices */
touch-action: none;
padding: 2px;
background-color: transparent;
transition: background-color ${p => p.theme.animation.duration} ease-out;
&[data-orientation='horizontal'] {
flex-direction: column;
height: ${() => SIZE};
}
`;

const Thumb = styled(RadixScrollArea.Thumb)`
position: relative;
bottom: 1px;
flex: 1;
background-color: ${p => transparentize(0.25, p.theme.colors.bg2)};
border-radius: ${() => SIZE};
backdrop-filter: blur(10px);
z-index: 2;
`;
5 changes: 3 additions & 2 deletions data-browser/src/components/SideBar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ const SideBarStyled = styled('nav').attrs<SideBarStyledProps>(p => ({
z-index: ${p => p.theme.zIndex.sidebar};
box-sizing: border-box;
background: ${p => p.theme.colors.bg};
border-right: solid 1px ${p => p.theme.colors.bg2};
transition: opacity 0.3s, left 0.3s;
left: ${p => (p.exposed ? '0' : `calc(var(--width) * -1 + 0.5rem)`)};
/* When the user is hovering, show half opacity */
Expand Down Expand Up @@ -132,7 +131,9 @@ const SideBarOverlay = styled.div<SideBarOverlayProps>`
`;

const SideBarDragArea = styled(DragAreaBase)`
height: 100%;
--handle-margin: 1rem;
height: calc(100% - var(--handle-margin) * 2);
margin-top: var(--handle-margin);
width: 12px;
right: -6px;
top: 0;
Expand Down
122 changes: 122 additions & 0 deletions data-browser/src/components/TableEditor/ActiveCellIndicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { transparentize } from 'polished';
import React, { useCallback, useEffect, useState } from 'react';
import styled from 'styled-components';
import { CursorMode, useTableEditorContext } from './TableEditorContext';

export interface ActiveCellIndicatorProps {
sizeStr: string;
scrollerRef: React.RefObject<HTMLDivElement>;
setOnScroll: (onScroll: () => void) => void;
}

export function ActiveCellIndicator({
sizeStr,
scrollerRef,
setOnScroll,
}: ActiveCellIndicatorProps): JSX.Element | null {
const [visible, setVisible] = useState(false);
const [scrolling, setScrolling] = useState(false);

const [{ top, left, width }, setSize] = useState({
top: '0px',
left: '0px',
width: '0px',
});

const { selectedColumn, selectedRow, activeCellRef, isDragging, cursorMode } =
useTableEditorContext();

const updatePosition = useCallback(() => {
if (!activeCellRef.current || !scrollerRef.current) {
setVisible(false);

return;
}

const rect = activeCellRef.current!.getBoundingClientRect();
const tableRect = scrollerRef.current!.getBoundingClientRect();

if (rect.top === 0 && rect.left === 0) {
return;
}

setSize({
top: `${rect.top - tableRect.top - 1}px`,
left: `${
rect.left - tableRect.left + scrollerRef.current!.scrollLeft! - 1
}px`,
width:
selectedColumn === 0
? 'calc(var(--table-content-width) + 1px)'
: `${rect.width + 1}px`,
});
}, [selectedColumn, selectedRow, sizeStr]);

useEffect(() => {
setOnScroll(() => (_, __, requested: boolean) => {
if (requested) {
return;
}

setScrolling(true);
updatePosition();
});
}, [updatePosition]);

useEffect(() => {
if (!activeCellRef.current || !scrollerRef.current) {
setVisible(false);

return;
}

setVisible(true);
setScrolling(false);

updatePosition();
}, [selectedColumn, selectedRow, sizeStr]);

if (!visible) {
return null;
}

return (
<Indicator
top={top}
left={left}
width={width}
noTransition={isDragging || scrolling}
cursorMode={cursorMode}
/>
);
}

interface IndicatorProps {
top: string;
left: string;
width: string;
noTransition: boolean;
cursorMode: CursorMode;
}

const Indicator = styled.div.attrs<IndicatorProps>(p => ({
style: {
transform: `translate(${p.left}, ${p.top})`,
width: p.width,
},
}))<IndicatorProps>`
--speed: ${p => (p.noTransition ? 0 : 80)}ms;
position: absolute;
top: 0;
left: 0;
height: calc(var(--table-row-height) + 1px);
border: 2px solid ${p => p.theme.colors.main};
pointer-events: none;
will-change: transform, width;
transition: transform var(--speed) ease-out, width var(--speed) ease-out;
z-index: 1;
background-color: ${p =>
p.cursorMode === CursorMode.Visual
? transparentize(0.85, p.theme.colors.main)
: 'none'};
`;
121 changes: 121 additions & 0 deletions data-browser/src/components/TableEditor/Cell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import React, { useCallback, useEffect, useRef } from 'react';
import styled from 'styled-components';
import {
CursorMode,
TableEvent,
useTableEditorContext,
} from './TableEditorContext';

export enum CellAlign {
Start = 'flex-start',
End = 'flex-end',
Center = 'center',
}

export interface CellProps {
rowIndex: number;
columnIndex: number;
className?: string;
align?: CellAlign;
onClearCell?: () => void;
onEnterEditModeWithCharacter?: (key: string) => void;
}

export function Cell({
rowIndex,
columnIndex,
className,
children,
align,
onClearCell,
onEnterEditModeWithCharacter,
}: React.PropsWithChildren<CellProps>): JSX.Element {
const ref = useRef<HTMLDivElement>(null);

const {
selectedRow,
selectedColumn,
setActiveCell,
activeCellRef,
setCursorMode,
registerEventListener,
} = useTableEditorContext();

const isActive = rowIndex === selectedRow && columnIndex === selectedColumn;

const handleClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (isActive) {
// @ts-ignore
if (e.target.tagName === 'INPUT') {
// If the user clicked on an input don't enter edit mode. (Necessary for normal checkbox behavior)
return;
}

return setCursorMode(CursorMode.Edit);
}

setActiveCell(rowIndex, columnIndex);
},
[setActiveCell, isActive],
);

useEffect(() => {
if (!ref.current) {
return;
}

if (isActive) {
ref.current.focus();
activeCellRef.current = ref.current;

const listeners = [
registerEventListener(
TableEvent.ClearCell,
onClearCell ?? (() => undefined),
),
registerEventListener(
TableEvent.EnterEditModeWithCharacter,
onEnterEditModeWithCharacter ?? (() => undefined),
),
];

return () => {
listeners.forEach(unregister => unregister());
};
}
}, [isActive, onClearCell, onEnterEditModeWithCharacter]);

return (
<CellWrapper
ref={ref}
className={className}
onClick={handleClick}
align={align}
>
{children}
</CellWrapper>
);
}

export const IndexCell = styled(Cell)`
justify-content: flex-end !important;
color: ${p => p.theme.colors.textLight};
`;

export interface CellWrapperProps {
align?: CellAlign;
}

export const CellWrapper = styled.div<CellWrapperProps>`
background: ${p => p.theme.colors.bg};
display: flex;
width: 100%;
justify-content: ${p => p.align ?? 'flex-start'};
align-items: center;
padding-inline: var(--table-inner-padding);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: relative;
`;
Loading

0 comments on commit 46ed308

Please sign in to comment.