) {
12 | e.preventDefault();
13 | const { onSubmit, form } = this.props;
14 |
15 | form.validateFields((err: any, values: any) => {
16 | if (!err) {
17 | if (typeof onSubmit === "function") {
18 | // sends values to parent if has onSubmit
19 | onSubmit(values);
20 | }
21 | }
22 | });
23 | }
24 |
25 | static defaultProps = {
26 | submitButton: true
27 | };
28 |
29 | render() {
30 | const { form, data, submitButton, ...rest } = this.props,
31 | contextValue = getDefaultContext(rest as IQuizzContext);
32 | return (
33 |
34 |
35 |
60 |
67 |
68 | ) : null}
69 |
70 |
71 |
72 | );
73 | }
74 | }
75 |
76 | export default Form.create()(Quizz);
77 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormBuilder.tsx:
--------------------------------------------------------------------------------
1 | import React, { useReducer, memo, useEffect } from "react";
2 | import { reducer, initialState } from "./reducer/reducer";
3 | import Row from "antd/es/row/index";
4 | import Col from "antd/es/col/index";
5 | import ToolBox from "./ToolBoxContainer";
6 | import FormPreview from "./FormPreview/FormPreview";
7 | import QuizzContext, { getDefaultContext } from "../QuizzContext";
8 | import { usePrevious } from "../customHooks";
9 | import isEqual from "lodash.isequal";
10 | import "../assets/FormBuilder.css";
11 |
12 | export default memo(function(props: any) {
13 | const { onChange, initialValue, ...rest } = props,
14 | [state, dispatch] = useReducer(reducer, initialState(initialValue)),
15 | contextValue = {
16 | ...getDefaultContext(rest),
17 | state,
18 | dispatch
19 | },
20 | formData = state.get("data");
21 | const previousFormData = usePrevious(formData);
22 |
23 | // effect to set
24 | useEffect(() => {
25 | // effect to notify parent that form data had a diff
26 | if (
27 | typeof onChange === "function" &&
28 | previousFormData &&
29 | !isEqual(previousFormData, formData)
30 | ) {
31 | onChange(formData);
32 | }
33 | }, [onChange, formData, previousFormData]);
34 |
35 | // creates two colons 1 with toolbox and the other with the added inputs
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | );
50 | });
51 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/DnDWrapper/index.tsx:
--------------------------------------------------------------------------------
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) => {
22 | const { id, index, moveCard, children } = props;
23 | const ref = useRef(null);
24 | const [, drop] = useDrop({
25 | accept: ItemTypes.CARD,
26 | hover(item: DragItem, monitor: DropTargetMonitor) {
27 | if (!ref.current) {
28 | return;
29 | }
30 | const dragIndex = item.index;
31 | const hoverIndex = index;
32 |
33 | // Don't replace items with themselves
34 | if (dragIndex === hoverIndex) {
35 | return;
36 | }
37 |
38 | // Determine rectangle on screen
39 | const hoverBoundingRect = ref.current?.getBoundingClientRect();
40 |
41 | // Get vertical middle
42 | const hoverMiddleY =
43 | (hoverBoundingRect.bottom - hoverBoundingRect.top) / 2;
44 |
45 | // Determine mouse position
46 | const clientOffset = monitor.getClientOffset();
47 |
48 | // Get pixels to the top
49 | const hoverClientY = (clientOffset as XYCoord).y - hoverBoundingRect.top;
50 |
51 | // Only perform the move when the mouse has crossed half of the items height
52 | // When dragging downwards, only move when the cursor is below 50%
53 | // When dragging upwards, only move when the cursor is above 50%
54 |
55 | // Dragging downwards
56 | if (dragIndex < hoverIndex && hoverClientY < hoverMiddleY) {
57 | return;
58 | }
59 |
60 | // Dragging upwards
61 | if (dragIndex > hoverIndex && hoverClientY > hoverMiddleY) {
62 | return;
63 | }
64 |
65 | // Time to actually perform the action
66 | moveCard(dragIndex, hoverIndex);
67 |
68 | // Note: we're mutating the monitor item here!
69 | // Generally it's better to avoid mutations,
70 | // but it's good here for the sake of performance
71 | // to avoid expensive index searches.
72 | item.index = hoverIndex;
73 | },
74 | });
75 |
76 | const [{ isDragging }, drag] = useDrag({
77 | item: { type: ItemTypes.CARD, id, index },
78 | collect: (monitor: any) => ({
79 | isDragging: monitor.isDragging(),
80 | }),
81 | });
82 |
83 | const opacity = isDragging ? 0 : 1;
84 | drag(drop(ref));
85 | return (
86 |
87 | {children}
88 |
89 | );
90 | };
91 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/DeleteButton.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import Button from "antd/es/button/index";
3 | import Popconfirm from "antd/es/popconfirm/index";
4 | import { deleteElement } from "../../reducer/actions";
5 | import QuizzContext, { IQuizzBuilderContext } from "../../../QuizzContext";
6 | import TranslatedText from "../../../translations/TranslatedText";
7 |
8 | // applies delete and edit capabilities
9 | export default (props: any) => {
10 | const { dispatch } = useContext(QuizzContext) as IQuizzBuilderContext;
11 |
12 | return (
13 | dispatch(deleteElement(props.id))}
17 | title={}
18 | okText={}
19 | cancelText={}
20 | >
21 |
22 |
23 | );
24 | };
25 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/EditButton.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Button, { ButtonProps } from "antd/es/button/index";
3 |
4 | // applies delete and edit capabilities
5 | export default (props: ButtonProps) => {
6 | return ;
7 | };
8 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/SettingsForm/CustomFormInput/OptionsInput/AddInputOption.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState } from "react";
2 | import BottomButtons from "../../../../../../../../ReusableComponents/BottomButtons";
3 | import Button from "antd/es/button/button";
4 | import Drawer from "antd/es/drawer";
5 | import Input from "antd/es/input/Input";
6 | import FormItem from "antd/es/form/FormItem";
7 | import message from "antd/es/message";
8 | import TextWithInfo from "../../../../../../../../ReusableComponents/TextWithInfo";
9 | import ISO6391 from "iso-639-1";
10 | import TranslatedText from "../../../../../../../../translations/TranslatedText";
11 |
12 | const { getNativeName } = ISO6391;
13 |
14 | export default ({ onAdd, languagesList, existingOptions }: any) => {
15 | const [drawerVisibility, setDrawerVisibility] = useState(false);
16 | const [newOption, setNewOption] = useState({ text: {} } as any);
17 |
18 | // opens drawer when add button is clicked
19 | function openDrawer() {
20 | setDrawerVisibility(true);
21 | }
22 |
23 | // close drawer
24 | function closeDrawer() {
25 | setDrawerVisibility(false);
26 | }
27 |
28 | // on change input value | text
29 | function onChangeInput(
30 | e: React.ChangeEvent,
31 | language?: string
32 | ) {
33 | const tempNewOption = { ...newOption };
34 |
35 | if (language) {
36 | tempNewOption["text"][language] = e.target.value;
37 | } else {
38 | tempNewOption["value"] = e.target.value;
39 | }
40 | setNewOption(tempNewOption);
41 | }
42 |
43 | // save new added option
44 | function onSaveOption() {
45 | const newOptionValue = newOption["value"];
46 | const newOptionListText = Object.values(newOption["text"]);
47 |
48 | // check if value already exists (iterates over existing options)
49 | for (let index = 0; index < existingOptions.length; index++) {
50 | const currentOptionValue = existingOptions[index]["value"];
51 | if (newOptionValue === currentOptionValue) {
52 | // error message for already existing value (values must be unique by question)
53 | return message.error("Value already exists");
54 | }
55 | }
56 |
57 | if (newOptionListText.length === languagesList.length) {
58 | // sends new option back with text(on each language) and value
59 | onAdd(newOption);
60 | // closes this drawer
61 | closeDrawer();
62 | } else {
63 | // error message for required languages
64 | message.error("All fields are required");
65 | }
66 | }
67 |
68 | return (
69 |
70 |
73 |
80 |
81 | {languagesList.map((language: string, i: number) => {
82 | return (
83 |
84 |
85 | {getNativeName(language)}
86 |
87 | onChangeInput(e, language)} />
88 |
89 | );
90 | })}
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 | );
99 | };
100 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/SettingsForm/CustomFormInput/OptionsInput/OptionsInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from "react";
2 | import Tabs from "antd/es/tabs/index";
3 | import Row from "antd/es/row/index";
4 | import Col from "antd/es/col/index";
5 | import Input from "antd/es/input/Input";
6 | import Button from "antd/es/button/button";
7 | import cloneDeep from "lodash.clonedeep";
8 | import TextWithInfo from "../../../../../../../../ReusableComponents/TextWithInfo";
9 | import AddInputOption from "./AddInputOption";
10 |
11 | const { TabPane } = Tabs;
12 |
13 | export default forwardRef((props: any, ref) => {
14 | const {
15 | value,
16 | onChange,
17 | currentLanguage,
18 | setLanguage,
19 | languagesList
20 | } = props;
21 |
22 | // handles on change ofr text and value
23 | function onChangeInput(index: number, content: string, language?: string) {
24 | const valueClone = cloneDeep(value);
25 | if (language) {
26 | // language exists means its text editing
27 | valueClone[index]["text"][language] = content;
28 | } else {
29 | // value editing
30 | valueClone[index]["value"] = content;
31 | }
32 |
33 | onChange(valueClone);
34 | }
35 |
36 | // handles delete option
37 | function onDelete(index: number) {
38 | const valueClone = cloneDeep(value);
39 | // removes provided index
40 | valueClone.splice(index, 1);
41 |
42 | onChange(valueClone);
43 | }
44 |
45 | // on add new option
46 | function onAddOption(newOption: object) {
47 | const valueClone = cloneDeep(value);
48 | valueClone.push(newOption);
49 |
50 | onChange(valueClone);
51 | }
52 |
53 | return (
54 |
55 |
56 |
61 |
62 |
63 |
64 |
67 | Each option can be translated
68 | Current Language: {currentLanguage}
69 | >
70 | }
71 | >
72 | Text:
73 |
74 | {/* Text options */}
75 | }
79 | >
80 | {languagesList.map((language: string) => (
81 |
82 | {value.map((option: any, i: number) => (
83 | onChangeInput(i, e.target.value, language)}
87 | />
88 | ))}
89 |
90 | ))}
91 |
92 |
93 |
94 |
95 | Value:
96 |
97 |
98 | {/* VALUES ARE THE SAME BETWEEN LANGUAGES */}
99 | {value.map((option: any, i: number) => (
100 |
101 |
102 | onChangeInput(i, e.target.value)}
105 | />
106 |
107 |
108 | {i > 0 ? (
109 | /* only show delete button if not first option */
110 |
119 | ))}
120 |
121 |
122 |
123 | );
124 | });
125 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/SettingsForm/CustomFormInput/QuillFormInput/AddDropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { memo } from "react";
2 | import ISO6391 from "iso-639-1";
3 | import { Dropdown, Button, Menu } from "antd";
4 | import styles from "../../../drawer.module.css";
5 |
6 | const { Item: MenuItem } = Menu;
7 | const { getNativeName, getAllCodes } = ISO6391;
8 | const ISO6391AllCodes = getAllCodes();
9 |
10 | interface AddDropdownProps {
11 | onChange?: (lnaguageCode: string) => void;
12 | disabled?: string[];
13 | }
14 |
15 | function AddDropdown(props: AddDropdownProps) {
16 | const { onChange, disabled } = props;
17 |
18 | function onMenuItemClick({ key: lang }: any) {
19 | if (onChange) {
20 | onChange(lang);
21 | }
22 | }
23 |
24 | const menu = (
25 |
35 | );
36 |
37 | return (
38 |
39 |
40 |
41 | );
42 | }
43 |
44 | export default memo(AddDropdown);
45 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/SettingsForm/CustomFormInput/QuillFormInput/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { forwardRef } from "react";
2 | import ReactQuill, { Quill } from "react-quill";
3 | import { Tabs, Tooltip, Row, Popconfirm, Button } from "antd";
4 | import "react-quill/dist/quill.snow.css";
5 | import "./quillFormInput.css";
6 | import ISO6391 from "iso-639-1";
7 | import AddDropdown from "./AddDropdown";
8 | import TranslatedText from "../../../../../../../../translations/TranslatedText";
9 |
10 | const { TabPane } = Tabs;
11 | const { getNativeName } = ISO6391;
12 |
13 | const Size = Quill.import("attributors/style/size");
14 | const text_size = [
15 | "6px",
16 | "7px",
17 | "8px",
18 | "9px",
19 | "10px",
20 | "10.5px",
21 | "11px",
22 | "12px",
23 | "13px",
24 | "14px",
25 | "15px",
26 | "16px",
27 | "18px",
28 | "20px",
29 | "22px",
30 | "24px",
31 | "26px",
32 | "28px",
33 | "32px",
34 | "36px",
35 | "40px",
36 | "44px",
37 | "48px",
38 | "54px",
39 | "60px",
40 | "66px",
41 | "72px",
42 | "80px",
43 | "88px",
44 | "96px",
45 | ];
46 | Size.whitelist = text_size;
47 | Quill.register(Size, true);
48 |
49 | const modules = {
50 | toolbar: {
51 | container: [
52 | [{ header: [1, 2, false] }, { font: [] }, { size: text_size }],
53 | [{ color: [] }, { background: [] }],
54 | ["bold", "italic", "underline", "strike", "blockquote"],
55 | [
56 | { list: "ordered" },
57 | { list: "bullet" },
58 | { indent: "-1" },
59 | { indent: "+1" },
60 | ],
61 | ["link" /* 'image', 'video' */],
62 | ["clean"],
63 | ],
64 | },
65 | };
66 |
67 | export default forwardRef((props: any, ref) => {
68 | const {
69 | value,
70 | defaultValue,
71 | onChange,
72 | currentLanguage,
73 | setLanguage,
74 | onNewLanguage,
75 | onRemoveLanguage,
76 | } = props;
77 | const questionLanguages: Array = Object.keys(value);
78 |
79 | function onChangeHandler(
80 | content: string
81 | // delta: Quill.Delta,
82 | // source: Quill.Sources,
83 | // editor: UnprivilegedEditor
84 | ) {
85 | onChange({ ...value, [currentLanguage]: content });
86 | }
87 |
88 | function preventPropagation(e?: React.MouseEvent) {
89 | if (e) {
90 | e.stopPropagation();
91 | e.preventDefault();
92 | }
93 | }
94 | return (
95 |
101 | }
102 | >
103 | {questionLanguages.map((lang) => (
104 |
108 | {getNativeName(lang)}
109 |
110 | {questionLanguages.length > 1 ? (
111 | }
113 | okText={}
114 | cancelText={}
115 | onConfirm={(e) => {
116 | preventPropagation(e);
117 | onRemoveLanguage(lang);
118 | }}
119 | >
120 |
126 |
127 | ) : null}
128 |
129 | }
130 | key={lang}
131 | >
132 |
140 |
141 | ))}
142 |
143 | );
144 | });
145 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/SettingsForm/CustomFormInput/QuillFormInput/quillFormInput.css:
--------------------------------------------------------------------------------
1 | .ql-container.ql-snow {
2 | max-height: 150px;
3 | overflow: auto;
4 | }
5 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/SettingsForm/SettingsForm.tsx:
--------------------------------------------------------------------------------
1 | import React, { PureComponent } from "react";
2 | import { FormComponentProps } from "antd/lib/form/Form";
3 | import { Form, Checkbox } from "antd";
4 | import QuillFormInput from "./CustomFormInput/QuillFormInput";
5 | import TranslatedText from "../../../../../../translations/TranslatedText";
6 | import OptionsInput from "./CustomFormInput/OptionsInput/OptionsInput";
7 | import cloneDeep from "lodash.clonedeep";
8 |
9 | // this must be a class component because of parent components acessing the prop "wrappedComponentRef"
10 | // to be able to access form props and make a custom submit on parent for example.
11 | interface DrawerFormProps extends FormComponentProps {
12 | [k: string]: any;
13 | }
14 | class DrawerForm extends PureComponent {
15 | constructor(props: any) {
16 | super(props);
17 |
18 | const languagesList = Object.keys(props.inputData.questions);
19 |
20 | this.state = {
21 | languagesList,
22 | currentLanguage: languagesList[0]
23 | };
24 | }
25 |
26 | setLanguage = (currentLanguage: any) => {
27 | this.setState({ currentLanguage });
28 | };
29 |
30 | onNewLanguage = (lang: string) => {
31 | const { form, inputData } = this.props;
32 | const { setFieldsValue, getFieldsValue } = form;
33 |
34 | const fieldsNames = ["questions"];
35 | if (inputData.options) {
36 | fieldsNames.push("options");
37 | }
38 | const { questions, options } = cloneDeep(getFieldsValue(fieldsNames));
39 | questions[lang] = "";
40 |
41 | if (options) {
42 | for (let index = 0; index < options.length; index++) {
43 | options[index].text[lang] = "";
44 | }
45 | }
46 |
47 | setFieldsValue({ questions, options });
48 | this.setState({
49 | languagesList: Object.keys(questions)
50 | });
51 | };
52 |
53 | onRemoveLanguage = (lang: string) => {
54 | /* REMOVE LANGUAGE HERE */
55 | const { form, inputData } = this.props;
56 | const { setFieldsValue, getFieldsValue } = form;
57 |
58 | const fieldsNames = ["questions"];
59 | if (inputData.options) {
60 | fieldsNames.push("options");
61 | }
62 | const { questions, options } = cloneDeep(getFieldsValue(fieldsNames));
63 |
64 | delete questions[lang];
65 |
66 | if (options) {
67 | for (let index = 0; index < options.length; index++) {
68 | delete options[index].text[lang];
69 | }
70 | }
71 | const listLanguages = Object.keys(questions);
72 |
73 | this.setState(
74 | {
75 | currentLanguage: listLanguages[0],
76 | languagesList: listLanguages
77 | },
78 | () => {
79 | // fix's bug that some times one of the updates was lost
80 | setFieldsValue({ questions, options });
81 | }
82 | );
83 | };
84 |
85 | render() {
86 | const {
87 | form,
88 | // toolboxData,
89 | inputData
90 | } = this.props,
91 | { currentLanguage, languagesList } = this.state,
92 | { getFieldDecorator } = form,
93 | { questions, required, options } = inputData;
94 |
95 | return (
96 | }>
98 | {getFieldDecorator("questions", {
99 | initialValue: questions,
100 | rules: [{ required: true, message: "Required field" }]
101 | })(
102 |
108 | )}
109 |
110 |
111 | {options ? (
112 | }>
113 | {getFieldDecorator("options", {
114 | initialValue: options,
115 | rules: [{ required: true, message: "Required field" }]
116 | })(
117 |
121 | )}
122 |
123 | ) : null}
124 |
125 |
126 | {getFieldDecorator("required", {
127 | initialValue: required,
128 | valuePropName: "checked"
129 | })(
130 |
131 |
132 |
133 | )}
134 |
135 |
136 | );
137 | }
138 | }
139 |
140 | export default Form.create()(DrawerForm);
141 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/drawer.module.css:
--------------------------------------------------------------------------------
1 | .settingsForm :global(.ant-drawer-wrapper-body) {
2 | width: 100%;
3 | height: 100%;
4 | }
5 |
6 | .settingsForm :global(.ant-drawer-body) {
7 | height: calc(100% - 55px);
8 | }
9 |
10 | /* makes settings drawer wider */
11 | .settingsForm :global(.ant-drawer-content-wrapper) {
12 | width: auto !important;
13 | max-width: 500px;
14 | }
15 |
16 | @media only screen and (max-width: 500px) {
17 | .settingsForm :global(.ant-drawer-content-wrapper) {
18 | max-width: 300px;
19 | }
20 | }
21 | /* Quill/tabs styles */
22 | .settingsForm :global(.ql-container):global(.ql-snow) {
23 | max-height: 150px;
24 | overflow: auto;
25 | }
26 |
27 | .settingsForm :global(.ant-tabs-nav) :global(.ant-tabs-tab) :global(.anticon) {
28 | margin-right: 0px !important;
29 | }
30 | .settingsForm
31 | :global(.ant-btn):global(.ant-btn-circle):global(.ant-btn-sm):global(.ant-btn-icon-only) {
32 | font-size: x-small !important;
33 | }
34 | /* languages dropdown */
35 | .dropdownLanguages {
36 | overflow-y: auto;
37 | max-height: 50vh;
38 | }
39 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/SideDrawer/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useRef } from "react";
2 | import { Drawer, Row } from "antd";
3 | import BottomButtons from "../../../../../ReusableComponents/BottomButtons";
4 | import SettingsForm from "./SettingsForm/SettingsForm";
5 | import TranslatedText from "../../../../../translations/TranslatedText";
6 | import QuizzContext, {
7 | IQuizzBuilderContext
8 | } from "../../../../../QuizzContext";
9 | import { patchElement } from "../../../../reducer/actions";
10 | import styles from "./drawer.module.css";
11 |
12 | // applies delete and edit capabilities
13 | export default function SettingsDrawer(props: any) {
14 | const { closeDrawer, showDrawer, toolboxData, inputData } = props,
15 | { dispatch } = useContext(QuizzContext) as IQuizzBuilderContext,
16 | formRef = useRef() as any;
17 |
18 | function onFormSubmit(e: Event) {
19 | e.preventDefault();
20 |
21 | // gets the form ref from child
22 | const { validateFields } = formRef.current.props.form;
23 | validateFields((err: any, values: any) => {
24 | if (!err) {
25 | // adds to reducer
26 | dispatch(patchElement(inputData.id, values));
27 | // close drawer had no errors
28 | closeDrawer();
29 | }
30 | });
31 | }
32 |
33 | return (
34 | }
36 | onClose={closeDrawer}
37 | visible={showDrawer}
38 | destroyOnClose
39 | className={styles.settingsForm}
40 | >
41 |
42 |
47 |
48 |
54 |
55 |
56 |
57 | );
58 | }
59 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/EditElement/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment, useState } from "react";
2 | import EditButton from "./EditButton";
3 | import SideDrawer from "./SideDrawer/index";
4 |
5 | // applies delete and edit capabilities
6 | export default (props: any) => {
7 | const [showDrawer, setShowDrawer] = useState(false);
8 |
9 | function openDrawer() {
10 | setShowDrawer(true);
11 | }
12 |
13 | function closeDrawer() {
14 | setShowDrawer(false);
15 | }
16 |
17 | return (
18 |
19 |
24 |
25 |
26 | );
27 | };
28 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/ElementWrapper/ElementWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Row from "antd/es/row/index";
3 | import Col from "antd/es/col/index";
4 | import DeleteButton from "./DeleteButton";
5 | import EditElement from "./EditElement/index";
6 | import TranslatedText from "../../../translations/TranslatedText";
7 |
8 | // applies delete and edit capabilities
9 | export default (props: any) => {
10 | const { toolboxData, inputData } = props;
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | {props.children}
24 |
25 | );
26 | };
27 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/FormPreview/FormPreview.tsx:
--------------------------------------------------------------------------------
1 | import DnDWrapper from "./DnDWrapper";
2 | import React, { useContext } from "react";
3 | import Empty from "antd/es/empty/index";
4 | import Form from "antd/es/form/index";
5 | import QuizzContext, { IQuizzBuilderContext } from "../../QuizzContext";
6 | 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";
10 |
11 | function PreviewForm(props: any) {
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 | }
21 |
22 | // shows the inputs from the toolbox that are added to the current builder state
23 | return (
24 |
63 | );
64 | }
65 |
66 | export default Form.create()(PreviewForm);
67 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/ToolBoxContainer.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext } from "react";
2 | import QuizzContext, { IQuizzBuilderContext } from "../QuizzContext";
3 | import List from "antd/es/list/index";
4 | import { addElement } from "./reducer/actions";
5 | import uuidV4 from "uuid/v4";
6 | import TranslatedText from "../translations/TranslatedText";
7 |
8 | const ListItem = List.Item;
9 | const ListItemMeta = List.Item.Meta;
10 |
11 | interface IinputData {
12 | id: string;
13 | element: string;
14 | required: boolean;
15 | questions: { [key: string]: any }; // @TODO CHANGE TO QUESTIONS TYPE
16 | options?: { [key: string]: any }; // @TODO CHANGE TO OTIONS TYPE
17 | }
18 |
19 | export default (/* props */) => {
20 | const { dispatch, toolBox } = useContext(
21 | QuizzContext
22 | ) as IQuizzBuilderContext;
23 |
24 | function onClickHandler(item: any) {
25 | const elementOptions: IinputData = {
26 | id: uuidV4(),
27 | element: item.key,
28 | required: false,
29 | questions: item.questions
30 | };
31 |
32 | if (item.options) {
33 | elementOptions["options"] = item.options;
34 | }
35 |
36 | dispatch(addElement(elementOptions));
37 | }
38 |
39 | return (
40 | (
45 | onClickHandler(item)}>
46 | }
49 | description={
50 | item.description ? : null
51 | }
52 | />
53 |
54 | )}
55 | />
56 | );
57 | };
58 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/reducer/actions.ts:
--------------------------------------------------------------------------------
1 | export const addElement = (item: object) => {
2 | return {
3 | type: "ADD_ELEMENT",
4 | item,
5 | } as const;
6 | };
7 |
8 | export const patchElement = (id: string, newitem: Object) => {
9 | return {
10 | type: "PATCH_ELEMENT",
11 | id,
12 | newitem,
13 | } as const;
14 | };
15 |
16 | export const deleteElement = (id: string) => {
17 | return {
18 | type: "DELETE_ELEMENT",
19 | id,
20 | } as const;
21 | };
22 |
23 | export const moveElement = (oldIndex: number, newIndex: number) => {
24 | return {
25 | type: "MOVE_ELEMENT",
26 | oldIndex,
27 | newIndex,
28 | } as const;
29 | };
30 |
31 | export type IActions = ReturnType<
32 | | typeof addElement
33 | | typeof deleteElement
34 | | typeof patchElement
35 | | typeof moveElement
36 | >;
37 |
--------------------------------------------------------------------------------
/src/QuizzBuilder/reducer/reducer.ts:
--------------------------------------------------------------------------------
1 | import { IActions } from "./actions";
2 | import clonedeep from "lodash.clonedeep";
3 |
4 | type Maptype = Map;
5 |
6 | function mapClone(map: Maptype) {
7 | return clonedeep(map);
8 | }
9 |
10 | export const initialState = (initialValue: any) =>
11 | new Map([["data", initialValue || []]]);
12 |
13 | export type IState = ReturnType;
14 |
15 | export function reducer(state: IState, action: IActions) {
16 | switch (action.type) {
17 | case "ADD_ELEMENT": {
18 | const new_state = mapClone(state),
19 | new_data = new_state.get("data");
20 | const { item } = action;
21 | new_data.push(item);
22 |
23 | return new_state.set("data", new_data);
24 | }
25 |
26 | case "DELETE_ELEMENT": {
27 | const new_state = mapClone(state),
28 | new_data = new_state.get("data"),
29 | { id } = action;
30 |
31 | for (let index = 0; index < new_data.length; index++) {
32 | const element = new_data[index];
33 | if (element["id"] === id) {
34 | // if it finds equal id on list deletes it
35 | new_data.splice(index, 1);
36 | }
37 | }
38 |
39 | return new_state.set("data", new_data);
40 | }
41 |
42 | case "PATCH_ELEMENT": {
43 | const new_state = mapClone(state),
44 | new_data = new_state.get("data"),
45 | { id, newitem } = action;
46 |
47 | for (let index = 0; index < new_data.length; index++) {
48 | const element = new_data[index];
49 | if (element["id"] === id) {
50 | // if it finds equal id on list updates it
51 | new_data[index] = Object.assign(element, newitem);
52 | break;
53 | }
54 | }
55 |
56 | return new_state.set("data", new_data);
57 | }
58 |
59 | case "MOVE_ELEMENT": {
60 | const new_state = mapClone(state);
61 | const new_data = new_state.get("data");
62 | const { newIndex, oldIndex } = action;
63 |
64 | if (newIndex >= new_data.length) {
65 | var k = newIndex - new_data.length + 1;
66 | while (k--) {
67 | new_data.push(undefined);
68 | }
69 | }
70 | new_data.splice(newIndex, 0, new_data.splice(oldIndex, 1)[0]);
71 |
72 | return new_state.set("data", new_data);
73 | }
74 |
75 | default:
76 | throw new Error("unkown reducer action type");
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/QuizzContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext } from "react";
2 | import { MessagesRecord } from "./translations/TranslatedText";
3 | import { IState } from "./QuizzBuilder/reducer/reducer";
4 | import { IActions } from "./QuizzBuilder/reducer/actions";
5 | import { defaultMessages } from "./translations/TranslatedText";
6 | import defaulttoolBox from "./ToolBox/index";
7 |
8 | interface IContextProps {
9 | toolBox: Array