@capytale/meta-player 0.0.1 → 0.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capytale/meta-player",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "type": "module",
5
5
  "scripts": {
6
6
  "dev": "vite",
@@ -24,6 +24,7 @@
24
24
  "react-dom": "^18.2.0",
25
25
  "react-html-props": "^2.0.9",
26
26
  "react-redux": "^9.1.0",
27
+ "react-reverse-portal": "^2.1.2",
27
28
  "screenfull": "^6.0.2"
28
29
  },
29
30
  "devDependencies": {
package/src/App.tsx CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  selectOrientation,
14
14
  toggleIsPedagoVisible,
15
15
  } from "./features/layout/layoutSlice";
16
- import { FC, PropsWithChildren } from "react";
16
+ import { FC, PropsWithChildren, useMemo } from "react";
17
17
  import { Tooltip } from "primereact/tooltip";
18
18
  import {
19
19
  selectHasGradingOrComments,
@@ -23,6 +23,8 @@ import {
23
23
  import settings from "./settings";
24
24
  import ReviewNavbar from "./features/navbar/ReviewNavbar";
25
25
 
26
+ import * as portals from "react-reverse-portal";
27
+
26
28
  type AppProps = PropsWithChildren<{}>;
27
29
 
28
30
  const App: FC<AppProps> = (props) => {
@@ -34,6 +36,9 @@ const App: FC<AppProps> = (props) => {
34
36
  const hasGradingOrComments = useAppSelector(selectHasGradingOrComments);
35
37
  const hasPedago = hasInstructions || hasGradingOrComments;
36
38
  const dispatch = useAppDispatch();
39
+
40
+ const portalNode = useMemo(() => portals.createHtmlPortalNode(), []);
41
+
37
42
  return (
38
43
  <div
39
44
  className={classNames(
@@ -42,6 +47,9 @@ const App: FC<AppProps> = (props) => {
42
47
  isHorizontal ? "layout-horizontal" : "layout-vertical",
43
48
  )}
44
49
  >
50
+ <portals.InPortal node={portalNode}>
51
+ <div id="meta-player-content">{props.children}</div>
52
+ </portals.InPortal>
45
53
  <div>
46
54
  <Navbar />
47
55
  {mode === "review" && <ReviewNavbar />}
@@ -55,7 +63,7 @@ const App: FC<AppProps> = (props) => {
55
63
  <Pedago key="pedago" />
56
64
  </SplitterPanel>
57
65
  <SplitterPanel minSize={40} size={70}>
58
- {props.children}
66
+ <portals.OutPortal node={portalNode} />
59
67
  </SplitterPanel>
60
68
  </Splitter>
61
69
  )}
@@ -106,7 +114,9 @@ const App: FC<AppProps> = (props) => {
106
114
  mouseTrack={!isHorizontal}
107
115
  />
108
116
  </div>
109
- <div className={styles.hiddenPedagoContent}>{props.children}</div>
117
+ <div className={styles.hiddenPedagoContent}>
118
+ <portals.OutPortal node={portalNode} />
119
+ </div>
110
120
  </div>
111
121
  )}
112
122
  </div>
@@ -1,5 +1,5 @@
1
1
  import { FC, useEffect } from "react";
2
- import { setIsDirty } from "./activityDataSlice";
2
+ import { setIsPlayerDirty } from "./activityDataSlice";
3
3
  import { useAppDispatch } from "../../app/hooks";
4
4
 
5
5
  type IsDirtySetterProps = {
@@ -9,9 +9,15 @@ type IsDirtySetterProps = {
9
9
  const IsDirtySetter: FC<IsDirtySetterProps> = (props) => {
10
10
  const dispatch = useAppDispatch();
11
11
  useEffect(() => {
12
- dispatch(setIsDirty(props.isDirty));
12
+ dispatch(setIsPlayerDirty(props.isDirty));
13
13
  }, [props]);
14
14
  return null;
15
15
  };
16
16
 
17
+ const notifyIsDirty = (isDirty: boolean = true) => {
18
+ const dispatch = useAppDispatch();
19
+ dispatch(setIsPlayerDirty(isDirty));
20
+ };
21
+
17
22
  export default IsDirtySetter;
23
+ export { notifyIsDirty };
@@ -29,7 +29,6 @@ export type Icon = {
29
29
  interface ActivityJSData {
30
30
  title: string;
31
31
  mode: ActivityMode;
32
- isDirty: boolean;
33
32
  returnUrl: string;
34
33
  helpUrl: string;
35
34
  code: string;
@@ -54,6 +53,8 @@ interface UIState {
54
53
  canSaveInstructions: boolean;
55
54
  canSaveAnswerSheet: boolean;
56
55
  saveState: SaveState;
56
+ isPlayerDirty: boolean;
57
+ isMPDirty: boolean;
57
58
  }
58
59
 
59
60
  export interface ActivityDataState
@@ -64,7 +65,6 @@ export interface ActivityDataState
64
65
  const initialState: ActivityDataState = {
65
66
  title: "",
66
67
  mode: "view",
67
- isDirty: false,
68
68
  returnUrl: "",
69
69
  helpUrl: "",
70
70
  code: "",
@@ -91,6 +91,8 @@ const initialState: ActivityDataState = {
91
91
  canSaveInstructions: true,
92
92
  canSaveAnswerSheet: true,
93
93
  saveState: "idle",
94
+ isPlayerDirty: false,
95
+ isMPDirty: false,
94
96
  };
95
97
 
96
98
  // If you are not using async thunks you can use the standalone `createSlice`.
@@ -169,8 +171,13 @@ export const activityDataSlice = createAppSlice({
169
171
  setWorkflow: create.reducer((state, action: PayloadAction<wf>) => {
170
172
  state.workflow = action.payload;
171
173
  }),
172
- setIsDirty: create.reducer((state, action: PayloadAction<boolean>) => {
173
- state.isDirty = action.payload;
174
+ setIsPlayerDirty: create.reducer(
175
+ (state, action: PayloadAction<boolean>) => {
176
+ state.isPlayerDirty = action.payload;
177
+ },
178
+ ),
179
+ setIsMPDirty: create.reducer((state, action: PayloadAction<boolean>) => {
180
+ state.isMPDirty = action.payload;
174
181
  }),
175
182
  }),
176
183
  // You can define your selectors here. These selectors receive the slice
@@ -198,7 +205,9 @@ export const activityDataSlice = createAppSlice({
198
205
  selectActivityNid: (data) => data.activityNid,
199
206
  selectIcon: (data) => data.icon,
200
207
  selectWorkflow: (data) => data.workflow,
201
- selectIsDirty: (data) => data.isDirty,
208
+ selectIsPlayerDirty: (data) => data.isPlayerDirty,
209
+ selectIsMPDirty: (data) => data.isMPDirty,
210
+ selectIsDirty: (data) => data.isPlayerDirty || data.isMPDirty,
202
211
 
203
212
  selectCanChoosePedagoLayout: (data) =>
204
213
  data.pedagoLayout === "default-horizontal" ||
@@ -206,6 +215,11 @@ export const activityDataSlice = createAppSlice({
206
215
 
207
216
  selectCanChooseTheme: (data) =>
208
217
  data.supportsDarkTheme && data.supportsLightTheme,
218
+
219
+ selectShowSaveButton: (data) =>
220
+ data.mode === "create" ||
221
+ data.mode === "review" ||
222
+ (data.mode === "assignment" && data.workflow === "current"),
209
223
  },
210
224
  });
211
225
 
@@ -222,7 +236,8 @@ export const {
222
236
  setGrading,
223
237
  setComments,
224
238
  setWorkflow,
225
- setIsDirty,
239
+ setIsPlayerDirty,
240
+ setIsMPDirty,
226
241
  } = activityDataSlice.actions;
227
242
 
228
243
  // Selectors returned by `slice.selectors` take the root state as their first argument.
@@ -243,8 +258,11 @@ export const {
243
258
  selectActivityNid,
244
259
  selectIcon,
245
260
  selectWorkflow,
261
+ selectIsPlayerDirty,
262
+ selectIsMPDirty,
246
263
  selectIsDirty,
247
264
 
248
265
  selectCanChoosePedagoLayout,
249
266
  selectCanChooseTheme,
267
+ selectShowSaveButton,
250
268
  } = activityDataSlice.selectors;
@@ -53,7 +53,6 @@ export const ActivityJSProvider: FC<ActivityJSProviderProps> = (props) => {
53
53
  setActivityJSData({
54
54
  title: ab.title.value || "",
55
55
  mode: data.mode,
56
- isDirty: false,
57
56
  returnUrl: data.returnUrl,
58
57
  helpUrl: data.type.helpUrl,
59
58
  nid: ab.mainNode.nid,
@@ -7,6 +7,8 @@ import {
7
7
  selectInstructions,
8
8
  selectSaveState,
9
9
  selectWorkflow,
10
+ setIsMPDirty,
11
+ setIsPlayerDirty,
10
12
  setSaveState,
11
13
  } from "../activityData/activityDataSlice";
12
14
  import { useActivityJS } from "./ActivityJSProvider";
@@ -94,6 +96,8 @@ const Saver: FC<{}> = () => {
94
96
  const saveData = await ab.save();
95
97
  console.log("Save return data", saveData);
96
98
  dispatch(setSaveState("idle"));
99
+ dispatch(setIsPlayerDirty(false));
100
+ dispatch(setIsMPDirty(false));
97
101
  toast.current!.show({
98
102
  summary: "Sauvegarde réussie",
99
103
  detail: "L'activité a bien été enregistrée.",
@@ -12,6 +12,7 @@ import {
12
12
  selectMode,
13
13
  selectSaveState,
14
14
  selectSharingInfo,
15
+ selectShowSaveButton,
15
16
  selectWorkflow,
16
17
  setSaveState,
17
18
  setWorkflow,
@@ -28,6 +29,7 @@ const CapytaleMenu: React.FC = () => {
28
29
  const mode = useAppSelector(selectMode);
29
30
  const workflow = useAppSelector(selectWorkflow);
30
31
  const isDirty = useAppSelector(selectIsDirty);
32
+ const showSaveButton = useAppSelector(selectShowSaveButton);
31
33
  const isLarge = useMemo(
32
34
  () => windowsSize.width && windowsSize.width >= XL,
33
35
  [windowsSize.width],
@@ -54,21 +56,26 @@ const CapytaleMenu: React.FC = () => {
54
56
  };
55
57
  return (
56
58
  <div className={styles.capytaleMenu}>
57
- <Button
58
- label={isQuiteSmall ? undefined : "Enregistrer"}
59
- severity={isDirty ? "danger" : "warning"}
60
- size="small"
61
- icon="pi pi-save"
62
- loading={saveState !== "idle"}
63
- tooltip="Enregistrer l'activité"
64
- tooltipOptions={{
65
- position: "bottom",
66
- showDelay: settings.TOOLTIP_SHOW_DELAY,
67
- }}
68
- onClick={() => {
69
- dispatch(setSaveState("should-save"));
70
- }}
71
- />
59
+ {showSaveButton && (
60
+ <Button
61
+ label={isQuiteSmall ? undefined : "Enregistrer"}
62
+ disabled={!isDirty}
63
+ severity={"warning"}
64
+ size="small"
65
+ icon="pi pi-save"
66
+ loading={saveState !== "idle"}
67
+ tooltip={isDirty ? "Enregistrer l'activité" : "Rien à enregistrer"}
68
+ tooltipOptions={{
69
+ position: "bottom",
70
+ showDelay: settings.TOOLTIP_SHOW_DELAY,
71
+ showOnDisabled: true,
72
+ }}
73
+ onClick={() => {
74
+ dispatch(setSaveState("should-save"));
75
+ }}
76
+ />
77
+ )}
78
+
72
79
  {mode === "create" && (
73
80
  <SplitButton
74
81
  label={isLarge ? sharingInfo.code : undefined}
@@ -4,6 +4,7 @@ import {
4
4
  selectAnswerSheetContent,
5
5
  selectMode,
6
6
  setCanSaveAnswerSheet,
7
+ setIsMPDirty,
7
8
  setLexicalAnswerSheetState,
8
9
  } from "../activityData/activityDataSlice";
9
10
  import { forwardRef, useImperativeHandle, useRef } from "react";
@@ -18,6 +19,7 @@ const AnswerSheetEditor: React.FC = forwardRef((_props, ref) => {
18
19
  const isEditable =
19
20
  mode === "create" || mode === "assignment" || mode === "review";
20
21
  const initialEditorState = useAppSelector(selectAnswerSheetContent);
22
+ const initialStateOnChangeDone = useRef<boolean>(false);
21
23
 
22
24
  useImperativeHandle(ref, () => {
23
25
  return {
@@ -37,6 +39,11 @@ const AnswerSheetEditor: React.FC = forwardRef((_props, ref) => {
37
39
  jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
38
40
  onChange={(editorState) => {
39
41
  dispatch(setLexicalAnswerSheetState(editorState));
42
+ if (initialStateOnChangeDone.current) {
43
+ dispatch(setIsMPDirty(true));
44
+ } else {
45
+ initialStateOnChangeDone.current = true;
46
+ }
40
47
  }}
41
48
  onJsonSizeLimitExceeded={() => {
42
49
  dispatch(setCanSaveAnswerSheet(false));
@@ -4,14 +4,10 @@ import {
4
4
  selectInstructions,
5
5
  selectMode,
6
6
  setCanSaveInstructions,
7
+ setIsMPDirty,
7
8
  setLexicalInstructionsState,
8
9
  } from "../activityData/activityDataSlice";
9
- import {
10
- forwardRef,
11
- useImperativeHandle,
12
- useMemo,
13
- useRef,
14
- } from "react";
10
+ import { forwardRef, useImperativeHandle, useMemo, useRef } from "react";
15
11
  import { Toast } from "primereact/toast";
16
12
  import settings from "../../settings";
17
13
 
@@ -34,6 +30,7 @@ const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
34
30
  ? initialInstructions?.value
35
31
  : undefined;
36
32
  }, [initialInstructions]);
33
+ const initialStateOnChangeDone = useRef<boolean>(false);
37
34
 
38
35
  useImperativeHandle(ref, () => {
39
36
  return {
@@ -54,6 +51,11 @@ const InstructionsEditor: React.FC = forwardRef((_props, ref) => {
54
51
  jsonSizeLimit={settings.STATEMENT_MAX_SIZE}
55
52
  onChange={(editorState) => {
56
53
  dispatch(setLexicalInstructionsState(editorState));
54
+ if (initialStateOnChangeDone.current) {
55
+ dispatch(setIsMPDirty(true));
56
+ } else {
57
+ initialStateOnChangeDone.current = true;
58
+ }
57
59
  }}
58
60
  onJsonSizeLimitExceeded={() => {
59
61
  dispatch(setCanSaveInstructions(false));
@@ -21,6 +21,7 @@ import {
21
21
  selectMode,
22
22
  setComments,
23
23
  setGrading,
24
+ setIsMPDirty,
24
25
  } from "../activityData/activityDataSlice";
25
26
  import { ChangeEventHandler } from "react";
26
27
  import settings from "../../settings";
@@ -45,15 +46,17 @@ const Pedago: React.FC<DivProps> = ({ className, ...props }) => {
45
46
  const handleCommentsChange: ChangeEventHandler<HTMLTextAreaElement> = (
46
47
  event,
47
48
  ) => {
48
- console.log("Comments change:", event.target.value);
49
49
  dispatch(setComments(event.target.value));
50
+ dispatch(setIsMPDirty(true));
51
+ console.log("51");
50
52
  };
51
53
 
52
54
  const handleGradingChange: ChangeEventHandler<HTMLTextAreaElement> = (
53
55
  event,
54
56
  ) => {
55
- console.log("Grading change:", event.target.value);
56
57
  dispatch(setGrading(event.target.value));
58
+ dispatch(setIsMPDirty(true));
59
+ console.log("59");
57
60
  };
58
61
 
59
62
  return (
package/src/index.css CHANGED
@@ -12,7 +12,9 @@ code {
12
12
  monospace;
13
13
  }
14
14
 
15
- html, body, #root {
15
+ html,
16
+ body,
17
+ #root {
16
18
  height: 100%;
17
19
  }
18
20
 
@@ -20,11 +22,18 @@ html, body, #root {
20
22
  background-color: var(--surface-200);
21
23
  }
22
24
 
23
- .p-splitter-gutter-handle, .p-splitter-gutter-resizing {
25
+ .p-splitter-gutter-handle,
26
+ .p-splitter-gutter-resizing {
24
27
  background-color: var(--surface-400);
25
28
  }
26
29
 
27
- *:has(>.editor-shell) { /* Rich text editor */
30
+ *:has(> .editor-shell) {
31
+ /* Rich text editor */
28
32
  background-color: white;
29
33
  color-scheme: light;
30
- }
34
+ }
35
+
36
+ #meta-player-content,
37
+ :has(> #meta-player-content) {
38
+ height: 100%;
39
+ }