Skip to content

Commit a344259

Browse files
authored
Teacher Tool: Block Picker (#9936)
This change adds a "Block Picker" which can be used for customizable block parameters. Doing this involved: 1. Making it so the iframe always loads something, even when no sharelink has been provided. We need this so we can call into it to get the blocks. It's invisible when there is no share link, but it's still loaded. 2. Adding commands we can send to the iframe to get all the toolbox categories & blocks, then to get images of blocks (as xml, when we have it, or simply by id) 3. Adding a new modal for the block picker, which displays categories and blocks to choose from 4. Some adjustments to the LazyImage to make the loading look a little smoother (I think)
1 parent 950f217 commit a344259

36 files changed

+752
-102
lines changed

gulpfile.js

+1
Original file line numberDiff line numberDiff line change
@@ -777,6 +777,7 @@ exports.tt = teacherTool;
777777
exports.icons = buildSVGIcons;
778778
exports.testhelpers = testhelpers;
779779
exports.testpxteditor = testpxteditor;
780+
exports.reactCommon = reactCommon;
780781
exports.cli = gulp.series(
781782
gulp.parallel(pxtlib, pxtweb),
782783
gulp.parallel(pxtcompiler, pxtsim, backendutils),

localtypings/pxteditor.d.ts

+41-1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ declare namespace pxt.editor {
5454
| "redo"
5555
| "renderblocks"
5656
| "renderpython"
57+
| "renderxml"
58+
| "renderbyblockid"
5759
| "setscale"
5860
| "startactivity"
5961
| "saveproject"
@@ -64,6 +66,7 @@ declare namespace pxt.editor {
6466
| "requestprojectcloudstatus"
6567
| "convertcloudprojectstolocal"
6668
| "setlanguagerestriction"
69+
| "gettoolboxcategories"
6770

6871
| "toggletrace" // EditorMessageToggleTraceRequest
6972
| "togglehighcontrast"
@@ -294,6 +297,21 @@ declare namespace pxt.editor {
294297
layout?: BlockLayout;
295298
}
296299

300+
export interface EditorMessageRenderXmlRequest extends EditorMessageRequest {
301+
action: "renderxml";
302+
// xml to render
303+
xml: string;
304+
snippetMode?: boolean;
305+
layout?: BlockLayout;
306+
}
307+
308+
export interface EditorMessageRenderByBlockIdRequest extends EditorMessageRequest {
309+
action: "renderbyblockid";
310+
blockId: string;
311+
snippetMode?: boolean;
312+
layout?: BlockLayout;
313+
}
314+
297315
export interface EditorMessageRunEvalRequest extends EditorMessageRequest {
298316
action: "runeval";
299317
validatorPlan: pxt.blocks.ValidatorPlan;
@@ -305,6 +323,16 @@ declare namespace pxt.editor {
305323
xml: Promise<any>;
306324
}
307325

326+
export interface EditorMessageRenderXmlResponse {
327+
svg: SVGSVGElement;
328+
resultXml: Promise<any>;
329+
}
330+
331+
export interface EditorMessageRenderByBlockIdResponse {
332+
svg: SVGSVGElement;
333+
resultXml: Promise<any>;
334+
}
335+
308336
export interface EditorMessageRenderPythonRequest extends EditorMessageRequest {
309337
action: "renderpython";
310338
// typescript code to render
@@ -398,6 +426,15 @@ declare namespace pxt.editor {
398426
restriction: pxt.editor.LanguageRestriction;
399427
}
400428

429+
export interface EditorMessageGetToolboxCategoriesRequest extends EditorMessageRequest {
430+
action: "gettoolboxcategories";
431+
advanced?: boolean;
432+
}
433+
434+
export interface EditorMessageGetToolboxCategoriesResponse {
435+
categories: pxt.editor.ToolboxCategoryDefinition[];
436+
}
437+
401438
export interface DataStreams<T> {
402439
console?: T;
403440
messages?: T;
@@ -927,10 +964,13 @@ declare namespace pxt.editor {
927964
blocksScreenshotAsync(pixelDensity?: number, encodeBlocks?: boolean): Promise<string>;
928965
renderBlocksAsync(req: pxt.editor.EditorMessageRenderBlocksRequest): Promise<pxt.editor.EditorMessageRenderBlocksResponse>;
929966
renderPythonAsync(req: pxt.editor.EditorMessageRenderPythonRequest): Promise<pxt.editor.EditorMessageRenderPythonResponse>;
967+
renderXml(req: pxt.editor.EditorMessageRenderXmlRequest): pxt.editor.EditorMessageRenderXmlResponse;
968+
renderByBlockIdAsync(req: pxt.editor.EditorMessageRenderByBlockIdRequest): Promise<pxt.editor.EditorMessageRenderByBlockIdResponse>;
930969

931970
// FIXME (riknoll) need to figure out how to type this better
932971
// getBlocks(): Blockly.Block[];
933972
getBlocks(): any[];
973+
getToolboxCategories(advanced?: boolean): pxt.editor.EditorMessageGetToolboxCategoriesResponse;
934974

935975
toggleHighContrast(): void;
936976
setHighContrast(on: boolean): void;
@@ -1218,4 +1258,4 @@ declare namespace pxt.workspace {
12181258

12191259
fireEvent?: (ev: pxt.editor.EditorEvent) => void;
12201260
}
1221-
}
1261+
}

pxteditor/editorcontroller.ts

+28-1
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,26 @@ export function bindEditorMessages(getEditorAsync: () => Promise<IProjectView>)
154154
})
155155
});
156156
}
157+
case "renderxml": {
158+
const rendermsg = data as pxt.editor.EditorMessageRenderXmlRequest;
159+
return Promise.resolve()
160+
.then(() => {
161+
const r = projectView.renderXml(rendermsg);
162+
return r.resultXml.then((svg: any) => {
163+
resp = svg.xml;
164+
})
165+
});
166+
}
167+
case "renderbyblockid": {
168+
const rendermsg = data as pxt.editor.EditorMessageRenderByBlockIdRequest;
169+
return Promise.resolve()
170+
.then(() => projectView.renderByBlockIdAsync(rendermsg))
171+
.then(r => {
172+
return r.resultXml.then((svg: any) => {
173+
resp = svg.xml;
174+
})
175+
});
176+
}
157177
case "runeval": {
158178
const evalmsg = data as pxt.editor.EditorMessageRunEvalRequest;
159179
const plan = evalmsg.validatorPlan;
@@ -166,6 +186,13 @@ export function bindEditorMessages(getEditorAsync: () => Promise<IProjectView>)
166186
resp = { result: results };
167187
});
168188
}
189+
case "gettoolboxcategories": {
190+
const msg = data as pxt.editor.EditorMessageGetToolboxCategoriesRequest;
191+
return Promise.resolve()
192+
.then(() => {
193+
resp = projectView.getToolboxCategories(msg.advanced);
194+
});
195+
}
169196
case "renderpython": {
170197
const rendermsg = data as pxt.editor.EditorMessageRenderPythonRequest;
171198
return Promise.resolve()
@@ -346,4 +373,4 @@ export function postHostMessageAsync(msg: pxt.editor.EditorMessageRequest): Prom
346373
if (!msg.response)
347374
resolve(undefined)
348375
})
349-
}
376+
}

pxtlib/util.ts

+8
Original file line numberDiff line numberDiff line change
@@ -1426,6 +1426,14 @@ namespace ts.pxtc.Util {
14261426
return (n || "").split(/(?=[A-Z])/g).join(" ").toLowerCase();
14271427
}
14281428

1429+
export function camelCaseToLowercaseWithSpaces(n: string) {
1430+
return n.replace(/([A-Z])/gm, ' $1').toLocaleLowerCase().trim();
1431+
}
1432+
1433+
export function snakeCaseToLowercaseWithSpaces(n: string) {
1434+
return n.replace(/_/g, ' ').toLocaleLowerCase().trim();
1435+
}
1436+
14291437
export function range(len: number) {
14301438
let r: number[] = []
14311439
for (let i = 0; i < len; ++i) r.push(i)

pxtservices/iframeDriver.ts

+37-1
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,42 @@ export class IframeDriver {
352352
return (resp.resp as pxt.editor.EditorMessageRenderPythonResponse).python;
353353
}
354354

355+
async renderXml(xml: string) {
356+
const resp = await this.sendRequest(
357+
{
358+
type: "pxteditor",
359+
action: "renderxml",
360+
xml
361+
} as pxt.editor.EditorMessageRenderXmlRequest
362+
) as pxt.editor.EditorMessageResponse;
363+
364+
return resp.resp;
365+
}
366+
367+
async renderByBlockId(blockId: string) {
368+
const resp = await this.sendRequest(
369+
{
370+
type: "pxteditor",
371+
action: "renderbyblockid",
372+
blockId: blockId
373+
} as pxt.editor.EditorMessageRenderByBlockIdRequest
374+
) as pxt.editor.EditorMessageResponse;
375+
376+
return resp.resp;
377+
}
378+
379+
async getToolboxCategories(advanced?: boolean): Promise<pxt.editor.ToolboxCategoryDefinition[]> {
380+
const resp = await this.sendRequest(
381+
{
382+
type: "pxteditor",
383+
action: "gettoolboxcategories",
384+
advanced
385+
} as pxt.editor.EditorMessageGetToolboxCategoriesRequest
386+
) as pxt.editor.EditorMessageResponse;
387+
388+
return (resp.resp as pxt.editor.EditorMessageGetToolboxCategoriesResponse).categories;
389+
}
390+
355391
async runValidatorPlan(validatorPlan: pxt.blocks.ValidatorPlan, planLib: pxt.blocks.ValidatorPlan[]) {
356392
const resp = await this.sendRequest(
357393
{
@@ -550,4 +586,4 @@ export class IframeDriver {
550586
}
551587
}
552588
}
553-
}
589+
}

react-common/components/controls/LazyImage.tsx

+4-4
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export interface LazyImageProps extends ControlProps {
55
src: string;
66
alt: string;
77
title?: string;
8+
loadingElement?: JSX.Element;
89
}
910

1011
let observer: IntersectionObserver;
@@ -20,6 +21,7 @@ export const LazyImage = (props: LazyImageProps) => {
2021
ariaLabel,
2122
ariaHidden,
2223
ariaDescribedBy,
24+
loadingElement,
2325
} = props;
2426

2527
initObserver();
@@ -34,9 +36,8 @@ export const LazyImage = (props: LazyImageProps) => {
3436
observer.observe(ref);
3537
}
3638

37-
38-
3939
return <div className="common-lazy-image-wrapper">
40+
<div className="loading-element">{loadingElement ? loadingElement : <div className="common-spinner" />}</div>
4041
<img
4142
id={id}
4243
ref={handleImageRef}
@@ -49,7 +50,6 @@ export const LazyImage = (props: LazyImageProps) => {
4950
aria-hidden={ariaHidden}
5051
aria-describedby={ariaDescribedBy}
5152
/>
52-
<div className="common-spinner" />
5353
<i className="fas fa-image" aria-hidden={true} />
5454
</div>
5555
}
@@ -85,4 +85,4 @@ function initObserver() {
8585
})
8686
}
8787
observer = new IntersectionObserver(onIntersection, config);
88-
}
88+
}

react-common/styles/controls/LazyImage.less

+6-4
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
align-items: center;
55

66
.common-spinner {
7-
position: absolute;
87
width: 60px;
98
height: 60px;
9+
}
1010

11+
.loading-element {
12+
position: absolute;
1113
opacity: 1;
1214
transition: opacity 0.3s ease;
1315
}
@@ -28,7 +30,7 @@
2830
}
2931

3032
.common-lazy-image-wrapper.loaded {
31-
.common-spinner {
33+
.loading-element {
3234
opacity: 0;
3335
}
3436

@@ -42,7 +44,7 @@
4244
}
4345

4446
.common-lazy-image-wrapper.error {
45-
.common-spinner {
47+
.loading-element {
4648
opacity: 0;
4749
}
4850

@@ -53,4 +55,4 @@
5355
img {
5456
opacity: 0;
5557
}
56-
}
58+
}

teachertool/src/App.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { loadValidatorPlansAsync } from "./transforms/loadValidatorPlansAsync";
1515
import { tryLoadLastActiveRubricAsync } from "./transforms/tryLoadLastActiveRubricAsync";
1616
import { ImportRubricModal } from "./components/ImportRubricModal";
1717
import { ConfirmationModal } from "./components/ConfirmationModal";
18+
import { BlockPickerModal } from "./components/BlockPickerModal";
1819

1920
export const App = () => {
2021
const { state, dispatch } = useContext(AppStateContext);
@@ -59,6 +60,7 @@ export const App = () => {
5960
<CatalogModal />
6061
<ImportRubricModal />
6162
<ConfirmationModal />
63+
<BlockPickerModal />
6264
<Toasts />
6365
</>
6466
);

teachertool/src/components/AddCriteriaButton.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { AppStateContext } from "../state/appStateContext";
55
import { useContext, useMemo } from "react";
66
import { classList } from "react-common/components/util";
77
import { Strings } from "../constants";
8+
import { CatalogDisplayOptions } from "../types/modalOptions";
89

910
interface IProps {}
1011

@@ -19,7 +20,7 @@ export const AddCriteriaButton: React.FC<IProps> = ({}) => {
1920
<Button
2021
className={classList("inline", "outline-button")}
2122
label={Strings.AddCriteria}
22-
onClick={() => showModal("catalog-display")}
23+
onClick={() => showModal({ modal: "catalog-display" } as CatalogDisplayOptions)}
2324
title={Strings.AddCriteria}
2425
leftIcon="fas fa-plus-circle"
2526
disabled={!hasAvailableCriteria}

0 commit comments

Comments
 (0)