Skip to content

Commit

Permalink
Teacher tool: set up infrastructure to send messages to iframe (#9811)
Browse files Browse the repository at this point in the history
* makecodeframe only holds ui, message handling for iframe split into the makecodeeditor service, file made for the transform

* successfully sending message to set high contrast to the iframe when the evaluate button is pressed

* cleaned up a bit

* do something with the results of sending the message

* consolidated ref logic in the service, got rid of extra logic in editor container

* got rid of the editor container, it doesn't do anything meaningful anymore

* got rid of the load event listener

* changed ref handling, some minor changes in the service

* no longer exposing sendMessageAsync

* pushing messages to the queue even if  the iframe isn't ready yet

* changed functions from anonymous to declarative

* fixed set editor ref logic, added check to see if message is undefined

* upgrade codeql analysis to v2
  • Loading branch information
srietkerk authored Jan 9, 2024
1 parent 1fcb477 commit ecd3cb5
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 161 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/codeql-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,4 @@ jobs:
# make release

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
uses: github/codeql-action/analyze@v2
6 changes: 3 additions & 3 deletions teachertool/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import HeaderBar from "./components/HeaderBar";
import Notifications from "./components/Notifications";
import * as NotificationService from "./services/notificationService";
import { postNotification } from "./transforms/postNotification";
import { makeNotification } from "./utils";
import { createIFrameUrl, makeNotification } from "./utils";
import DebugInput from "./components/DebugInput";
import { EditorContainer } from "./components/EditorContainer";
import { MakeCodeFrame } from "./components/MakecodeFrame";


function App() {
Expand All @@ -36,7 +36,7 @@ function App() {
<HeaderBar />
<div className="app-container">
<DebugInput />
<EditorContainer />
<MakeCodeFrame pageSourceUrl={createIFrameUrl()} />
</div>

<Notifications />
Expand Down
4 changes: 4 additions & 0 deletions teachertool/src/components/DebugInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,20 @@ import { useState } from "react";
import { Button } from "react-common/components/controls/Button";
import { Input } from "react-common/components/controls/Input";
import { Textarea } from "react-common/components/controls/Textarea";
import { loadProjectAsync } from "../transforms/loadProjectAsync";
import { runEvaluateAsync } from "../transforms/runEvaluateAsync";

interface IProps {}

const DebugInput: React.FC<IProps> = ({}) => {
const [shareLink, setShareLink] = useState("https://arcade.makecode.com/S50644-45891-08403-36583");
const [rubric, setRubric] = useState("");
const [bools, setBools] = useState(true);

const evaluate = async () => {
setBools(!bools);
await runEvaluateAsync(shareLink, rubric);
await loadProjectAsync("hi", bools);
}

return (
Expand Down
32 changes: 0 additions & 32 deletions teachertool/src/components/EditorContainer.tsx

This file was deleted.

130 changes: 5 additions & 125 deletions teachertool/src/components/MakecodeFrame.tsx
Original file line number Diff line number Diff line change
@@ -1,141 +1,21 @@
/// <reference path="../../../built/pxteditor.d.ts" />
import * as React from "react";
import { useState, useEffect, useRef } from "react";
import { ProgressBar } from "react-common/components/controls/ProgressBar";
import { setEditorRef } from "../services/makecodeEditorService";
interface MakeCodeFrameProps {
pageSourceUrl: string;
highContrast?: boolean;
onFrameOpen: () => void;
onFrameClose: () => void;
tutorialEventHandler?: (event: pxt.editor.EditorMessageTutorialEventRequest) => void;
}

type FrameState = "loading" | "no-project" | "opening-project" | "project-open" | "closing-project";

interface PendingMessage {
original: pxt.editor.EditorMessageRequest;
handler: (response: any) => void;
}

export const MakeCodeFrame: React.FC<MakeCodeFrameProps> =
( { pageSourceUrl,
highContrast,
onFrameClose,
onFrameOpen
} ) => {
let ref: HTMLIFrameElement | undefined;
const messageQueue: pxt.editor.EditorMessageRequest[] = [];
let nextId: number = 0;
let pendingMessages: {[index: string]: PendingMessage} = {};
const [frameState, setFrameState] = useState<FrameState>("loading");
const [loadPercent, setLoadPercent] = useState(0);
const [workspaceReady, setWorkspaceReady] = useState(false);

let frameRef = useRef<HTMLIFrameElement>(null);

useEffect(() => {
// logic we want to do when the iframe is loaded
if (ref && ref.contentWindow) {
window.addEventListener("message", onMessageReceived);
ref.addEventListener("load", handleFrameReload)

}

// logic we want when the iframe unmounts
return () => {
window.removeEventListener("message", onMessageReceived);
if (ref && ref.contentWindow) {
ref.removeEventListener("load", handleFrameReload)
}
}
}, [])

useEffect(() => {
sendMessageAsync({
type: "pxteditor",
action: "sethighcontrast",
on: highContrast
} as pxt.editor.EditorMessageSetHighContrastRequest);
}, [highContrast])


useEffect(() => {
if (frameState === "project-open") {
setFrameState("closing-project");
onFrameClose();
}
else if (frameState === "no-project") {
setFrameState("opening-project");
onFrameOpen();
}
}, [frameState]);


const handleFrameReload = () => {
setFrameState("loading")
}

const onMessageReceived = (event: MessageEvent) => {
const data = event.data as pxt.editor.EditorMessageRequest;
if (frameState === "opening-project") setLoadPercent(Math.min((loadPercent || 0) + 7, 95));

if (data.type === "pxteditor" && data.id && pendingMessages[data.id]) {
const pending = pendingMessages[data.id];
pending.handler(data);
delete pendingMessages[data.id];
return;
}

switch (data.action) {
case "newproject":
if (!workspaceReady) {
setWorkspaceReady(true);
sendMessageAsync(); // Flush message queue
}
if (frameState === "loading") {
setFrameState("no-project");
}
break;
default:
console.log(JSON.stringify(data, null, 4));
}
}

const sendMessageAsync = (message?: any) => {
return new Promise(resolve => {
const sendMessageCore = (message: any) => {
message.response = true;
message.id = nextId++ + "";
pendingMessages[message.id] = {
original: message,
handler: resolve
};
ref!.contentWindow!.postMessage(message, "*");
}
( { pageSourceUrl} ) => {

if (ref) {
if (!workspaceReady) {
messageQueue.push(message);
}
else {
while (messageQueue.length) {
sendMessageCore(messageQueue.shift());
}
if (message) sendMessageCore(message);
}
}
});
const handleIFrameRef = (el: HTMLIFrameElement | null) => {
setEditorRef(el ?? undefined);
}

const openingProject = frameState === "opening-project";
const showLoader = openingProject || frameState === "closing-project";
/* eslint-disable @microsoft/sdl/react-iframe-missing-sandbox */
return <div className="makecode-frame-outer" style={{ display: "block" }}>
<div className={`makecode-frame-loader ${showLoader ? "" : "hidden"}`}>
{openingProject && <ProgressBar className="makecode-frame-loader-bar" value={loadPercent! / 100} />}
<div className="makecode-frame-loader-text">{lf("Loading...")}</div>
</div>
<iframe className="makecode-frame" src={pageSourceUrl} title={"title"} ref={frameRef}></iframe>
<iframe className="makecode-frame" src={pageSourceUrl} title={"title"} ref={handleIFrameRef}></iframe>
</div>
/* eslint-enable @microsoft/sdl/react-iframe-missing-sandbox */
}
61 changes: 61 additions & 0 deletions teachertool/src/services/makecodeEditorService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
interface PendingMessage {
original: pxt.editor.EditorMessageRequest;
handler: (response: any) => void;
}

let makecodeEditorRef: HTMLIFrameElement | undefined;
const messageQueue: pxt.editor.EditorMessageRequest[] = [];
let nextId: number = 0;
let pendingMessages: {[index: string]: PendingMessage} = {};

function onMessageReceived(event: MessageEvent) {
const data = event.data as pxt.editor.EditorMessageRequest;
if (data.type === "pxteditor" && data.id && pendingMessages[data.id]) {
const pending = pendingMessages[data.id];
pending.handler(data);
delete pendingMessages[data.id];
return;
}

console.log("Received message from iframe:", data);
}

function sendMessageAsync(message?: any) {
return new Promise(resolve => {
const sendMessageCore = (message: any) => {
message.response = true;
message.id = nextId++ + "";
pendingMessages[message.id] = {
original: message,
handler: resolve
};
makecodeEditorRef!.contentWindow!.postMessage(message, "*");
}

if (message) messageQueue.push(message);
if (makecodeEditorRef) {
while (messageQueue.length) {
sendMessageCore(messageQueue.shift());
}
}
});
}

export function setEditorRef(ref: HTMLIFrameElement | undefined) {
makecodeEditorRef = ref ?? undefined;
window.removeEventListener("message", onMessageReceived);
if (ref) {
window.addEventListener("message", onMessageReceived);
sendMessageAsync();
}
}

// an example of events that we want to/can send to the editor
export async function setHighContrastAsync(on: boolean) {
const result = await sendMessageAsync({
type: "pxteditor",
action: "sethighcontrast",
on: on
});
console.log(result);
}
12 changes: 12 additions & 0 deletions teachertool/src/transforms/loadProjectAsync.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { stateAndDispatch } from "../state";
import * as Actions from "../state/actions";
import { postNotification } from "./postNotification";
import { makeNotification } from "../utils";
import { setHighContrastAsync } from "../services/makecodeEditorService";

export async function loadProjectAsync(projectId: string, bool: boolean) {
const { dispatch } = stateAndDispatch();
await setHighContrastAsync(bool);
postNotification(makeNotification(`project ${projectId} evaluated`, 2000));

}
11 changes: 11 additions & 0 deletions teachertool/src/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,15 @@ export const getEditorUrl = (embedUrl: string) => {
// example: https://arcade.makecode.com/abc123 and this would get returned
const path = /\/([\da-zA-Z\.]+)(?:--)?/i.exec(window.location.pathname);
return `${embedUrl.replace(/\/$/, "")}/${path?.[1] || ""}`;
}

export const createIFrameUrl = (): string => {
const editorUrl: string = isLocal() ? "http://localhost:3232/index.html#editor" : getEditorUrl((window as any).pxtTargetBundle.appTheme.embedUrl);

let url = editorUrl
if (editorUrl.charAt(editorUrl.length - 1) === "/" && !isLocal()) {
url = editorUrl.substr(0, editorUrl.length - 1);
}
url += `?controller=1&ws=browser&nocookiebanner=1`;
return url;
}

0 comments on commit ecd3cb5

Please sign in to comment.