Skip to content

Commit

Permalink
Teacher Tool: Block Picker (#9936)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
thsparks authored Mar 27, 2024
1 parent 950f217 commit a344259
Show file tree
Hide file tree
Showing 36 changed files with 752 additions and 102 deletions.
1 change: 1 addition & 0 deletions gulpfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -777,6 +777,7 @@ exports.tt = teacherTool;
exports.icons = buildSVGIcons;
exports.testhelpers = testhelpers;
exports.testpxteditor = testpxteditor;
exports.reactCommon = reactCommon;
exports.cli = gulp.series(
gulp.parallel(pxtlib, pxtweb),
gulp.parallel(pxtcompiler, pxtsim, backendutils),
Expand Down
42 changes: 41 additions & 1 deletion localtypings/pxteditor.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ declare namespace pxt.editor {
| "redo"
| "renderblocks"
| "renderpython"
| "renderxml"
| "renderbyblockid"
| "setscale"
| "startactivity"
| "saveproject"
Expand All @@ -64,6 +66,7 @@ declare namespace pxt.editor {
| "requestprojectcloudstatus"
| "convertcloudprojectstolocal"
| "setlanguagerestriction"
| "gettoolboxcategories"

| "toggletrace" // EditorMessageToggleTraceRequest
| "togglehighcontrast"
Expand Down Expand Up @@ -294,6 +297,21 @@ declare namespace pxt.editor {
layout?: BlockLayout;
}

export interface EditorMessageRenderXmlRequest extends EditorMessageRequest {
action: "renderxml";
// xml to render
xml: string;
snippetMode?: boolean;
layout?: BlockLayout;
}

export interface EditorMessageRenderByBlockIdRequest extends EditorMessageRequest {
action: "renderbyblockid";
blockId: string;
snippetMode?: boolean;
layout?: BlockLayout;
}

export interface EditorMessageRunEvalRequest extends EditorMessageRequest {
action: "runeval";
validatorPlan: pxt.blocks.ValidatorPlan;
Expand All @@ -305,6 +323,16 @@ declare namespace pxt.editor {
xml: Promise<any>;
}

export interface EditorMessageRenderXmlResponse {
svg: SVGSVGElement;
resultXml: Promise<any>;
}

export interface EditorMessageRenderByBlockIdResponse {
svg: SVGSVGElement;
resultXml: Promise<any>;
}

export interface EditorMessageRenderPythonRequest extends EditorMessageRequest {
action: "renderpython";
// typescript code to render
Expand Down Expand Up @@ -398,6 +426,15 @@ declare namespace pxt.editor {
restriction: pxt.editor.LanguageRestriction;
}

export interface EditorMessageGetToolboxCategoriesRequest extends EditorMessageRequest {
action: "gettoolboxcategories";
advanced?: boolean;
}

export interface EditorMessageGetToolboxCategoriesResponse {
categories: pxt.editor.ToolboxCategoryDefinition[];
}

export interface DataStreams<T> {
console?: T;
messages?: T;
Expand Down Expand Up @@ -927,10 +964,13 @@ declare namespace pxt.editor {
blocksScreenshotAsync(pixelDensity?: number, encodeBlocks?: boolean): Promise<string>;
renderBlocksAsync(req: pxt.editor.EditorMessageRenderBlocksRequest): Promise<pxt.editor.EditorMessageRenderBlocksResponse>;
renderPythonAsync(req: pxt.editor.EditorMessageRenderPythonRequest): Promise<pxt.editor.EditorMessageRenderPythonResponse>;
renderXml(req: pxt.editor.EditorMessageRenderXmlRequest): pxt.editor.EditorMessageRenderXmlResponse;
renderByBlockIdAsync(req: pxt.editor.EditorMessageRenderByBlockIdRequest): Promise<pxt.editor.EditorMessageRenderByBlockIdResponse>;

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

toggleHighContrast(): void;
setHighContrast(on: boolean): void;
Expand Down Expand Up @@ -1218,4 +1258,4 @@ declare namespace pxt.workspace {

fireEvent?: (ev: pxt.editor.EditorEvent) => void;
}
}
}
29 changes: 28 additions & 1 deletion pxteditor/editorcontroller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,26 @@ export function bindEditorMessages(getEditorAsync: () => Promise<IProjectView>)
})
});
}
case "renderxml": {
const rendermsg = data as pxt.editor.EditorMessageRenderXmlRequest;
return Promise.resolve()
.then(() => {
const r = projectView.renderXml(rendermsg);
return r.resultXml.then((svg: any) => {
resp = svg.xml;
})
});
}
case "renderbyblockid": {
const rendermsg = data as pxt.editor.EditorMessageRenderByBlockIdRequest;
return Promise.resolve()
.then(() => projectView.renderByBlockIdAsync(rendermsg))
.then(r => {
return r.resultXml.then((svg: any) => {
resp = svg.xml;
})
});
}
case "runeval": {
const evalmsg = data as pxt.editor.EditorMessageRunEvalRequest;
const plan = evalmsg.validatorPlan;
Expand All @@ -166,6 +186,13 @@ export function bindEditorMessages(getEditorAsync: () => Promise<IProjectView>)
resp = { result: results };
});
}
case "gettoolboxcategories": {
const msg = data as pxt.editor.EditorMessageGetToolboxCategoriesRequest;
return Promise.resolve()
.then(() => {
resp = projectView.getToolboxCategories(msg.advanced);
});
}
case "renderpython": {
const rendermsg = data as pxt.editor.EditorMessageRenderPythonRequest;
return Promise.resolve()
Expand Down Expand Up @@ -346,4 +373,4 @@ export function postHostMessageAsync(msg: pxt.editor.EditorMessageRequest): Prom
if (!msg.response)
resolve(undefined)
})
}
}
8 changes: 8 additions & 0 deletions pxtlib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1426,6 +1426,14 @@ namespace ts.pxtc.Util {
return (n || "").split(/(?=[A-Z])/g).join(" ").toLowerCase();
}

export function camelCaseToLowercaseWithSpaces(n: string) {
return n.replace(/([A-Z])/gm, ' $1').toLocaleLowerCase().trim();
}

export function snakeCaseToLowercaseWithSpaces(n: string) {
return n.replace(/_/g, ' ').toLocaleLowerCase().trim();
}

export function range(len: number) {
let r: number[] = []
for (let i = 0; i < len; ++i) r.push(i)
Expand Down
38 changes: 37 additions & 1 deletion pxtservices/iframeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,42 @@ export class IframeDriver {
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<pxt.editor.ToolboxCategoryDefinition[]> {
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(
{
Expand Down Expand Up @@ -550,4 +586,4 @@ export class IframeDriver {
}
}
}
}
}
8 changes: 4 additions & 4 deletions react-common/components/controls/LazyImage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface LazyImageProps extends ControlProps {
src: string;
alt: string;
title?: string;
loadingElement?: JSX.Element;
}

let observer: IntersectionObserver;
Expand All @@ -20,6 +21,7 @@ export const LazyImage = (props: LazyImageProps) => {
ariaLabel,
ariaHidden,
ariaDescribedBy,
loadingElement,
} = props;

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



return <div className="common-lazy-image-wrapper">
<div className="loading-element">{loadingElement ? loadingElement : <div className="common-spinner" />}</div>
<img
id={id}
ref={handleImageRef}
Expand All @@ -49,7 +50,6 @@ export const LazyImage = (props: LazyImageProps) => {
aria-hidden={ariaHidden}
aria-describedby={ariaDescribedBy}
/>
<div className="common-spinner" />
<i className="fas fa-image" aria-hidden={true} />
</div>
}
Expand Down Expand Up @@ -85,4 +85,4 @@ function initObserver() {
})
}
observer = new IntersectionObserver(onIntersection, config);
}
}
10 changes: 6 additions & 4 deletions react-common/styles/controls/LazyImage.less
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
align-items: center;

.common-spinner {
position: absolute;
width: 60px;
height: 60px;
}

.loading-element {
position: absolute;
opacity: 1;
transition: opacity 0.3s ease;
}
Expand All @@ -28,7 +30,7 @@
}

.common-lazy-image-wrapper.loaded {
.common-spinner {
.loading-element {
opacity: 0;
}

Expand All @@ -42,7 +44,7 @@
}

.common-lazy-image-wrapper.error {
.common-spinner {
.loading-element {
opacity: 0;
}

Expand All @@ -53,4 +55,4 @@
img {
opacity: 0;
}
}
}
2 changes: 2 additions & 0 deletions teachertool/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { loadValidatorPlansAsync } from "./transforms/loadValidatorPlansAsync";
import { tryLoadLastActiveRubricAsync } from "./transforms/tryLoadLastActiveRubricAsync";
import { ImportRubricModal } from "./components/ImportRubricModal";
import { ConfirmationModal } from "./components/ConfirmationModal";
import { BlockPickerModal } from "./components/BlockPickerModal";

export const App = () => {
const { state, dispatch } = useContext(AppStateContext);
Expand Down Expand Up @@ -59,6 +60,7 @@ export const App = () => {
<CatalogModal />
<ImportRubricModal />
<ConfirmationModal />
<BlockPickerModal />
<Toasts />
</>
);
Expand Down
3 changes: 2 additions & 1 deletion teachertool/src/components/AddCriteriaButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AppStateContext } from "../state/appStateContext";
import { useContext, useMemo } from "react";
import { classList } from "react-common/components/util";
import { Strings } from "../constants";
import { CatalogDisplayOptions } from "../types/modalOptions";

interface IProps {}

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

0 comments on commit a344259

Please sign in to comment.