mirror of
https://github.com/alexmickelson/canvasManagement.git
synced 2026-03-25 23:28:33 -06:00
isolating the context menu more
This commit is contained in:
@@ -1,5 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useRef, useEffect, FC, useState } from "react";
|
import { useEffect, FC, useState } from "react";
|
||||||
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
||||||
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
import { LocalAssignment } from "@/features/local/assignments/models/localAssignment";
|
||||||
import { useCalendarItemsContext } from "../../../context/calendarItemsContext";
|
import { useCalendarItemsContext } from "../../../context/calendarItemsContext";
|
||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
import { useLocalCourseSettingsQuery } from "@/features/local/course/localCoursesHooks";
|
||||||
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
import { baseCanvasUrl } from "@/features/canvas/services/canvasServiceUtils";
|
||||||
import { useCourseContext } from "../../../context/courseContext";
|
import { useCourseContext } from "../../../context/courseContext";
|
||||||
|
import Modal, { ModalControl } from "@/components/Modal";
|
||||||
|
|
||||||
function getDuplicateName(name: string, existingNames: string[]): string {
|
function getDuplicateName(name: string, existingNames: string[]): string {
|
||||||
const match = name.match(/^(.*)\s+(\d+)$/);
|
const match = name.match(/^(.*)\s+(\d+)$/);
|
||||||
@@ -27,15 +28,12 @@ function getDuplicateName(name: string, existingNames: string[]): string {
|
|||||||
return `${baseName} ${num}`;
|
return `${baseName} ${num}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DayItemContextMenu: FC<{
|
export const AssignmentDayItemContextMenu: FC<{
|
||||||
x: number;
|
modalControl: ModalControl;
|
||||||
y: number;
|
|
||||||
onClose: () => void;
|
|
||||||
item: IModuleItem;
|
item: IModuleItem;
|
||||||
moduleName: string;
|
moduleName: string;
|
||||||
}> = ({ x, y, onClose, item, moduleName }) => {
|
}> = ({ modalControl, item, moduleName }) => {
|
||||||
const { courseName } = useCourseContext();
|
const { courseName } = useCourseContext();
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
|
||||||
const calendarItems = useCalendarItemsContext();
|
const calendarItems = useCalendarItemsContext();
|
||||||
const createAssignment = useCreateAssignmentMutation();
|
const createAssignment = useCreateAssignmentMutation();
|
||||||
const deleteLocal = useDeleteAssignmentMutation();
|
const deleteLocal = useDeleteAssignmentMutation();
|
||||||
@@ -55,29 +53,21 @@ export const DayItemContextMenu: FC<{
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (e: MouseEvent) => {
|
|
||||||
if (ref.current && !ref.current.contains(e.target as Node)) {
|
|
||||||
setConfirmingDelete(false);
|
|
||||||
onClose();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleEscape = (e: KeyboardEvent) => {
|
const handleEscape = (e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
setConfirmingDelete(false);
|
setConfirmingDelete(false);
|
||||||
onClose();
|
modalControl.closeModal();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
document.addEventListener("mousedown", handleClickOutside);
|
|
||||||
document.addEventListener("keydown", handleEscape);
|
document.addEventListener("keydown", handleEscape);
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener("mousedown", handleClickOutside);
|
|
||||||
document.removeEventListener("keydown", handleEscape);
|
document.removeEventListener("keydown", handleEscape);
|
||||||
};
|
};
|
||||||
}, [onClose]);
|
}, [modalControl]);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setConfirmingDelete(false);
|
setConfirmingDelete(false);
|
||||||
onClose();
|
modalControl.closeModal();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDuplicate = () => {
|
const handleDuplicate = () => {
|
||||||
@@ -120,19 +110,14 @@ export const DayItemContextMenu: FC<{
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const baseButtonClasses = "unstyled w-full text-left px-4 py-2";
|
const baseButtonClasses = "w-full text-left px-4 py-2";
|
||||||
const normalHoverClasses = "hover:bg-slate-700 disabled:opacity-50";
|
const normalHoverClasses = "hover:bg-slate-700 disabled:opacity-50";
|
||||||
const dangerClasses =
|
const dangerClasses =
|
||||||
"bg-rose-900/30 hover:bg-rose-950 disabled:opacity-50 text-rose-50";
|
"bg-rose-900/30 hover:bg-rose-950 disabled:opacity-50 text-rose-50";
|
||||||
return (
|
return (
|
||||||
<div
|
<Modal modalControl={modalControl}>
|
||||||
ref={ref}
|
{() => (
|
||||||
className="
|
<>
|
||||||
fixed z-50 bg-slate-900 border-2 border-slate-700
|
|
||||||
rounded shadow-xl overflow-hidden
|
|
||||||
"
|
|
||||||
style={{ left: x, top: y }}
|
|
||||||
>
|
|
||||||
{confirmingDelete ? (
|
{confirmingDelete ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
@@ -162,7 +147,7 @@ export const DayItemContextMenu: FC<{
|
|||||||
href={canvasUrl}
|
href={canvasUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="unstyled block px-4 py-2 text-sm hover:bg-slate-700 cursor-pointer"
|
className="unstyled block px-4 py-2 hover:bg-slate-700 cursor-pointer"
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
>
|
>
|
||||||
View in Canvas
|
View in Canvas
|
||||||
@@ -170,14 +155,14 @@ export const DayItemContextMenu: FC<{
|
|||||||
<button
|
<button
|
||||||
onClick={handleUpdateCanvas}
|
onClick={handleUpdateCanvas}
|
||||||
disabled={updateInCanvas.isPending}
|
disabled={updateInCanvas.isPending}
|
||||||
className="w-full text-left px-4 py-2 text-sm hover:bg-slate-700 disabled:opacity-50"
|
className="unstyled w-full text-left px-4 py-2 hover:bg-slate-700 disabled:opacity-50"
|
||||||
>
|
>
|
||||||
Update in Canvas
|
Update in Canvas
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleDeleteFromCanvas}
|
onClick={handleDeleteFromCanvas}
|
||||||
disabled={deleteFromCanvas.isPending}
|
disabled={deleteFromCanvas.isPending}
|
||||||
className={`${baseButtonClasses} ${dangerClasses}`}
|
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
|
||||||
>
|
>
|
||||||
Delete from Canvas
|
Delete from Canvas
|
||||||
</button>
|
</button>
|
||||||
@@ -186,19 +171,21 @@ export const DayItemContextMenu: FC<{
|
|||||||
{!canvasUrl && (
|
{!canvasUrl && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmingDelete(true)}
|
onClick={() => setConfirmingDelete(true)}
|
||||||
className={`${baseButtonClasses} ${dangerClasses}`}
|
className={`unstyled ${baseButtonClasses} ${dangerClasses}`}
|
||||||
>
|
>
|
||||||
Delete from Disk
|
Delete from Disk
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleDuplicate}
|
onClick={handleDuplicate}
|
||||||
className={`${baseButtonClasses} ${normalHoverClasses}`}
|
className={`unstyled ${baseButtonClasses} ${normalHoverClasses}`}
|
||||||
>
|
>
|
||||||
Duplicate
|
Duplicate
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
import { IModuleItem } from "@/features/local/modules/IModuleItem";
|
||||||
import { getModuleItemUrl } from "@/services/urlUtils";
|
import { getModuleItemUrl } from "@/services/urlUtils";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { FC, ReactNode, useCallback, useState } from "react";
|
import { FC, ReactNode } from "react";
|
||||||
import { useCourseContext } from "../../../context/courseContext";
|
import { useCourseContext } from "../../../context/courseContext";
|
||||||
import { useTooltip } from "@/components/useTooltip";
|
import { useTooltip } from "@/components/useTooltip";
|
||||||
import { DraggableItem } from "../../../context/drag/draggingContext";
|
import { DraggableItem } from "../../../context/drag/draggingContext";
|
||||||
import ClientOnly from "@/components/ClientOnly";
|
import ClientOnly from "@/components/ClientOnly";
|
||||||
import { useDragStyleContext } from "../../../context/drag/dragStyleContext";
|
import { useDragStyleContext } from "../../../context/drag/dragStyleContext";
|
||||||
import { Tooltip } from "../../../../../../components/Tooltip";
|
import { Tooltip } from "../../../../../../components/Tooltip";
|
||||||
import { DayItemContextMenu } from "./DayItemContextMenu";
|
import { AssignmentDayItemContextMenu } from "./DayItemContextMenu";
|
||||||
import { GetPreviewContent } from "./GetPreviewContent";
|
import { GetPreviewContent } from "./GetPreviewContent";
|
||||||
|
import { useModal } from "@/components/Modal";
|
||||||
|
|
||||||
export const ItemInDay: FC<{
|
export const ItemInDay: FC<{
|
||||||
type: "assignment" | "page" | "quiz";
|
type: "assignment" | "page" | "quiz";
|
||||||
@@ -22,23 +23,15 @@ export const ItemInDay: FC<{
|
|||||||
const { courseName } = useCourseContext();
|
const { courseName } = useCourseContext();
|
||||||
const { setIsDragging } = useDragStyleContext();
|
const { setIsDragging } = useDragStyleContext();
|
||||||
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
|
const { visible, targetRef, showTooltip, hideTooltip } = useTooltip(500);
|
||||||
|
const modalControl = useModal();
|
||||||
const [contextMenuPos, setContextMenuPos] = useState<{
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
} | null>(null);
|
|
||||||
|
|
||||||
const handleContextMenu = (e: React.MouseEvent) => {
|
const handleContextMenu = (e: React.MouseEvent) => {
|
||||||
if (type !== "assignment") return;
|
if (type !== "assignment") return;
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setContextMenuPos({ x: e.clientX, y: e.clientY });
|
modalControl.openModal({ x: e.clientX, y: e.clientY });
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeContextMenu = useCallback(() => {
|
|
||||||
setContextMenuPos(null);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={" relative group "}>
|
<div className={" relative group "}>
|
||||||
<Link
|
<Link
|
||||||
@@ -87,11 +80,9 @@ export const ItemInDay: FC<{
|
|||||||
) : (
|
) : (
|
||||||
<Tooltip message={message} targetRef={targetRef} visible={visible} />
|
<Tooltip message={message} targetRef={targetRef} visible={visible} />
|
||||||
)}
|
)}
|
||||||
{contextMenuPos && type === "assignment" && (
|
{type === "assignment" && (
|
||||||
<DayItemContextMenu
|
<AssignmentDayItemContextMenu
|
||||||
x={contextMenuPos.x}
|
modalControl={modalControl}
|
||||||
y={contextMenuPos.y}
|
|
||||||
onClose={closeContextMenu}
|
|
||||||
item={item}
|
item={item}
|
||||||
moduleName={moduleName}
|
moduleName={moduleName}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export default function EditQuiz({
|
|||||||
const updateQuizMutation = useUpdateQuizMutation();
|
const updateQuizMutation = useUpdateQuizMutation();
|
||||||
const { data: globalSettings } = useGlobalSettingsQuery();
|
const { data: globalSettings } = useGlobalSettingsQuery();
|
||||||
const feedbackDelimiters = getFeedbackDelimitersFromSettings(
|
const feedbackDelimiters = getFeedbackDelimitersFromSettings(
|
||||||
(globalSettings ?? ({} as GlobalSettings)) as GlobalSettings
|
(globalSettings ?? ({} as GlobalSettings)) as GlobalSettings,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { clientIsAuthoritative, text, textUpdate, monacoKey } =
|
const { clientIsAuthoritative, text, textUpdate, monacoKey } =
|
||||||
@@ -141,14 +141,14 @@ export default function EditQuiz({
|
|||||||
quizMarkdownUtils.toMarkdown(quiz, feedbackDelimiters) !==
|
quizMarkdownUtils.toMarkdown(quiz, feedbackDelimiters) !==
|
||||||
quizMarkdownUtils.toMarkdown(
|
quizMarkdownUtils.toMarkdown(
|
||||||
quizMarkdownUtils.parseMarkdown(text, name, feedbackDelimiters),
|
quizMarkdownUtils.parseMarkdown(text, name, feedbackDelimiters),
|
||||||
feedbackDelimiters
|
feedbackDelimiters,
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (clientIsAuthoritative) {
|
if (clientIsAuthoritative) {
|
||||||
const updatedQuiz = quizMarkdownUtils.parseMarkdown(
|
const updatedQuiz = quizMarkdownUtils.parseMarkdown(
|
||||||
text,
|
text,
|
||||||
quizName,
|
quizName,
|
||||||
feedbackDelimiters
|
feedbackDelimiters,
|
||||||
);
|
);
|
||||||
await updateQuizMutation.mutateAsync({
|
await updateQuizMutation.mutateAsync({
|
||||||
quiz: updatedQuiz,
|
quiz: updatedQuiz,
|
||||||
@@ -160,7 +160,7 @@ export default function EditQuiz({
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
"client not authoritative, updating client with server quiz"
|
"client not authoritative, updating client with server quiz",
|
||||||
);
|
);
|
||||||
textUpdate(quizMarkdownUtils.toMarkdown(quiz), true);
|
textUpdate(quizMarkdownUtils.toMarkdown(quiz), true);
|
||||||
}
|
}
|
||||||
@@ -178,6 +178,7 @@ export default function EditQuiz({
|
|||||||
}, [
|
}, [
|
||||||
clientIsAuthoritative,
|
clientIsAuthoritative,
|
||||||
courseName,
|
courseName,
|
||||||
|
feedbackDelimiters,
|
||||||
isFetching,
|
isFetching,
|
||||||
moduleName,
|
moduleName,
|
||||||
quiz,
|
quiz,
|
||||||
|
|||||||
@@ -3,23 +3,35 @@ import React, { ReactNode, useCallback, useMemo, useState } from "react";
|
|||||||
|
|
||||||
export interface ModalControl {
|
export interface ModalControl {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
openModal: () => void;
|
openModal: (position?: { x: number; y: number }) => void;
|
||||||
closeModal: () => void;
|
closeModal: () => void;
|
||||||
|
position?: { x: number; y: number };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useModal() {
|
export function useModal() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [position, setPosition] = useState<
|
||||||
|
{ x: number; y: number } | undefined
|
||||||
|
>(undefined);
|
||||||
|
|
||||||
const openModal = useCallback(() => setIsOpen(true), []);
|
const openModal = useCallback((pos?: { x: number; y: number }) => {
|
||||||
const closeModal = useCallback(() => setIsOpen(false), []);
|
setPosition(pos);
|
||||||
|
setIsOpen(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const closeModal = useCallback(() => {
|
||||||
|
setIsOpen(false);
|
||||||
|
setPosition(undefined);
|
||||||
|
}, []);
|
||||||
|
|
||||||
return useMemo(
|
return useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
isOpen,
|
isOpen,
|
||||||
openModal,
|
openModal,
|
||||||
closeModal,
|
closeModal,
|
||||||
|
position,
|
||||||
}),
|
}),
|
||||||
[closeModal, isOpen, openModal]
|
[closeModal, isOpen, openModal, position],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,10 +52,13 @@ export default function Modal({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{buttonComponent ? (
|
{buttonComponent
|
||||||
buttonComponent({ openModal: modalControl.openModal })
|
? buttonComponent({ openModal: () => modalControl.openModal() })
|
||||||
) : (
|
: buttonText && (
|
||||||
<button onClick={modalControl.openModal} className={buttonClass}>
|
<button
|
||||||
|
onClick={() => modalControl.openModal()}
|
||||||
|
className={buttonClass}
|
||||||
|
>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
@@ -51,7 +66,7 @@ export default function Modal({
|
|||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
modalControl.isOpen
|
modalControl.isOpen
|
||||||
? "transition-all duration-400 fixed inset-0 flex items-center justify-center h-screen bg-black/80 z-50 w-screen"
|
? `transition-all duration-400 fixed inset-0 ${modalControl.position ? "" : "flex items-center justify-center"} h-screen bg-black/80 z-50 w-screen`
|
||||||
: "hidden h-0 w-0 p-1 -z-50"
|
: "hidden h-0 w-0 p-1 -z-50"
|
||||||
}
|
}
|
||||||
onClick={modalControl.closeModal}
|
onClick={modalControl.closeModal}
|
||||||
@@ -60,11 +75,15 @@ export default function Modal({
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
className={
|
className={`bg-slate-800 ${modalControl.position ? "" : "p-6"} rounded-lg shadow-lg ${modalControl.position ? "" : modalWidth} transition-all duration-400 ${modalControl.isOpen ? "opacity-100" : "opacity-0"}`}
|
||||||
` bg-slate-800 p-6 rounded-lg shadow-lg ` +
|
style={
|
||||||
modalWidth +
|
modalControl.position
|
||||||
` transition-all duration-400 ` +
|
? {
|
||||||
` ${modalControl.isOpen ? "opacity-100" : "opacity-0"}`
|
position: "fixed",
|
||||||
|
left: modalControl.position.x,
|
||||||
|
top: modalControl.position.y,
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{modalControl.isOpen &&
|
{modalControl.isOpen &&
|
||||||
|
|||||||
Reference in New Issue
Block a user