From e96939fcfc97f49399b67f3599feba46d9f9b608 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Mon, 11 Mar 2024 10:59:39 -0700 Subject: [PATCH 1/3] Add iframe driver class --- pxtservices/iframeDriver.ts | 553 ++++++++++++++++++ teachertool/src/components/MakecodeFrame.tsx | 10 +- .../src/services/makecodeEditorService.ts | 107 +--- teachertool/tsconfig.paths.json | 3 +- webapp/src/app.tsx | 26 +- webapp/src/cloud.ts | 11 +- 6 files changed, 618 insertions(+), 92 deletions(-) create mode 100644 pxtservices/iframeDriver.ts diff --git a/pxtservices/iframeDriver.ts b/pxtservices/iframeDriver.ts new file mode 100644 index 000000000000..46f8447c3b69 --- /dev/null +++ b/pxtservices/iframeDriver.ts @@ -0,0 +1,553 @@ +/// + +type MessageHandler = (response: any) => void; + +const MessageReceivedEvent = "message"; +const MessageSentEvent = "sent"; + +interface PendingMessage { + original: pxt.editor.EditorMessageRequest; + resolve: MessageHandler; + reject: MessageHandler; +} + +export interface IframeWorkspaceStatus { + projects: pxt.workspace.Project[]; + editor?: pxt.editor.EditorSyncState; + controllerId?: string; +} + +export interface IFrameWorkspaceHost { + saveProject(project: pxt.workspace.Project): Promise; + getWorkspaceProjects(): Promise; + resetWorkspace(): Promise; + onWorkspaceLoaded?(): Promise; +} + +/** + * Manages communication with the editor iframe. + * + * TODO: Currently only supports a single iframe as all incoming messages + * are posted on the window. Would be nice in the future to refactor the + * iframe messages to use a MessageChannel. + */ +export class IframeDriver { + protected readyForMessages = false; + protected messageQueue: pxt.editor.EditorMessageRequest[] = []; + protected nextId = 0; + protected pendingMessages: { [index: string]: PendingMessage } = {}; + protected editorEventListeners: { [index: string]: MessageHandler[] } = {}; + + constructor(public iframe: HTMLIFrameElement, public host?: IFrameWorkspaceHost) { + window.addEventListener("message", this.onMessageReceived); + } + + dispose() { + window.removeEventListener("message", this.onMessageReceived); + } + + async switchEditorLanguage(lang: "typescript" | "blocks" | "python") { + let action; + switch (lang) { + case "blocks": + action = "switchblocks"; + break; + case "typescript": + action = "switchjavascript"; + break; + case "python": + action = "switchpython"; + break; + } + + await this.sendRequest( + { + type: "pxteditor", + action + } as pxt.editor.EditorMessageRequest + ); + } + + async setLanguageRestriction(restriction: pxt.editor.LanguageRestriction) { + await this.sendRequest( + { + type: "pxteditor", + action: "setlanguagerestriction", + restriction + } as pxt.editor.EditorSetLanguageRestriction + ); + } + + async startSimulator() { + await this.sendRequest( + { + type: "pxteditor", + action: "startsimulator" + } as pxt.editor.EditorMessageRequest + ); + } + + async stopSimulator(unload = false) { + await this.sendRequest( + { + type: "pxteditor", + action: "stopsimulator", + unload + } as pxt.editor.EditorMessageStopRequest + ); + } + + async restartSimulator() { + await this.sendRequest( + { + type: "pxteditor", + action: "restartsimulator" + } as pxt.editor.EditorMessageRequest + ); + } + + async hideSimulator() { + await this.sendRequest( + { + type: "pxteditor", + action: "hidesimulator" + } as pxt.editor.EditorMessageRequest + ); + } + + async showSimulator() { + await this.sendRequest( + { + type: "pxteditor", + action: "showsimulator" + } as pxt.editor.EditorMessageRequest + ); + } + + async setSimulatorFullscreen(on: boolean) { + await this.sendRequest( + { + type: "pxteditor", + action: "setsimulatorfullscreen", + enabled: on + } as pxt.editor.EditorMessageSetSimulatorFullScreenRequest + ); + } + + async closeFlyout() { + await this.sendRequest( + { + type: "pxteditor", + action: "closeflyout" + } as pxt.editor.EditorMessageRequest + ); + } + + async unloadProject() { + await this.sendRequest( + { + type: "pxteditor", + action: "unloadproject" + } as pxt.editor.EditorMessageRequest + ); + } + + async saveProject() { + await this.sendRequest( + { + type: "pxteditor", + action: "saveproject" + } as pxt.editor.EditorMessageRequest + ); + } + + async undo() { + await this.sendRequest( + { + type: "pxteditor", + action: "undo" + } as pxt.editor.EditorMessageRequest + ); + } + + async redo() { + await this.sendRequest( + { + type: "pxteditor", + action: "redo" + } as pxt.editor.EditorMessageRequest + ); + } + + async setHighContrast(on: boolean) { + await this.sendRequest( + { + type: "pxteditor", + action: "sethighcontrast", + on + } as pxt.editor.EditorMessageSetHighContrastRequest + ); + } + + async toggleHighContrast() { + await this.sendRequest( + { + type: "pxteditor", + action: "togglehighcontrast" + } as pxt.editor.EditorMessageRequest + ); + } + + async toggleGreenScreen() { + await this.sendRequest( + { + type: "pxteditor", + action: "togglegreenscreen" + } as pxt.editor.EditorMessageRequest + ); + } + + async toggleSloMo(intervalSpeed?: number) { + await this.sendRequest( + { + type: "pxteditor", + action: "toggletrace", + intervalSpeed + } as pxt.editor.EditorMessageToggleTraceRequest + ); + } + + async setSloMoEnabled(enabled: boolean, intervalSpeed?: number) { + await this.sendRequest( + { + type: "pxteditor", + action: "settracestate", + enabled, + intervalSpeed + } as pxt.editor.EditorMessageSetTraceStateRequest + ); + } + + async printProject() { + await this.sendRequest( + { + type: "pxteditor", + action: "print" + } as pxt.editor.EditorMessageRequest + ); + } + + async getInfo(): Promise { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "info" + } as pxt.editor.EditorMessageRequest + ) as pxt.editor.EditorMessageResponse; + + return (resp.resp as pxt.editor.InfoMessage); + } + + async newProject(options: pxt.editor.ProjectCreationOptions) { + await this.sendRequest( + { + type: "pxteditor", + action: "newproject", + options + } as pxt.editor.EditorMessageNewProjectRequest + ); + } + + async importProject(project: pxt.workspace.Project, filters?: pxt.editor.ProjectFilters, searchBar?: boolean) { + await this.sendRequest( + { + type: "pxteditor", + action: "importproject", + project, + filters, + searchBar + } as pxt.editor.EditorMessageImportProjectRequest + ); + } + + async openHeader(headerId: string) { + await this.sendRequest( + { + type: "pxteditor", + action: "openheader", + headerId + } as pxt.editor.EditorMessageOpenHeaderRequest + ); + } + + async shareHeader(headerId: string, projectName: string) { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "shareproject", + headerId, + projectName + } as pxt.editor.EditorShareRequest + ) as pxt.editor.EditorMessageResponse; + + return resp.resp as pxt.editor.ShareData; + } + + async startActivity(activityType: "tutorial" | "example" | "recipe", path: string, title?: string, previousProjectHeaderId?: string, carryoverPreviousCode?: boolean) { + await this.sendRequest( + { + type: "pxteditor", + action: "startactivity", + activityType, + path, + title, + previousProjectHeaderId, + carryoverPreviousCode + } as pxt.editor.EditorMessageStartActivity + ); + } + + async importTutorial(markdown: string) { + await this.sendRequest( + { + type: "pxteditor", + action: "importtutorial", + markdown + } as pxt.editor.EditorMessageImportTutorialRequest + ); + } + + async pair() { + await this.sendRequest( + { + type: "pxteditor", + action: "pair" + } as pxt.editor.EditorMessageRequest + ); + } + + async decompileToBlocks(ts: string, snippetMode?: boolean, layout?: pxt.editor.BlockLayout) { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "renderblocks", + ts, + snippetMode, + layout + } as pxt.editor.EditorMessageRenderBlocksRequest + ) as pxt.editor.EditorMessageResponse; + + return resp.resp as pxt.editor.EditorMessageRenderBlocksResponse; + } + + async decompileToPython(ts: string) { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "renderpython", + ts + } as pxt.editor.EditorMessageRenderPythonRequest + ) as pxt.editor.EditorMessageResponse; + + return (resp.resp as pxt.editor.EditorMessageRenderPythonResponse).python; + } + + async runValidatorPlan(validatorPlan: pxt.blocks.ValidatorPlan, planLib: pxt.blocks.ValidatorPlan[]) { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "runeval", + validatorPlan, + planLib, + } as pxt.editor.EditorMessageRunEvalRequest + ) as pxt.editor.EditorMessageResponse; + + return resp.resp as pxt.blocks.EvaluationResult; + } + + async saveLocalProjectsToCloud(headerIds: string[]) { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "savelocalprojectstocloud", + headerIds + } as pxt.editor.EditorMessageSaveLocalProjectsToCloud + ) as pxt.editor.EditorMessageResponse; + + return resp.resp as pxt.editor.EditorMessageSaveLocalProjectsToCloudResponse; + } + + async convertCloudProjectsToLocal(userId: string) { + await this.sendRequest( + { + type: "pxteditor", + action: "convertcloudprojectstolocal", + userId + } as pxt.editor.EditorMessageConvertCloudProjectsToLocal + ); + } + + async requestProjectCloudStatus(headerIds: string[]) { + await this.sendRequest( + { + type: "pxteditor", + action: "requestprojectcloudstatus", + headerIds + } as pxt.editor.EditorMessageRequestProjectCloudStatus + ); + } + + addEventListener(event: typeof MessageSentEvent, handler: (ev: pxt.editor.EditorMessage) => void): void; + addEventListener(event: typeof MessageReceivedEvent, handler: (ev: pxt.editor.EditorMessage) => void): void; + addEventListener(event: "event", handler: (ev: pxt.editor.EditorMessageEventRequest) => void): void; + addEventListener(event: "simevent", handler: (ev: pxt.editor.EditorSimulatorEvent) => void): void; + addEventListener(event: "tutorialevent", handler: (ev: pxt.editor.EditorMessageTutorialEventRequest) => void): void; + addEventListener(event: "editorcontentloaded", handler: (ev: pxt.editor.EditorContentLoadedRequest) => void): void; + addEventListener(event: "workspacesave", handler: (ev: pxt.editor.EditorWorkspaceSaveRequest) => void): void; + addEventListener(event: "workspaceevent", handler: (ev: pxt.editor.EditorWorkspaceEvent) => void): void; + addEventListener(event: "workspacereset", handler: (ev: pxt.editor.EditorWorkspaceSyncRequest) => void): void; + addEventListener(event: "workspacesync", handler: (ev: pxt.editor.EditorWorkspaceSyncRequest) => void): void; + addEventListener(event: "workspaceloaded", handler: (ev: pxt.editor.EditorWorkspaceSyncRequest) => void): void; + addEventListener(event: "workspacediagnostics", handler: (ev: pxt.editor.EditorWorkspaceDiagnostics) => void): void; + addEventListener(event: "editorcontentloaded", handler: (ev: pxt.editor.EditorContentLoadedRequest) => void): void; + addEventListener(event: "projectcloudstatus", handler: (ev: pxt.editor.EditorMessageProjectCloudStatus) => void): void; + addEventListener(event: string, handler: (ev: any) => void): void { + if (!this.editorEventListeners[event]) this.editorEventListeners[event] = []; + this.editorEventListeners[event].push(handler); + } + + removeEventListener(event: string, handler: (ev: any) => void) { + if (this.editorEventListeners[event]) { + const filtered = this.editorEventListeners[event].filter(h => h !== handler); + + if (filtered.length === 0) { + delete this.editorEventListeners[event]; + } + else { + this.editorEventListeners[event] = filtered; + } + } + } + + sendMessage(message: pxt.editor.EditorMessageRequest): Promise { + return this.sendRequest(message) as Promise; + } + + protected onMessageReceived = (event: MessageEvent) => { + const data = event.data as pxt.editor.EditorMessageRequest; + if (!data || !/^pxt(host|editor|pkgext|sim)$/.test(data.type)) return; + + if (data.type === "pxteditor") { + if (data.id && this.pendingMessages[data.id]) { + const resp = event.data as pxt.editor.EditorMessageResponse; + const pending = this.pendingMessages[resp.id!]; + delete this.pendingMessages[resp.id!]; + + if (resp.success) { + pending.resolve(resp); + } + else { + pending.reject(resp.error || new Error("Unknown error: iFrame returned failure")); + } + } + } + else if (data.type === "pxthost") { + if (data.action === "editorcontentloaded") { + this.readyForMessages = true; + this.sendMessageCore(); // flush message queue. + } + else if (data.action === "workspacesync" || data.action === "workspacesave" || data.action === "workspacereset" || data.action === "workspaceloaded") { + this.handleWorkspaceSync(data as pxt.editor.EditorWorkspaceSyncRequest); + } + + this.fireEvent(data.action, data); + } + + this.fireEvent(MessageReceivedEvent, data); + } + + protected fireEvent(event: string, data: any) { + const listeners = this.editorEventListeners[event]; + if (!listeners) return; + + for (const handler of listeners) { + try { + handler(data); + } + catch (e) { + console.error(e); + } + } + } + + protected async handleWorkspaceSync(event: pxt.editor.EditorWorkspaceSyncRequest | pxt.editor.EditorWorkspaceSaveRequest) { + if (!this.host) return; + + let error: any = undefined; + try { + if (event.action === "workspacesync") { + const status = await this.host.getWorkspaceProjects(); + this.sendMessageCore({ + type: "pxthost", + id: event.id, + success: !!status, + projects: status?.projects, + editor: status?.editor, + controllerId: status?.controllerId + } as pxt.editor.EditorWorkspaceSyncResponse); + } + else if (event.action === "workspacereset") { + await this.host.resetWorkspace(); + } + else if (event.action === "workspacesave") { + await this.host.saveProject(event.project); + } + else if (event.action === "workspaceloaded") { + if (this.host.onWorkspaceLoaded) { + await this.host.onWorkspaceLoaded(); + } + } + } + catch (e) { + error = e; + console.error(e); + } + finally { + if (event.response) { + this.sendMessageCore({ + type: "pxthost", + id: event.id, + success: !error, + error + } as pxt.editor.EditorMessageResponse); + } + } + } + + protected sendRequest(message: any) { + return new Promise((resolve, reject) => { + message.response = true; + message.id = this.nextId++ + ""; + this.pendingMessages[message.id] = { + original: message, + resolve, + reject + }; + this.sendMessageCore(message); + }); + } + + protected sendMessageCore(message?: any) { + if (message) { + this.messageQueue.push(message); + } + + if (this.readyForMessages) { + while (this.messageQueue.length) { + const toSend = this.messageQueue.shift(); + this.iframe.contentWindow?.postMessage(toSend, "*"); + this.fireEvent(MessageSentEvent, toSend); + } + } + } +} \ No newline at end of file diff --git a/teachertool/src/components/MakecodeFrame.tsx b/teachertool/src/components/MakecodeFrame.tsx index f5a65cdf345c..34c0c8be95f4 100644 --- a/teachertool/src/components/MakecodeFrame.tsx +++ b/teachertool/src/components/MakecodeFrame.tsx @@ -2,7 +2,7 @@ import css from "./styling/MakeCodeFrame.module.scss"; import { useContext, useEffect } from "react"; -import { clearReady, setEditorRef } from "../services/makecodeEditorService"; +import { setEditorRef } from "../services/makecodeEditorService"; import { AppStateContext } from "../state/appStateContext"; import { getEditorUrl } from "../utils"; @@ -13,7 +13,9 @@ export const MakeCodeFrame: React.FC = () => { // Clear iframe state when the iframe url is changed useEffect(() => { - clearReady(); + if (!teacherTool.projectMetadata?.id) { + setEditorRef(undefined); + } }, [teacherTool.projectMetadata?.id]); function createIFrameUrl(shareId: string): string { @@ -30,7 +32,9 @@ export const MakeCodeFrame: React.FC = () => { } const handleIFrameRef = (el: HTMLIFrameElement | null) => { - setEditorRef(el ?? undefined); + if (el) { + setEditorRef(el); + } }; /* eslint-disable @microsoft/sdl/react-iframe-missing-sandbox */ diff --git a/teachertool/src/services/makecodeEditorService.ts b/teachertool/src/services/makecodeEditorService.ts index 9df72cda030b..666114ab5450 100644 --- a/teachertool/src/services/makecodeEditorService.ts +++ b/teachertool/src/services/makecodeEditorService.ts @@ -3,92 +3,37 @@ import { ErrorCode } from "../types/errorCode"; import { logDebug, logError } from "./loggingService"; import * as AutorunService from "./autorunService"; +import { IframeDriver } from "pxtservices/iframeDriver"; -interface PendingMessage { - original: pxt.editor.EditorMessageRequest; - handler: (response: any) => void; -} - -let makecodeEditorRef: HTMLIFrameElement | undefined; -let readyForMessages: boolean; -const messageQueue: pxt.editor.EditorMessageRequest[] = []; -let nextId: number = 0; -let pendingMessages: { [index: string]: PendingMessage } = {}; - -function onMessageReceived(event: MessageEvent) { - logDebug(`Message received from iframe: ${JSON.stringify(event.data)}`); - const data = event.data as pxt.editor.EditorMessageRequest; - if (data.type === "pxteditor") { - if (data.action === "editorcontentloaded") { - readyForMessages = true; - sendMessageAsync(); // flush message queue. - AutorunService.poke(); - } +let driver: IframeDriver | undefined; - if (data.id && pendingMessages[data.id]) { - const pending = pendingMessages[data.id]; - pending.handler(data); - delete pendingMessages[data.id]; - } - } -} - -function sendMessageAsync(message?: any) { - return new Promise(resolve => { - const sendMessageCore = (message: any) => { - logDebug(`Sending message to iframe: ${JSON.stringify(message)}`); - makecodeEditorRef!.contentWindow!.postMessage(message, "*"); - }; - - if (message) { - message.response = true; - message.id = nextId++ + ""; - pendingMessages[message.id] = { - original: message, - handler: resolve, - }; - messageQueue.push(message); - } - if (readyForMessages) { - while (messageQueue.length) { - sendMessageCore(messageQueue.shift()); - } - } - }); -} +export function setEditorRef(ref: HTMLIFrameElement | undefined) { + if (driver) { + if (driver.iframe === ref) return; -// Check if the result was successful and (if expected) has data. -// Logs errors and throws if the result was not successful. -function validateResponse(result: pxt.editor.EditorMessageResponse, expectResponseData: boolean) { - if (!result.success) { - throw new Error(`Server returned failed status.`); + driver.dispose(); + driver = undefined; } - if (expectResponseData && !result?.resp) { - throw new Error(`Missing response data.`); - } -} -export function clearReady() { - readyForMessages = false; -} - -export function setEditorRef(ref: HTMLIFrameElement | undefined) { - makecodeEditorRef = ref ?? undefined; - window.removeEventListener("message", onMessageReceived); if (ref) { - window.addEventListener("message", onMessageReceived); + driver = new IframeDriver(ref); + + driver.addEventListener("message", ev => { + logDebug(`Message received from iframe: ${JSON.stringify(ev)}`); + }); + driver.addEventListener("sent", ev => { + logDebug(`Sent message to iframe: ${JSON.stringify(ev)}`); + }); + driver.addEventListener("editorcontentloaded", ev => { + AutorunService.poke(); + }); } } // 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); + await driver!.setHighContrast(on) } export async function runValidatorPlanAsync( @@ -98,15 +43,11 @@ export async function runValidatorPlanAsync( let evalResults = undefined; try { - const response = await sendMessageAsync({ - type: "pxteditor", - action: "runeval", - validatorPlan: validatorPlan, - planLib: planLib, - } as pxt.editor.EditorMessageRunEvalRequest); - const result = response as pxt.editor.EditorMessageResponse; - validateResponse(result, true); // Throws on failure - evalResults = result.resp as pxt.blocks.EvaluationResult; + evalResults = await driver!.runValidatorPlan(validatorPlan, planLib); + + if (!evalResults) { + throw new Error(`Missing response data.`); + } } catch (e: any) { logError(ErrorCode.runEval, e); } diff --git a/teachertool/tsconfig.paths.json b/teachertool/tsconfig.paths.json index ae46474ee0ea..91128d01bc3b 100644 --- a/teachertool/tsconfig.paths.json +++ b/teachertool/tsconfig.paths.json @@ -3,7 +3,8 @@ "paths": { "react-common/*": ["../react-common/*"], "react/*": ["../node_modules/react/*"], - "react-dom/*": ["../node_modules/react-dom/*"] + "react-dom/*": ["../node_modules/react-dom/*"], + "pxtservices/*": ["../pxtservices/*"] } } } diff --git a/webapp/src/app.tsx b/webapp/src/app.tsx index 6f39d909e06b..8305bdfcc720 100644 --- a/webapp/src/app.tsx +++ b/webapp/src/app.tsx @@ -3801,12 +3801,20 @@ export class ProjectView const cloudStatus = cloudMd?.cloudStatus(); if (cloudStatus) { const msg: pxt.editor.EditorMessageProjectCloudStatus = { - type: "pxteditor", + type: "pxthost", action: "projectcloudstatus", headerId: cloudMd.headerId, status: cloudStatus.value }; pxteditor.postHostMessageAsync(msg); + + // Deprecated: This was originally fired with the "pxteditor" + // type, which should only be used for responses, not events. + // Use the pxthost version above instead + pxteditor.postHostMessageAsync({ + ...msg, + type: "pxteditor" + }); } } @@ -4831,10 +4839,20 @@ export class ProjectView } } - pxteditor.postHostMessageAsync({ - type: "pxteditor", + const msg: pxt.editor.EditorContentLoadedRequest = { + type: "pxthost", action: "editorcontentloaded" - } as pxt.editor.EditorContentLoadedRequest) + }; + + pxteditor.postHostMessageAsync(msg); + + // Deprecated: This was originally fired with the "pxteditor" + // type, which should only be used for responses, not events. + // Use the pxthost version above instead + pxteditor.postHostMessageAsync({ + ...msg, + type: "pxteditor" + }); if (this.pendingImport) { this.pendingImport.resolve(); diff --git a/webapp/src/cloud.ts b/webapp/src/cloud.ts index 378efe38ecda..4548c215b172 100644 --- a/webapp/src/cloud.ts +++ b/webapp/src/cloud.ts @@ -564,13 +564,22 @@ export async function requestProjectCloudStatus(headerIds: string[]): Promise Date: Mon, 11 Mar 2024 11:05:16 -0700 Subject: [PATCH 2/3] add tsconfig --- pxtservices/tsconfig.json | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 pxtservices/tsconfig.json diff --git a/pxtservices/tsconfig.json b/pxtservices/tsconfig.json new file mode 100644 index 000000000000..12a27c367831 --- /dev/null +++ b/pxtservices/tsconfig.json @@ -0,0 +1,31 @@ +{ + "compilerOptions": { + "target": "es2017", + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "module": "commonjs", + "outDir": "../built/pxtservices", + "rootDir": ".", + "newLine": "LF", + "sourceMap": false, + "moduleResolution": "node", + "isolatedModules": false, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "declaration": true, + "preserveConstEnums": true, + "lib": [ + "dom", + "dom.iterable", + "scripthost", + "es2017", + "ES2018.Promise" + ], + "incremental": false, + "skipLibCheck": true + }, + "exclude": [ + "node_modules" + ] +} \ No newline at end of file From 71a67e194f6bba87a5d3b4d1130803f3225de2cd Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Wed, 13 Mar 2024 14:24:05 -0700 Subject: [PATCH 3/3] PR feedback --- teachertool/src/services/makecodeEditorService.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/teachertool/src/services/makecodeEditorService.ts b/teachertool/src/services/makecodeEditorService.ts index 666114ab5450..c66bf4d0082d 100644 --- a/teachertool/src/services/makecodeEditorService.ts +++ b/teachertool/src/services/makecodeEditorService.ts @@ -7,6 +7,7 @@ import { IframeDriver } from "pxtservices/iframeDriver"; let driver: IframeDriver | undefined; +let highContrast: boolean; export function setEditorRef(ref: HTMLIFrameElement | undefined) { if (driver) { @@ -28,12 +29,18 @@ export function setEditorRef(ref: HTMLIFrameElement | undefined) { driver.addEventListener("editorcontentloaded", ev => { AutorunService.poke(); }); + + driver.setHighContrast(highContrast); } } // an example of events that we want to/can send to the editor export async function setHighContrastAsync(on: boolean) { - await driver!.setHighContrast(on) + highContrast = on; + + if (driver) { + await driver!.setHighContrast(on) + } } export async function runValidatorPlanAsync(