🐉 Drag and Drop scaffold
This commit is contained in:
@@ -3,6 +3,7 @@ module.exports = {
|
||||
"plugin:react/recommended", // Uses the recommended rules from @eslint-plugin-react
|
||||
"plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin
|
||||
"plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors.
|
||||
"plugin:css-modules/recommended",
|
||||
],
|
||||
parser: "@typescript-eslint/parser",
|
||||
parserOptions: {
|
||||
@@ -12,7 +13,7 @@ module.exports = {
|
||||
jsx: true, // Allows for the parsing of JSX
|
||||
},
|
||||
},
|
||||
plugins: ["@typescript-eslint"],
|
||||
plugins: ["@typescript-eslint", "css-modules"],
|
||||
settings: {
|
||||
"import/resolver": {
|
||||
node: {
|
||||
|
||||
24
declarations.d.ts
vendored
Normal file
24
declarations.d.ts
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
declare module "*.module.css" {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module "*.module.scss" {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module "*.module.sass" {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module "*.module.less" {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
|
||||
declare module "*.module.styl" {
|
||||
const classes: { [key: string]: string };
|
||||
export default classes;
|
||||
}
|
||||
@@ -18,6 +18,9 @@
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.17",
|
||||
"@bluelovers/fast-glob": "https://github.com/rishighan/fast-glob-v2-api.git",
|
||||
"@dnd-kit/core": "^4.0.0",
|
||||
"@dnd-kit/modifiers": "^4.0.0",
|
||||
"@dnd-kit/sortable": "^5.0.0",
|
||||
"@types/event-stream": "^3.3.34",
|
||||
"@types/mime-types": "^2.1.0",
|
||||
"@types/react": "^17.0.3",
|
||||
@@ -112,10 +115,12 @@
|
||||
"concurrently": "^4.0.0",
|
||||
"connected-react-router": "^6.9.1",
|
||||
"css-loader": "^5.1.2",
|
||||
"css-modules-typescript-loader": "^4.0.1",
|
||||
"eslint": "^7.22.0",
|
||||
"eslint-config-airbnb": "^18.2.1",
|
||||
"eslint-config-airbnb-base": "^14.2.1",
|
||||
"eslint-config-prettier": "^8.1.0",
|
||||
"eslint-plugin-css-modules": "^2.11.0",
|
||||
"eslint-plugin-import": "^2.22.1",
|
||||
"eslint-plugin-jsx-a11y": "^6.0.3",
|
||||
"eslint-plugin-prettier": "^3.3.1",
|
||||
|
||||
@@ -294,24 +294,36 @@ $border-color: red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-status {
|
||||
position: absolute;
|
||||
background-color: #999;
|
||||
text-shadow: none;
|
||||
border-radius: 5px;
|
||||
top: 10px;
|
||||
right: 30px;
|
||||
padding: 5px;
|
||||
font-size: 10px;
|
||||
color: #fff;
|
||||
}
|
||||
// next and prev controls
|
||||
.control-next {
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
&:hover {
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
&:hover {
|
||||
border-top-right-radius: 10px;
|
||||
border-bottom-right-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.control-prev {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
&:hover {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
&:hover {
|
||||
border-top-left-radius: 10px;
|
||||
border-bottom-left-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// airdcpp downloads tab
|
||||
.tabs {
|
||||
|
||||
@@ -7,21 +7,16 @@ import ComicVineSearchForm from "./ComicVineSearchForm";
|
||||
import AcquisitionPanel from "./AcquisitionPanel";
|
||||
import DownloadsPanel from "./DownloadsPanel";
|
||||
import SlidingPane from "react-sliding-pane";
|
||||
import "react-responsive-carousel/lib/styles/carousel.min.css";
|
||||
import { Carousel } from "react-responsive-carousel";
|
||||
|
||||
import Select, { components } from "react-select";
|
||||
import { RemovableItems } from "./SortableGrid/SortableGrid";
|
||||
|
||||
import "react-sliding-pane/dist/react-sliding-pane.css";
|
||||
import "react-loader-spinner/dist/loader/css/react-spinner-loader.css";
|
||||
import Loader from "react-loader-spinner";
|
||||
import { isEmpty, isUndefined, isNil, map } from "lodash";
|
||||
import { isEmpty, isUndefined, isNil } from "lodash";
|
||||
import { RootState } from "threetwo-ui-typings";
|
||||
import { fetchComicVineMatches } from "../actions/fileops.actions";
|
||||
import {
|
||||
extractComicArchive,
|
||||
getComicBookDetailById,
|
||||
} from "../actions/comicinfo.actions";
|
||||
import { getComicBookDetailById } from "../actions/comicinfo.actions";
|
||||
import { detectTradePaperbacks } from "../shared/utils/tradepaperback.utils";
|
||||
import dayjs from "dayjs";
|
||||
const prettyBytes = require("pretty-bytes");
|
||||
@@ -136,43 +131,7 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
|
||||
},
|
||||
},
|
||||
editComicArchive: {
|
||||
content: () => (
|
||||
<>
|
||||
<div className="content">
|
||||
<span className="tags has-addons">
|
||||
<span className="tag">Pages</span>
|
||||
<span className="tag is-warning">
|
||||
{extractedComicBookArchive.length}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<Carousel
|
||||
width={300}
|
||||
dynamicHeight
|
||||
showStatus={false}
|
||||
showIndicators={false}
|
||||
showThumbs={false}
|
||||
useKeyboardArrows
|
||||
>
|
||||
{map(extractedComicBookArchive, (page, idx) => {
|
||||
return (
|
||||
<div key={idx}>
|
||||
<img
|
||||
src={
|
||||
"http://localhost:3000/" +
|
||||
page.containedIn +
|
||||
"/" +
|
||||
page.name +
|
||||
page.extension
|
||||
}
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</Carousel>
|
||||
</>
|
||||
),
|
||||
content: () => <></>,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -182,25 +141,6 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
|
||||
setVisible(true);
|
||||
}, [dispatch, comicBookDetailData]);
|
||||
|
||||
const openDrawerForEditingComicArchive = useCallback(() => {
|
||||
dispatch(
|
||||
extractComicArchive(
|
||||
comicBookDetailData.rawFileDetails.containedIn +
|
||||
"/" +
|
||||
comicBookDetailData.rawFileDetails.name +
|
||||
comicBookDetailData.rawFileDetails.extension,
|
||||
{
|
||||
extractTarget: "book",
|
||||
targetExtractionFolder:
|
||||
"./userdata/expanded/" + comicBookDetailData.rawFileDetails.name,
|
||||
extractionMode: "all",
|
||||
},
|
||||
),
|
||||
);
|
||||
setSlidingPanelContentId("editComicArchive");
|
||||
setVisible(true);
|
||||
}, [dispatch, comicBookDetailData, extractedComicBookArchive]);
|
||||
|
||||
const [active, setActive] = useState(1);
|
||||
const createDescriptionMarkup = (html) => {
|
||||
return { __html: html };
|
||||
@@ -274,9 +214,13 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
icon: <i className="fas fa-puzzle-piece"></i>,
|
||||
name: "Other Metadata",
|
||||
content: <div key={2}>bastard</div>,
|
||||
icon: <i className="fas fa-file-archive"></i>,
|
||||
name: "Archive Operations",
|
||||
content: (
|
||||
<div key={2}>
|
||||
<RemovableItems />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -449,7 +393,7 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
|
||||
};
|
||||
|
||||
// Determine which cover image to use:
|
||||
// 1. from the locally imported, non-CV-scraped version, or
|
||||
// 1. from the locally imported or
|
||||
// 2. from the CV-scraped version
|
||||
let imagePath = "";
|
||||
let comicBookTitle = "";
|
||||
@@ -506,9 +450,6 @@ export const ComicDetail = ({}: ComicDetailProps): ReactElement => {
|
||||
case "match-on-comic-vine":
|
||||
openDrawerWithCVMatches();
|
||||
break;
|
||||
case "edit-comic-archive":
|
||||
openDrawerForEditingComicArchive();
|
||||
break;
|
||||
default:
|
||||
console.log("No valid action selected.");
|
||||
break;
|
||||
|
||||
329
src/client/components/SortableGrid/Sortable.tsx
Normal file
329
src/client/components/SortableGrid/Sortable.tsx
Normal file
@@ -0,0 +1,329 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
|
||||
import {
|
||||
Announcements,
|
||||
closestCenter,
|
||||
CollisionDetection,
|
||||
DragOverlay,
|
||||
DndContext,
|
||||
DropAnimation,
|
||||
defaultDropAnimation,
|
||||
KeyboardSensor,
|
||||
Modifiers,
|
||||
MouseSensor,
|
||||
MeasuringConfiguration,
|
||||
PointerActivationConstraint,
|
||||
ScreenReaderInstructions,
|
||||
TouchSensor,
|
||||
UniqueIdentifier,
|
||||
useSensor,
|
||||
useSensors,
|
||||
} from "@dnd-kit/core";
|
||||
import {
|
||||
arrayMove,
|
||||
useSortable,
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
SortingStrategy,
|
||||
rectSortingStrategy,
|
||||
AnimateLayoutChanges,
|
||||
} from "@dnd-kit/sortable";
|
||||
|
||||
import { createRange } from "./utilities";
|
||||
import { Item, List, Wrapper } from "./components";
|
||||
|
||||
export interface Props {
|
||||
activationConstraint?: PointerActivationConstraint;
|
||||
animateLayoutChanges?: AnimateLayoutChanges;
|
||||
adjustScale?: boolean;
|
||||
collisionDetection?: CollisionDetection;
|
||||
Container?: any; // To-do: Fix me
|
||||
dropAnimation?: DropAnimation | null;
|
||||
itemCount?: number;
|
||||
items?: string[];
|
||||
handle?: boolean;
|
||||
measuring?: MeasuringConfiguration;
|
||||
modifiers?: Modifiers;
|
||||
renderItem?: any;
|
||||
removable?: boolean;
|
||||
strategy?: SortingStrategy;
|
||||
useDragOverlay?: boolean;
|
||||
getItemStyles?(args: {
|
||||
id: UniqueIdentifier;
|
||||
index: number;
|
||||
isSorting: boolean;
|
||||
isDragOverlay: boolean;
|
||||
overIndex: number;
|
||||
isDragging: boolean;
|
||||
}): React.CSSProperties;
|
||||
wrapperStyle?(args: {
|
||||
index: number;
|
||||
isDragging: boolean;
|
||||
id: string;
|
||||
}): React.CSSProperties;
|
||||
isDisabled?(id: UniqueIdentifier): boolean;
|
||||
}
|
||||
|
||||
const defaultDropAnimationConfig: DropAnimation = {
|
||||
...defaultDropAnimation,
|
||||
dragSourceOpacity: 0.5,
|
||||
};
|
||||
|
||||
const screenReaderInstructions: ScreenReaderInstructions = {
|
||||
draggable: `
|
||||
To pick up a sortable item, press the space bar.
|
||||
While sorting, use the arrow keys to move the item.
|
||||
Press space again to drop the item in its new position, or press escape to cancel.
|
||||
`,
|
||||
};
|
||||
|
||||
export function Sortable({
|
||||
activationConstraint,
|
||||
animateLayoutChanges,
|
||||
adjustScale = false,
|
||||
Container = List,
|
||||
collisionDetection = closestCenter,
|
||||
dropAnimation = defaultDropAnimationConfig,
|
||||
getItemStyles = () => ({}),
|
||||
handle = false,
|
||||
itemCount = 16,
|
||||
items: initialItems,
|
||||
isDisabled = () => false,
|
||||
measuring,
|
||||
modifiers,
|
||||
removable,
|
||||
renderItem,
|
||||
strategy = rectSortingStrategy,
|
||||
useDragOverlay = true,
|
||||
wrapperStyle = () => ({}),
|
||||
}: Props) {
|
||||
const [items, setItems] = useState<string[]>(
|
||||
() =>
|
||||
initialItems ??
|
||||
createRange<string>(itemCount, (index) => (index + 1).toString()),
|
||||
);
|
||||
const [activeId, setActiveId] = useState<string | null>(null);
|
||||
const sensors = useSensors(
|
||||
useSensor(MouseSensor, {
|
||||
activationConstraint,
|
||||
}),
|
||||
useSensor(TouchSensor, {
|
||||
activationConstraint,
|
||||
}),
|
||||
useSensor(KeyboardSensor, {
|
||||
coordinateGetter: sortableKeyboardCoordinates,
|
||||
}),
|
||||
);
|
||||
const isFirstAnnouncement = useRef(true);
|
||||
const getIndex = items.indexOf.bind(items);
|
||||
const getPosition = (id: string) => getIndex(id) + 1;
|
||||
const activeIndex = activeId ? getIndex(activeId) : -1;
|
||||
const handleRemove = removable
|
||||
? (id: string) => setItems((items) => items.filter((item) => item !== id))
|
||||
: undefined;
|
||||
const announcements: Announcements = {
|
||||
onDragStart(id) {
|
||||
return `Picked up sortable item ${id}. Sortable item ${id} is in position ${getPosition(
|
||||
id,
|
||||
)} of ${items.length}`;
|
||||
},
|
||||
onDragOver(id, overId) {
|
||||
// In this specific use-case, the picked up item's `id` is always the same as the first `over` id.
|
||||
// The first `onDragOver` event therefore doesn't need to be announced, because it is called
|
||||
// immediately after the `onDragStart` announcement and is redundant.
|
||||
if (isFirstAnnouncement.current === true) {
|
||||
isFirstAnnouncement.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (overId) {
|
||||
return `Sortable item ${id} was moved into position ${getPosition(
|
||||
overId,
|
||||
)} of ${items.length}`;
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
onDragEnd(id, overId) {
|
||||
if (overId) {
|
||||
return `Sortable item ${id} was dropped at position ${getPosition(
|
||||
overId,
|
||||
)} of ${items.length}`;
|
||||
}
|
||||
|
||||
return;
|
||||
},
|
||||
onDragCancel(id) {
|
||||
return `Sorting was cancelled. Sortable item ${id} was dropped and returned to position ${getPosition(
|
||||
id,
|
||||
)} of ${items.length}.`;
|
||||
},
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeId) {
|
||||
isFirstAnnouncement.current = true;
|
||||
}
|
||||
}, [activeId]);
|
||||
|
||||
return (
|
||||
<DndContext
|
||||
announcements={announcements}
|
||||
screenReaderInstructions={screenReaderInstructions}
|
||||
sensors={sensors}
|
||||
collisionDetection={collisionDetection}
|
||||
onDragStart={({ active }) => {
|
||||
if (!active) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveId(active.id);
|
||||
}}
|
||||
onDragEnd={({ over }) => {
|
||||
setActiveId(null);
|
||||
|
||||
if (over) {
|
||||
const overIndex = getIndex(over.id);
|
||||
if (activeIndex !== overIndex) {
|
||||
setItems((items) => arrayMove(items, activeIndex, overIndex));
|
||||
}
|
||||
}
|
||||
}}
|
||||
onDragCancel={() => setActiveId(null)}
|
||||
measuring={measuring}
|
||||
modifiers={modifiers}
|
||||
>
|
||||
<Wrapper center>
|
||||
<SortableContext items={items} strategy={strategy}>
|
||||
<Container>
|
||||
{items.map((value, index) => (
|
||||
<SortableItem
|
||||
key={value}
|
||||
id={value}
|
||||
handle={handle}
|
||||
index={index}
|
||||
style={getItemStyles}
|
||||
wrapperStyle={wrapperStyle}
|
||||
disabled={isDisabled(value)}
|
||||
renderItem={renderItem}
|
||||
onRemove={handleRemove}
|
||||
animateLayoutChanges={animateLayoutChanges}
|
||||
useDragOverlay={useDragOverlay}
|
||||
/>
|
||||
))}
|
||||
</Container>
|
||||
</SortableContext>
|
||||
</Wrapper>
|
||||
{useDragOverlay
|
||||
? createPortal(
|
||||
<DragOverlay
|
||||
adjustScale={adjustScale}
|
||||
dropAnimation={dropAnimation}
|
||||
>
|
||||
{activeId ? (
|
||||
<Item
|
||||
value={items[activeIndex]}
|
||||
handle={handle}
|
||||
renderItem={renderItem}
|
||||
wrapperStyle={wrapperStyle({
|
||||
index: activeIndex,
|
||||
isDragging: true,
|
||||
id: items[activeIndex],
|
||||
})}
|
||||
style={getItemStyles({
|
||||
id: items[activeIndex],
|
||||
index: activeIndex,
|
||||
isSorting: activeId !== null,
|
||||
isDragging: true,
|
||||
overIndex: -1,
|
||||
isDragOverlay: true,
|
||||
})}
|
||||
dragOverlay
|
||||
/>
|
||||
) : null}
|
||||
</DragOverlay>,
|
||||
document.body,
|
||||
)
|
||||
: null}
|
||||
</DndContext>
|
||||
);
|
||||
}
|
||||
|
||||
interface SortableItemProps {
|
||||
animateLayoutChanges?: AnimateLayoutChanges;
|
||||
disabled?: boolean;
|
||||
id: string;
|
||||
index: number;
|
||||
handle: boolean;
|
||||
useDragOverlay?: boolean;
|
||||
onRemove?(id: string): void;
|
||||
style(values: any): React.CSSProperties;
|
||||
renderItem?(args: any): React.ReactElement;
|
||||
wrapperStyle({
|
||||
index,
|
||||
isDragging,
|
||||
id,
|
||||
}: {
|
||||
index: number;
|
||||
isDragging: boolean;
|
||||
id: string;
|
||||
}): React.CSSProperties;
|
||||
}
|
||||
|
||||
export function SortableItem({
|
||||
disabled,
|
||||
animateLayoutChanges,
|
||||
id,
|
||||
index,
|
||||
handle,
|
||||
onRemove,
|
||||
style,
|
||||
renderItem,
|
||||
useDragOverlay,
|
||||
wrapperStyle,
|
||||
}: SortableItemProps) {
|
||||
const {
|
||||
attributes,
|
||||
isDragging,
|
||||
isSorting,
|
||||
listeners,
|
||||
overIndex,
|
||||
setNodeRef,
|
||||
transform,
|
||||
transition,
|
||||
} = useSortable({
|
||||
animateLayoutChanges,
|
||||
id,
|
||||
disabled,
|
||||
});
|
||||
|
||||
return (
|
||||
<Item
|
||||
ref={setNodeRef}
|
||||
value={id}
|
||||
disabled={disabled}
|
||||
dragging={isDragging}
|
||||
sorting={isSorting}
|
||||
handle={handle}
|
||||
renderItem={renderItem}
|
||||
index={index}
|
||||
style={style({
|
||||
index,
|
||||
id,
|
||||
isDragging,
|
||||
isSorting,
|
||||
overIndex,
|
||||
})}
|
||||
onRemove={onRemove ? () => onRemove(id) : undefined}
|
||||
transform={transform}
|
||||
transition={!useDragOverlay && isDragging ? "none" : transition}
|
||||
wrapperStyle={wrapperStyle({ index, isDragging, id })}
|
||||
listeners={listeners}
|
||||
data-index={index}
|
||||
data-id={id}
|
||||
dragOverlay={!useDragOverlay && isDragging}
|
||||
{...attributes}
|
||||
/>
|
||||
);
|
||||
}
|
||||
141
src/client/components/SortableGrid/SortableGrid.tsx
Normal file
141
src/client/components/SortableGrid/SortableGrid.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React from "react";
|
||||
import { MeasuringStrategy } from "@dnd-kit/core";
|
||||
import {
|
||||
AnimateLayoutChanges,
|
||||
defaultAnimateLayoutChanges,
|
||||
rectSortingStrategy,
|
||||
} from "@dnd-kit/sortable";
|
||||
|
||||
import { Sortable, Props as SortableProps } from "./Sortable";
|
||||
import { GridContainer } from "./components";
|
||||
|
||||
export default {
|
||||
title: "Presets/Sortable/Grid",
|
||||
};
|
||||
|
||||
const props: Partial<SortableProps> = {
|
||||
adjustScale: true,
|
||||
Container: (props: any) => <GridContainer {...props} columns={5} />,
|
||||
strategy: rectSortingStrategy,
|
||||
wrapperStyle: () => ({
|
||||
width: 140,
|
||||
height: 140,
|
||||
}),
|
||||
};
|
||||
|
||||
export const BasicSetup = () => <Sortable {...props} />;
|
||||
|
||||
export const WithoutDragOverlay = () => (
|
||||
<Sortable {...props} useDragOverlay={false} />
|
||||
);
|
||||
|
||||
export const LargeFirstTile = () => (
|
||||
<Sortable
|
||||
{...props}
|
||||
getItemStyles={({ index }) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
fontSize: "2rem",
|
||||
padding: "36px 40px",
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}}
|
||||
wrapperStyle={({ index }) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
height: 288,
|
||||
gridRowStart: "span 2",
|
||||
gridColumnStart: "span 2",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: 140,
|
||||
height: 140,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const VariableSizes = () => (
|
||||
<Sortable
|
||||
{...props}
|
||||
itemCount={14}
|
||||
getItemStyles={({ index }) => {
|
||||
if (index === 0 || index === 9) {
|
||||
return {
|
||||
fontSize: "2rem",
|
||||
padding: "36px 40px",
|
||||
};
|
||||
}
|
||||
|
||||
return {};
|
||||
}}
|
||||
wrapperStyle={({ index }) => {
|
||||
if (index === 0 || index === 9) {
|
||||
return {
|
||||
height: 288,
|
||||
gridRowStart: "span 2",
|
||||
gridColumnStart: "span 2",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
width: 140,
|
||||
height: 140,
|
||||
};
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const DragHandle = () => <Sortable {...props} handle />;
|
||||
|
||||
export const ScrollContainer = () => (
|
||||
<div
|
||||
style={{
|
||||
height: "50vh",
|
||||
margin: "0 auto",
|
||||
overflow: "auto",
|
||||
}}
|
||||
>
|
||||
<Sortable {...props} />
|
||||
</div>
|
||||
);
|
||||
|
||||
export const PressDelay = () => (
|
||||
<Sortable
|
||||
{...props}
|
||||
activationConstraint={{
|
||||
delay: 250,
|
||||
tolerance: 5,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const MinimumDistance = () => (
|
||||
<Sortable
|
||||
{...props}
|
||||
activationConstraint={{
|
||||
distance: 15,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export const RemovableItems = () => {
|
||||
const animateLayoutChanges: AnimateLayoutChanges = (args) =>
|
||||
args.isSorting || args.wasDragging
|
||||
? defaultAnimateLayoutChanges(args)
|
||||
: true;
|
||||
|
||||
return (
|
||||
<Sortable
|
||||
{...props}
|
||||
animateLayoutChanges={animateLayoutChanges}
|
||||
measuring={{ droppable: { strategy: MeasuringStrategy.Always } }}
|
||||
removable
|
||||
handle={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
.Button {
|
||||
padding: 14px 20px;
|
||||
border: none;
|
||||
background-color: #242836;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #f6f8ff;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transform: scale(1);
|
||||
transition: transform 0.2s, background 0.4s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: #2f3545;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 4px #4c9ffe;
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
}
|
||||
7
src/client/components/SortableGrid/components/Button/Button.module.css.d.ts
vendored
Normal file
7
src/client/components/SortableGrid/components/Button/Button.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'Button': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,16 @@
|
||||
import React, {HTMLAttributes} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './Button.module.css';
|
||||
|
||||
export interface Props extends HTMLAttributes<HTMLButtonElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function Button({children, ...props}: Props) {
|
||||
return (
|
||||
<button className={classNames(styles.Button)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {Button} from './Button';
|
||||
@@ -0,0 +1,42 @@
|
||||
.ConfirmModal {
|
||||
--width: 250px;
|
||||
--height: 120px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: var(--width);
|
||||
height: var(--height);
|
||||
position: fixed;
|
||||
top: calc(100vh / 2 - var(--height) / 2);
|
||||
left: calc(100vw / 2 - var(--width) / 2);
|
||||
border-radius: 10px;
|
||||
box-shadow: -1px 0 15px 0 rgba(34, 33, 81, 0.01),
|
||||
0px 15px 15px 0 rgba(34, 33, 81, 0.25);
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
background-color: white;
|
||||
text-align: center;
|
||||
z-index: 1;
|
||||
|
||||
h1 {
|
||||
flex: 1;
|
||||
font-size: 16px;
|
||||
font-weight: normal;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 50px;
|
||||
height: 23px;
|
||||
background: #242836;
|
||||
color: #f6f8ff;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
margin: 0 5px;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: #2f3545;
|
||||
}
|
||||
}
|
||||
}
|
||||
7
src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.module.css.d.ts
vendored
Normal file
7
src/client/components/SortableGrid/components/ConfirmModal/ConfirmModal.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'ConfirmModal': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React, {PropsWithChildren} from 'react';
|
||||
import styles from './ConfirmModal.module.css';
|
||||
|
||||
interface Props {
|
||||
onConfirm(): void;
|
||||
onDeny(): void;
|
||||
}
|
||||
|
||||
export const ConfirmModal = ({
|
||||
onConfirm,
|
||||
onDeny,
|
||||
children,
|
||||
}: PropsWithChildren<Props>) => (
|
||||
<div className={styles.ConfirmModal}>
|
||||
<h1>{children}</h1>
|
||||
<div>
|
||||
<button onClick={onConfirm}>Yes</button>
|
||||
<button onClick={onDeny}>No</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export {ConfirmModal} from './ConfirmModal';
|
||||
@@ -0,0 +1,103 @@
|
||||
.Container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
grid-auto-rows: max-content;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
min-width: 350px;
|
||||
margin: 10px;
|
||||
border-radius: 5px;
|
||||
min-height: 200px;
|
||||
transition: background-color 350ms ease;
|
||||
background-color: rgba(246, 246, 246, 1);
|
||||
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||
font-size: 1em;
|
||||
|
||||
ul {
|
||||
display: grid;
|
||||
grid-gap: 10px;
|
||||
grid-template-columns: repeat(var(--columns, 1), 1fr);
|
||||
list-style: none;
|
||||
padding: 20px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&.scrollable {
|
||||
ul {
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
&.placeholder {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
background-color: transparent;
|
||||
border-style: dashed;
|
||||
border-color: rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
&.hover {
|
||||
background-color: rgb(235, 235, 235, 1);
|
||||
}
|
||||
|
||||
&.unstyled {
|
||||
overflow: visible;
|
||||
background-color: transparent !important;
|
||||
border: none !important;
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
width: 100%;
|
||||
|
||||
ul {
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
}
|
||||
|
||||
&.shadow {
|
||||
box-shadow: 0 1px 10px 0 rgba(34, 33, 81, 0.1);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border-color: transparent;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0), 0 0px 0px 2px #4c9ffe;
|
||||
}
|
||||
}
|
||||
|
||||
.Header {
|
||||
display: flex;
|
||||
padding: 5px 20px;
|
||||
padding-right: 8px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: #fff;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
|
||||
&:hover {
|
||||
.Actions > * {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Actions {
|
||||
display: flex;
|
||||
|
||||
> *:first-child:not(:last-child) {
|
||||
opacity: 0;
|
||||
|
||||
&:focus-visible {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
15
src/client/components/SortableGrid/components/Container/Container.module.css.d.ts
vendored
Normal file
15
src/client/components/SortableGrid/components/Container/Container.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'Actions': string;
|
||||
'Container': string;
|
||||
'Header': string;
|
||||
'horizontal': string;
|
||||
'hover': string;
|
||||
'placeholder': string;
|
||||
'scrollable': string;
|
||||
'shadow': string;
|
||||
'unstyled': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,81 @@
|
||||
import React, {forwardRef} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {Handle, Remove} from '../Item';
|
||||
|
||||
import styles from './Container.module.css';
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode;
|
||||
columns?: number;
|
||||
label?: string;
|
||||
style?: React.CSSProperties;
|
||||
horizontal?: boolean;
|
||||
hover?: boolean;
|
||||
handleProps?: React.HTMLAttributes<any>;
|
||||
scrollable?: boolean;
|
||||
shadow?: boolean;
|
||||
placeholder?: boolean;
|
||||
unstyled?: boolean;
|
||||
onClick?(): void;
|
||||
onRemove?(): void;
|
||||
}
|
||||
|
||||
export const Container = forwardRef<HTMLDivElement, Props>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
columns = 1,
|
||||
handleProps,
|
||||
horizontal,
|
||||
hover,
|
||||
onClick,
|
||||
onRemove,
|
||||
label,
|
||||
placeholder,
|
||||
style,
|
||||
scrollable,
|
||||
shadow,
|
||||
unstyled,
|
||||
...props
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const Component = onClick ? 'button' : 'div';
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
ref={ref}
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
'--columns': columns,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={classNames(
|
||||
styles.Container,
|
||||
unstyled && styles.unstyled,
|
||||
horizontal && styles.horizontal,
|
||||
hover && styles.hover,
|
||||
placeholder && styles.placeholder,
|
||||
scrollable && styles.scrollable,
|
||||
shadow && styles.shadow
|
||||
)}
|
||||
onClick={onClick}
|
||||
tabIndex={onClick ? 0 : undefined}
|
||||
>
|
||||
{label ? (
|
||||
<div className={styles.Header}>
|
||||
{label}
|
||||
<div className={styles.Actions}>
|
||||
{onRemove ? <Remove onClick={onRemove} /> : undefined}
|
||||
<Handle {...handleProps} />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{placeholder ? children : <ul>{children}</ul>}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export {Container} from './Container';
|
||||
export type {Props as ContainerProps} from './Container';
|
||||
21
src/client/components/SortableGrid/components/Draggable.tsx
Normal file
21
src/client/components/SortableGrid/components/Draggable.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import React from "react";
|
||||
import { useDraggable } from "@dnd-kit/core";
|
||||
|
||||
export const Draggable = (props) => {
|
||||
const { attributes, listeners, setNodeRef, transform } = useDraggable({
|
||||
id: "draggable",
|
||||
});
|
||||
const style = transform
|
||||
? {
|
||||
transform: `translate3d(${transform.x}px, ${transform.y}px, 0)`,
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<button ref={setNodeRef} style={style} {...listeners} {...attributes}>
|
||||
{props.children}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default Draggable;
|
||||
@@ -0,0 +1,128 @@
|
||||
.Draggable {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
transition: transform 250ms ease;
|
||||
transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0);
|
||||
|
||||
button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin: 0 auto;
|
||||
min-height: 54px;
|
||||
flex-shrink: 1;
|
||||
appearance: none;
|
||||
outline: none;
|
||||
border: 0;
|
||||
padding: 8px 18px;
|
||||
background-color: #181a22;
|
||||
border-radius: 5px;
|
||||
box-shadow: var(--box-shadow);
|
||||
transform: scale(var(--scale, 1));
|
||||
transition: transform 250ms cubic-bezier(0.18, 0.67, 0.6, 1.22),
|
||||
box-shadow 300ms ease;
|
||||
}
|
||||
|
||||
&:not(.handle) {
|
||||
button {
|
||||
touch-action: none;
|
||||
cursor: grab;
|
||||
|
||||
&:focus-visible:not(.active &) {
|
||||
box-shadow: 0 0 0 3px #4c9ffe;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.handle {
|
||||
button {
|
||||
--action-background: rgba(255, 255, 255, 0.1);
|
||||
|
||||
> svg {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
> button {
|
||||
margin-right: -10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 140px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
flex-shrink: 1;
|
||||
padding: 10px;
|
||||
transition: opacity 250ms ease;
|
||||
|
||||
text-align: center;
|
||||
font-size: 1rem;
|
||||
font-weight: 300;
|
||||
color: #8d8d8d;
|
||||
user-select: none;
|
||||
cursor: url('/cursor.svg'), auto;
|
||||
|
||||
animation-name: pulse;
|
||||
animation-duration: 1.5s;
|
||||
animation-delay: 2s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: ease;
|
||||
animation-direction: alternate;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
z-index: 1;
|
||||
transition: none;
|
||||
|
||||
* {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
button {
|
||||
--scale: 1.06;
|
||||
--box-shadow: -1px 0 15px 0 rgba(34, 33, 81, 0.01),
|
||||
0px 15px 15px 0 rgba(34, 33, 81, 0.25);
|
||||
|
||||
&:focus-visible {
|
||||
--box-shadow: 0 0px 10px 2px #4c9ffe;
|
||||
}
|
||||
}
|
||||
|
||||
label {
|
||||
animation: none;
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.dragOverlay {
|
||||
button {
|
||||
animation: pop 250ms cubic-bezier(0.18, 0.67, 0.6, 1.22);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(var(--scale));
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import React, {forwardRef} from 'react';
|
||||
import classNames from 'classnames';
|
||||
import type {DraggableSyntheticListeners, Translate} from '@dnd-kit/core';
|
||||
|
||||
import {Handle} from '../Item/components/Handle';
|
||||
|
||||
import {
|
||||
draggable,
|
||||
draggableHorizontal,
|
||||
draggableVertical,
|
||||
} from './draggable-svg';
|
||||
import styles from './Draggable.module.css';
|
||||
|
||||
export enum Axis {
|
||||
All,
|
||||
Vertical,
|
||||
Horizontal,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
axis?: Axis;
|
||||
dragOverlay?: boolean;
|
||||
dragging?: boolean;
|
||||
handle?: boolean;
|
||||
label?: string;
|
||||
listeners?: DraggableSyntheticListeners;
|
||||
style?: React.CSSProperties;
|
||||
translate?: Translate;
|
||||
}
|
||||
|
||||
export const Draggable = forwardRef<HTMLButtonElement, Props>(
|
||||
function Draggable(
|
||||
{
|
||||
axis,
|
||||
dragOverlay,
|
||||
dragging,
|
||||
handle,
|
||||
label,
|
||||
listeners,
|
||||
translate,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
styles.Draggable,
|
||||
dragOverlay && styles.dragOverlay,
|
||||
dragging && styles.dragging,
|
||||
handle && styles.handle
|
||||
)}
|
||||
style={
|
||||
{
|
||||
'--translate-x': `${translate?.x ?? 0}px`,
|
||||
'--translate-y': `${translate?.y ?? 0}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<button
|
||||
ref={ref}
|
||||
{...props}
|
||||
aria-label="Draggable"
|
||||
data-cypress="draggable-item"
|
||||
{...(handle ? {} : listeners)}
|
||||
tabIndex={handle ? -1 : undefined}
|
||||
>
|
||||
{axis === Axis.Vertical
|
||||
? draggableVertical
|
||||
: axis === Axis.Horizontal
|
||||
? draggableHorizontal
|
||||
: draggable}
|
||||
{handle ? <Handle {...(handle ? listeners : {})} /> : null}
|
||||
</button>
|
||||
{label ? <label>{label}</label> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
export { Axis, Draggable } from "./Draggable";
|
||||
19
src/client/components/SortableGrid/components/Droppable.tsx
Normal file
19
src/client/components/SortableGrid/components/Droppable.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import React from "react";
|
||||
import { useDroppable } from "@dnd-kit/core";
|
||||
|
||||
export const Droppable = (props) => {
|
||||
const { isOver, setNodeRef } = useDroppable({
|
||||
id: "droppable",
|
||||
});
|
||||
const style = {
|
||||
color: isOver ? "green" : undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={setNodeRef} style={style}>
|
||||
{props.children}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Droppable;
|
||||
@@ -0,0 +1,51 @@
|
||||
.Droppable {
|
||||
position: relative;
|
||||
padding-top: 80px;
|
||||
text-align: center;
|
||||
border-radius: 10px;
|
||||
width: 340px;
|
||||
height: 340px;
|
||||
box-sizing: border-box;
|
||||
background-color: #fff;
|
||||
box-shadow: inset rgba(201, 211, 219, 0.5) 0 0 0 2px,
|
||||
rgba(255, 255, 255, 0) 0 0 0 1px, rgba(201, 211, 219, 0.25) 20px 14px 24px;
|
||||
transition: box-shadow 250ms ease;
|
||||
|
||||
> svg {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
width: 200px;
|
||||
transform: translate3d(-50%, -50%, 0);
|
||||
opacity: 0.8;
|
||||
transition: opacity 300ms ease, transform 200ms ease;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
> svg {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
&.over {
|
||||
box-shadow: inset #1eb99d 0 0 0 3px, rgba(201, 211, 219, 0.5) 20px 14px 24px;
|
||||
|
||||
> svg {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.dropped {
|
||||
box-shadow: inset rgba(201, 211, 219, 0.7) 0 0 0 3px,
|
||||
rgba(201, 211, 219, 0.5) 20px 14px 24px;
|
||||
}
|
||||
}
|
||||
|
||||
&.dropped {
|
||||
> svg {
|
||||
opacity: 0.2;
|
||||
transform: translate3d(-50%, 100%, 0) scale(0.8);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import {useDroppable, UniqueIdentifier} from '@dnd-kit/core';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import {droppable} from './droppable-svg';
|
||||
import styles from './Droppable.module.css';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
dragging: boolean;
|
||||
id: UniqueIdentifier;
|
||||
}
|
||||
|
||||
export function Droppable({children, id, dragging}: Props) {
|
||||
const {isOver, setNodeRef} = useDroppable({
|
||||
id,
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
className={classNames(
|
||||
styles.Droppable,
|
||||
isOver && styles.over,
|
||||
dragging && styles.dragging,
|
||||
children && styles.dropped
|
||||
)}
|
||||
aria-label="Droppable region"
|
||||
>
|
||||
{children}
|
||||
{droppable}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
export {Droppable} from './Droppable';
|
||||
@@ -0,0 +1,35 @@
|
||||
import React, { useState } from "react";
|
||||
import { DndContext } from "@dnd-kit/core";
|
||||
|
||||
import { Draggable } from "./Draggable";
|
||||
import { Droppable } from "./Droppable";
|
||||
|
||||
export const DroppableGrid = () => {
|
||||
const containers = ["A", "B", "C"];
|
||||
const [parent, setParent] = useState(null);
|
||||
const draggableMarkup = <Draggable id="draggable">Drag me</Draggable>;
|
||||
|
||||
return (
|
||||
<DndContext onDragEnd={handleDragEnd}>
|
||||
{parent === null ? draggableMarkup : null}
|
||||
|
||||
{containers.map((id) => (
|
||||
// We updated the Droppable component so it would accept an `id`
|
||||
// prop and pass it to `useDroppable`
|
||||
<Droppable key={id} id={id}>
|
||||
{parent === id ? draggableMarkup : "Drop here"}
|
||||
</Droppable>
|
||||
))}
|
||||
</DndContext>
|
||||
);
|
||||
|
||||
function handleDragEnd(event) {
|
||||
const { over } = event;
|
||||
|
||||
// If the item is dropped over a container, set it as the parent
|
||||
// otherwise reset the parent to `null`
|
||||
setParent(over ? over.id : null);
|
||||
}
|
||||
};
|
||||
|
||||
export default DroppableGrid;
|
||||
@@ -0,0 +1,5 @@
|
||||
.FloatingControls {
|
||||
position: fixed;
|
||||
top: 25px;
|
||||
right: 25px;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'FloatingControls': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './FloatingControls.module.css';
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function FloatingControls({children}: Props) {
|
||||
return <div className={classNames(styles.FloatingControls)}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {FloatingControls} from './FloatingControls';
|
||||
@@ -0,0 +1,30 @@
|
||||
.Grid {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent,
|
||||
transparent calc(var(--grid-size) - 1px),
|
||||
#ddd calc(var(--grid-size) - 1px),
|
||||
#ddd var(--grid-size)
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
-90deg,
|
||||
transparent,
|
||||
transparent calc(var(--grid-size) - 1px),
|
||||
#ddd calc(var(--grid-size) - 1px),
|
||||
#ddd var(--grid-size)
|
||||
);
|
||||
background-size: var(--grid-size) var(--grid-size);
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.RangeSlider {
|
||||
position: fixed;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
8
src/client/components/SortableGrid/components/Grid/Grid.module.css.d.ts
vendored
Normal file
8
src/client/components/SortableGrid/components/Grid/Grid.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'Grid': string;
|
||||
'RangeSlider': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
22
src/client/components/SortableGrid/components/Grid/Grid.tsx
Normal file
22
src/client/components/SortableGrid/components/Grid/Grid.tsx
Normal file
@@ -0,0 +1,22 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './Grid.module.css';
|
||||
|
||||
export interface Props {
|
||||
size: number;
|
||||
step?: number;
|
||||
onSizeChange(size: number): void;
|
||||
}
|
||||
|
||||
export function Grid({size}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={styles.Grid}
|
||||
style={
|
||||
{
|
||||
'--grid-size': `${size}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {Grid} from './Grid';
|
||||
@@ -0,0 +1,15 @@
|
||||
.GridContainer {
|
||||
max-width: 800px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(var(--col-count), 1fr);
|
||||
grid-gap: 10px;
|
||||
padding: 20px;
|
||||
|
||||
@media (max-width: 850px) {
|
||||
grid-template-columns: repeat(calc(var(--col-count) - 1), 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 650px) {
|
||||
grid-template-columns: repeat(calc(var(--col-count) - 2), 1fr);
|
||||
}
|
||||
}
|
||||
7
src/client/components/SortableGrid/components/GridContainer/GridContainer.module.css.d.ts
vendored
Normal file
7
src/client/components/SortableGrid/components/GridContainer/GridContainer.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'GridContainer': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './GridContainer.module.css';
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode;
|
||||
columns: number;
|
||||
}
|
||||
|
||||
export function GridContainer({children, columns}: Props) {
|
||||
return (
|
||||
<ul
|
||||
className={styles.GridContainer}
|
||||
style={
|
||||
{
|
||||
'--col-count': columns,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {GridContainer} from './GridContainer';
|
||||
@@ -0,0 +1,145 @@
|
||||
$font-weight: 400;
|
||||
$background-color: #000;
|
||||
$border-color: #efefef;
|
||||
$text-color: #333;
|
||||
$handle-color: rgba(0, 0, 0, 0.25);
|
||||
$box-shadow-border: 0 0 0 calc(1px / var(--scale-x, 1)) rgba(63, 63, 68, 0.05);
|
||||
$box-shadow-common: 0 1px calc(3px / var(--scale-x, 1)) 0 rgba(34, 33, 81, 0.15);
|
||||
$box-shadow: $box-shadow-border, $box-shadow-common;
|
||||
$focused-outline-color: #4c9ffe;
|
||||
|
||||
@keyframes pop {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: var(--box-shadow);
|
||||
}
|
||||
100% {
|
||||
transform: scale(var(--scale));
|
||||
box-shadow: var(--box-shadow-picked-up);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.Wrapper {
|
||||
display: flex;
|
||||
box-sizing: border-box;
|
||||
transform: translate3d(var(--translate-x, 0), var(--translate-y, 0), 0)
|
||||
scaleX(var(--scale-x, 1)) scaleY(var(--scale-y, 1));
|
||||
transform-origin: 0 0;
|
||||
touch-action: manipulation;
|
||||
|
||||
&.fadeIn {
|
||||
animation: fadeIn 500ms ease;
|
||||
}
|
||||
|
||||
&.dragOverlay {
|
||||
--scale: 1.05;
|
||||
--box-shadow: $box-shadow;
|
||||
--box-shadow-picked-up: $box-shadow-border,
|
||||
-1px 0 15px 0 rgba(34, 33, 81, 0.01),
|
||||
0px 15px 15px 0 rgba(34, 33, 81, 0.25);
|
||||
z-index: 999;
|
||||
}
|
||||
}
|
||||
|
||||
.Item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
align-items: center;
|
||||
padding: 18px 20px;
|
||||
background-color: #ddd;
|
||||
box-shadow: $box-shadow;
|
||||
outline: none;
|
||||
border-radius: calc(4px / var(--scale-x, 1));
|
||||
box-sizing: border-box;
|
||||
list-style: none;
|
||||
transform-origin: 50% 50%;
|
||||
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
color: $text-color;
|
||||
font-weight: $font-weight;
|
||||
font-size: 1rem;
|
||||
white-space: nowrap;
|
||||
|
||||
transform: scale(var(--scale, 1));
|
||||
transition: box-shadow 200ms cubic-bezier(0.18, 0.67, 0.6, 1.22);
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: 0 0px 4px 1px $focused-outline-color, $box-shadow;
|
||||
}
|
||||
|
||||
&:not(.withHandle) {
|
||||
touch-action: manipulation;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&.dragging:not(.dragOverlay) {
|
||||
opacity: var(--dragging-opacity, 0.5);
|
||||
z-index: 0;
|
||||
|
||||
&:focus {
|
||||
box-shadow: $box-shadow;
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
color: #999;
|
||||
background-color: #f1f1f1;
|
||||
&:focus {
|
||||
box-shadow: 0 0px 4px 1px rgba(0, 0, 0, 0.1), $box-shadow;
|
||||
}
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&.dragOverlay {
|
||||
cursor: inherit;
|
||||
/* box-shadow: 0 0px 6px 2px $focused-outline-color; */
|
||||
animation: pop 200ms cubic-bezier(0.18, 0.67, 0.6, 1.22);
|
||||
transform: scale(var(--scale));
|
||||
box-shadow: var(--box-shadow-picked-up);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.color:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
display: block;
|
||||
border-top-left-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
background-color: var(--color);
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.Remove {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Remove {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.Actions {
|
||||
display: flex;
|
||||
align-self: flex-start;
|
||||
margin-top: -12px;
|
||||
margin-left: auto;
|
||||
margin-bottom: -15px;
|
||||
margin-right: -10px;
|
||||
}
|
||||
150
src/client/components/SortableGrid/components/Item/Item.tsx
Normal file
150
src/client/components/SortableGrid/components/Item/Item.tsx
Normal file
@@ -0,0 +1,150 @@
|
||||
import React, { useEffect } from "react";
|
||||
import classNames from "classnames";
|
||||
import type { DraggableSyntheticListeners } from "@dnd-kit/core";
|
||||
import type { Transform } from "@dnd-kit/utilities";
|
||||
|
||||
import { Handle, Remove } from "./components";
|
||||
|
||||
import styles from "./Item.module.css";
|
||||
|
||||
export interface Props {
|
||||
dragOverlay?: boolean;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
dragging?: boolean;
|
||||
handle?: boolean;
|
||||
height?: number;
|
||||
index?: number;
|
||||
fadeIn?: boolean;
|
||||
transform?: Transform | null;
|
||||
listeners?: DraggableSyntheticListeners;
|
||||
sorting?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
transition?: string | null;
|
||||
wrapperStyle?: React.CSSProperties;
|
||||
value: React.ReactNode;
|
||||
onRemove?(): void;
|
||||
renderItem?(args: {
|
||||
dragOverlay: boolean;
|
||||
dragging: boolean;
|
||||
sorting: boolean;
|
||||
index: number | undefined;
|
||||
fadeIn: boolean;
|
||||
listeners: DraggableSyntheticListeners;
|
||||
ref: React.Ref<HTMLElement>;
|
||||
style: React.CSSProperties | undefined;
|
||||
transform: Props["transform"];
|
||||
transition: Props["transition"];
|
||||
value: Props["value"];
|
||||
}): React.ReactElement;
|
||||
}
|
||||
|
||||
export const Item = React.memo(
|
||||
React.forwardRef<HTMLLIElement, Props>(
|
||||
(
|
||||
{
|
||||
color,
|
||||
dragOverlay,
|
||||
dragging,
|
||||
disabled,
|
||||
fadeIn,
|
||||
handle,
|
||||
height,
|
||||
index,
|
||||
listeners,
|
||||
onRemove,
|
||||
renderItem,
|
||||
sorting,
|
||||
style,
|
||||
transition,
|
||||
transform,
|
||||
value,
|
||||
wrapperStyle,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
useEffect(() => {
|
||||
if (!dragOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
document.body.style.cursor = "grabbing";
|
||||
|
||||
return () => {
|
||||
document.body.style.cursor = "";
|
||||
};
|
||||
}, [dragOverlay]);
|
||||
|
||||
return renderItem ? (
|
||||
renderItem({
|
||||
dragOverlay: Boolean(dragOverlay),
|
||||
dragging: Boolean(dragging),
|
||||
sorting: Boolean(sorting),
|
||||
index,
|
||||
fadeIn: Boolean(fadeIn),
|
||||
listeners,
|
||||
ref,
|
||||
style,
|
||||
transform,
|
||||
transition,
|
||||
value,
|
||||
})
|
||||
) : (
|
||||
<li
|
||||
className={classNames(
|
||||
styles.Wrapper,
|
||||
fadeIn && styles.fadeIn,
|
||||
sorting && styles.sorting,
|
||||
dragOverlay && styles.dragOverlay,
|
||||
)}
|
||||
style={
|
||||
{
|
||||
...wrapperStyle,
|
||||
transition,
|
||||
"--translate-x": transform
|
||||
? `${Math.round(transform.x)}px`
|
||||
: undefined,
|
||||
"--translate-y": transform
|
||||
? `${Math.round(transform.y)}px`
|
||||
: undefined,
|
||||
"--scale-x": transform?.scaleX
|
||||
? `${transform.scaleX}`
|
||||
: undefined,
|
||||
"--scale-y": transform?.scaleY
|
||||
? `${transform.scaleY}`
|
||||
: undefined,
|
||||
"--index": index,
|
||||
"--color": color,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
ref={ref}
|
||||
>
|
||||
<div
|
||||
className={classNames(
|
||||
styles.Item,
|
||||
dragging && styles.dragging,
|
||||
handle && styles.withHandle,
|
||||
dragOverlay && styles.dragOverlay,
|
||||
disabled && styles.disabled,
|
||||
color && styles.color,
|
||||
)}
|
||||
style={style}
|
||||
data-cypress="draggable-item"
|
||||
{...(!handle ? listeners : undefined)}
|
||||
{...props}
|
||||
tabIndex={!handle ? 0 : undefined}
|
||||
>
|
||||
{value}
|
||||
<span className={styles.Actions}>
|
||||
{onRemove ? (
|
||||
<Remove className={styles.Remove} onClick={onRemove} />
|
||||
) : null}
|
||||
{handle ? <Handle {...listeners} /> : null}
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -0,0 +1,50 @@
|
||||
$focused-outline-color: #4c9ffe;
|
||||
|
||||
.Action {
|
||||
display: flex;
|
||||
width: 12px;
|
||||
padding: 15px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex: 0 0 auto;
|
||||
touch-action: none;
|
||||
cursor: var(--cursor, pointer);
|
||||
border-radius: 5px;
|
||||
border: none;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background-color: var(--action-background, rgba(0, 0, 0, 0.05));
|
||||
|
||||
svg {
|
||||
fill: #6f7b88;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
flex: 0 0 auto;
|
||||
margin: auto;
|
||||
height: 100%;
|
||||
overflow: visible;
|
||||
fill: #919eab;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: var(--background, rgba(0, 0, 0, 0.05));
|
||||
|
||||
svg {
|
||||
fill: var(--fill, #788491);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0),
|
||||
0 0px 0px 2px $focused-outline-color;
|
||||
}
|
||||
}
|
||||
7
src/client/components/SortableGrid/components/Item/components/Action/Action.module.css.d.ts
vendored
Normal file
7
src/client/components/SortableGrid/components/Item/components/Action/Action.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'Action': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,30 @@
|
||||
import React, {CSSProperties} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './Action.module.css';
|
||||
|
||||
export interface Props extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
active?: {
|
||||
fill: string;
|
||||
background: string;
|
||||
};
|
||||
cursor?: CSSProperties['cursor'];
|
||||
}
|
||||
|
||||
export function Action({active, className, cursor, style, ...props}: Props) {
|
||||
return (
|
||||
<button
|
||||
{...props}
|
||||
className={classNames(styles.Action, className)}
|
||||
tabIndex={0}
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
cursor,
|
||||
'--fill': active?.fill,
|
||||
'--background': active?.background,
|
||||
} as CSSProperties
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export {Action} from './Action';
|
||||
export type {Props as ActionProps} from './Action';
|
||||
@@ -0,0 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import {Action, ActionProps} from '../Action';
|
||||
|
||||
export function Handle(props: ActionProps) {
|
||||
return (
|
||||
<Action cursor="grab" data-cypress="draggable-handle" {...props}>
|
||||
<svg viewBox="0 0 20 20" width="12">
|
||||
<path d="M7 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 2zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 7 14zm6-8a2 2 0 1 0-.001-4.001A2 2 0 0 0 13 6zm0 2a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 8zm0 6a2 2 0 1 0 .001 4.001A2 2 0 0 0 13 14z"></path>
|
||||
</svg>
|
||||
</Action>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {Handle} from './Handle';
|
||||
@@ -0,0 +1,19 @@
|
||||
import React from 'react';
|
||||
|
||||
import {Action, ActionProps} from '../Action';
|
||||
|
||||
export function Remove(props: ActionProps) {
|
||||
return (
|
||||
<Action
|
||||
{...props}
|
||||
active={{
|
||||
fill: 'rgba(255, 70, 70, 0.95)',
|
||||
background: 'rgba(255, 70, 70, 0.1)',
|
||||
}}
|
||||
>
|
||||
<svg width="8" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M2.99998 -0.000206962C2.7441 -0.000206962 2.48794 0.0972617 2.29294 0.292762L0.292945 2.29276C-0.0980552 2.68376 -0.0980552 3.31682 0.292945 3.70682L7.58591 10.9998L0.292945 18.2928C-0.0980552 18.6838 -0.0980552 19.3168 0.292945 19.7068L2.29294 21.7068C2.68394 22.0978 3.31701 22.0978 3.70701 21.7068L11 14.4139L18.2929 21.7068C18.6829 22.0978 19.317 22.0978 19.707 21.7068L21.707 19.7068C22.098 19.3158 22.098 18.6828 21.707 18.2928L14.414 10.9998L21.707 3.70682C22.098 3.31682 22.098 2.68276 21.707 2.29276L19.707 0.292762C19.316 -0.0982383 18.6829 -0.0982383 18.2929 0.292762L11 7.58573L3.70701 0.292762C3.51151 0.0972617 3.25585 -0.000206962 2.99998 -0.000206962Z" />
|
||||
</svg>
|
||||
</Action>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {Remove} from './Remove';
|
||||
@@ -0,0 +1,3 @@
|
||||
export {Action} from './Action';
|
||||
export {Handle} from './Handle';
|
||||
export {Remove} from './Remove';
|
||||
@@ -0,0 +1,2 @@
|
||||
export {Item} from './Item';
|
||||
export {Action, Handle, Remove} from './components';
|
||||
@@ -0,0 +1,25 @@
|
||||
.List {
|
||||
display: grid;
|
||||
grid-auto-rows: max-content;
|
||||
box-sizing: border-box;
|
||||
min-width: 350px;
|
||||
grid-gap: 10px;
|
||||
padding: 20px;
|
||||
padding-bottom: 0;
|
||||
margin: 10px;
|
||||
border-radius: 5px;
|
||||
min-height: 200px;
|
||||
transition: background-color 350ms ease;
|
||||
grid-template-columns: repeat(var(--columns, 1), 1fr);
|
||||
|
||||
&:after {
|
||||
content: '';
|
||||
height: 10px;
|
||||
grid-column-start: span var(--columns, 1);
|
||||
}
|
||||
|
||||
&.horizontal {
|
||||
width: 100%;
|
||||
grid-auto-flow: column;
|
||||
}
|
||||
}
|
||||
8
src/client/components/SortableGrid/components/List/List.module.css.d.ts
vendored
Normal file
8
src/client/components/SortableGrid/components/List/List.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'List': string;
|
||||
'horizontal': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
30
src/client/components/SortableGrid/components/List/List.tsx
Normal file
30
src/client/components/SortableGrid/components/List/List.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, {forwardRef} from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './List.module.css';
|
||||
|
||||
export interface Props {
|
||||
children: React.ReactNode;
|
||||
columns?: number;
|
||||
style?: React.CSSProperties;
|
||||
horizontal?: boolean;
|
||||
}
|
||||
|
||||
export const List = forwardRef<HTMLUListElement, Props>(
|
||||
({children, columns = 1, horizontal, style}: Props, ref) => {
|
||||
return (
|
||||
<ul
|
||||
ref={ref}
|
||||
style={
|
||||
{
|
||||
...style,
|
||||
'--columns': columns,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={classNames(styles.List, horizontal && styles.horizontal)}
|
||||
>
|
||||
{children}
|
||||
</ul>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
export {List} from './List';
|
||||
@@ -0,0 +1,8 @@
|
||||
.OverflowWrapper {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'OverflowWrapper': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import styles from './OverflowWrapper.module.css';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function OverflowWrapper({children}: Props) {
|
||||
return <div className={styles.OverflowWrapper}>{children}</div>;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {OverflowWrapper} from './OverflowWrapper';
|
||||
@@ -0,0 +1,11 @@
|
||||
.Wrapper {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 20px;
|
||||
justify-content: flex-start;
|
||||
|
||||
&.center {
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
8
src/client/components/SortableGrid/components/Wrapper/Wrapper.module.css.d.ts
vendored
Normal file
8
src/client/components/SortableGrid/components/Wrapper/Wrapper.module.css.d.ts
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
// This file is automatically generated.
|
||||
// Please do not change this file!
|
||||
interface CssExports {
|
||||
'Wrapper': string;
|
||||
'center': string;
|
||||
}
|
||||
export const cssExports: CssExports;
|
||||
export default cssExports;
|
||||
@@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import styles from './Wrapper.module.css';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
center?: boolean;
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
export function Wrapper({children, center, style}: Props) {
|
||||
return (
|
||||
<div
|
||||
className={classNames(styles.Wrapper, center && styles.center)}
|
||||
style={style}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export {Wrapper} from './Wrapper';
|
||||
13
src/client/components/SortableGrid/components/index.ts
Normal file
13
src/client/components/SortableGrid/components/index.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
export { Button } from "./Button";
|
||||
export { ConfirmModal } from "./ConfirmModal";
|
||||
export { Container } from "./Container";
|
||||
export type { ContainerProps } from "./Container";
|
||||
export { Draggable } from "./Draggable";
|
||||
export { Droppable } from "./Droppable";
|
||||
export { Item, Action, Handle, Remove } from "./Item";
|
||||
export { FloatingControls } from "./FloatingControls";
|
||||
export { Grid } from "./Grid";
|
||||
export { GridContainer } from "./GridContainer";
|
||||
export { List } from "./List";
|
||||
export { OverflowWrapper } from "./OverflowWrapper";
|
||||
export { Wrapper } from "./Wrapper";
|
||||
@@ -0,0 +1,8 @@
|
||||
const defaultInitializer = (index: number) => index;
|
||||
|
||||
export function createRange<T = number>(
|
||||
length: number,
|
||||
initializer: (index: number) => any = defaultInitializer
|
||||
): T[] {
|
||||
return [...new Array(length)].map((_, index) => initializer(index));
|
||||
}
|
||||
1
src/client/components/SortableGrid/utilities/index.ts
Normal file
1
src/client/components/SortableGrid/utilities/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export {createRange} from './createRange';
|
||||
@@ -10,13 +10,31 @@
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist/",
|
||||
"skipLibCheck": true,
|
||||
"lib": ["dom", "dom.iterable", "esnext", "webworker"]
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext",
|
||||
"webworker"
|
||||
],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "typescript-plugin-css-modules"
|
||||
}
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"eslint.workingDirectories": [
|
||||
{ "directory": "./node_modules", "changeProcessCWD": true }
|
||||
{
|
||||
"directory": "./node_modules",
|
||||
"changeProcessCWD": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"exclude": ["./src/server "],
|
||||
"include": ["./src/client/*", "./src/client/types/**/*.d.ts"]
|
||||
}
|
||||
"exclude": [
|
||||
"./src/server "
|
||||
],
|
||||
"include": [
|
||||
"./src/client/*",
|
||||
"./src/client/types/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -40,7 +40,17 @@ module.exports = {
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ["style-loader", "css-loader"],
|
||||
use: [
|
||||
{ loader: "style-loader" },
|
||||
{
|
||||
loader: "css-loader",
|
||||
options: {
|
||||
modules: true,
|
||||
importLoaders: 1,
|
||||
sourceMap: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(scss|sass)$/,
|
||||
|
||||
80
yarn.lock
80
yarn.lock
@@ -1035,6 +1035,45 @@
|
||||
resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.5.tgz#9283c9ce5b289a3c4f61c12757469e59377f81f3"
|
||||
integrity sha512-6nFkfkmSeV/rqSaS4oWHgmpnYw194f6hmWF5is6b0J1naJZoiD0NTc9AiUwPHvWsowkjuHErCZT1wa0jg+BLIA==
|
||||
|
||||
"@dnd-kit/accessibility@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/accessibility/-/accessibility-3.0.0.tgz#b56e3750414fd907b7d6972b3116aa8f96d07fde"
|
||||
integrity sha512-QwaQ1IJHQHMMuAGOOYHQSx7h7vMZPfO97aDts8t5N/MY7n2QTDSnW+kF7uRQ1tVBkr6vJ+BqHWG5dlgGvwVjow==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/core@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/core/-/core-4.0.0.tgz#14e530f859d95ca49aea57878c95bc5f15cf5aac"
|
||||
integrity sha512-ZbQxOoZs/eH8FJlQAipw+FVF6AvjoTR6aAF6JrweJ4Ic45WhuxHHRN/7u50MYhaMuvG6HF24qks0hZKQUe1EDg==
|
||||
dependencies:
|
||||
"@dnd-kit/accessibility" "^3.0.0"
|
||||
"@dnd-kit/utilities" "^3.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/modifiers@^4.0.0":
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/modifiers/-/modifiers-4.0.0.tgz#d1577b806b2319f14a1a0a155f270e672cfca636"
|
||||
integrity sha512-4OkNTamneH9u3YMJqG6yJ6cwFoEd/4yY9BF39TgmDh9vyMK2MoPZFVAV0vOEm193ZYsPczq3Af5tJFtJhR9jJQ==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/sortable@^5.0.0":
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/sortable/-/sortable-5.0.0.tgz#33841fcbc2f2490aaae42906b7497a2033242faa"
|
||||
integrity sha512-CvTuaTIqzvZe8r1Lt+2tPK7VZKfE0SQkFTcAMayf8jS6MFgX6TV5GAIb1tKvbqrGXkqqTZeA2EaeMzjFyyfqkQ==
|
||||
dependencies:
|
||||
"@dnd-kit/utilities" "^3.0.0"
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@dnd-kit/utilities@^3.0.0":
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/@dnd-kit/utilities/-/utilities-3.0.0.tgz#43b86e9d911b625ee54786e9daae5a521949b8a6"
|
||||
integrity sha512-E2UG8guqa532xB8kEe2rFEI/U2W7zuxv0zYorL3knnGJxMGBoI7132i4ULmQs/aunQvNy8kj6Ts39n+APeU7BQ==
|
||||
dependencies:
|
||||
tslib "^2.0.0"
|
||||
|
||||
"@emotion/cache@^11.4.0":
|
||||
version "11.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.4.0.tgz#293fc9d9a7a38b9aad8e9337e5014366c3b09ac0"
|
||||
@@ -4299,6 +4338,14 @@ css-loader@^5.1.2:
|
||||
schema-utils "^3.0.0"
|
||||
semver "^7.3.5"
|
||||
|
||||
css-modules-typescript-loader@^4.0.1:
|
||||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/css-modules-typescript-loader/-/css-modules-typescript-loader-4.0.1.tgz#0b818cf647fefd8f9fb3d4469374e69ab1e72742"
|
||||
integrity sha512-vXrUAwPGcRaopnGdg7I5oqv/NSSKQRN5L80m3f49uSGinenU5DTNsMFHS+2roh5tXqpY5+yAAKAl7A2HDvumzg==
|
||||
dependencies:
|
||||
line-diff "^2.0.1"
|
||||
loader-utils "^1.2.3"
|
||||
|
||||
css-select-base-adapter@^0.1.1:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz#3b2ff4972cc362ab88561507a95408a1432135d7"
|
||||
@@ -5242,6 +5289,14 @@ eslint-module-utils@^2.6.2:
|
||||
debug "^3.2.7"
|
||||
pkg-dir "^2.0.0"
|
||||
|
||||
eslint-plugin-css-modules@^2.11.0:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-css-modules/-/eslint-plugin-css-modules-2.11.0.tgz#8de4d01d523a2d51c03043fa8004aab6b6cf3b1a"
|
||||
integrity sha512-CLvQvJOMlCywZzaI4HVu7QH/ltgNXvCg7giJGiE+sA9wh5zQ+AqTgftAzrERV22wHe1p688wrU/Zwxt1Ry922w==
|
||||
dependencies:
|
||||
gonzales-pe "^4.0.3"
|
||||
lodash "^4.17.2"
|
||||
|
||||
eslint-plugin-import@^2.22.1:
|
||||
version "2.24.2"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz#2c8cd2e341f3885918ee27d18479910ade7bb4da"
|
||||
@@ -6421,6 +6476,13 @@ globule@^1.0.0:
|
||||
lodash "~4.17.10"
|
||||
minimatch "~3.0.2"
|
||||
|
||||
gonzales-pe@^4.0.3:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.yarnpkg.com/gonzales-pe/-/gonzales-pe-4.3.0.tgz#fe9dec5f3c557eead09ff868c65826be54d067b3"
|
||||
integrity sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==
|
||||
dependencies:
|
||||
minimist "^1.2.5"
|
||||
|
||||
got@^6.7.1:
|
||||
version "6.7.1"
|
||||
resolved "https://registry.yarnpkg.com/got/-/got-6.7.1.tgz#240cd05785a9a18e561dc1b44b41c763ef1e8db0"
|
||||
@@ -8363,6 +8425,11 @@ lcid@^2.0.0:
|
||||
dependencies:
|
||||
invert-kv "^2.0.0"
|
||||
|
||||
levdist@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/levdist/-/levdist-1.0.0.tgz#91d7a3044964f2ccc421a0477cac827fe75c5718"
|
||||
integrity sha1-kdejBElk8szEIaBHfKyCf+dcVxg=
|
||||
|
||||
leven@2.1.0:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580"
|
||||
@@ -8499,6 +8566,13 @@ libnpmversion@^1.2.1:
|
||||
semver "^7.3.5"
|
||||
stringify-package "^1.0.1"
|
||||
|
||||
line-diff@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/line-diff/-/line-diff-2.1.1.tgz#a389799b931375a3b1e764964ad0b0b3ce60d6f6"
|
||||
integrity sha512-vswdynAI5AMPJacOo2o+JJ4caDJbnY2NEqms4MhMW0NJbjh3skP/brpVTAgBxrg55NRZ2Vtw88ef18hnagIpYQ==
|
||||
dependencies:
|
||||
levdist "^1.0.0"
|
||||
|
||||
lines-and-columns@^1.1.6:
|
||||
version "1.1.6"
|
||||
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.1.6.tgz#1c00c743b433cd0a4e80758f7b64a57440d9ff00"
|
||||
@@ -8542,7 +8616,7 @@ loader-runner@^4.2.0:
|
||||
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-4.2.0.tgz#d7022380d66d14c5fb1d496b89864ebcfd478384"
|
||||
integrity sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==
|
||||
|
||||
loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.4.0:
|
||||
loader-utils@^1.0.0, loader-utils@^1.1.0, loader-utils@^1.2.3, loader-utils@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.4.0.tgz#c579b5e34cb34b1a74edc6c1fb36bfa371d5a613"
|
||||
integrity sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==
|
||||
@@ -8625,7 +8699,7 @@ lodash.truncate@^4.4.2:
|
||||
resolved "https://registry.yarnpkg.com/lodash.truncate/-/lodash.truncate-4.4.2.tgz#5a350da0b1113b837ecfffd5812cbe58d6eae193"
|
||||
integrity sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=
|
||||
|
||||
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.10:
|
||||
lodash@^4.0.0, lodash@^4.17.11, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.2, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.7.0, lodash@~4.17.10:
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
|
||||
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
|
||||
@@ -13370,7 +13444,7 @@ tslib@^1.13.0, tslib@^1.8.1, tslib@^1.9.0:
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
|
||||
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
|
||||
|
||||
tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.0, tslib@^2.3.1:
|
||||
tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.3.0, tslib@^2.3.1:
|
||||
version "2.3.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01"
|
||||
integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==
|
||||
|
||||
Reference in New Issue
Block a user