2020-08-12 14:44:28 -05:00

635 lines
16 KiB
TypeScript

import React, { useState, useRef, useEffect } from 'react';
import styled from 'styled-components';
import { CheckSquare, Trash, Square, CheckSquareOutline, Clock, Cross, AccountPlus } from 'shared/icons';
import { DragDropContext, Droppable, Draggable, DropResult } from 'react-beautiful-dnd';
import {
isPositionChanged,
getSortedDraggables,
getNewDraggablePosition,
getAfterDropDraggableList,
} from 'shared/utils/draggables';
import Button from 'shared/components/Button';
import TextareaAutosize from 'react-autosize-textarea';
import Control from 'react-select/src/components/Control';
import useOnOutsideClick from 'shared/hooks/onOutsideClick';
const Wrapper = styled.div`
margin-bottom: 24px;
`;
const WindowTitle = styled.div`
padding: 8px 0;
position: relative;
margin: 0 0 4px 40px;
`;
const WindowTitleIcon = styled(CheckSquareOutline)`
top: 10px;
left: -40px;
position: absolute;
`;
const WindowChecklistTitle = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
flex-flow: row wrap;
`;
const WindowTitleText = styled.h3`
cursor: pointer;
color: rgba(${props => props.theme.colors.text.primary});
margin: 6px 0;
display: inline-block;
width: auto;
min-height: 18px;
font-size: 16px;
line-height: 20px;
min-width: 40px;
`;
const WindowOptions = styled.div`
margin: 0 2px 0 auto;
float: right;
`;
const DeleteButton = styled(Button)`
padding: 6px 12px;
`;
const ChecklistProgress = styled.div`
margin-bottom: 6px;
position: relative;
`;
const ChecklistProgressPercent = styled.span`
color: #5e6c84;
font-size: 11px;
line-height: 10px;
position: absolute;
left: 5px;
top: -1px;
text-align: center;
width: 32px;
`;
const ChecklistProgressBar = styled.div`
background: rgba(${props => props.theme.colors.bg.primary});
border-radius: 4px;
clear: both;
height: 8px;
margin: 0 0 0 40px;
overflow: hidden;
position: relative;
`;
const ChecklistProgressBarCurrent = styled.div<{ width: number }>`
width: ${props => props.width}%;
background: rgba(${props => (props.width === 100 ? props.theme.colors.success : props.theme.colors.primary)});
bottom: 0;
left: 0;
position: absolute;
top: 0;
transition: width 0.14s ease-in, background 0.14s ease-in;
`;
export const ChecklistItems = styled.div`
min-height: 8px;
`;
const ChecklistItemUncheckedIcon = styled(Square)``;
const ChecklistIcon = styled.div`
cursor: pointer;
position: absolute;
left: 0;
top: 0;
margin: 10px;
text-align: center;
&:hover {
opacity: 0.8;
}
`;
const ChecklistItemCheckedIcon = styled(CheckSquare)`
fill: rgba(${props => props.theme.colors.primary});
`;
const ChecklistItemDetails = styled.div`
word-break: break-word;
word-wrap: break-word;
overflow-wrap: break-word;
`;
const ChecklistItemRow = styled.div`
cursor: pointer;
display: flex;
flex-direction: row;
`;
const ChecklistItemTextControls = styled.div`
padding: 6px 0;
width: 100%;
display: inline-flex;
align-items: center;
`;
const ChecklistItemText = styled.span<{ complete: boolean }>`
color: ${props => (props.complete ? '#5e6c84' : `rgba(${props.theme.colors.text.primary})`)};
${props => props.complete && 'text-decoration: line-through;'}
line-height: 20px;
font-size: 16px;
min-height: 20px;
margin-bottom: 0;
align-self: center;
flex: 1;
`;
const ChecklistControls = styled.div`
display: inline-flex;
flex-direction: row;
float: right;
`;
const ControlButton = styled.div`
opacity: 0;
margin-left: 4px;
padding: 4px 6px;
border-radius: 6px;
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.8);
display: flex;
width: 32px;
height: 32px;
align-items: center;
justify-content: center;
&:hover {
background-color: rgba(${props => props.theme.colors.primary}, 1);
}
`;
const ChecklistNameEditorWrapper = styled.div`
display: block;
float: left;
padding-top: 6px;
padding-bottom: 8px;
z-index: 50;
width: 100%;
`;
export const ChecklistNameEditor = styled(TextareaAutosize)`
overflow: hidden;
overflow-wrap: break-word;
resize: none;
height: 54px;
width: 100%;
background: none;
border: none;
box-shadow: none;
max-height: 162px;
min-height: 54px;
padding: 8px 12px;
font-size: 16px;
line-height: 20px;
border: 1px solid rgba(${props => props.theme.colors.primary});
border-radius: 3px;
color: rgba(${props => props.theme.colors.text.primary});
border-color: rgba(${props => props.theme.colors.border});
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
&:focus {
border-color: rgba(${props => props.theme.colors.primary});
}
`;
const AssignUserButton = styled(AccountPlus)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const ClockButton = styled(Clock)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const TrashButton = styled(Trash)`
fill: rgba(${props => props.theme.colors.text.primary});
`;
const ChecklistItemWrapper = styled.div<{ ref: any }>`
user-select: none;
clear: both;
padding-left: 40px;
position: relative;
border-radius: 6px;
& ${ControlButton}:last-child {
margin-right: 4px;
}
&:hover {
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
}
&:hover ${ControlButton} {
opacity: 1;
}
`;
const EditControls = styled.div`
clear: both;
display: flex;
padding-bottom: 9px;
flex-direction: row;
`;
const SaveButton = styled(Button)`
margin-right: 4px;
padding: 6px 12px;
`;
const CancelButton = styled.div`
cursor: pointer;
margin: 5px;
& svg {
fill: rgba(${props => props.theme.colors.text.primary});
}
&:hover svg {
fill: rgba(${props => props.theme.colors.text.secondary});
}
`;
const Spacer = styled.div`
flex: 1;
`;
const EditableDeleteButton = styled.button`
cursor: pointer;
display: flex;
margin: 0 2px;
padding: 6px 8px;
border-radius: 3px;
&:hover {
background: rgba(${props => props.theme.colors.primary}, 0.8);
}
`;
const NewItemButton = styled(Button)`
padding: 6px 8px;
`;
const ChecklistNewItem = styled.div`
margin: 8px 0;
margin-left: 40px;
`;
type ChecklistItemProps = {
itemID: string;
checklistID: string;
complete: boolean;
name: string;
onChangeName: (itemID: string, currentName: string) => void;
wrapperProps: any;
handleProps: any;
onToggleItem: (itemID: string, complete: boolean) => void;
onDeleteItem: (checklistIDID: string, itemID: string) => void;
};
export const ChecklistItem = React.forwardRef(
(
{
itemID,
checklistID,
complete,
name,
wrapperProps,
handleProps,
onChangeName,
onToggleItem,
onDeleteItem,
}: ChecklistItemProps,
$item,
) => {
const $editor = useRef<HTMLTextAreaElement>(null);
const [editting, setEditting] = useState(false);
const [currentName, setCurrentName] = useState(name);
useEffect(() => {
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}, [editting]);
// useOnOutsideClick($item, true, () => setEditting(false), null);
return (
<ChecklistItemWrapper ref={$item} {...wrapperProps} {...handleProps}>
<ChecklistIcon
onClick={e => {
e.stopPropagation();
onToggleItem(itemID, !complete);
}}
>
{complete ? (
<ChecklistItemCheckedIcon width={20} height={20} />
) : (
<ChecklistItemUncheckedIcon width={20} height={20} />
)}
</ChecklistIcon>
{editting ? (
<>
<ChecklistNameEditorWrapper>
<ChecklistNameEditor
ref={$editor}
onKeyDown={e => {
if (e.key === 'Enter') {
onChangeName(itemID, currentName);
setEditting(false);
}
}}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
value={currentName}
/>
</ChecklistNameEditorWrapper>
<EditControls>
<SaveButton
onClick={() => {
onChangeName(itemID, currentName);
setEditting(false);
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
setEditting(false);
}}
>
<Cross width={20} height={20} />
</CancelButton>
<Spacer />
<EditableDeleteButton
onClick={e => {
e.stopPropagation();
setEditting(false);
onDeleteItem(checklistID, itemID);
}}
>
<Trash width={16} height={16} />
</EditableDeleteButton>
</EditControls>
</>
) : (
<ChecklistItemDetails
onClick={() => {
setEditting(true);
}}
>
<ChecklistItemRow>
<ChecklistItemTextControls>
<ChecklistItemText complete={complete}>{name}</ChecklistItemText>
<ChecklistControls>
<ControlButton>
<AssignUserButton width={14} height={14} />
</ControlButton>
<ControlButton>
<ClockButton width={14} height={14} />
</ControlButton>
<ControlButton
onClick={e => {
e.stopPropagation();
onDeleteItem(checklistID, itemID);
}}
>
<TrashButton width={14} height={14} />
</ControlButton>
</ChecklistControls>
</ChecklistItemTextControls>
</ChecklistItemRow>
</ChecklistItemDetails>
)}
</ChecklistItemWrapper>
);
},
);
type AddNewItemProps = {
onAddItem: (name: string) => void;
};
const AddNewItem: React.FC<AddNewItemProps> = ({ onAddItem }) => {
const $editor = useRef<HTMLTextAreaElement>(null);
const $wrapper = useRef<HTMLDivElement>(null);
const [currentName, setCurrentName] = useState('');
const [editting, setEditting] = useState(false);
useEffect(() => {
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}, [editting]);
useOnOutsideClick($wrapper, true, () => setEditting(false), null);
return (
<ChecklistNewItem ref={$wrapper}>
{editting ? (
<>
<ChecklistNameEditorWrapper>
<ChecklistNameEditor
ref={$editor}
onKeyDown={e => {
if (e.key === 'Enter') {
e.preventDefault();
onAddItem(currentName);
setCurrentName('');
}
}}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
value={currentName}
/>
</ChecklistNameEditorWrapper>
<EditControls>
<SaveButton
onClick={() => {
onAddItem(currentName);
setCurrentName('');
if (editting && $editor && $editor.current) {
$editor.current.focus();
$editor.current.select();
}
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
setEditting(false);
}}
>
<Cross width={20} height={20} />
</CancelButton>
</EditControls>
</>
) : (
<NewItemButton onClick={() => setEditting(true)}>Add an item</NewItemButton>
)}
</ChecklistNewItem>
);
};
type ChecklistTitleEditorProps = {
name: string;
onChangeName: (item: string) => void;
onCancel: () => void;
};
const ChecklistTitleEditor = React.forwardRef(
({ name, onChangeName, onCancel }: ChecklistTitleEditorProps, $name: any) => {
const [currentName, setCurrentName] = useState(name);
return (
<>
<ChecklistNameEditor
ref={$name}
value={currentName}
onChange={e => {
setCurrentName(e.currentTarget.value);
}}
onKeyDown={e => {
if (e.key === 'Enter') {
onChangeName(currentName);
}
}}
/>
<EditControls>
<SaveButton
onClick={() => {
onChangeName(currentName);
}}
variant="relief"
>
Save
</SaveButton>
<CancelButton
onClick={e => {
e.stopPropagation();
onCancel();
}}
>
<Cross width={20} height={20} />
</CancelButton>
</EditControls>
</>
);
},
);
type ChecklistProps = {
checklistID: string;
onDeleteChecklist: ($target: React.RefObject<HTMLElement>, checklistID: string) => void;
name: string;
children: React.ReactNode;
onChangeName: (item: string) => void;
onToggleItem: (taskID: string, complete: boolean) => void;
onChangeItemName: (itemID: string, currentName: string) => void;
wrapperProps: any;
handleProps: any;
onDeleteItem: (checklistID: string, itemID: string) => void;
onAddItem: (itemName: string) => void;
items: Array<TaskChecklistItem>;
};
const Checklist = React.forwardRef(
(
{
checklistID,
children,
onDeleteChecklist,
name,
items,
wrapperProps,
handleProps,
onToggleItem,
onAddItem,
onChangeItemName,
onChangeName,
onDeleteItem,
}: ChecklistProps,
$container,
) => {
const $name = useRef<HTMLTextAreaElement>(null);
const complete = items.reduce((prev, item) => prev + (item.complete ? 1 : 0), 0);
const percent = items.length === 0 ? 0 : Math.floor((complete / items.length) * 100);
const [editting, setEditting] = useState(false);
// useOnOutsideClick($name, true, () => setEditting(false), null);
useEffect(() => {
if (editting && $name && $name.current) {
$name.current.focus();
$name.current.select();
}
}, [editting]);
return (
<Wrapper ref={$container} {...wrapperProps}>
<WindowTitle>
<WindowTitleIcon width={24} height={24} />
{editting ? (
<ChecklistTitleEditor
ref={$name}
name={name}
onChangeName={currentName => {
onChangeName(currentName);
setEditting(false);
}}
onCancel={() => {
setEditting(false);
}}
/>
) : (
<WindowChecklistTitle {...handleProps}>
<WindowTitleText onClick={() => setEditting(true)}>{name}</WindowTitleText>
<WindowOptions>
<DeleteButton
onClick={$target => {
onDeleteChecklist($target, checklistID);
}}
color="danger"
variant="outline"
>
Delete
</DeleteButton>
</WindowOptions>
</WindowChecklistTitle>
)}
</WindowTitle>
<ChecklistProgress>
<ChecklistProgressPercent>{`${percent}%`}</ChecklistProgressPercent>
<ChecklistProgressBar>
<ChecklistProgressBarCurrent width={percent} />
</ChecklistProgressBar>
</ChecklistProgress>
{children}
<AddNewItem onAddItem={onAddItem} />
</Wrapper>
);
},
);
/*
<ChecklistItems>
{items
.slice()
.sort((a, b) => a.position - b.position)
.map((item, idx) => (
<ChecklistItem
index={idx}
key={item.id}
itemID={item.id}
name={item.name}
complete={item.complete}
onDeleteItem={onDeleteItem}
onChangeName={onChangeItemName}
onToggleItem={onToggleItem}
/>
))}
</ChecklistItems>
*/
export default Checklist;