UNPKG

@webwriter/block-based-code

Version:

Write block-based code (e.g. Scratch) and run it.

204 lines (173 loc) 6.77 kB
import { customElement, property, query } from "lit/decorators.js"; import { LitElementWw } from "@webwriter/lit"; import { CSSResult, html, LitElement, TemplateResult, } from "lit"; import { PropertyValues } from "@lit/reactive-element"; import { styles } from "./editor.styles"; import { BlocklyApplication, SelectedBlocks } from "../../lib/blockly"; import { EditorChangeEvent } from "../../types"; import { Toolbar } from "../toolbar"; import { ToolbarButton } from "../toolbar-button"; import { Logger } from "../../utils"; import SlDialog from "@shoelace-style/shoelace/dist/components/dialog/dialog.component.js" import SlInput from "@shoelace-style/shoelace/dist/components/input/input.component.js" import SlButton from "@shoelace-style/shoelace/dist/components/button/button.component.js" export class Editor extends LitElementWw { public editorApplication: BlocklyApplication; @property({ type: Boolean }) public accessor readonly: boolean; @property({ type: Array }) public accessor selectedBlocks: SelectedBlocks; @property({ type: Object }) public accessor state: object; private resizeObserver: ResizeObserver; @query("#prompt") private accessor promptDialog!: SlDialog; @query("#confirm") private accessor confirmDialog!: SlDialog; @query("#alert") private accessor alertDialog!: SlDialog; private dialogOpenedAt = 0; public static get scopedElements(): Record<string, typeof LitElement> { return { "webwriter-blocks-toolbar": Toolbar, "webwriter-blocks-toolbar-button": ToolbarButton, "sl-dialog": SlDialog, "sl-input": SlInput, "sl-button": SlButton, }; } public static get styles(): CSSResult[] { return [ styles, ]; } constructor() { super(); this.resizeObserver = new ResizeObserver(() => this.handleResize()); } public connectedCallback() { super.connectedCallback(); this.resizeObserver.observe(this); this.editorApplication = new BlocklyApplication(this.readonly, this.selectedBlocks); this.editorApplication.load(this.state); this.editorApplication.addEventListener("CHANGE", this.handleChange.bind(this)); this.editorApplication.addEventListener("PROMPT", this.handlePrompt.bind(this)); this.editorApplication.addEventListener("CONFIRM", this.handleConfirm.bind(this)); this.editorApplication.addEventListener("ALERT", this.handleAlert.bind(this)); } public disconnectedCallback() { super.disconnectedCallback(); this.resizeObserver.disconnect(); this.editorApplication.destroy(); } public render(): TemplateResult { return html` <sl-dialog id="prompt" no-header @sl-request-close=${this.handleDialogRequestClose}> <span></span> <sl-input autofocus placeholder=""></sl-input> <sl-button slot="footer" @click="${() => this.promptDialog.hide()}">Cancel</sl-button> <sl-button slot="footer" variant="primary">Save</sl-button> </sl-dialog> <sl-dialog id="confirm" no-header> <span></span> <sl-button slot="footer">No</sl-button> <sl-button slot="footer" variant="primary">Yes</sl-button> </sl-dialog> <sl-dialog id="alert" no-header> <span></span> <sl-button slot="footer" variant="primary" @click=${() => this.alertDialog.hide()}>Ok</sl-button> </sl-dialog> `; } protected shouldUpdate(changedProperties: PropertyValues): boolean { let shouldUpdate = false; if (changedProperties.has("readonly")) { shouldUpdate = true; } if (changedProperties.get("selectedBlocks")) { this.editorApplication.updateToolbox(this.selectedBlocks); } return shouldUpdate; } protected firstUpdated(_changedProperties: PropertyValues): void { super.firstUpdated(_changedProperties); this.shadowRoot.appendChild(this.editorApplication.container); } protected updated(_changedProperties: PropertyValues): void { super.updated(_changedProperties); } private handleResize(): void { this.editorApplication.resize(); } private handlePrompt( promptText: string, defaultText: string, callback: (newText: string) => void, ): void { Logger.log(this, promptText, defaultText, callback); const titleSpan = this.promptDialog.querySelector("span"); titleSpan.textContent = promptText; const input = this.promptDialog.querySelector("sl-input"); input.value = defaultText; const button = this.promptDialog.querySelector("sl-button[variant=primary]"); const clonedButton = button.cloneNode(true); clonedButton.addEventListener("click", () => { try { callback(input.value); } catch (error) { Logger.log(this, error); } this.promptDialog.hide().catch(); }); button.parentNode.replaceChild(clonedButton, button); this.dialogOpenedAt = Date.now(); this.promptDialog.show().catch(); } private handleDialogRequestClose(event: CustomEvent): void { // Prevent dialog from closing by the same event that opened it // (affected change value pop-up on mobile devices) if (event.detail.source === "overlay" && Date.now() - this.dialogOpenedAt < 100) { event.preventDefault(); } } private handleConfirm(message: string, callback: (confirmed: boolean) => void): void { Logger.log(this, message, callback); const titleSpan = this.confirmDialog.querySelector("span"); titleSpan.textContent = message; const noButton = this.confirmDialog.querySelector("sl-button"); const clonedNoButton = noButton.cloneNode(true); clonedNoButton.addEventListener("click", () => { callback(false); this.confirmDialog.hide().catch(); }); noButton.parentNode.replaceChild(clonedNoButton, noButton); const yesButton = this.confirmDialog.querySelector("sl-button[variant=primary]"); const clonedYesButton = yesButton.cloneNode(true); clonedYesButton.addEventListener("click", () => { try { callback(true); } catch (error) { Logger.log(this, error); } this.confirmDialog.hide().catch(); }); yesButton.parentNode.replaceChild(clonedYesButton, yesButton); this.confirmDialog.show().catch(); } private handleAlert(message: string): void { Logger.log(this, message); const titleSpan = this.alertDialog.querySelector("span"); titleSpan.textContent = message; this.alertDialog.show().catch(); } private handleChange(): void { const changeEvent = new EditorChangeEvent( this.editorApplication.save(), this.editorApplication.readableCode, this.editorApplication.executableCode, ); this.dispatchEvent(changeEvent); } }