@webwriter/block-based-code
Version:
Write block-based code (e.g. Scratch) and run it.
230 lines (210 loc) • 7.43 kB
text/typescript
import { property, query } from "lit/decorators.js";
import { LitElementWw } from "@webwriter/lit";
import {
CSSResult, html, LitElement, TemplateResult,
} from "lit";
import HelpCircleIcon from "@tabler/icons/outline/help-circle.svg";
import SlIcon from "@shoelace-style/shoelace/dist/components/icon/icon.component.js";
import SlCheckbox from "@shoelace-style/shoelace/dist/components/checkbox/checkbox.component.js";
import SlOption from "@shoelace-style/shoelace/dist/components/option/option.component.js";
import SlSelect from "@shoelace-style/shoelace/dist/components/select/select.component.js";
import SlTooltip from "@shoelace-style/shoelace/dist/components/tooltip/tooltip.component.js";
import SlTree from "@shoelace-style/shoelace/dist/components/tree/tree.component.js";
import SlTreeItem from "@shoelace-style/shoelace/dist/components/tree-item/tree-item.component.js";
import { styles } from "./options.styles";
import { msg } from "../../locales";
import { OptionsChangeEvent, StageType } from "../../types";
import { BlockTypes, SelectedBlocks } from "../../lib/blockly";
enum TreeItemState {
UNSELECTED,
INDETERMINATE,
SELECTED,
}
/**
* The options component.
*/
export class Options extends LitElementWw {
/**
* Whether the widget are readonly.
*/
({ type: Number })
public accessor readonly: 0 | 1;
/**
* The selected stage type.
*/
({ type: String })
public accessor stageType: StageType;
/**
* The selected blocks.
*/
({ type: Array })
public accessor selectedBlocks: SelectedBlocks;
/**
* The available blocks.
*/
({ type: Array })
public accessor availableBlocks: BlockTypes[];
/**
* @inheritDoc
*/
public static get scopedElements(): Record<string, typeof LitElement> {
return {
"sl-checkbox": SlCheckbox,
"sl-select": SlSelect,
"sl-option": SlOption,
"sl-tree": SlTree,
"sl-tree-item": SlTreeItem,
"sl-tooltip": SlTooltip,
"sl-icon": SlIcon,
};
}
/**
* @inheritDoc
*/
public static get styles(): CSSResult[] {
return [
styles,
];
}
/**
* @inheritDoc
*/
public connectedCallback(): void {
super.connectedCallback();
}
/**
* Determines in which state a tree item folder should be.
* @param category The category of the tree item.
* @param blocks The blocks under the category.
* @param selectedBlocksSet The set of selected blocks.
* @returns True if the tree item should be indeterminate, false otherwise.
* @private
*/
private getTreeItemFolderState(category: string, blocks: string[], selectedBlocksSet: Set<BlockTypes>): TreeItemState {
const totalBlocks = blocks.length;
const selectedBlocks = blocks.filter((name) => selectedBlocksSet.has(`${category}:${name}` as BlockTypes)).length;
if (selectedBlocks === 0) {
return TreeItemState.UNSELECTED;
}
if (selectedBlocks === totalBlocks) {
return TreeItemState.SELECTED;
}
return TreeItemState.INDETERMINATE;
}
/**
* @inheritDoc
*/
public render(): TemplateResult {
const selectedBlocksSet = new Set(this.selectedBlocks);
const availableBlocksMap = new Map<string, string[]>();
this.availableBlocks.forEach((block: BlockTypes) => {
const [category, name] = block.split(":") as [string, string];
if (!availableBlocksMap.has(category)) {
availableBlocksMap.set(category, []);
}
if (name) {
availableBlocksMap.get(category)!.push(name);
}
});
return html`
<div class="group">
<sl-checkbox .checked=${this.readonly === 1} @sl-change=${this.handleReadonlyChange}>
<span class="label">
${msg("OPTIONS.READONLY")}
<sl-tooltip content=${msg("OPTIONS.READONLY_TOOLTIP")}>
<sl-icon src="${HelpCircleIcon}"></sl-icon>
</sl-tooltip>
</span>
</sl-checkbox>
</div>
<div class="group">
<span class="label">
${msg("OPTIONS.STAGE")}
<sl-tooltip content=${msg("OPTIONS.STAGE_TOOLTIP")}>
<sl-icon src="${HelpCircleIcon}"></sl-icon>
</sl-tooltip>
</span>
<sl-select value=${this.stageType} @sl-change=${this.handleStageTypeChange} hoist>
${Object.values(StageType).map((type) => html`
<sl-option value=${type}>
${msg(`OPTIONS.STAGE_TYPES.${type.toUpperCase() as Uppercase<StageType>}`)}
</sl-option>
`)}
</sl-select>
</div>
<div class="group">
<span class="label">
${msg("OPTIONS.AVAILABLE_BLOCKS")}
<sl-tooltip content=${msg("OPTIONS.AVAILABLE_BLOCKS_TOOLTIP")}>
<sl-icon src="${HelpCircleIcon}"></sl-icon>
</sl-tooltip>
</span>
<sl-tree selection="multiple" @sl-selection-change=${this.handleSelectedBlocksChange}>
<sl-tree-item expanded>
all
${Array.from(availableBlocksMap.entries()).map(([category, blocks]) => {
if (blocks.length === 0) {
return html`
<sl-tree-item ?selected=${selectedBlocksSet.has(category as BlockTypes)}
data-block-key=${`${category}`}>
${category}
</sl-tree-item>
`;
}
const folderState = this.getTreeItemFolderState(category, blocks, selectedBlocksSet);
return html`
<sl-tree-item
.indeterminate=${folderState === TreeItemState.INDETERMINATE}
?selected=${folderState === TreeItemState.SELECTED}>
${category}
${blocks.map((name) => html`
<sl-tree-item ?selected=${selectedBlocksSet.has(`${category}:${name}` as BlockTypes)}
data-block-key=${`${category}:${name}`}>
${name}
</sl-tree-item>
`)}
</sl-tree-item>
`;
})}
</sl-tree-item>
</sl-tree>
</div>
`;
}
/**
* Handles the readonly change event.
* @param event The change event.
* @private
*/
private handleReadonlyChange(event): void {
const changeEvent = new OptionsChangeEvent({
readonly: event.target.checked ? 1 : 0,
});
this.dispatchEvent(changeEvent);
}
/**
* Handles the stage type change event.
* @param event The change event.
* @private
*/
private handleStageTypeChange(event): void {
const changeEvent = new OptionsChangeEvent({
stageType: event.target.value,
});
this.dispatchEvent(changeEvent);
}
/**
* Handles the selected blocks change event.
* @param event The change event.
* @private
*/
private handleSelectedBlocksChange(event: CustomEvent<{ selection: SlTreeItem[] }>): void {
const selectedBlocks = event.detail.selection
.filter((item) => item.getAttribute("data-block-key"))
.map((item) => item.getAttribute("data-block-key") as BlockTypes);
const changeEvent = new OptionsChangeEvent({
selectedBlocks,
});
this.dispatchEvent(changeEvent);
}
}