From f4e9aad00274ae9b384a9c308d10fa6459a2db50 Mon Sep 17 00:00:00 2001 From: Richard Knoll Date: Tue, 2 Apr 2024 10:04:21 -0700 Subject: [PATCH] Refactor iframe communication and use MessageChannel (#9945) * Refactor iframe communication and use MessageChannel * remove unused typing * Update pxtservices/iframeEmbeddedClient.ts Co-authored-by: Eric Anderson * Update pxtservices/iframeEmbeddedClient.ts Co-authored-by: Eric Anderson * pr feedback * lint --------- Co-authored-by: Eric Anderson --- gulpfile.js | 14 +- localtypings/pxteditor.d.ts | 70 +++ pxteditor/editorcontroller.ts | 37 +- pxtservices/.eslintrc.js | 5 + pxtservices/assetEditorDriver.ts | 65 ++ pxtservices/editorDriver.ts | 502 +++++++++++++++ pxtservices/iframeDriver.ts | 575 +++--------------- pxtservices/iframeEmbeddedClient.ts | 86 +++ .../src/services/makecodeEditorService.ts | 6 +- webapp/src/assetEditor.tsx | 86 +-- 10 files changed, 865 insertions(+), 581 deletions(-) create mode 100644 pxtservices/.eslintrc.js create mode 100644 pxtservices/assetEditorDriver.ts create mode 100644 pxtservices/editorDriver.ts create mode 100644 pxtservices/iframeEmbeddedClient.ts diff --git a/gulpfile.js b/gulpfile.js index 7d96ef8361cd..cf5a14b63026 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -48,6 +48,7 @@ const cli = () => compileTsProject("cli", "built", true); const webapp = () => compileTsProject("webapp", "built", true); const reactCommon = () => compileTsProject("react-common", "built/react-common", true); const pxtblocks = () => compileTsProject("pxtblocks", "built/pxtblocks", true); +const pxtservices = () => compileTsProject("pxtservices", "built/pxtservices", true); const pxtapp = () => gulp.src([ "node_modules/lzma/src/lzma_worker-min.js", @@ -118,7 +119,7 @@ function initWatch() { pxtlib, gulp.parallel(pxtcompiler, pxtsim, backendutils), pxtpy, - gulp.parallel(pxtblocks, pxteditor), + gulp.parallel(pxtblocks, pxteditor, pxtservices), gulp.parallel(pxtrunner, cli, pxtcommon), gulp.parallel(updatestrings, browserifyEmbed), gulp.parallel(pxtjs, pxtdts, pxtapp, pxtworker, pxtembed), @@ -139,6 +140,7 @@ function initWatch() { gulp.watch("./pxtpy/**/*", gulp.series(pxtpy, ...tasks.slice(3))); gulp.watch("./pxtblocks/**/*", gulp.series(pxtblocks, ...tasks.slice(4))); + gulp.watch("./pxtservices/**/*", gulp.series(pxtservices, ...tasks.slice(4))); gulp.watch("./pxteditor/**/*", gulp.series(pxteditor, ...tasks.slice(4))); @@ -219,6 +221,7 @@ function updatestrings() { return buildStrings("built/strings.json", [ "cli", "pxtblocks", + "pxtservices", "pxtcompiler", "pxteditor", "pxtlib", @@ -610,8 +613,9 @@ const maybeBuildWebapps = () => { const lintWithEslint = () => Promise.all( ["cli", "pxtblocks", "pxteditor", "pxtlib", "pxtcompiler", - "pxtpy", "pxtrunner", "pxtsim", "webapp", - "docfiles/pxtweb", "skillmap", "authcode", "multiplayer"/*, "kiosk"*/, "teachertool", "docs/static/streamer"].map(dirname => + "pxtpy", "pxtrunner", "pxtsim", "webapp", "pxtservices", + "docfiles/pxtweb", "skillmap", "authcode", + "multiplayer"/*, "kiosk"*/, "teachertool", "docs/static/streamer"].map(dirname => exec(`node node_modules/eslint/bin/eslint.js -c .eslintrc.js --ext .ts,.tsx ./${dirname}/`, true))) .then(() => console.log("linted")) const lint = lintWithEslint @@ -723,7 +727,7 @@ const buildAll = gulp.series( gulp.parallel(pxtlib, pxtweb), gulp.parallel(pxtcompiler, pxtsim, backendutils), pxtpy, - gulp.parallel(pxteditor, pxtblocks), + gulp.parallel(pxteditor, pxtblocks, pxtservices), gulp.parallel(pxtrunner, cli, pxtcommon), browserifyEmbed, gulp.parallel(pxtjs, pxtdts, pxtapp, pxtworker, pxtembed), @@ -747,7 +751,7 @@ exports.clean = clean; exports.build = buildAll; exports.webapp = gulp.series( - gulp.parallel(reactCommon, pxtblocks, pxteditor), + gulp.parallel(reactCommon, pxtblocks, pxteditor, pxtservices), webapp, browserifyWebapp, browserifyAssetEditor diff --git a/localtypings/pxteditor.d.ts b/localtypings/pxteditor.d.ts index 5e72b5bf2b89..ea9d93e15214 100644 --- a/localtypings/pxteditor.d.ts +++ b/localtypings/pxteditor.d.ts @@ -16,6 +16,11 @@ declare namespace pxt.editor { * flag to request response */ response?: boolean; + + /** + * Frame identifier that can be passed to the iframe by adding the frameId query parameter + */ + frameId?: string; } export interface EditorMessageResponse extends EditorMessage { @@ -1237,6 +1242,71 @@ declare namespace pxt.editor { */ blockId?: string; } + + interface BaseAssetEditorRequest { + id?: number; + files: pxt.Map; + palette?: string[]; + } + + interface OpenAssetEditorRequest extends BaseAssetEditorRequest { + type: "open"; + assetId: string; + assetType: pxt.AssetType; + } + + interface CreateAssetEditorRequest extends BaseAssetEditorRequest { + type: "create"; + assetType: pxt.AssetType; + displayName?: string; + } + + interface SaveAssetEditorRequest extends BaseAssetEditorRequest { + type: "save"; + } + + interface DuplicateAssetEditorRequest extends BaseAssetEditorRequest { + type: "duplicate"; + assetId: string; + assetType: pxt.AssetType; + } + + type AssetEditorRequest = OpenAssetEditorRequest | CreateAssetEditorRequest | SaveAssetEditorRequest | DuplicateAssetEditorRequest; + + interface BaseAssetEditorResponse { + id?: number; + } + + interface OpenAssetEditorResponse extends BaseAssetEditorResponse { + type: "open"; + } + + interface CreateAssetEditorResponse extends BaseAssetEditorResponse { + type: "create"; + } + + interface SaveAssetEditorResponse extends BaseAssetEditorResponse { + type: "save"; + files: pxt.Map; + } + + interface DuplicateAssetEditorResponse extends BaseAssetEditorResponse { + type: "duplicate"; + } + + type AssetEditorResponse = OpenAssetEditorResponse | CreateAssetEditorResponse | SaveAssetEditorResponse | DuplicateAssetEditorResponse; + + interface AssetEditorRequestSaveEvent { + type: "event"; + kind: "done-clicked"; + } + + interface AssetEditorReadyEvent { + type: "event"; + kind: "ready"; + } + + type AssetEditorEvent = AssetEditorRequestSaveEvent | AssetEditorReadyEvent; } declare namespace pxt.workspace { diff --git a/pxteditor/editorcontroller.ts b/pxteditor/editorcontroller.ts index 964415ea16b3..5819c6491de8 100644 --- a/pxteditor/editorcontroller.ts +++ b/pxteditor/editorcontroller.ts @@ -3,10 +3,15 @@ import { runValidatorPlan } from "./code-validation/runValidatorPlan"; import IProjectView = pxt.editor.IProjectView; +import { IFrameEmbeddedClient } from "../pxtservices/iframeEmbeddedClient"; + const pendingRequests: pxt.Map<{ resolve: (res?: pxt.editor.EditorMessageResponse | PromiseLike) => void; reject: (err: any) => void; }> = {}; + +let iframeClient: IFrameEmbeddedClient; + /** * Binds incoming window messages to the project view. * Requires the "allowParentController" flag in the pxtarget.json/appTheme object. @@ -24,7 +29,7 @@ export function bindEditorMessages(getEditorAsync: () => Promise) if (!allowEditorMessages && !allowExtensionMessages && !allowSimTelemetry) return; - window.addEventListener("message", (msg: MessageEvent) => { + const handleMessage = (msg: MessageEvent) => { const data = msg.data as pxt.editor.EditorMessage; if (!data || !/^pxt(host|editor|pkgext|sim)$/.test(data.type)) return false; @@ -154,7 +159,7 @@ export function bindEditorMessages(getEditorAsync: () => Promise) }) }); } - case "renderxml": { +case "renderxml": { const rendermsg = data as pxt.editor.EditorMessageRenderXmlRequest; return Promise.resolve() .then(() => { @@ -186,7 +191,7 @@ export function bindEditorMessages(getEditorAsync: () => Promise) resp = { result: results }; }); } - case "gettoolboxcategories": { +case "gettoolboxcategories": { const msg = data as pxt.editor.EditorMessageGetToolboxCategoriesRequest; return Promise.resolve() .then(() => { @@ -289,7 +294,9 @@ export function bindEditorMessages(getEditorAsync: () => Promise) } return true; - }, false) + }; + + iframeClient = new IFrameEmbeddedClient(handleMessage); } /** @@ -343,13 +350,20 @@ export function enableControllerAnalytics() { function sendResponse(request: pxt.editor.EditorMessage, resp: any, success: boolean, error: any) { if (request.response) { - window.parent.postMessage({ + const toSend = { type: request.type, id: request.id, resp, success, error - }, "*"); + }; + + if (iframeClient) { + iframeClient.postMessage(toSend) + } + else { + window.parent.postMessage(toSend, "*"); + } } } @@ -369,8 +383,15 @@ export function postHostMessageAsync(msg: pxt.editor.EditorMessageRequest): Prom env.id = ts.pxtc.Util.guidGen(); if (msg.response) pendingRequests[env.id] = { resolve, reject }; - window.parent.postMessage(env, "*"); + + if (iframeClient) { + iframeClient.postMessage(env); + } + else { + window.parent.postMessage(env, "*"); + } + if (!msg.response) resolve(undefined) }) -} +} \ No newline at end of file diff --git a/pxtservices/.eslintrc.js b/pxtservices/.eslintrc.js new file mode 100644 index 000000000000..0bb476d3747b --- /dev/null +++ b/pxtservices/.eslintrc.js @@ -0,0 +1,5 @@ +module.exports = { + "parserOptions": { + "project": "pxtservices/tsconfig.json", + } +} \ No newline at end of file diff --git a/pxtservices/assetEditorDriver.ts b/pxtservices/assetEditorDriver.ts new file mode 100644 index 000000000000..ad0043020abf --- /dev/null +++ b/pxtservices/assetEditorDriver.ts @@ -0,0 +1,65 @@ +import { IframeDriver } from "./iframeDriver"; + +export class AssetEditorDriver extends IframeDriver { + constructor(frame: HTMLIFrameElement) { + super(frame); + } + + async openAsset(assetId: string, assetType: pxt.AssetType, files: pxt.Map, palette?: string[]) { + await this.sendRequest( + { + type: "open", + assetId, + assetType, + files, + palette + } as pxt.editor.OpenAssetEditorRequest + ); + } + + async createAsset(assetType: pxt.AssetType, files: pxt.Map, displayName?: string, palette?: string[]) { + await this.sendRequest({ + type: "create", + assetType, + files, + displayName, + palette + } as pxt.editor.CreateAssetEditorRequest); + } + + async saveAsset() { + const resp = await this.sendRequest({ + type: "save" + } as pxt.editor.SaveAssetEditorRequest); + + return (resp as pxt.editor.SaveAssetEditorResponse).files; + } + + async duplicateAsset(assetId: string, assetType: pxt.AssetType, files: pxt.Map, palette?: string[]) { + await this.sendRequest({ + type: "duplicate", + assetId, + assetType, + files, + palette + } as pxt.editor.DuplicateAssetEditorRequest); + } + + addEventListener(event: "ready", handler: (ev: pxt.editor.AssetEditorReadyEvent) => void): void; + addEventListener(event: "done-clicked", handler: (ev: pxt.editor.AssetEditorRequestSaveEvent) => void): void; + addEventListener(event: string, handler: (ev: any) => void): void { + super.addEventListener(event, handler); + } + + protected handleMessage(event: MessageEvent): void { + const data = event.data; + if (!data) return; + + if (data.type === "event") { + this.fireEvent((data as pxt.editor.AssetEditorEvent).kind, data); + } + else { + this.resolvePendingMessage(event); + } + } +} \ No newline at end of file diff --git a/pxtservices/editorDriver.ts b/pxtservices/editorDriver.ts new file mode 100644 index 000000000000..50379cca223b --- /dev/null +++ b/pxtservices/editorDriver.ts @@ -0,0 +1,502 @@ +/// + +import { IframeDriver } from "./iframeDriver"; + +const MessageReceivedEvent = "message"; +const MessageSentEvent = "sent"; + +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. + */ +export class EditorDriver extends IframeDriver { + constructor(public iframe: HTMLIFrameElement, public host?: IFrameWorkspaceHost) { + super(iframe); + } + + 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 renderXml(xml: string) { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "renderxml", + xml + } as pxt.editor.EditorMessageRenderXmlRequest + ) as pxt.editor.EditorMessageResponse; + + return resp.resp; + } + + async renderByBlockId(blockId: string) { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "renderbyblockid", + blockId: blockId + } as pxt.editor.EditorMessageRenderByBlockIdRequest + ) as pxt.editor.EditorMessageResponse; + + return resp.resp; + } + + async getToolboxCategories(advanced?: boolean): Promise { + const resp = await this.sendRequest( + { + type: "pxteditor", + action: "gettoolboxcategories", + advanced + } as pxt.editor.EditorMessageGetToolboxCategoriesRequest + ) as pxt.editor.EditorMessageResponse; + + return (resp.resp as pxt.editor.EditorMessageGetToolboxCategoriesResponse).categories; + } + + 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: "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 { + super.addEventListener(event, handler); + } + + sendMessage(message: pxt.editor.EditorMessageRequest): Promise { + return this.sendRequest(message) as Promise; + } + + protected handleMessage(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") { + this.resolvePendingMessage(event); + } + 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 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); + } + } + } +} \ No newline at end of file diff --git a/pxtservices/iframeDriver.ts b/pxtservices/iframeDriver.ts index cd906f0ae11f..bef798c45622 100644 --- a/pxtservices/iframeDriver.ts +++ b/pxtservices/iframeDriver.ts @@ -1,5 +1,7 @@ /// +import { IframeClientMessage } from "./iframeEmbeddedClient"; + type MessageHandler = (response: any) => void; const MessageReceivedEvent = "message"; @@ -11,442 +13,46 @@ interface PendingMessage { 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. + * Abstract class for driving communication with an embedded iframe */ -export class IframeDriver { +export abstract class IframeDriver { protected readyForMessages = false; protected messageQueue: pxt.editor.EditorMessageRequest[] = []; protected nextId = 0; protected pendingMessages: { [index: string]: PendingMessage } = {}; protected editorEventListeners: { [index: string]: MessageHandler[] } = {}; + protected port: MessagePort; + protected portRequestPending: boolean; + protected frameId: string; - constructor(public iframe: HTMLIFrameElement, public host?: IFrameWorkspaceHost) { - window.addEventListener("message", this.onMessageReceived); - } - - dispose() { - window.removeEventListener("message", this.onMessageReceived); - } + constructor(public iframe: HTMLIFrameElement) { + const queryParams = new URLSearchParams(new URL(iframe.src).search); - 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; + if (queryParams.has("frameid")) { + this.frameId = queryParams.get("frameid")!; } - 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 renderXml(xml: string) { - const resp = await this.sendRequest( - { - type: "pxteditor", - action: "renderxml", - xml - } as pxt.editor.EditorMessageRenderXmlRequest - ) as pxt.editor.EditorMessageResponse; - - return resp.resp; - } - - async renderByBlockId(blockId: string) { - const resp = await this.sendRequest( - { - type: "pxteditor", - action: "renderbyblockid", - blockId: blockId - } as pxt.editor.EditorMessageRenderByBlockIdRequest - ) as pxt.editor.EditorMessageResponse; - - return resp.resp; - } - - async getToolboxCategories(advanced?: boolean): Promise { - const resp = await this.sendRequest( - { - type: "pxteditor", - action: "gettoolboxcategories", - advanced - } as pxt.editor.EditorMessageGetToolboxCategoriesRequest - ) as pxt.editor.EditorMessageResponse; - - return (resp.resp as pxt.editor.EditorMessageGetToolboxCategoriesResponse).categories; - } - - 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; + window.addEventListener("message", this.onMessageReceived); - return resp.resp as pxt.editor.EditorMessageSaveLocalProjectsToCloudResponse; + // In case this driver was created after the iframe was loaded and we missed + // the ready event, send a ready request for it to parrot back + if (iframe.contentWindow) { + iframe.contentWindow.postMessage({ + type: "iframeclientready" + } as IframeClientMessage, "*"); + } } - async convertCloudProjectsToLocal(userId: string) { - await this.sendRequest( - { - type: "pxteditor", - action: "convertcloudprojectstolocal", - userId - } as pxt.editor.EditorMessageConvertCloudProjectsToLocal - ); - } + protected abstract handleMessage(message: MessageEvent): void; - async requestProjectCloudStatus(headerIds: string[]) { - await this.sendRequest( - { - type: "pxteditor", - action: "requestprojectcloudstatus", - headerIds - } as pxt.editor.EditorMessageRequestProjectCloudStatus - ); + dispose() { + window.removeEventListener("message", this.onMessageReceived); + if (this.port) { + this.port.close(); + } } - 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); @@ -465,41 +71,46 @@ export class IframeDriver { } } - 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; + const data = event.data; - 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 (data) { + if (this.frameId && data.frameId !== this.frameId) { + return; + } - if (resp.success) { - pending.resolve(resp); + if (data.type === "iframeclientready") { + if (this.frameId && !this.port) { + this.createMessagePort(); } else { - pending.reject(resp.error || new Error("Unknown error: iFrame returned failure")); + this.readyForMessages = true; + this.sendMessageCore(); } + return; + } + else if (data.type === "iframeclientsetmessageport") { + return; } } - else if (data.type === "pxthost") { - if (data.action === "editorcontentloaded") { - this.readyForMessages = true; - this.sendMessageCore(); // flush message queue. + + this.handleMessage(event); + } + + protected resolvePendingMessage(event: MessageEvent) { + const data = event.data as pxt.editor.EditorMessageResponse; + 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 if (data.action === "workspacesync" || data.action === "workspacesave" || data.action === "workspacereset" || data.action === "workspaceloaded") { - this.handleWorkspaceSync(data as pxt.editor.EditorWorkspaceSyncRequest); + else { + pending.reject(resp.error || new Error("Unknown error: iFrame returned failure")); } - - this.fireEvent(data.action, data); } - - this.fireEvent(MessageReceivedEvent, data); } protected fireEvent(event: string, data: any) { @@ -516,50 +127,6 @@ export class IframeDriver { } } - 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; @@ -581,9 +148,39 @@ export class IframeDriver { if (this.readyForMessages) { while (this.messageQueue.length) { const toSend = this.messageQueue.shift(); - this.iframe.contentWindow?.postMessage(toSend, "*"); + if (this.port) { + this.port.postMessage(toSend); + } + else { + this.iframe.contentWindow?.postMessage(toSend, "*"); + } this.fireEvent(MessageSentEvent, toSend); } } } -} + + protected createMessagePort() { + if (this.port || this.portRequestPending) return; + + this.portRequestPending = true + + const channel = new MessageChannel(); + + channel.port1.onmessage = (message: MessageEvent) => { + if (!this.port) { + this.port = channel.port1; + this.frameId = undefined; + window.removeEventListener("message", this.onMessageReceived); + + this.readyForMessages = true; + this.sendMessageCore(); + } + + this.onMessageReceived(message); + }; + + this.iframe.contentWindow!.postMessage({ + type: "iframeclientsetmessageport", + } as IframeClientMessage, "*", [channel.port2]); + } +} \ No newline at end of file diff --git a/pxtservices/iframeEmbeddedClient.ts b/pxtservices/iframeEmbeddedClient.ts new file mode 100644 index 000000000000..c98465fd14b8 --- /dev/null +++ b/pxtservices/iframeEmbeddedClient.ts @@ -0,0 +1,86 @@ +type IframeClientSetMessagePortRequest = { + type: "iframeclientsetmessageport"; +}; + +type IFrameClientReadyMessage = { + type: "iframeclientready"; +}; + +export type IframeClientMessage = IframeClientSetMessagePortRequest | IFrameClientReadyMessage; + +export class IFrameEmbeddedClient { + protected frameId: string | undefined; + protected port: MessagePort; + + constructor(protected messageHandler: (message: MessageEvent) => void) { + this.frameId = frameId(); + + window.addEventListener("message", this.onMessageReceived); + this.sendReadyMessage(); + } + + dispose() { + window.removeEventListener("message", this.onMessageReceived); + if (this.port) { + this.port.close(); + } + } + + postMessage(message: any) { + this.postMessageCore(message); + } + + protected onMessageReceived = (event: MessageEvent) => { + const data = event.data; + + if (data) { + if (data.type === "iframeclientsetmessageport") { + this.port = event.ports[0]; + this.port.onmessage = this.onMessageReceived; + + this.postMessage({ + type: "iframeclientsetmessageport" + } as IframeClientSetMessagePortRequest); + return; + } + else if (data.type === "iframeclientready") { + this.sendReadyMessage(); + return; + } + } + + this.messageHandler(event); + } + + protected postMessageCore(message: any) { + if (this.frameId) { + message.frameId = this.frameId; + } + + if (this.port) { + this.port.postMessage(message); + } + else if ((window as any).acquireVsCodeApi) { + (window as any).acquireVsCodeApi().postMessage(message) + } + else { + window.parent.postMessage(message, "*"); + } + } + + protected sendReadyMessage() { + this.postMessage({ + type: "iframeclientready" + }); + } +} + +function frameId(): string | undefined { + const match = /frameid=([a-zA-Z0-9\-]+)/i.exec(window.location.href); + + if (match) { + return match[1]; + } + + return undefined; +} \ No newline at end of file diff --git a/teachertool/src/services/makecodeEditorService.ts b/teachertool/src/services/makecodeEditorService.ts index 9cad8af4b6fc..a1b5a66d8326 100644 --- a/teachertool/src/services/makecodeEditorService.ts +++ b/teachertool/src/services/makecodeEditorService.ts @@ -3,10 +3,10 @@ import { ErrorCode } from "../types/errorCode"; import { logDebug, logError } from "./loggingService"; import * as AutorunService from "./autorunService"; -import { IframeDriver } from "pxtservices/iframeDriver"; +import { EditorDriver } from "pxtservices/editorDriver"; import { loadToolboxCategoriesAsync } from "../transforms/loadToolboxCategoriesAsync"; -let driver: IframeDriver | undefined; +let driver: EditorDriver | undefined; let highContrast: boolean = false; export function setEditorRef(ref: HTMLIFrameElement | undefined) { @@ -18,7 +18,7 @@ export function setEditorRef(ref: HTMLIFrameElement | undefined) { } if (ref) { - driver = new IframeDriver(ref); + driver = new EditorDriver(ref); driver.addEventListener("message", ev => { logDebug(`Message received from iframe: ${JSON.stringify(ev)}`); diff --git a/webapp/src/assetEditor.tsx b/webapp/src/assetEditor.tsx index 3dacf09f415c..6deb11e92326 100644 --- a/webapp/src/assetEditor.tsx +++ b/webapp/src/assetEditor.tsx @@ -1,10 +1,12 @@ /// +/// import * as React from "react"; import * as ReactDOM from "react-dom"; import { ImageFieldEditor } from "./components/ImageFieldEditor"; import { setTelemetryFunction } from './components/ImageEditor/store/imageReducer'; +import { IFrameEmbeddedClient } from "../../pxtservices/iframeEmbeddedClient"; document.addEventListener("DOMContentLoaded", () => { @@ -21,71 +23,6 @@ interface AssetEditorState { isEmptyAsset?: boolean; } -interface BaseAssetEditorRequest { - id?: number; - files: pxt.Map; - palette?: string[]; -} - -interface OpenAssetEditorRequest extends BaseAssetEditorRequest { - type: "open"; - assetId: string; - assetType: pxt.AssetType; -} - -interface CreateAssetEditorRequest extends BaseAssetEditorRequest { - type: "create"; - assetType: pxt.AssetType; - displayName?: string; -} - -interface SaveAssetEditorRequest extends BaseAssetEditorRequest { - type: "save"; -} - -interface DuplicateAssetEditorRequest extends BaseAssetEditorRequest { - type: "duplicate"; - assetId: string; - assetType: pxt.AssetType; -} - -type AssetEditorRequest = OpenAssetEditorRequest | CreateAssetEditorRequest | SaveAssetEditorRequest | DuplicateAssetEditorRequest; - -interface BaseAssetEditorResponse { - id?: number; -} - -interface OpenAssetEditorResponse extends BaseAssetEditorResponse { - type: "open"; -} - -interface CreateAssetEditorResponse extends BaseAssetEditorResponse { - type: "create"; -} - -interface SaveAssetEditorResponse extends BaseAssetEditorResponse { - type: "save"; - files: pxt.Map; -} - -interface DuplicateAssetEditorResponse extends BaseAssetEditorResponse { - type: "duplicate"; -} - -type AssetEditorResponse = OpenAssetEditorResponse | CreateAssetEditorResponse | SaveAssetEditorResponse | DuplicateAssetEditorResponse; - -interface AssetEditorRequestSaveEvent { - type: "event"; - kind: "done-clicked"; -} - -interface AssetEditorReadyEvent { - type: "event"; - kind: "ready"; -} - -type AssetEditorEvent = AssetEditorRequestSaveEvent | AssetEditorReadyEvent; - export class AssetEditor extends React.Component<{}, AssetEditorState> { private editor: ImageFieldEditor; protected saveProject: pxt.TilemapProject; @@ -95,6 +32,7 @@ export class AssetEditor extends React.Component<{}, AssetEditorState> { protected files: pxt.Map; protected galleryTiles: any[]; protected lastValue: pxt.Asset; + protected iframeClient: IFrameEmbeddedClient; constructor(props: {}) { super(props); @@ -104,8 +42,8 @@ export class AssetEditor extends React.Component<{}, AssetEditorState> { setTelemetryFunction(tickAssetEditorEvent); } - handleMessage = (msg: MessageEvent) => { - const request = msg.data as AssetEditorRequest; + handleMessage = (msg: MessageEvent) => { + const request = msg.data as pxt.editor.AssetEditorRequest; switch (request.type) { case "create": @@ -178,7 +116,8 @@ export class AssetEditor extends React.Component<{}, AssetEditorState> { pollingInterval: number; componentDidMount() { - window.addEventListener("message", this.handleMessage, null); + this.iframeClient = new IFrameEmbeddedClient(this.handleMessage); + window.addEventListener("keydown", this.handleKeydown, null); this.sendEvent({ type: "event", @@ -238,21 +177,16 @@ export class AssetEditor extends React.Component<{}, AssetEditorState> { return
} - protected sendResponse(response: AssetEditorResponse) { + protected sendResponse(response: pxt.editor.AssetEditorResponse) { this.postMessage(response); } - protected sendEvent(event: AssetEditorEvent) { + protected sendEvent(event: pxt.editor.AssetEditorEvent) { this.postMessage(event); } protected postMessage(message: any) { - if ((window as any).acquireVsCodeApi) { - (window as any).acquireVsCodeApi().postMessage(message) - } - else { - window.parent.postMessage(message, "*"); - } + this.iframeClient.postMessage(message); } protected updateAsset() {