Skip to content

Commit 81d1c79

Browse files
authored
added support for questions order with drag & drop (#4)
* added support for questions order with drag & drop * fixed lib compile problems * readme update * readme update
1 parent fb2980e commit 81d1c79

File tree

14 files changed

+4760
-4374
lines changed

14 files changed

+4760
-4374
lines changed

README.md

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Advantages:
2020
- Internationalization on questions and answers
2121
- Internationalization on Builder
2222
- Centralized form builder and from delivery
23+
- Drag & Drop to order/sort questions on `<QuizzBuilder/>`
2324

2425
## Installation
2526

@@ -48,13 +49,13 @@ import { QuizzBuilder } from "react-quizzes" <QuizzBuilder onChange={(QuizzData)
4849

4950
QuizzBuilder component objective is to provide the user a nice and smooth interface to build quizzes
5051

51-
| Props | Type | Default | Description |
52-
| -------------- | --------------- | ------------------------------------------------------------------------------ | -------------------------------------------------------------------------------- |
53-
| `onChange` | `Function` | `` | will returns builded quizz in QuizzData type |
54-
| `initialValue` | `QuizzData` | `` | initial value to QuizzBuilder, useful if user wants to edit a saved quizz |
55-
| `toolBox` | `QuizzToolBox` | `default QuizzToolBox` | list of inputs to use, defaults to react-quizz but custom inputs can be supplied |
56-
| `language` | `string` | `en` | Language that QuizzBuilder will show |
57-
| `messages` | `QuizzMessages` | `default QuizzMessages` | Object with each language and each language with each text translation |
52+
| Props | Type | Default | Description |
53+
| -------------- | --------------- | ----------------------- | -------------------------------------------------------------------------------- |
54+
| `onChange` | `Function` | `` | will returns builded quizz in QuizzData type |
55+
| `initialValue` | `QuizzData` | `` | initial value to QuizzBuilder, useful if user wants to edit a saved quizz |
56+
| `toolBox` | `QuizzToolBox` | `default QuizzToolBox` | list of inputs to use, defaults to react-quizz but custom inputs can be supplied |
57+
| `language` | `string` | `en` | Language that QuizzBuilder will show |
58+
| `messages` | `QuizzMessages` | `default QuizzMessages` | Object with each language and each language with each text translation |
5859

5960
# Quiz
6061

@@ -87,7 +88,7 @@ There is a prop `wrappedComponentRef` that gives you access to make basically an
8788
```jsx
8889
import { Quiz } from "react-quizzes";
8990

90-
saveQuizRef = quizRef => {
91+
saveQuizRef = (quizRef) => {
9192
// saves Quizz component ref
9293
this.quizRef = quizRef;
9394
};
@@ -117,7 +118,6 @@ handleCustomSubmit = () => {
117118
New languages support can be added or replace the existing ones
118119
[Existing translations](./src/translations/TranslatedText.tsx#defaultMessages)
119120

120-
121121
[![Edit react-quizzesExample RU locale](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/react-quizzesexample-ru-locale-wx50m?fontsize=14&hidenavigation=1&theme=dark)
122122

123123
```javascript
@@ -199,3 +199,21 @@ but if you need a custom one follow antd-design guidelines and probably you will
199199
# License
200200

201201
MIT License
202+
203+
# DEV
204+
205+
Start project
206+
207+
```
208+
npm i
209+
npm start
210+
```
211+
212+
# Build lib
213+
214+
Start project
215+
216+
```
217+
npm i
218+
npm build
219+
```

package-lock.json

Lines changed: 4396 additions & 4240 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"version": "0.2.0",
44
"private": false,
55
"description": "React quizz/form builder and delivery solution",
6-
"main": "lib/index.js",
6+
"main": "lib/App.js",
77
"types": "lib",
88
"repository": {
99
"type": "git",
@@ -25,34 +25,37 @@
2525
},
2626
"homepage": "https://github.com/hugobarragon/react-quizzes",
2727
"dependencies": {
28+
"@types/hoist-non-react-statics": "^3.3.1",
2829
"@types/lodash.clonedeep": "^4.5.6",
2930
"@types/lodash.isequal": "^4.5.5",
3031
"@types/node": "12.12.3",
31-
"@types/uuid": "^3.4.6",
32-
"antd": "^3.24.3",
33-
"iso-639-1": "^2.1.0",
32+
"@types/uuid": "^3.4.9",
33+
"antd": "^3.26.20",
34+
"iso-639-1": "^2.1.9",
3435
"lodash.clonedeep": "^4.5.0",
3536
"lodash.isequal": "^4.5.0",
36-
"react-quill": "^1.3.3",
37-
"uuid": "^3.3.3"
37+
"react-dnd": "^14.0.2",
38+
"react-dnd-html5-backend": "^14.0.0",
39+
"react-quill": "^1.3.5",
40+
"uuid": "^3.4.0"
3841
},
3942
"devDependencies": {
40-
"@types/jest": "^24.0.21",
41-
"@types/react": "^16.9.11",
42-
"@types/react-dom": "^16.9.3",
43-
"copyfiles": "^2.1.1",
44-
"react": "^16.12.0",
45-
"react-dom": "^16.12.0",
46-
"react-scripts": "^3.2.0",
47-
"typescript": "^3.7.4"
43+
"@types/jest": "^24.9.1",
44+
"@types/react": "^16.14.8",
45+
"@types/react-dom": "^16.9.13",
46+
"copyfiles": "^2.4.1",
47+
"react": "^16.14.0",
48+
"react-dom": "^16.14.0",
49+
"react-scripts": "^3.4.4",
50+
"typescript": "^3.9.9"
4851
},
4952
"peerDependencies": {
5053
"react": ">=16.8",
5154
"react-dom": ">=16.8"
5255
},
5356
"scripts": {
5457
"start": "react-scripts start",
55-
"build": "tsc -p .",
58+
"build": "tsc -p ./tsconfig.compile.json",
5659
"postbuild": "copyfiles -u 1 src/**/*.css lib/",
5760
"test": "react-scripts test",
5861
"eject": "react-scripts eject"

src/index.ts renamed to src/App.ts

File renamed without changes.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import React, { PropsWithChildren, useRef } from "react";
2+
import { useDrag, useDrop, DropTargetMonitor } from "react-dnd";
3+
import { XYCoord } from "dnd-core";
4+
5+
const ItemTypes = {
6+
CARD: "card",
7+
};
8+
9+
export interface CardProps {
10+
id: string;
11+
index: number;
12+
moveCard: (dragIndex: number, hoverIndex: number) => void;
13+
}
14+
15+
interface DragItem {
16+
index: number;
17+
id: string;
18+
type: string;
19+
}
20+
21+
export default (props: PropsWithChildren<CardProps>) => {
22+
const { id, index, moveCard, children } = props;
23+
const ref = useRef<HTMLDivElement>(null);
24+
const [{ handlerId }, drop] = useDrop({
25+
accept: ItemTypes.CARD,
26+
collect(monitor) {
27+
return {
28+
handlerId: monitor.getHandlerId(),
29+
};
30+
},
31+
hover(item: DragItem, monitor: DropTargetMonitor) {
32+
if (!ref.current) {
33+
return;
34+
}
35+
const dragIndex = item.index;
36+
const hoverIndex = index;
37+
38+
// Don't replace items with themselves
39+
if (dragIndex === hoverIndex) {
40+
return;
41+
}
42+
43+
// Determine rectangle on screen
44+
const hoverBoundingRect = ref.current?.getBoundingClientRect();
45+
46+
// Get vertical middle
47+
const hoverMiddleY =
48+
(hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
49+
50+
// Determine mouse position
51+
const clientOffset = monitor.getClientOffset();
52+
53+
// Get pixels to the top
54+
const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
55+
56+
// Only perform the move when the mouse has crossed half of the items height
57+
// When dragging downwards, only move when the cursor is below 50%
58+
// When dragging upwards, only move when the cursor is above 50%
59+
60+
// Dragging downwards
61+
if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
62+
return;
63+
}
64+
65+
// Dragging upwards
66+
if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
67+
return;
68+
}
69+
70+
// Time to actually perform the action
71+
moveCard(dragIndex, hoverIndex);
72+
73+
// Note: we're mutating the monitor item here!
74+
// Generally it's better to avoid mutations,
75+
// but it's good here for the sake of performance
76+
// to avoid expensive index searches.
77+
item.index = hoverIndex;
78+
},
79+
});
80+
81+
const [{ isDragging }, drag] = useDrag({
82+
type: ItemTypes.CARD,
83+
item: () => {
84+
return { id, index };
85+
},
86+
collect: (monitor: any) => ({
87+
isDragging: monitor.isDragging(),
88+
}),
89+
});
90+
91+
const opacity = isDragging ? 0 : 1;
92+
drag(drop(ref));
93+
return (
94+
<div ref={ref} style={{ opacity }} data-handler-id={handlerId}>
95+
{children}
96+
</div>
97+
);
98+
};

src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/SettingsForm/CustomFormInput/QuillFormInput/index.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const text_size = [
4141
"72px",
4242
"80px",
4343
"88px",
44-
"96px"
44+
"96px",
4545
];
4646
Size.whitelist = text_size;
4747
Quill.register(Size, true);
@@ -56,12 +56,12 @@ const modules = {
5656
{ list: "ordered" },
5757
{ list: "bullet" },
5858
{ indent: "-1" },
59-
{ indent: "+1" }
59+
{ indent: "+1" },
6060
],
6161
["link" /* 'image', 'video' */],
62-
["clean"]
63-
]
64-
}
62+
["clean"],
63+
],
64+
},
6565
};
6666

6767
export default forwardRef((props: any, ref) => {
@@ -72,7 +72,7 @@ export default forwardRef((props: any, ref) => {
7272
currentLanguage,
7373
setLanguage,
7474
onNewLanguage,
75-
onRemoveLanguage
75+
onRemoveLanguage,
7676
} = props;
7777
const questionLanguages: Array<string> = Object.keys(value);
7878

@@ -100,7 +100,7 @@ export default forwardRef((props: any, ref) => {
100100
<AddDropdown disabled={questionLanguages} onChange={onNewLanguage} />
101101
}
102102
>
103-
{questionLanguages.map(lang => (
103+
{questionLanguages.map((lang) => (
104104
<TabPane
105105
// native language name
106106
tab={
@@ -112,7 +112,7 @@ export default forwardRef((props: any, ref) => {
112112
title={<TranslatedText id="confirm.action" />}
113113
okText={<TranslatedText id="btn.yes" />}
114114
cancelText={<TranslatedText id="btn.no" />}
115-
onConfirm={e => {
115+
onConfirm={(e) => {
116116
preventPropagation(e);
117117
onRemoveLanguage(lang);
118118
}}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.ql-container.ql-snow {
2+
max-height: 150px;
3+
overflow: auto;
4+
}

src/QuizzBuilder/FormPreview/FormPreview.tsx

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,64 @@
1+
import DnDWrapper from "./DnDWrapper";
12
import React, { useContext } from "react";
23
import Empty from "antd/es/empty/index";
34
import Form from "antd/es/form/index";
45
import QuizzContext, { IQuizzBuilderContext } from "../../QuizzContext";
56
import ElementWrapper from "./ElementWrapper/ElementWrapper";
7+
import { moveElement } from "../reducer/actions";
8+
import { DndProvider } from "react-dnd";
9+
import { HTML5Backend } from "react-dnd-html5-backend";
610

711
function PreviewForm(props: any) {
8-
const { state, toolBox, language } = useContext(
9-
QuizzContext
10-
) as IQuizzBuilderContext,
11-
{ form } = props,
12-
form_data = state.get("data");
12+
const { state, toolBox, language, dispatch } = useContext(
13+
QuizzContext
14+
) as IQuizzBuilderContext;
15+
const { form } = props;
16+
const form_data = state.get("data");
17+
18+
function moveCard(dragIndex: number, hoverIndex: number) {
19+
dispatch(moveElement(dragIndex, hoverIndex));
20+
}
1321

1422
// shows the inputs from the toolbox that are added to the current builder state
1523
return (
1624
<Form>
17-
{form_data.map((item: any) => {
18-
const current_key = item.element;
19-
// searchs for the current input on the toolbox to get the component
20-
const found_toolbox_input = toolBox.find(
21-
(toolBoxInput: any) => current_key === toolBoxInput.key
22-
) as any;
23-
// finds input and wrapps it with delete and edit button
24-
const Component = found_toolbox_input
25-
? found_toolbox_input.Component
26-
: Empty;
25+
<DndProvider backend={HTML5Backend}>
26+
{form_data.map((item: any, i: number) => {
27+
const current_key = item.element;
28+
// searchs for the current input on the toolbox to get the component
29+
const found_toolbox_input = toolBox.find(
30+
(toolBoxInput: any) => current_key === toolBoxInput.key
31+
) as any;
32+
// finds input and wrapps it with delete and edit button
33+
const Component = found_toolbox_input
34+
? found_toolbox_input.Component
35+
: Empty;
2736

28-
return (
29-
<ElementWrapper
30-
// render the input wrapper with delete and edit button
31-
toolboxData={found_toolbox_input}
32-
inputData={item}
33-
key={item.id}
34-
>
35-
<Component
36-
// render the toolbox component
37-
form={form}
38-
toolboxData={found_toolbox_input}
39-
inputData={item}
40-
language={language}
41-
/>
42-
</ElementWrapper>
43-
);
44-
})}
37+
return (
38+
<DnDWrapper
39+
key={item.id}
40+
index={i}
41+
id={item.id}
42+
moveCard={moveCard}
43+
>
44+
<ElementWrapper
45+
// render the input wrapper with delete and edit button
46+
toolboxData={found_toolbox_input}
47+
inputData={item}
48+
key={item.id}
49+
>
50+
<Component
51+
// render the toolbox component
52+
form={form}
53+
toolboxData={found_toolbox_input}
54+
inputData={item}
55+
language={language}
56+
/>
57+
</ElementWrapper>
58+
</DnDWrapper>
59+
);
60+
})}
61+
</DndProvider>
4562
</Form>
4663
);
4764
}

0 commit comments

Comments
 (0)