mirror of
https://github.com/ets-cfuhrman-pfe/EvalueTonSavoir.git
synced 2025-08-11 21:23:54 -04:00
dragable question in Editor
This commit is contained in:
parent
5ff5b2018f
commit
11b719e2ca
4 changed files with 284 additions and 52 deletions
113
client/package-lock.json
generated
113
client/package-lock.json
generated
|
|
@ -28,6 +28,7 @@
|
|||
"marked": "^14.1.2",
|
||||
"nanoid": "^5.1.2",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-modal": "^3.16.3",
|
||||
"react-router-dom": "^6.26.2",
|
||||
|
|
@ -48,6 +49,7 @@
|
|||
"@types/jest": "^29.5.13",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-latex": "^2.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||
|
|
@ -4555,6 +4557,15 @@
|
|||
"@types/unist": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/hoist-non-react-statics": {
|
||||
"version": "3.3.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
|
||||
"integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==",
|
||||
"dependencies": {
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
|
|
@ -4695,6 +4706,15 @@
|
|||
"csstype": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-beautiful-dnd": {
|
||||
"version": "13.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-beautiful-dnd/-/react-beautiful-dnd-13.1.8.tgz",
|
||||
"integrity": "sha512-E3TyFsro9pQuK4r8S/OL6G99eq7p8v29sX0PM7oT8Z+PJfZvSQTx4zTQbUJ+QZXioAF0e7TGBEcA1XhYhCweyQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz",
|
||||
|
|
@ -4715,6 +4735,17 @@
|
|||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-redux": {
|
||||
"version": "7.1.34",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
|
||||
"integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.0",
|
||||
"@types/react": "*",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"redux": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-transition-group": {
|
||||
"version": "4.4.12",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
||||
|
|
@ -5978,6 +6009,14 @@
|
|||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/css-box-model": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
|
||||
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
|
||||
"dependencies": {
|
||||
"tiny-invariant": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/css.escape": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
|
|
@ -9945,6 +9984,11 @@
|
|||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/memoize-one": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
|
||||
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz",
|
||||
|
|
@ -11188,6 +11232,11 @@
|
|||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/raf-schd": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
|
||||
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ=="
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
|
|
@ -11200,6 +11249,25 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-beautiful-dnd": {
|
||||
"version": "13.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz",
|
||||
"integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==",
|
||||
"deprecated": "react-beautiful-dnd is now deprecated. Context and options: https://github.com/atlassian/react-beautiful-dnd/issues/2672",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2",
|
||||
"css-box-model": "^1.2.0",
|
||||
"memoize-one": "^5.1.1",
|
||||
"raf-schd": "^4.0.2",
|
||||
"react-redux": "^7.2.0",
|
||||
"redux": "^4.0.4",
|
||||
"use-memo-one": "^1.1.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.5 || ^17.0.0 || ^18.0.0",
|
||||
"react-dom": "^16.8.5 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
|
|
@ -11241,6 +11309,35 @@
|
|||
"react-dom": "^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "7.2.9",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@types/react-redux": "^7.1.20",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
"loose-envify": "^1.4.0",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-is": "^17.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.3 || ^17 || ^18"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react-dom": {
|
||||
"optional": true
|
||||
},
|
||||
"react-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-redux/node_modules/react-is": {
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="
|
||||
},
|
||||
"node_modules/react-router": {
|
||||
"version": "6.30.0",
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.0.tgz",
|
||||
|
|
@ -11316,6 +11413,14 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
},
|
||||
"node_modules/reflect.getprototypeof": {
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||
|
|
@ -12841,6 +12946,14 @@
|
|||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-memo-one": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz",
|
||||
"integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "9.0.1",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
"marked": "^14.1.2",
|
||||
"nanoid": "^5.1.2",
|
||||
"react": "^18.3.1",
|
||||
"react-beautiful-dnd": "^13.1.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-modal": "^3.16.3",
|
||||
"react-router-dom": "^6.26.2",
|
||||
|
|
@ -52,6 +53,7 @@
|
|||
"@types/jest": "^29.5.13",
|
||||
"@types/node": "^22.13.5",
|
||||
"@types/react": "^18.2.15",
|
||||
"@types/react-beautiful-dnd": "^13.1.8",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"@types/react-latex": "^2.0.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.25.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
import React from 'react';
|
||||
import { TextField, Typography, IconButton, Box } from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import { TextField, Typography, IconButton, Box, Collapse, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, Button } from '@mui/material';
|
||||
import DeleteIcon from '@mui/icons-material/Delete';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
|
||||
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
|
||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd';
|
||||
|
||||
interface EditorProps {
|
||||
label: string;
|
||||
|
|
@ -11,6 +14,10 @@ interface EditorProps {
|
|||
}
|
||||
|
||||
const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange, onFocusQuestion }) => {
|
||||
const [collapsed, setCollapsed] = useState<boolean[]>(Array(values.length).fill(false));
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [deleteIndex, setDeleteIndex] = useState<number | null>(null);
|
||||
|
||||
const handleChange = (index: number) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValues = [...values];
|
||||
newValues[index] = event.target.value;
|
||||
|
|
@ -18,16 +25,67 @@ const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange, onFocusQ
|
|||
};
|
||||
|
||||
const handleDeleteQuestion = (index: number) => () => {
|
||||
const newValues = values.filter((_, i) => i !== index); // Remove the question at the specified index
|
||||
if (values[index].trim() === '') {
|
||||
const newValues = values.filter((_, i) => i !== index);
|
||||
onValuesChange(newValues);
|
||||
setCollapsed((prev) => prev.filter((_, i) => i !== index));
|
||||
} else {
|
||||
setDeleteIndex(index);
|
||||
setDialogOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmDelete = () => {
|
||||
if (deleteIndex !== null) {
|
||||
const newValues = values.filter((_, i) => i !== deleteIndex);
|
||||
onValuesChange(newValues);
|
||||
setCollapsed((prev) => prev.filter((_, i) => i !== deleteIndex));
|
||||
}
|
||||
setDialogOpen(false);
|
||||
setDeleteIndex(null);
|
||||
};
|
||||
|
||||
const handleCancelDelete = () => {
|
||||
setDialogOpen(false);
|
||||
setDeleteIndex(null);
|
||||
};
|
||||
|
||||
const handleFocusQuestion = (index: number) => () => {
|
||||
if (onFocusQuestion) {
|
||||
onFocusQuestion(index); // Call the focus function if provided
|
||||
}
|
||||
onFocusQuestion(index);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleCollapse = (index: number) => () => {
|
||||
setCollapsed((prev) => {
|
||||
const newCollapsed = [...prev];
|
||||
newCollapsed[index] = !newCollapsed[index];
|
||||
return newCollapsed;
|
||||
});
|
||||
};
|
||||
|
||||
const onDragEnd = (result: any) => {
|
||||
if (!result.destination) return;
|
||||
|
||||
const newValues = [...values];
|
||||
const [reorderedItem] = newValues.splice(result.source.index, 1);
|
||||
newValues.splice(result.destination.index, 0, reorderedItem);
|
||||
onValuesChange(newValues);
|
||||
|
||||
const newCollapsed = [...collapsed];
|
||||
const [reorderedCollapsed] = newCollapsed.splice(result.source.index, 1);
|
||||
newCollapsed.splice(result.destination.index, 0, reorderedCollapsed);
|
||||
setCollapsed(newCollapsed);
|
||||
};
|
||||
|
||||
if (collapsed.length !== values.length) {
|
||||
setCollapsed((prev) => {
|
||||
const newCollapsed = [...prev];
|
||||
while (newCollapsed.length < values.length) newCollapsed.push(false);
|
||||
while (newCollapsed.length > values.length) newCollapsed.pop();
|
||||
return newCollapsed;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
|
@ -35,26 +93,53 @@ const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange, onFocusQ
|
|||
{label}
|
||||
</Typography>
|
||||
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="questions">
|
||||
{(provided) => (
|
||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||
{values.map((value, index) => (
|
||||
<Box key={index} style={{ marginBottom: '24px' }}>
|
||||
<Draggable key={index} draggableId={`question-${index}`} index={index}>
|
||||
{(provided) => (
|
||||
<Box
|
||||
ref={provided.innerRef}
|
||||
{...provided.draggableProps}
|
||||
{...provided.dragHandleProps}
|
||||
sx={{
|
||||
marginBottom: '24px',
|
||||
boxShadow: '0px 2px 4px rgba(0, 0, 0, 0.1)',
|
||||
border: '1px solid rgba(0, 0, 0, 0.1)',
|
||||
padding: '16px',
|
||||
borderRadius: '4px',
|
||||
...provided.draggableProps.style,
|
||||
}}
|
||||
>
|
||||
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||
<Typography variant="subtitle1" fontWeight="bold" style={{ marginBottom: '8px' }}>
|
||||
Question {index + 1}
|
||||
</Typography>
|
||||
<Box>
|
||||
{/* Focus (Eye) Button */}
|
||||
<IconButton
|
||||
onClick={handleToggleCollapse(index)}
|
||||
aria-label="toggle collapse"
|
||||
sx={{
|
||||
color: 'gray',
|
||||
'&:hover': { color: 'blue' },
|
||||
mr: 1,
|
||||
}}
|
||||
>
|
||||
{collapsed[index] ? <ExpandMoreIcon /> : <ExpandLessIcon />}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={handleFocusQuestion(index)}
|
||||
aria-label="focus question"
|
||||
sx={{
|
||||
color: 'gray',
|
||||
'&:hover': { color: 'blue' },
|
||||
marginRight: '8px', // Space between eye and delete
|
||||
mr: 1,
|
||||
}}
|
||||
>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
{/* Delete Button */}
|
||||
<IconButton
|
||||
onClick={handleDeleteQuestion(index)}
|
||||
aria-label="delete"
|
||||
|
|
@ -67,6 +152,7 @@ const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange, onFocusQ
|
|||
</IconButton>
|
||||
</Box>
|
||||
</Box>
|
||||
<Collapse in={!collapsed[index]}>
|
||||
<TextField
|
||||
value={value}
|
||||
onChange={handleChange(index)}
|
||||
|
|
@ -77,8 +163,39 @@ const Editor: React.FC<EditorProps> = ({ label, values, onValuesChange, onFocusQ
|
|||
variant="outlined"
|
||||
style={{ overflow: 'auto' }}
|
||||
/>
|
||||
</Collapse>
|
||||
</Box>
|
||||
)}
|
||||
</Draggable>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
|
||||
{/* Confirmation Dialog */}
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onClose={handleCancelDelete}
|
||||
aria-labelledby="delete-confirmation-title"
|
||||
aria-describedby="delete-confirmation-description"
|
||||
>
|
||||
<DialogTitle id="delete-confirmation-title" sx={{ textAlign: 'center'}}>Suppression</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="delete-confirmation-description">
|
||||
Confirmez vous la suppression de Question {deleteIndex !== null ? deleteIndex + 1 : ''} ?
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ justifyContent: 'center', pb: 2 }}>
|
||||
<Button onClick={handleCancelDelete} color="primary" sx={{ mx: 1 }}>
|
||||
Annuler
|
||||
</Button>
|
||||
<Button onClick={handleConfirmDelete} color="error" sx={{ mx: 1 }} autoFocus>
|
||||
Supprimer
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -272,7 +272,7 @@ const QuizForm: React.FC = () => {
|
|||
|
||||
<div className='edit'>
|
||||
<Editor
|
||||
label="Contenu GIFT de chaque question:"
|
||||
label=""
|
||||
values={values}
|
||||
onValuesChange={handleUpdatePreview}
|
||||
onFocusQuestion={handleFocusQuestion} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue