diff --git a/.vscode/launch.json b/.vscode/launch.json index f2e435922ad5..38cc33fa1346 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,21 @@ "localRoot": "${workspaceRoot}/../pxt-bedrock/codegen", "console": "integratedTerminal" }, + { + "name": "codegen2 (pxt-bedrock)", + "type": "node", + "request": "launch", + "runtimeArgs": [ + "run-script", + "vscode:debug" + ], + "runtimeExecutable": "npm", + "stopOnEntry": false, + "sourceMaps": true, + "outFiles": [], + "localRoot": "${workspaceRoot}/../pxt-bedrock/codegen2", + "console": "integratedTerminal" + }, { "name": "pxt ci (pxt-core)", "type": "node", diff --git a/docs/static/icons/ts-logo-512.svg b/docs/static/icons/ts-logo-512.svg new file mode 100644 index 000000000000..1437810ea3b0 --- /dev/null +++ b/docs/static/icons/ts-logo-512.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/pxtcompiler/emitter/backjs.ts b/pxtcompiler/emitter/backjs.ts index ceb026bdefee..90a6d4d67234 100644 --- a/pxtcompiler/emitter/backjs.ts +++ b/pxtcompiler/emitter/backjs.ts @@ -426,6 +426,7 @@ function ${id}(s) { else if (e.data === null) return "null" else if (e.data === undefined) return "undefined" else if (typeof e.data == "number") return e.data + "" + else if (typeof e.data == "string") return e.data else throw oops("invalid data: " + typeof e.data); case EK.PointerLiteral: if (e.ptrlabel()) { diff --git a/pxtcompiler/emitter/emitter.ts b/pxtcompiler/emitter/emitter.ts index f3e215c5b863..7bd8d9b66dd1 100644 --- a/pxtcompiler/emitter/emitter.ts +++ b/pxtcompiler/emitter/emitter.ts @@ -3507,8 +3507,8 @@ ${lbl}: .short 0xffff return ev; if (/^0x[A-Fa-f\d]{2,8}$/.test(ev)) return ev; - U.userError("enumval only support number literals") - return "0" + //U.userError("enumval only support number literals") + return ev; } function emitFolded(f: Folded) { @@ -3535,9 +3535,7 @@ ${lbl}: .short 0xffff if (ev == null) { info.constantFolded = constantFold(en.initializer) } else { - const v = parseInt(ev) - if (!isNaN(v)) - info.constantFolded = { val: v } + info.constantFolded = { val: ev } } } else if (decl.kind == SK.PropertyDeclaration && isStatic(decl) && isReadOnly(decl)) { const pd = decl as PropertyDeclaration diff --git a/pxtcompiler/emitter/service.ts b/pxtcompiler/emitter/service.ts index 28f101c16c3e..5aebaa346a1e 100644 --- a/pxtcompiler/emitter/service.ts +++ b/pxtcompiler/emitter/service.ts @@ -1258,6 +1258,7 @@ namespace ts.pxtc.service { host.opts.fileSystem = prevFS for (let k of Object.keys(newFS)) host.setFile(k, newFS[k]) // update version numbers + res.fileSystem = U.flatClone(newFS) if (res.diagnostics.length == 0) { host.opts.skipPxtModulesEmit = false host.opts.skipPxtModulesTSC = false @@ -1275,6 +1276,7 @@ namespace ts.pxtc.service { let ts2asm = compile(host.opts, service) res = { sourceMap: res.sourceMap, + fileSystem: res.fileSystem, ...ts2asm, } if (res.needsFullRecompile || ((!res.success || res.diagnostics.length) && host.opts.clearIncrBuildAndRetryOnError)) { diff --git a/pxtlib/service.ts b/pxtlib/service.ts index 198b039c5b3c..7ee28eed1ee2 100644 --- a/pxtlib/service.ts +++ b/pxtlib/service.ts @@ -260,6 +260,7 @@ namespace ts.pxtc { sourceMap?: SourceInterval[]; globalNames?: pxt.Map; builtVariants?: string[]; + fileSystem?: pxt.Map; } export interface Breakpoint extends LocationInfo { diff --git a/theme/themes/pxt/globals/site.variables b/theme/themes/pxt/globals/site.variables index c291bef67ff2..b84f9e84dd9a 100644 --- a/theme/themes/pxt/globals/site.variables +++ b/theme/themes/pxt/globals/site.variables @@ -139,6 +139,8 @@ @pyIcon: data-uri(@pyIconUrl); @jsIconUrl: "../docs/static/icons/js.svg"; @jsIcon: data-uri(@jsIconUrl); +@tsIconUrl: "../docs/static/icons/ts-logo-512.svg"; +@tsIcon: data-uri(@tsIconUrl); @immersiveReaderUrl: "../docs/static/icons/immersive-reader.svg"; @immersiveReaderIcon: data-uri(@immersiveReaderUrl); @immersiveReaderLightUrl: "../docs/static/icons/immersive-reader-light.svg"; diff --git a/webapp/src/container.tsx b/webapp/src/container.tsx index b9805b72c87a..4c40ae0f0164 100644 --- a/webapp/src/container.tsx +++ b/webapp/src/container.tsx @@ -120,6 +120,7 @@ export interface SettingsMenuState { greenScreen?: boolean; accessibleBlocks?: boolean; showShare?: boolean; + extDownloadMenuItems?: sui.MenuItem[]; } export class SettingsMenu extends data.Component { @@ -263,6 +264,12 @@ export class SettingsMenu extends data.Component { + // Ensure dropdown always has the editor's most up-to-date menu items + const extDownloadMenuItems = pxt.commands.getDownloadMenuItems?.() || []; + this.setState({ extDownloadMenuItems }); + } + UNSAFE_componentWillReceiveProps(nextProps: SettingsMenuProps) { const newState: SettingsMenuState = {}; if (nextProps.greenScreen !== undefined) { @@ -282,6 +289,7 @@ export class SettingsMenu extends data.Component this.dropdown = ref}> + return this.dropdown = ref} onShow={this.onDropdownShow}> {showHome && } {showShare && } {(showHome || showShare) &&
} @@ -333,7 +340,7 @@ export class SettingsMenu extends data.Component : undefined} {showDownloadMenuItems && <>
- {extMenuItems.map((props, index) => )} + {this.state.extDownloadMenuItems.map((props, index) => )} }
{targetTheme.selectLanguage ? : undefined} diff --git a/webapp/src/sui.tsx b/webapp/src/sui.tsx index 499e54442e32..6c4ffc1c1082 100644 --- a/webapp/src/sui.tsx +++ b/webapp/src/sui.tsx @@ -68,6 +68,8 @@ export interface DropdownProps extends UiProps { id?: string; onChange?: (v: string) => void; onClick?: () => boolean; // Return 'true' to toggle open/close + onShow?: () => void; + onHide?: () => void; titleContent?: React.ReactNode; displayAbove?: boolean; @@ -84,10 +86,12 @@ export interface DropdownState { export class DropdownMenu extends UIElement { show() { + this.props.onShow?.(); this.setState({ open: true, focus: true }); } hide() { + this.props.onHide?.(); this.setState({ open: false }); } @@ -167,32 +171,52 @@ export class DropdownMenu extends UIElement { } } - componentDidMount() { + childFocus = (child: HTMLElement) => { + this.setActive(child); + } + childBlur = (child: HTMLElement) => { + this.blur(child); + } + childClick = (child: HTMLElement) => { + this.hide(); + } + childKeyDown = (child: HTMLElement, e: KeyboardEvent, prev: HTMLElement, next: HTMLElement) => { + this.navigateToNextElement(e, prev, next); + } + lastChildKeyDown = (child: HTMLElement, e: KeyboardEvent) => { + const charCode = core.keyCodeFromEvent(e); + if (!e.shiftKey && charCode === core.TAB_KEY) { + this.hide(); + } + } + + UNSAFE_componentWillUpdate(nextProps: Readonly, nextState: Readonly, nextContext: any): void { const children = this.getChildren(); for (let i = 0; i < children.length; i++) { const prev = i > 0 ? children[i - 1] as HTMLElement : undefined; const child = children[i] as HTMLElement; const next = i < children.length ? children[i + 1] as HTMLElement : undefined; + child.removeEventListener('keydown', (e) => this.childKeyDown(child, e, prev, next)); + child.removeEventListener('focus', () => this.childFocus(child)); + child.removeEventListener('blur', () => this.childBlur(child)); + child.removeEventListener('click', () => this.childClick(child)); + child.removeEventListener('keydown', (e) => this.lastChildKeyDown(child, e)); + } + } - child.addEventListener('keydown', (e) => { - this.navigateToNextElement(e, prev, next); - }) - - child.addEventListener('focus', (e: FocusEvent) => { - this.setActive(child); - }) - child.addEventListener('blur', (e: FocusEvent) => { - this.blur(child); - }) - + componentDidMount() { + const children = this.getChildren(); + for (let i = 0; i < children.length; i++) { + const prev = i > 0 ? children[i - 1] as HTMLElement : undefined; + const child = children[i] as HTMLElement; + const next = i < children.length ? children[i + 1] as HTMLElement : undefined; + child.addEventListener('keydown', (e) => this.childKeyDown(child, e, prev, next)); + child.addEventListener('focus', (e: FocusEvent) => this.childFocus(child)); + child.addEventListener('blur', (e: FocusEvent) => this.childBlur(child)); + child.addEventListener('click', (e) => this.childClick(child)); if (i == children.length - 1) { // set tab on last child to clear focus - child.addEventListener('keydown', (e) => { - const charCode = core.keyCodeFromEvent(e); - if (!e.shiftKey && charCode === core.TAB_KEY) { - this.hide(); - } - }) + child.addEventListener('keydown', (e) => this.lastChildKeyDown(child, e)); } } } @@ -204,6 +228,17 @@ export class DropdownMenu extends UIElement { const child = children[i] as HTMLElement; // On allow tabbing to valid child nodes (ie: no separators or mobile only items) child.tabIndex = this.state.open ? 0 : -1; + // Set event handlers + const prev = i > 0 ? children[i - 1] as HTMLElement : undefined; + const next = i < children.length ? children[i + 1] as HTMLElement : undefined; + child.addEventListener('keydown', (e) => this.childKeyDown(child, e, prev, next)); + child.addEventListener('focus', (e: FocusEvent) => this.childFocus(child)); + child.addEventListener('blur', (e: FocusEvent) => this.childBlur(child)); + child.addEventListener('click', (e) => this.childClick(child)); + if (i == children.length - 1) { + // set tab on last child to clear focus + child.addEventListener('keydown', (e) => this.lastChildKeyDown(child, e)); + } } // Check if dropdown width exceeds the bounds, add the left class to the menu