Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Teacher Tool: Make Splitter Functional #9887

Merged
merged 18 commits into from
Feb 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion teachertool/src/components/MainPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,30 @@ import css from "./styling/MainPanel.module.scss";
import { SplitPane } from "./SplitPane";
import { RubricWorkspace } from "./RubricWorkspace";
import { ProjectWorkspace } from "./ProjectWorkspace";
import { getLastSplitPosition, setLastSplitPosition } from "../services/storageService";

interface IProps {}

export const MainPanel: React.FC<IProps> = () => {
const defaultSize = "50%";
srietkerk marked this conversation as resolved.
Show resolved Hide resolved

function handleResizeEnd(size: number | string) {
setLastSplitPosition(size.toString());
}

const lastSavedSplitPosition = getLastSplitPosition();
return (
<div className={css["main-panel"]}>
<SplitPane
split={"vertical"}
defaultSize={"80%"}
defaultSize={defaultSize}
startingSize={lastSavedSplitPosition}
primary={"left"}
left={<RubricWorkspace />}
right={<ProjectWorkspace />}
leftMinSize="5rem"
rightMinSize="5rem"
onResizeEnd={handleResizeEnd}
/>
</div>
);
Expand Down
108 changes: 101 additions & 7 deletions teachertool/src/components/SplitPane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,114 @@ import { classList } from "react-common/components/util";
interface IProps {
className?: string;
split: "horizontal" | "vertical";
defaultSize: number | string;
defaultSize: number | string; // The size to reset to when double clicking the splitter.
startingSize?: number | string; // The size to use initially when creating the splitter. Defaults to `defaultSize`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I understand the benefit of having this extra parameter. I can understand that the parent of the SplitPane should be able to declare the default size of the panes, but I think starting size is something that can be determined inside SplitPane.

I think just having defaultSize is all we need, and then when first setting the size in the useState, you can just have the getLastSplitPosition call as the lastSavedPosition variable and the useState are essentially doing the same thing right now.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, I see. You're trying to keep the state/service stuff outside of SplitPane to make it easier to move in the future. I wonder if saving the size in localstorage is something you just want to bring with this, though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we should have this class interface with local storage. The caller would still need to pass in the local storage key to use, which would feel a little weird, but more importantly, if we re-use it somewhere else, that behavior may not be desirable. We may not want to save it at all, or we may want to save it somewhere else, like an account-level setting for example.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. Can lastSavedSplitPosition in MainPanel just be const lastSavedSplitPosition = getLastSplitPosition(), then? Or we can even just get rid of the variable and pass the function call into startingSize.

primary: "left" | "right";
left: React.ReactNode;
right: React.ReactNode;
leftMinSize: number | string;
rightMinSize: number | string;
onResizeEnd?: (size: number | string) => void;
}

export const SplitPane: React.FC<IProps> = ({ className, split, left, right }) => {
export const SplitPane: React.FC<IProps> = ({
className,
split,
defaultSize,
startingSize,
left,
right,
leftMinSize,
rightMinSize,
onResizeEnd,
}) => {
const [size, setSize] = React.useState(startingSize ?? defaultSize);
const [isResizing, setIsResizing] = React.useState(false);
const containerRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
if (!isResizing) {
onResizeEnd?.(size);
}
}, [isResizing]);

function handleResizeMouse(event: MouseEvent) {
handleResize(event.clientX, event.clientY);
}

function handleResizeTouch(event: TouchEvent) {
if (event.touches.length >= 1) {
handleResize(event.touches[0].clientX, event.touches[0].clientY);
}
}

function handleResize(clientX: number, clientY: number) {
const containerRect = containerRef.current?.getBoundingClientRect();
if (containerRect) {
setIsResizing(true); // Do this here rather than inside startResizing to prevent interference with double click detection.
const newSize =
split === "vertical"
? `${((clientX - containerRect.left) / containerRect.width) * 100}%`
: `${((clientY - containerRect.top) / containerRect.height) * 100}%`;
setSize(newSize);
}
}

function startResizing(event: React.MouseEvent | React.TouchEvent) {
event.preventDefault();
document.addEventListener("mousemove", handleResizeMouse);
document.addEventListener("mouseup", endResizing);
document.addEventListener("touchmove", handleResizeTouch);
document.addEventListener("touchend", endResizing);
}

function endResizing() {
document.removeEventListener("mousemove", handleResizeMouse);
document.removeEventListener("mouseup", endResizing);
document.removeEventListener("touchmove", handleResizeTouch);
document.removeEventListener("touchend", endResizing);

setIsResizing(false);
}

function setToDefaultSize() {
setSize(defaultSize);
onResizeEnd?.(defaultSize);
}

const leftStyle: React.CSSProperties = { flexBasis: size };
if (split === "vertical") {
leftStyle.minWidth = leftMinSize;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to make the leftMinSize and rightMinSize props required? If they aren't set, I think these calculations will fail sine we don't have default values for leftMinSize and rightMinSize.

Copy link
Contributor Author

@thsparks thsparks Feb 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I don't think we should allow them to be fully unset, since it's possible to get into a state where the splitter goes offscreen and then you can never get out of that state, even when you refresh the page.

We could have a default value, but when in doubt, I think it's simpler to just have the caller tell us what these values should be. The caller will always have more context than this component does, and it's not too burdensome to include.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree, but right now these props are optional: https://github.com/microsoft/pxt/pull/9887/files#diff-5c44e5a8b91e45e13e21ff9f1a12da3634d9282699fb7a757190de64abcff78dR13-R14. I think they should just be required props to avoid possible problems.


// Setting right's minWidth doesn't work because left is still allowed
// to expand beyond it. Instead, set left's maxWidth.
leftStyle.maxWidth = `calc(100% - ${rightMinSize})`;
} else {
leftStyle.minHeight = leftMinSize;
leftStyle.maxHeight = `calc(100% - ${rightMinSize})`;
}

return (
<div className={classList(css[`split-pane-${split}`], className)}>
<div className={css[`left-${split}`]}>{left}</div>
<div className={css[`splitter-${split}`]}>
<div className={css[`splitter-${split}-inner`]} />
<div ref={containerRef} className={classList(css["split-pane"], css[`split-${split}`], className)}>
<div className={css[`left`]} style={leftStyle}>
srietkerk marked this conversation as resolved.
Show resolved Hide resolved
{left}
</div>
<div
className={css[`splitter`]}
onMouseDown={startResizing}
onTouchStart={startResizing}
onDoubleClick={setToDefaultSize}
>
<div className={css[`splitter-inner`]} />
</div>
<div className={css[`right-${split}`]}>{right}</div>
<div className={css[`right`]}>{right}</div>

{/*
This overlay is necessary to prevent any other parts of the page (particularly iframes)
from intercepting the mouse events while resizing. We simply add a transparent div over the
left and right sections.
*/}
<div className={classList(css["resizing-overlay"], isResizing ? undefined : "hidden")} />
</div>
);
};
127 changes: 61 additions & 66 deletions teachertool/src/components/styling/SplitPane.module.scss
Original file line number Diff line number Diff line change
@@ -1,67 +1,62 @@

// TODO make this scssy

.split-pane-vertical {
display: flex;
flex-direction: row;
height: 100%;
width: 100%;
}

.left-vertical {
flex: 1;
overflow: auto;
}

.right-vertical {
flex: 1;
overflow: auto;
}

.splitter-vertical {
background-color: var(--pxt-content-accent);
width: 1px;
}

.splitter-vertical-inner {
position: relative;
background-color: transparent;
transition: background-color 0.2s ease;
left: -2.5px;
width: 5px;
height: 100%;
cursor: ew-resize;
z-index: 1;
}

.splitter-vertical-inner:hover {
background-color: var(--pxt-content-foreground);
transition: background-color 0.2s ease;
transition-delay: 0.2s;
}

/* Horizontal split pane */

.split-pane-horizontal {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
}

.left-horizontal {
flex: 1;
overflow: auto;
}

.right-horizontal {
flex: 1;
overflow: auto;
}

.splitter-horizontal {
flex: 0 0 1px;
background-color: var(--pxt-headerbar-background);
height: 5px;
cursor: ns-resize;
.split-pane {
display: flex;
height: 100%;
width: 100%;

.left {
flex-grow: 0;
flex-shrink: 0;
overflow: auto;
}

.right {
flex-grow: 1;
flex-shrink: 1;
overflow: auto;
}

.resizing-overlay {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}

.split-vertical {
flex-direction: row;

.splitter {
background-color: var(--pxt-content-accent);
width: 1px;

.splitter-inner {
position: relative;
background-color: transparent;
transition: background-color 0.2s ease;
left: -3px;
width: 6px;
height: 100%;
cursor: ew-resize;
z-index: 1;

&:hover {
background-color: var(--pxt-content-foreground);
transition: background-color 0.2s ease;
transition-delay: 0.2s;
}
}
}
}

.split-horizontal {
flex-direction: column;

.splitter {
flex: 0 0 1px;
background-color: var(--pxt-headerbar-background);
height: 5px;
cursor: ns-resize;
}
}
18 changes: 18 additions & 0 deletions teachertool/src/services/storageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Rubric } from "../types/rubric";
const KEY_PREFIX = "teachertool";
const AUTORUN_KEY = [KEY_PREFIX, "autorun"].join("/");
const LAST_ACTIVE_RUBRIC_KEY = [KEY_PREFIX, "lastActiveRubric"].join("/");
const SPLIT_POSITION_KEY = [KEY_PREFIX, "splitPosition"].join("/");

function getValue(key: string, defaultValue?: string): string | undefined {
return localStorage.getItem(key) || defaultValue;
Expand Down Expand Up @@ -148,6 +149,23 @@ export function setLastActiveRubricName(name: string) {
}
}

export function getLastSplitPosition(): string {
try {
return getValue(SPLIT_POSITION_KEY) ?? "";
} catch (e) {
logError(ErrorCode.localStorageReadError, e);
return "";
}
}

export function setLastSplitPosition(position: string) {
try {
setValue(SPLIT_POSITION_KEY, position);
} catch (e) {
logError(ErrorCode.localStorageWriteError, e);
}
}

export async function getRubric(name: string): Promise<Rubric | undefined> {
const db = await getDb;

Expand Down
2 changes: 1 addition & 1 deletion teachertool/src/state/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export function getSelectableCatalogCriteria(state: AppState): CatalogCriteria[]
state.catalog?.filter(
catalogCriteria =>
((catalogCriteria.parameters && catalogCriteria.parameters.length > 0) ||
!usedCatalogCriteria.includes(catalogCriteria.id)) &&
!usedCatalogCriteria.includes(catalogCriteria.id)) &&
!catalogCriteria.hideInCatalog
) ?? []
);
Expand Down