@capytale/meta-player 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +35 -0
- package/.prettierrc.json +4 -0
- package/README.md +27 -0
- package/index.html +15 -0
- package/package.json +48 -0
- package/public/themes/lara-dark-blue/fonts/Inter-italic.var.woff2 +0 -0
- package/public/themes/lara-dark-blue/fonts/Inter-roman.var.woff2 +0 -0
- package/public/themes/lara-dark-blue/theme.css +7015 -0
- package/public/themes/lara-light-blue/fonts/Inter-italic.var.woff2 +0 -0
- package/public/themes/lara-light-blue/fonts/Inter-roman.var.woff2 +0 -0
- package/public/themes/lara-light-blue/theme.css +7005 -0
- package/src/App.tsx +116 -0
- package/src/AppRedux.css +39 -0
- package/src/MetaPlayer.tsx +51 -0
- package/src/app/createAppSlice.ts +6 -0
- package/src/app/hooks.ts +12 -0
- package/src/app/store.ts +46 -0
- package/src/app.module.scss +56 -0
- package/src/demo.tsx +81 -0
- package/src/features/activityData/IsDirtySetter.tsx +17 -0
- package/src/features/activityData/OptionSetter.tsx +35 -0
- package/src/features/activityData/activityDataSlice.ts +250 -0
- package/src/features/activityData/metaPlayerOptions.ts +17 -0
- package/src/features/activityJS/ActivityJSProvider.tsx +110 -0
- package/src/features/activityJS/AfterSaveAction.tsx +23 -0
- package/src/features/activityJS/BeforeSaveAction.tsx +23 -0
- package/src/features/activityJS/Saver.tsx +147 -0
- package/src/features/activityJS/hooks.ts +93 -0
- package/src/features/activityJS/saverSlice.ts +58 -0
- package/src/features/layout/layoutSlice.ts +76 -0
- package/src/features/navbar/ActivityInfo.tsx +41 -0
- package/src/features/navbar/ActivityMenu.tsx +56 -0
- package/src/features/navbar/ActivityQuickActions.tsx +52 -0
- package/src/features/navbar/ActivitySidebarActions.tsx +52 -0
- package/src/features/navbar/CapytaleMenu.tsx +183 -0
- package/src/features/navbar/GradingNav.tsx +120 -0
- package/src/features/navbar/ReviewNavbar.tsx +18 -0
- package/src/features/navbar/SidebarContent.tsx +125 -0
- package/src/features/navbar/index.tsx +33 -0
- package/src/features/navbar/navbarSlice.ts +51 -0
- package/src/features/navbar/student-utils.ts +11 -0
- package/src/features/navbar/style.module.scss +162 -0
- package/src/features/pedago/AnswerSheetEditor.tsx +65 -0
- package/src/features/pedago/InstructionsEditor.tsx +82 -0
- package/src/features/pedago/index.tsx +219 -0
- package/src/features/pedago/style.module.scss +104 -0
- package/src/features/theming/ThemeSwitcher.tsx +51 -0
- package/src/features/theming/themingSlice.ts +93 -0
- package/src/hooks/index.ts +8 -0
- package/src/index.css +30 -0
- package/src/index.tsx +6 -0
- package/src/logo.svg +1 -0
- package/src/my_json_data.js +4146 -0
- package/src/settings.ts +6 -0
- package/src/setupTests.ts +1 -0
- package/src/utils/ErrorBoundary.tsx +41 -0
- package/src/utils/PopupButton.tsx +135 -0
- package/src/utils/activity.ts +8 -0
- package/src/utils/breakpoints.ts +5 -0
- package/src/utils/clipboard.ts +11 -0
- package/src/utils/test-utils.tsx +65 -0
- package/src/utils/useFullscreen.ts +65 -0
- package/src/vite-env.d.ts +1 -0
- package/tsconfig.json +27 -0
- package/tsconfig.node.json +9 -0
- package/vite.config.ts +17 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { FC } from "react";
|
|
2
|
+
|
|
3
|
+
import { Fieldset } from "primereact/fieldset";
|
|
4
|
+
|
|
5
|
+
import { selectOrientation, setLayout } from "../layout/layoutSlice";
|
|
6
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
7
|
+
import { RadioButton } from "primereact/radiobutton";
|
|
8
|
+
|
|
9
|
+
import styles from "./style.module.scss";
|
|
10
|
+
import {
|
|
11
|
+
selectThemeNameOrAuto,
|
|
12
|
+
setAutoSwitch,
|
|
13
|
+
switchToTheme,
|
|
14
|
+
} from "../theming/themingSlice";
|
|
15
|
+
import { selectSidebarActions } from "./navbarSlice";
|
|
16
|
+
import ActivitySidebarActions from "./ActivitySidebarActions";
|
|
17
|
+
import { classNames } from "primereact/utils";
|
|
18
|
+
import {
|
|
19
|
+
selectCanChoosePedagoLayout,
|
|
20
|
+
selectCanChooseTheme,
|
|
21
|
+
} from "../activityData/activityDataSlice";
|
|
22
|
+
|
|
23
|
+
const SidebarContent: FC = () => {
|
|
24
|
+
const canChooseOrientation = useAppSelector(selectCanChoosePedagoLayout);
|
|
25
|
+
const canChooseTheme = useAppSelector(selectCanChooseTheme);
|
|
26
|
+
const orientation = useAppSelector(selectOrientation);
|
|
27
|
+
const themeName = useAppSelector(selectThemeNameOrAuto);
|
|
28
|
+
const sidebarActions = useAppSelector(selectSidebarActions);
|
|
29
|
+
const dispatch = useAppDispatch();
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
{sidebarActions.length > 0 && (
|
|
33
|
+
<Fieldset
|
|
34
|
+
legend="Actions"
|
|
35
|
+
className={classNames(
|
|
36
|
+
styles.sidebarFieldset,
|
|
37
|
+
styles.sidebarFieldsetButtons,
|
|
38
|
+
)}
|
|
39
|
+
>
|
|
40
|
+
<ActivitySidebarActions />
|
|
41
|
+
</Fieldset>
|
|
42
|
+
)}
|
|
43
|
+
{canChooseOrientation && (
|
|
44
|
+
<Fieldset legend="Disposition" className={styles.sidebarFieldset}>
|
|
45
|
+
<div className={styles.sidebarRadioButtons}>
|
|
46
|
+
<div className={styles.sidebarRadioGroup}>
|
|
47
|
+
<RadioButton
|
|
48
|
+
inputId="rb-horizontal"
|
|
49
|
+
name="horizontal"
|
|
50
|
+
value="horizontal"
|
|
51
|
+
onChange={(e) => dispatch(setLayout(e.value))}
|
|
52
|
+
checked={orientation === "horizontal"}
|
|
53
|
+
/>
|
|
54
|
+
<label htmlFor="rb-horizontal" className="ml-2">
|
|
55
|
+
Horizontale
|
|
56
|
+
</label>
|
|
57
|
+
</div>
|
|
58
|
+
<div className={styles.sidebarRadioGroup}>
|
|
59
|
+
<RadioButton
|
|
60
|
+
inputId="rb-vertical"
|
|
61
|
+
name="vertical"
|
|
62
|
+
value="vertical"
|
|
63
|
+
onChange={(e) => dispatch(setLayout(e.value))}
|
|
64
|
+
checked={orientation === "vertical"}
|
|
65
|
+
/>
|
|
66
|
+
<label htmlFor="rb-vertical" className="ml-2">
|
|
67
|
+
Verticale
|
|
68
|
+
</label>
|
|
69
|
+
</div>
|
|
70
|
+
</div>
|
|
71
|
+
</Fieldset>
|
|
72
|
+
)}
|
|
73
|
+
{canChooseTheme && (
|
|
74
|
+
<Fieldset legend="Thème" className={styles.sidebarFieldset}>
|
|
75
|
+
<div className={styles.sidebarRadioButtons}>
|
|
76
|
+
<div className={styles.sidebarRadioGroup}>
|
|
77
|
+
<RadioButton
|
|
78
|
+
inputId="rb-light"
|
|
79
|
+
name="light"
|
|
80
|
+
value="light"
|
|
81
|
+
onChange={(_e) => {
|
|
82
|
+
dispatch(setAutoSwitch(false));
|
|
83
|
+
dispatch(switchToTheme("light"));
|
|
84
|
+
}}
|
|
85
|
+
checked={themeName === "Clair"}
|
|
86
|
+
/>
|
|
87
|
+
<label htmlFor="rb-light" className="ml-2">
|
|
88
|
+
Clair
|
|
89
|
+
</label>
|
|
90
|
+
</div>
|
|
91
|
+
<div className={styles.sidebarRadioGroup}>
|
|
92
|
+
<RadioButton
|
|
93
|
+
inputId="rb-dark"
|
|
94
|
+
name="dark"
|
|
95
|
+
value="dark"
|
|
96
|
+
onChange={(_e) => {
|
|
97
|
+
dispatch(setAutoSwitch(false));
|
|
98
|
+
dispatch(switchToTheme("dark"));
|
|
99
|
+
}}
|
|
100
|
+
checked={themeName === "Sombre"}
|
|
101
|
+
/>
|
|
102
|
+
<label htmlFor="rb-dark" className="ml-2">
|
|
103
|
+
Sombre
|
|
104
|
+
</label>
|
|
105
|
+
</div>
|
|
106
|
+
<div className={styles.sidebarRadioGroup}>
|
|
107
|
+
<RadioButton
|
|
108
|
+
inputId="rb-auto"
|
|
109
|
+
name="auto"
|
|
110
|
+
value="auto"
|
|
111
|
+
onChange={(_e) => dispatch(setAutoSwitch(true))}
|
|
112
|
+
checked={themeName === "Système (auto)"}
|
|
113
|
+
/>
|
|
114
|
+
<label htmlFor="rb-auto" className="ml-2">
|
|
115
|
+
Système (auto)
|
|
116
|
+
</label>
|
|
117
|
+
</div>
|
|
118
|
+
</div>
|
|
119
|
+
</Fieldset>
|
|
120
|
+
)}
|
|
121
|
+
</>
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
export default SidebarContent;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { MD } from "../../utils/breakpoints";
|
|
2
|
+
import { useWindowSize } from "@uidotdev/usehooks";
|
|
3
|
+
import ActivityInfo from "./ActivityInfo";
|
|
4
|
+
import ActivityMenu from "./ActivityMenu";
|
|
5
|
+
import CapytaleMenu from "./CapytaleMenu";
|
|
6
|
+
import styles from "./style.module.scss";
|
|
7
|
+
import { useMemo } from "react";
|
|
8
|
+
import { useAppSelector } from "../../app/hooks";
|
|
9
|
+
import { selectReturnUrl } from "../activityData/activityDataSlice";
|
|
10
|
+
|
|
11
|
+
const Navbar: React.FC = () => {
|
|
12
|
+
const returnUrl = useAppSelector(selectReturnUrl);
|
|
13
|
+
const windowsSize = useWindowSize();
|
|
14
|
+
const isSmall = useMemo(
|
|
15
|
+
() => windowsSize.width && windowsSize.width < MD,
|
|
16
|
+
[windowsSize.width],
|
|
17
|
+
);
|
|
18
|
+
const logoText = isSmall ? "C" : "CAPYTALE";
|
|
19
|
+
return (
|
|
20
|
+
<div className={styles.navbar}>
|
|
21
|
+
<div className={styles.navbarContainer}>
|
|
22
|
+
<div className={styles.navbarLogo}>
|
|
23
|
+
<a href={returnUrl}>{logoText}</a>
|
|
24
|
+
</div>
|
|
25
|
+
<CapytaleMenu />
|
|
26
|
+
<ActivityInfo />
|
|
27
|
+
<ActivityMenu />
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default Navbar;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import type { PayloadAction } from "@reduxjs/toolkit";
|
|
2
|
+
import { createAppSlice } from "../../app/createAppSlice";
|
|
3
|
+
|
|
4
|
+
export type QuickAction = {
|
|
5
|
+
title: string;
|
|
6
|
+
icon: string;
|
|
7
|
+
action: () => void;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export interface NavbarState {
|
|
11
|
+
quickActions: QuickAction[];
|
|
12
|
+
sidebarActions: QuickAction[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const initialState: NavbarState = {
|
|
16
|
+
quickActions: [],
|
|
17
|
+
sidebarActions: [],
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// If you are not using async thunks you can use the standalone `createSlice`.
|
|
21
|
+
export const navbarSlice = createAppSlice({
|
|
22
|
+
name: "navbar",
|
|
23
|
+
// `createSlice` will infer the state type from the `initialState` argument
|
|
24
|
+
initialState,
|
|
25
|
+
// The `reducers` field lets us define reducers and generate associated actions
|
|
26
|
+
reducers: (create) => ({
|
|
27
|
+
setQuickActions: create.reducer(
|
|
28
|
+
(state, action: PayloadAction<QuickAction[]>) => {
|
|
29
|
+
state.quickActions = action.payload;
|
|
30
|
+
},
|
|
31
|
+
),
|
|
32
|
+
setSidebarActions: create.reducer(
|
|
33
|
+
(state, action: PayloadAction<QuickAction[]>) => {
|
|
34
|
+
state.sidebarActions = action.payload;
|
|
35
|
+
},
|
|
36
|
+
),
|
|
37
|
+
}),
|
|
38
|
+
// You can define your selectors here. These selectors receive the slice
|
|
39
|
+
// state as their first argument.
|
|
40
|
+
selectors: {
|
|
41
|
+
selectQuickActions: (navbar) => navbar.quickActions,
|
|
42
|
+
selectSidebarActions: (navbar) => navbar.sidebarActions,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Action creators are generated for each case reducer function.
|
|
47
|
+
export const { setQuickActions, setSidebarActions } = navbarSlice.actions;
|
|
48
|
+
|
|
49
|
+
// Selectors returned by `slice.selectors` take the root state as their first argument.
|
|
50
|
+
export const { selectQuickActions, selectSidebarActions } =
|
|
51
|
+
navbarSlice.selectors;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { StudentInfo } from "../activityData/activityDataSlice";
|
|
2
|
+
|
|
3
|
+
export function studentNameFromInfo(studentInfo: StudentInfo | null) {
|
|
4
|
+
if (!studentInfo || (!studentInfo.firstName && !studentInfo.lastName)) {
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
if (studentInfo.class && studentInfo.class !== "enseignants") {
|
|
8
|
+
return `${studentInfo.firstName} ${studentInfo.lastName} — ${studentInfo.class}`;
|
|
9
|
+
}
|
|
10
|
+
return `${studentInfo.firstName} ${studentInfo.lastName}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
.navbar {
|
|
2
|
+
--navbar-text-color: rgba(255, 255, 255, 0.95);
|
|
3
|
+
background-color: var(--surface-800);
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
color: var(--navbar-text-color);
|
|
7
|
+
overflow: hidden;
|
|
8
|
+
color-scheme: dark;
|
|
9
|
+
:global(.dark-theme) & {
|
|
10
|
+
--navbar-text-color: var(--text-color);
|
|
11
|
+
background-color: var(--surface-50);
|
|
12
|
+
}
|
|
13
|
+
& :global(.p-button-outlined),
|
|
14
|
+
& :global(.p-button-text) {
|
|
15
|
+
color: var(--navbar-text-color);
|
|
16
|
+
}
|
|
17
|
+
& :global(.p-button-outlined) {
|
|
18
|
+
border-color: var(--navbar-text-color);
|
|
19
|
+
}
|
|
20
|
+
:global(.light-theme) & :global(.p-button.p-button-secondary:enabled:focus) {
|
|
21
|
+
box-shadow:
|
|
22
|
+
0 0 0 2px #1c2127,
|
|
23
|
+
0 0 0 4px rgba(255, 255, 255, 0.7),
|
|
24
|
+
0 1px 2px 0 rgba(0, 0, 0, 0);
|
|
25
|
+
}
|
|
26
|
+
:global(.light-theme) & :global(.p-button.p-button-warning:enabled:focus) {
|
|
27
|
+
box-shadow:
|
|
28
|
+
0 0 0 2px #1c2127,
|
|
29
|
+
0 0 0 4px rgba(249, 115, 22, 0.7),
|
|
30
|
+
0 1px 2px 0 rgba(0, 0, 0, 0);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.reviewNavbar {
|
|
35
|
+
color-scheme: dark;
|
|
36
|
+
background-color: var(--surface-500);
|
|
37
|
+
--review-navbar-text-color: rgba(255, 255, 255, 0.95);
|
|
38
|
+
color: var(--review-navbar-text-color);
|
|
39
|
+
padding: 4px 16px;
|
|
40
|
+
display: flex;
|
|
41
|
+
align-items: center;
|
|
42
|
+
justify-content: space-between;
|
|
43
|
+
& :global(.p-button-secondary) {
|
|
44
|
+
border-color: var(--review-navbar-text-color);
|
|
45
|
+
color: var(--review-navbar-text-color);
|
|
46
|
+
}
|
|
47
|
+
& :global(.p-dropdown) {
|
|
48
|
+
border-color: var(--review-navbar-text-color);
|
|
49
|
+
border-left-width: 0;
|
|
50
|
+
border-right-width: 0;
|
|
51
|
+
}
|
|
52
|
+
:global(.dark-theme) & {
|
|
53
|
+
background-color: var(--surface-100);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.navbarContainer {
|
|
58
|
+
height: 60px;
|
|
59
|
+
flex-grow: 1;
|
|
60
|
+
display: flex;
|
|
61
|
+
justify-content: space-between;
|
|
62
|
+
align-items: center;
|
|
63
|
+
gap: 16px;
|
|
64
|
+
overflow: hidden;
|
|
65
|
+
& > * {
|
|
66
|
+
flex-shrink: 0;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.navbarLogo {
|
|
71
|
+
font-family: Ubuntu, Tahoma, "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
72
|
+
font-weight: 700;
|
|
73
|
+
font-size: 24px;
|
|
74
|
+
letter-spacing: 2px;
|
|
75
|
+
height: 100%;
|
|
76
|
+
& > * {
|
|
77
|
+
height: 100%;
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
justify-content: center;
|
|
81
|
+
padding: 0 16px;
|
|
82
|
+
color: var(--navbar-text-color);
|
|
83
|
+
text-decoration: none;
|
|
84
|
+
&:hover {
|
|
85
|
+
background-color: rgba(255, 255, 255, 0.1);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.capytaleMenu {
|
|
91
|
+
display: flex;
|
|
92
|
+
gap: 8px;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.activityInfo {
|
|
96
|
+
flex-shrink: 1;
|
|
97
|
+
flex-grow: 1;
|
|
98
|
+
display: flex;
|
|
99
|
+
justify-content: center;
|
|
100
|
+
align-items: center;
|
|
101
|
+
overflow: hidden;
|
|
102
|
+
gap: 12px;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.activityInfoText {
|
|
106
|
+
display: flex;
|
|
107
|
+
flex-direction: column;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
align-items: center;
|
|
110
|
+
overflow: hidden;
|
|
111
|
+
& > * {
|
|
112
|
+
overflow: hidden;
|
|
113
|
+
text-overflow: ellipsis;
|
|
114
|
+
white-space: nowrap;
|
|
115
|
+
max-width: 100%;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.activityInfoTitle {
|
|
120
|
+
font-size: 20px;
|
|
121
|
+
font-weight: 500;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.activityLogo {
|
|
125
|
+
width: 35px;
|
|
126
|
+
height: 37px;
|
|
127
|
+
border-radius: 6px;
|
|
128
|
+
flex-shrink: 0;
|
|
129
|
+
@media only screen and (max-width: 992px) {
|
|
130
|
+
display: none;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.activityMenu {
|
|
135
|
+
padding-right: 16px;
|
|
136
|
+
display: flex;
|
|
137
|
+
gap: 8px;
|
|
138
|
+
justify-content: center;
|
|
139
|
+
align-items: center;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.sidebarFieldset {
|
|
143
|
+
margin-bottom: 8px;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.sidebarFieldsetButtons :global(.p-fieldset-content) {
|
|
147
|
+
display: flex;
|
|
148
|
+
flex-direction: column;
|
|
149
|
+
gap: 6px;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.sidebarRadioButtons {
|
|
153
|
+
display: flex;
|
|
154
|
+
flex-direction: column;
|
|
155
|
+
gap: 4px;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.sidebarRadioGroup {
|
|
159
|
+
display: flex;
|
|
160
|
+
gap: 8px;
|
|
161
|
+
align-items: center;
|
|
162
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { useCapytaleRichTextEditor } from "@capytale/capytale-rich-text-editor";
|
|
2
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
3
|
+
import {
|
|
4
|
+
selectAnswerSheetContent,
|
|
5
|
+
selectMode,
|
|
6
|
+
setCanSaveAnswerSheet,
|
|
7
|
+
setLexicalAnswerSheetState,
|
|
8
|
+
} from "../activityData/activityDataSlice";
|
|
9
|
+
import { forwardRef, useImperativeHandle, useRef } from "react";
|
|
10
|
+
import { Toast } from "primereact/toast";
|
|
11
|
+
import settings from "../../settings";
|
|
12
|
+
|
|
13
|
+
const AnswerSheetEditor: React.FC = forwardRef((_props, ref) => {
|
|
14
|
+
const [Editor, getState, _canSave] = useCapytaleRichTextEditor();
|
|
15
|
+
const dispatch = useAppDispatch();
|
|
16
|
+
const toast = useRef<Toast>(null);
|
|
17
|
+
const mode = useAppSelector(selectMode);
|
|
18
|
+
const isEditable =
|
|
19
|
+
mode === "create" || mode === "assignment" || mode === "review";
|
|
20
|
+
const initialEditorState = useAppSelector(selectAnswerSheetContent);
|
|
21
|
+
|
|
22
|
+
useImperativeHandle(ref, () => {
|
|
23
|
+
return {
|
|
24
|
+
save: async () => {
|
|
25
|
+
const state = await getState();
|
|
26
|
+
dispatch(setLexicalAnswerSheetState(state.json));
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<>
|
|
33
|
+
<Toast position="bottom-right" ref={toast} />
|
|
34
|
+
<Editor
|
|
35
|
+
placeholderText="Écrivez le contenu initial de la fiche réponse ici..."
|
|
36
|
+
initialEditorState={initialEditorState}
|
|
37
|
+
jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
|
|
38
|
+
onChange={(editorState) => {
|
|
39
|
+
dispatch(setLexicalAnswerSheetState(editorState));
|
|
40
|
+
}}
|
|
41
|
+
onJsonSizeLimitExceeded={() => {
|
|
42
|
+
dispatch(setCanSaveAnswerSheet(false));
|
|
43
|
+
toast.current!.show({
|
|
44
|
+
summary: "Erreur",
|
|
45
|
+
detail: `Le contenu de la fiche réponse est trop volumineux. Veuillez le réduire avant de l'enregistrer.\nPeut-être avez-vous inséré une image trop volumineuse ?`,
|
|
46
|
+
severity: "error",
|
|
47
|
+
life: 10000,
|
|
48
|
+
});
|
|
49
|
+
}}
|
|
50
|
+
onJsonSizeLimitMet={() => {
|
|
51
|
+
dispatch(setCanSaveAnswerSheet(true));
|
|
52
|
+
toast.current!.show({
|
|
53
|
+
summary: "Succès",
|
|
54
|
+
detail: `Le contenu de la fiche réponse ne dépasse plus la taille limite.`,
|
|
55
|
+
severity: "success",
|
|
56
|
+
life: 4000,
|
|
57
|
+
});
|
|
58
|
+
}}
|
|
59
|
+
isEditable={isEditable}
|
|
60
|
+
/>
|
|
61
|
+
</>
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export default AnswerSheetEditor;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { useCapytaleRichTextEditor } from "@capytale/capytale-rich-text-editor";
|
|
2
|
+
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
|
3
|
+
import {
|
|
4
|
+
selectInstructions,
|
|
5
|
+
selectMode,
|
|
6
|
+
setCanSaveInstructions,
|
|
7
|
+
setLexicalInstructionsState,
|
|
8
|
+
} from "../activityData/activityDataSlice";
|
|
9
|
+
import {
|
|
10
|
+
forwardRef,
|
|
11
|
+
useImperativeHandle,
|
|
12
|
+
useMemo,
|
|
13
|
+
useRef,
|
|
14
|
+
} from "react";
|
|
15
|
+
import { Toast } from "primereact/toast";
|
|
16
|
+
import settings from "../../settings";
|
|
17
|
+
|
|
18
|
+
const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
|
|
19
|
+
const [Editor, getState, _canSave] = useCapytaleRichTextEditor();
|
|
20
|
+
const dispatch = useAppDispatch();
|
|
21
|
+
const toast = useRef<Toast>(null);
|
|
22
|
+
const mode = useAppSelector(selectMode);
|
|
23
|
+
const isEditable = mode === "create";
|
|
24
|
+
const initialInstructions = useAppSelector(selectInstructions);
|
|
25
|
+
const htmlInitialContent = useMemo(() => {
|
|
26
|
+
return initialInstructions?.format === "lexical"
|
|
27
|
+
? undefined
|
|
28
|
+
: initialInstructions?.value === null
|
|
29
|
+
? undefined
|
|
30
|
+
: (initialInstructions?.value as string) || ""; // HTML content in value
|
|
31
|
+
}, [initialInstructions]);
|
|
32
|
+
const initialEditorState = useMemo(() => {
|
|
33
|
+
return initialInstructions?.format === "lexical"
|
|
34
|
+
? initialInstructions?.value
|
|
35
|
+
: undefined;
|
|
36
|
+
}, [initialInstructions]);
|
|
37
|
+
|
|
38
|
+
useImperativeHandle(ref, () => {
|
|
39
|
+
return {
|
|
40
|
+
save: async () => {
|
|
41
|
+
const state = await getState();
|
|
42
|
+
dispatch(setLexicalInstructionsState(state.json));
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<>
|
|
49
|
+
<Toast position="bottom-right" ref={toast} />
|
|
50
|
+
<Editor
|
|
51
|
+
placeholderText="Écrivez la consigne ici..."
|
|
52
|
+
htmlInitialContent={htmlInitialContent}
|
|
53
|
+
initialEditorState={initialEditorState}
|
|
54
|
+
jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
|
|
55
|
+
onChange={(editorState) => {
|
|
56
|
+
dispatch(setLexicalInstructionsState(editorState));
|
|
57
|
+
}}
|
|
58
|
+
onJsonSizeLimitExceeded={() => {
|
|
59
|
+
dispatch(setCanSaveInstructions(false));
|
|
60
|
+
toast.current!.show({
|
|
61
|
+
summary: "Erreur",
|
|
62
|
+
detail: `Le contenu de la consigne est trop volumineux. Veuillez le réduire avant de l'enregistrer.\nPeut-être avez-vous inséré une image trop volumineuse ?`,
|
|
63
|
+
severity: "error",
|
|
64
|
+
life: 10000,
|
|
65
|
+
});
|
|
66
|
+
}}
|
|
67
|
+
onJsonSizeLimitMet={() => {
|
|
68
|
+
dispatch(setCanSaveInstructions(true));
|
|
69
|
+
toast.current!.show({
|
|
70
|
+
summary: "Succès",
|
|
71
|
+
detail: `Le contenu de la consigne ne dépasse plus la taille limite.`,
|
|
72
|
+
severity: "success",
|
|
73
|
+
life: 4000,
|
|
74
|
+
});
|
|
75
|
+
}}
|
|
76
|
+
isEditable={isEditable}
|
|
77
|
+
/>
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
export default InstructionsEditor;
|