diff --git a/src/components/editor/core/index.ts b/src/components/editor/core/index.ts index 27381cee..d0b9c505 100644 --- a/src/components/editor/core/index.ts +++ b/src/components/editor/core/index.ts @@ -6,13 +6,49 @@ import { generateSnapshot, registerElementSpecificationEntries, resetSyntaxTree, + getSpecificationSnapshot, } from '@sugarlabs/musicblocks-v4-lib'; +import { + addInstance, + getInstance, + removeInstance, +} from '@sugarlabs/musicblocks-v4-lib/syntax/warehouse/warehouse'; + import { librarySpecification } from '@sugarlabs/musicblocks-v4-lib'; registerElementSpecificationEntries(librarySpecification); // -- public functions ----------------------------------------------------------------------------- +/** + * Generates the API for the loaded specification. + * @returns list of valid instruction signatures + */ +export function generateAPI(): string { + const snapshot = getSpecificationSnapshot(); + const api: string[] = []; + + Object.entries(snapshot) + .filter( + ([_, specification]) => + specification.type === 'Statement' && + ['Graphics', 'Pen'].includes(specification.category), + ) + .forEach(([elementName, _]) => { + const instanceID = addInstance(elementName); + const instance = getInstance(instanceID)!.instance; + const args: [string, string][] = instance.argLabels.map((arg) => [ + arg, + instance.getArgType(arg).join('|'), + ]); + removeInstance(instanceID); + + api.push(`${elementName} ${args.map(([name, types]) => `${name}:${types}`).join(' ')}`); + }); + + return api.join('\n'); +} + /** * Validates code, transpiles it, and generates the Syntax Tree in the Programming Engine. * @param code editor's code diff --git a/src/components/editor/index.ts b/src/components/editor/index.ts index f2de7818..35e7136a 100644 --- a/src/components/editor/index.ts +++ b/src/components/editor/index.ts @@ -3,8 +3,16 @@ import { IComponentMenu } from '@/@types/components/menu'; import { getComponent } from '@/config'; import { setToolbarExtended, unsetToolbarExtended } from '@/view'; -import { getElement, resetStatus, setButtonState, setup as setupView } from './view'; -import { resetProgram } from './core'; +import { + getElement, + resetStates, + setButtonState, + setCode, + setHelp, + setStatus, + setup as setupView, +} from './view'; +import { generateAPI, resetProgram } from './core'; // -- public functions ----------------------------------------------------------------------------- @@ -28,11 +36,34 @@ export function setup(): Promise { const menu = getComponent('menu'); if (menu) { (menu as IComponentMenu).mountHook('reset', () => { - resetStatus(); + setStatus(''); resetProgram(); }); } + setCode(`set-thickness value:4 +set-color value:5 +repeat times:6 + move-forward steps:100 + turn-right angle:60 +set-color value:9 +repeat times:6 + move-forward steps:100 + turn-left angle:60`); + + setCode(`set-thickness value:4 +set-color value:5 +move-forward steps:100 +turn-right angle:60 +move-forward steps:100 +turn-right angle:60 +move-forward steps:100 +turn-right angle:60 +move-forward steps:100 +turn-right angle:60`); + + setHelp(generateAPI()); + const btn = getElement('button'); let state: 'initial' | 'float' | 'pinned' = 'initial'; @@ -40,6 +71,7 @@ export function setup(): Promise { const setState = (_state: 'initial' | 'float' | 'pinned') => { if (_state === 'initial') { unsetToolbarExtended(); + resetStates(); } else { const toolbarContent = setToolbarExtended('Editor', _state, { pin: () => setState('pinned'), diff --git a/src/components/editor/view/components/index.scss b/src/components/editor/view/components/index.scss index 1659842a..a571348a 100644 --- a/src/components/editor/view/components/index.scss +++ b/src/components/editor/view/components/index.scss @@ -1,85 +1,165 @@ @import '@/scss/colors'; #editor { - display: flex; - flex-direction: column; width: 100%; height: 100%; - > * { + .editor-wrapper { + display: flex; + flex-direction: column; width: 100%; - margin: 0; - } - - #editor-codebox { - flex-shrink: 1; height: 100%; - margin-bottom: 0.5rem; - padding: 0.5rem; - border: none; - border-radius: 0.25rem 0.25rem; - font-size: 0.9rem; - font-family: 'Courier New', Courier, monospace; - font-weight: bold; - color: $text-dark; - resize: none; - - &:focus { - outline: none; - } - } - #editor-console { - flex-shrink: 0; - display: flex; - flex-direction: row; - align-items: center; - width: 100%; - height: 2.5rem; - border-radius: 0 0 0.25rem 0.25rem; + &.editor-wrapper-hidden { + display: none; + } > * { - box-sizing: border-box; - height: 100%; + width: 100%; + margin: 0; } - #editor-status-wrapper { + #editor-codebox { flex-shrink: 1; + height: 100%; + margin-bottom: 0.5rem; + padding: 0.5rem; + border: none; + border-radius: 0.25rem 0.25rem; + font-size: 0.9rem; + font-family: 'Courier New', Courier, monospace; + font-weight: bold; + color: $text-dark; + resize: none; + + &:focus { + outline: none; + } + } + + #editor-console { + flex-shrink: 0; + display: flex; + flex-direction: row; + align-items: center; width: 100%; - margin-right: 0.5rem; + height: 2.5rem; + border-radius: 0 0 0.25rem 0.25rem; - #editor-status { - width: 100%; + > * { + box-sizing: border-box; height: 100%; - margin: 0; + margin-right: 0.5rem; + } + + #editor-btn-help { + flex-shrink: 0; + width: 2.5rem; padding: 0.5rem; + border: none; border-radius: 0.25rem; - font-size: 0.9rem; - font-family: 'Courier New', Courier, monospace; - font-weight: bold; - color: $text-dark; - background-color: $background-light; - overflow: auto; + text-transform: uppercase; + background-color: white; + cursor: pointer; + transition: all 0.25s ease; + + svg { + width: 100%; + height: 100%; + + .path-fill { + fill: $mb-accent; + transition: all 0.25s ease; + } + } + } + + #editor-status-wrapper { + flex-shrink: 1; + width: 100%; + + #editor-status { + width: 100%; + height: 100%; + margin: 0; + padding: 0.5rem; + border-radius: 0.25rem; + font-size: 0.9rem; + font-family: 'Courier New', Courier, monospace; + font-weight: bold; + color: $text-dark; + background-color: $background-light; + overflow: auto; + } + } + + #editor-btn-build { + flex-shrink: 0; + width: 2.5rem; + margin-right: 0; + padding: 0.5rem; + border: none; + border-radius: 0.25rem; + text-transform: uppercase; + background-color: white; + cursor: pointer; + transition: all 0.25s ease; + + svg { + width: 100%; + height: 100%; + + .path-fill { + fill: $mb-accent; + transition: all 0.25s ease; + } + } + + &:hover { + svg { + .path-fill { + fill: $mb-accent-light; + } + } + } + } + } + + #editor-help { + flex-shrink: 1; + height: 100%; + margin-bottom: 0.5rem; + padding: 0.5rem; + border: none; + border-radius: 0.25rem; + font-size: 0.9rem; + font-family: 'Courier New', Courier, monospace; + font-weight: bold; + color: $mb-accent-light; + background-color: $mb-accent-dark; + resize: none; + + &:focus { + outline: none; } } - #editor-btn-build { + #editor-help-close { flex-shrink: 0; width: 2.5rem; - padding: 0.5rem; + height: 2.5rem; border: none; border-radius: 0.25rem; - text-transform: uppercase; - background-color: white; + background-color: unset; cursor: pointer; - transition: all 0.25s ease; svg { - width: 100%; - height: 100%; + width: calc(100% - 0.5rem); + height: calc(100% - 0.5rem); + margin: 0.25rem; .path-fill { - fill: $mb-accent; + fill: $mb-accent-dark; transition: all 0.25s ease; } } @@ -87,7 +167,7 @@ &:hover { svg { .path-fill { - fill: $mb-accent-light; + fill: $text-light; } } } diff --git a/src/components/editor/view/components/index.tsx b/src/components/editor/view/components/index.tsx index fe268fb4..db254da0 100644 --- a/src/components/editor/view/components/index.tsx +++ b/src/components/editor/view/components/index.tsx @@ -1,9 +1,11 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; // -- resources ------------------------------------------------------------------------------------ -import buildSVG from '../resources/build.svg'; +import svgHelp from '../resources/help.svg'; +import svgBuild from '../resources/build.svg'; +import svgClose from '../resources/close.svg'; // -- stylesheet ----------------------------------------------------------------------------------- @@ -11,9 +13,13 @@ import './index.scss'; // -- private variables ---------------------------------------------------------------------------- +let _editor: HTMLDivElement; let _codeBox: HTMLTextAreaElement; +let _btnHelp: HTMLButtonElement; let _status: HTMLParagraphElement; let _btnBuild: HTMLButtonElement; +let _btnClose: HTMLButtonElement; +let _helpBox: HTMLTextAreaElement; let _mountedCallback: CallableFunction; @@ -25,31 +31,80 @@ let _mountedCallback: CallableFunction; */ function Editor(): JSX.Element { const codeBoxRef = useRef(null); + const btnHelpRef = useRef(null); const statusRef = useRef(null); const btnBuildRef = useRef(null); + const btnCloseRef = useRef(null); + const helpBoxRef = useRef(null); + + const [showingHelp, setShowingHelp] = useState(false); useEffect(() => { _codeBox = codeBoxRef.current!; + _btnHelp = btnHelpRef.current!; _status = statusRef.current!; _btnBuild = btnBuildRef.current!; + _btnClose = btnCloseRef.current!; + _helpBox = helpBoxRef.current!; _mountedCallback(); - fetch(buildSVG) - .then((res) => res.text()) - .then((svg) => (_btnBuild.innerHTML = svg)); + _editor.addEventListener('resetstates', () => { + setShowingHelp(false); + }); + + ( + [ + [svgHelp, _btnHelp], + [svgBuild, _btnBuild], + [svgClose, _btnClose], + ] as [string, HTMLButtonElement][] + ).forEach(([svgSrc, button]) => { + fetch(svgSrc) + .then((res) => res.text()) + .then((svg) => (button.innerHTML = svg)); + }); }, []); return ( -
- -
-
-

+ <> +
+ +
+ +
+

+
+
-
-
+
+ + +
+ ); } @@ -58,21 +113,44 @@ function Editor(): JSX.Element { * @param container DOM container * @returns a `Promise` of an object containing the DOM artboard and interactor elements */ -export function setup(container: HTMLElement): Promise<{ - codeBox: HTMLTextAreaElement; - status: HTMLParagraphElement; - btnBuild: HTMLButtonElement; -}> { +export function setup(container: HTMLElement): Promise { return new Promise((resolve) => { + _editor = container as HTMLDivElement; + _editor.id = 'editor'; + ReactDOM.render(, container); - _mountedCallback = () => - requestAnimationFrame(() => - resolve({ - codeBox: _codeBox, - status: _status, - btnBuild: _btnBuild, - }), - ); + _mountedCallback = () => requestAnimationFrame(() => resolve()); }); } + +/** + * Sets the text code content of the codebox. + * @param text text code content + */ +export function setCode(text: string): void { + _codeBox.value = text; +} + +/** + * Sets the text content of the status box. + * @param text text content + */ +export function setStatus(text: string): void { + _status.innerHTML = text; +} + +/** + * Sets the text content of the help box. + * @param text text content + */ +export function setHelp(text: string): void { + _helpBox.innerHTML = text; +} + +/** + * Resets the component states. + */ +export function resetStates(): void { + _editor.dispatchEvent(new Event('resetstates')); +} diff --git a/src/components/editor/view/index.ts b/src/components/editor/view/index.ts index 099468c5..be89842c 100644 --- a/src/components/editor/view/index.ts +++ b/src/components/editor/view/index.ts @@ -1,7 +1,7 @@ import { createItem } from '@/view'; import { buildProgram } from '../core'; -import { setup as setupComponent } from './components'; +import { setStatus, setup as setupComponent } from './components'; import { setButtonImg, setup as setupButton } from './components/button'; // -- private variables ---------------------------------------------------------------------------- @@ -9,8 +9,6 @@ import { setButtonImg, setup as setupButton } from './components/button'; let _editor: HTMLDivElement; let _editorToolbarBtn: HTMLElement; -let _editorStatus: HTMLParagraphElement; - // -- private functions ---------------------------------------------------------------------------- /** @@ -19,41 +17,14 @@ let _editorStatus: HTMLParagraphElement; function _createEditor(): Promise { return new Promise((resolve) => { _editor = document.createElement('div'); - _editor.id = 'editor'; - - setupComponent(_editor).then(({ codeBox, status, btnBuild }) => { - _editorStatus = status; - codeBox.addEventListener('input', () => { - _editorStatus.innerHTML = ''; - }); - - codeBox.innerHTML = `set-thickness value:4 -set-color value:5 -repeat times:6 - move-forward steps:100 - turn-right angle:60 -set-color value:9 -repeat times:6 - move-forward steps:100 - turn-left angle:60`; - - codeBox.innerHTML = `set-thickness value:4 -set-color value:5 -move-forward steps:100 -turn-right angle:60 -move-forward steps:100 -turn-right angle:60 -move-forward steps:100 -turn-right angle:60 -move-forward steps:100 -turn-right angle:60`; - - btnBuild.addEventListener('click', () => { - (async () => { - const response = await buildProgram(codeBox.value); - _editorStatus.innerHTML = response ? 'Successfully Built' : 'Invalid Code'; - })(); + setupComponent(_editor).then(() => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + _editor.addEventListener('buildprogram', function (e: CustomEvent) { + buildProgram(e.detail).then((response) => + setStatus(response ? 'Successfully Built' : 'Invalid Code'), + ); }); resolve(); @@ -108,9 +79,4 @@ export function setButtonState(state: 'clicked' | 'unclicked'): void { setButtonImg(state === 'clicked' ? 'cross' : 'code'); } -/** - * Resets the editor status. - */ -export function resetStatus(): void { - _editorStatus.innerHTML = ''; -} +export { setCode, setHelp, setStatus, resetStates } from './components'; diff --git a/src/components/editor/view/resources/help.svg b/src/components/editor/view/resources/help.svg new file mode 100644 index 00000000..f51f60c3 --- /dev/null +++ b/src/components/editor/view/resources/help.svg @@ -0,0 +1,33 @@ + + + + + + + +