From e07fce480c578596a1c275277886057338ed5d42 Mon Sep 17 00:00:00 2001 From: GermanBluefox Date: Tue, 19 Nov 2024 23:04:46 +0000 Subject: [PATCH] Working on typescript --- src-editor/package.json | 1 + .../{BlocklyEditor.jsx => BlocklyEditor.tsx} | 361 ++++++++++-------- .../Debugger/{Editor.jsx => Editor.tsx} | 52 +-- src-editor/src/Components/Debugger/Stack.tsx | 180 ++++++--- src-editor/src/Components/Debugger/index.jsx | 2 +- .../Components/ScriptEditorVanilaMonaco.tsx | 43 +-- src-editor/src/types.d.ts | 177 +++++++++ 7 files changed, 557 insertions(+), 259 deletions(-) rename src-editor/src/Components/{BlocklyEditor.jsx => BlocklyEditor.tsx} (65%) rename src-editor/src/Components/Debugger/{Editor.jsx => Editor.tsx} (60%) diff --git a/src-editor/package.json b/src-editor/package.json index 2a088db0e..1d7dff330 100644 --- a/src-editor/package.json +++ b/src-editor/package.json @@ -13,6 +13,7 @@ "@mui/material": "^6.1.7", "@mui/x-date-pickers": "^7.22.2", "@sentry/browser": "^8.38.0", + "blockly": "^11.1.1", "craco-module-federation": "^1.1.0", "lodash": "^4.17.21", "monaco-editor": "~0.52.0", diff --git a/src-editor/src/Components/BlocklyEditor.jsx b/src-editor/src/Components/BlocklyEditor.tsx similarity index 65% rename from src-editor/src/Components/BlocklyEditor.jsx rename to src-editor/src/Components/BlocklyEditor.tsx index db53d9034..999fbe36e 100644 --- a/src-editor/src/Components/BlocklyEditor.jsx +++ b/src-editor/src/Components/BlocklyEditor.tsx @@ -1,19 +1,52 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import { I18n, Message as DialogMessage } from '@iobroker/adapter-react-v5'; +import { I18n, Message as DialogMessage, type ThemeType } from '@iobroker/adapter-react-v5'; import DialogError from '../Dialogs/Error'; import DialogExport from '../Dialogs/Export'; import DialogImport from '../Dialogs/Import'; +import * as BlocklyTS from 'blockly/core'; +import type { WorkspaceSvg } from 'blockly/core/workspace_svg'; +import type { BlockSvg } from 'blockly/core/block_svg'; +import { javascriptGenerator } from 'blockly/javascript'; +import type { FlyoutDefinition } from 'blockly/core/utils/toolbox'; let languageBlocklyLoaded = false; let languageOwnLoaded = false; -let toolboxText = null; -let toolboxXml; -let scriptsLoaded = []; +let toolboxText: string | null = null; +let toolboxXml: Element | null = null; +const scriptsLoaded: string[] = []; -// BF (2020-10-31) I have no Idea, why it does not work as static in BlocklyEditor, but outside of BlocklyEditor it works -function searchXml(root, text, _id, _result) { +interface CustomBlock { + HUE: number; + blocks: Record; +} + +declare global { + interface Window { + ActiveXObject: any; + MSG: string[]; + scripts: { + loading?: boolean; + blocklyWorkspace: WorkspaceSvg; + scripts?: string[]; + }; + Blockly: { + CustomBlocks: string[]; + Words: Record>; + Action: CustomBlock; + Blocks: Record; + JavaScript: { + forBlock: Record string>; + }; + Procedures: { + flyoutCategoryNew: (workspace: WorkspaceSvg) => FlyoutDefinition; + }; + }; + } +} + +// BF (2020-10-31) I have no Idea, why it does not work as static in BlocklyEditor, but outside BlocklyEditor it works +function searchXml(root: Element, text: string, _id?: string, _result?: string[]): string[] { _result = _result || []; if (root.tagName === 'BLOCK' || root.tagName === 'block') { _id = root.id; @@ -22,25 +55,55 @@ function searchXml(root, text, _id, _result) { for (let a = 0; a < root.attributes.length; a++) { const val = (root.attributes[a].value || '').toLowerCase(); if (root.attributes[a].nodeName === 'name' && (val === 'oid' || val === 'text' || val === 'var')) { - if ((root.innerHTML || root.innerText || '').toLowerCase().includes(text)) { + if (_id && root.innerHTML?.toLowerCase().includes(text)) { _result.push(_id); } } } } - root.childNodes.forEach(node => searchXml(node, text, _id, _result)); + root.childNodes.forEach(node => searchXml(node as HTMLElement, text, _id, _result)); return _result; } -class BlocklyEditor extends React.Component { - constructor(props) { - super(props); +interface BlocklyEditorProps { + command: '' | 'check' | 'export' | 'import'; + onChange: (code: string) => void; + searchText: string; + code: string; + scriptId: string; + themeType: ThemeType; +} - this.blockly = null; - this.blocklyWorkspace = null; - this.toolbox = null; - this.Blockly = window.Blockly; +interface BlocklyEditorState { + languageOwnLoaded: boolean; + languageBlocklyLoaded: boolean; + changed: boolean; + message: string | { text: string; title: string }; + error: string | { text: string; title: string }; + themeType: ThemeType; + exportText: string; + importText: boolean; + searchText: string; +} + +class BlocklyEditor extends React.Component { + private blockly: HTMLElement | null = null; + private blocklyWorkspace: WorkspaceSvg | null = null; + private originalCode: string; + private someSelected: string[] | null = null; + private changeTimer: ReturnType | null = null; + private someSelectedTime: number = 0; + private ignoreChanges: boolean = false; + private darkTheme: any; + private blinkBlock: any; + private onResizeBind: () => void; + private didUpdate: ReturnType | null = null; + private lastCommand = ''; + private lastSearch: string; + + constructor(props: BlocklyEditorProps) { + super(props); this.state = { languageOwnLoaded, @@ -56,38 +119,39 @@ class BlocklyEditor extends React.Component { this.originalCode = props.code || ''; this.someSelected = null; - this.changeTimer = null; this.onResizeBind = this.onResize.bind(this); - this.lastCommand = ''; this.lastSearch = this.props.searchText || ''; this.blinkBlock = null; this.loadLanguages(); } - static loadJS(url, callback, location) { + static loadJS(url: string, callback: () => void, location?: HTMLElement): void { const scriptTag = document.createElement('script'); try { scriptTag.src = url; scriptTag.onload = callback; - scriptTag.onreadystatechange = callback; scriptTag.onerror = callback; (location || window.document.body).appendChild(scriptTag); } catch (e) { console.error(`Cannot load ${url}: ${e}`); - callback && callback(); + if (callback) { + callback(); + } } } - static loadScripts(scripts, callback) { - if (!scripts || !scripts.length) { - return callback && callback(); + static loadScripts(scripts: string[], callback: () => void): void { + if (!scripts?.length) { + if (callback) { + return callback(); + } } const adapter = scripts.pop(); - if (!scriptsLoaded.includes(adapter)) { + if (adapter && !scriptsLoaded.includes(adapter)) { scriptsLoaded.push(adapter); BlocklyEditor.loadJS(`../../adapter/${adapter}/blockly.js`, (/*data, textStatus, jqxhr*/) => setTimeout(() => BlocklyEditor.loadScripts(scripts, callback), 0),); @@ -96,12 +160,12 @@ class BlocklyEditor extends React.Component { } } - static loadCustomBlockly(adapters, callback) { + static loadCustomBlockly(adapters: ioBroker.AdapterObject[], callback: () => void): void { // get all adapters, that can have blockly - const toLoad = []; + const toLoad: string[] = []; for (const id in adapters) { if ( - !adapters.hasOwnProperty(id) || + !Object.prototype.hasOwnProperty.call(adapters, id) || !adapters[id] || !id.match(/^system\.adapter\./) || adapters[id].type !== 'adapter' @@ -109,7 +173,7 @@ class BlocklyEditor extends React.Component { continue; } - if (adapters[id].common && adapters[id].common.blockly) { + if (adapters[id].common?.blockly) { console.log(`Detected custom blockly: ${adapters[id].common.name}`); toLoad.push(adapters[id].common.name); } @@ -118,12 +182,12 @@ class BlocklyEditor extends React.Component { BlocklyEditor.loadScripts(toLoad, callback); } - static loadXMLDoc(text) { + static loadXMLDoc(text: string): Document | null { let parseXml; if (window.DOMParser) { - parseXml = xmlStr => new window.DOMParser().parseFromString(xmlStr, 'text/xml'); + parseXml = (xmlStr: string): Document => new window.DOMParser().parseFromString(xmlStr, 'text/xml'); } else if (typeof window.ActiveXObject !== 'undefined' && new window.ActiveXObject('Microsoft.XMLDOM')) { - parseXml = xmlStr => { + parseXml = (xmlStr: string): Document => { const xmlDoc = new window.ActiveXObject('Microsoft.XMLDOM'); xmlDoc.async = 'false'; xmlDoc.loadXML(xmlStr); @@ -135,9 +199,9 @@ class BlocklyEditor extends React.Component { return parseXml(text); } - searchBlocks(text) { + searchBlocks(text: string): string[] { if (this.blocklyWorkspace) { - const dom = this.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); + const dom: Element = BlocklyTS.Xml.workspaceToDom(this.blocklyWorkspace); const ids = searchXml(dom, text.toLowerCase()); console.log(`Search "${text}" found blocks: ${ids.length ? JSON.stringify(ids) : 'none'}`); @@ -148,20 +212,20 @@ class BlocklyEditor extends React.Component { return []; } - searchId() { - const ids = this.lastSearch && this.searchBlocks(this.lastSearch); - if (ids && ids.length) { + searchId(): void { + const ids = this.lastSearch ? this.searchBlocks(this.lastSearch) : null; + if (ids?.length) { this.someSelected = ids; - this.someSelected.forEach(id => this.blocklyWorkspace.highlightBlock(id, true)); + this.someSelected.forEach(id => this.blocklyWorkspace?.highlightBlock(id, true)); this.someSelectedTime = Date.now(); } else if (this.someSelected) { // remove selection - this.someSelected.forEach(id => this.blocklyWorkspace.highlightBlock(id, false)); + this.someSelected.forEach(id => this.blocklyWorkspace?.highlightBlock(id, false)); this.someSelected = null; } } - UNSAFE_componentWillReceiveProps(nextProps) { + UNSAFE_componentWillReceiveProps(nextProps: BlocklyEditorProps): void { if (nextProps.command && this.lastCommand !== nextProps.command) { this.lastCommand = nextProps.command; setTimeout(() => (this.lastCommand = ''), 300); @@ -170,7 +234,7 @@ class BlocklyEditor extends React.Component { if (!err) { this.setState({ message: I18n.t('Ok') }); } else { - badBlock && this.blocklyBlinkBlock(badBlock); + badBlock && BlocklyEditor.blocklyBlinkBlock(badBlock); this.setState({ error: { text: I18n.t(err), title: I18n.t('Error was found') } }); this.blinkBlock = badBlock; } @@ -198,7 +262,7 @@ class BlocklyEditor extends React.Component { } } - loadLanguages() { + loadLanguages(): void { // load blockly language if (!languageBlocklyLoaded) { const fileLang = window.document.createElement('script'); @@ -210,13 +274,6 @@ class BlocklyEditor extends React.Component { languageBlocklyLoaded = true; this.setState({ languageBlocklyLoaded }); }; - // IE 6 & 7 - fileLang.onreadystatechange = () => { - if (this.readyState === 'complete') { - languageBlocklyLoaded = true; - this.setState({ languageBlocklyLoaded }); - } - }; window.document.getElementsByTagName('head')[0].appendChild(fileLang); } if (!languageOwnLoaded) { @@ -228,22 +285,17 @@ class BlocklyEditor extends React.Component { languageOwnLoaded = true; this.setState({ languageOwnLoaded }); }; - // IE 6 & 7 - fileCustom.onreadystatechange = () => { - if (this.readyState === 'complete') { - languageOwnLoaded = true; - this.setState({ languageOwnLoaded }); - } - }; window.document.getElementsByTagName('head')[0].appendChild(fileCustom); } } - onResize() { - this.Blockly.svgResize(this.blocklyWorkspace); + onResize(): void { + if (this.blocklyWorkspace) { + BlocklyTS.svgResize(this.blocklyWorkspace); + } } - jsCode2Blockly(text) { + static jsCode2Blockly(text: string | undefined): string | null { text = text || ''; const lines = text.split(/[\r\n]+|\r|\n/g); let xml = ''; @@ -268,14 +320,14 @@ class BlocklyEditor extends React.Component { return code; } - blocklyBlinkBlock(block) { + static blocklyBlinkBlock(block: BlockSvg): void { for (let i = 300; i < 3000; i += 300) { setTimeout(() => block.select(), i); setTimeout(() => block.unselect(), i + 150); } } - blocklyRemoveOrphanedShadows() { + blocklyRemoveOrphanedShadows(): void { if (this.blocklyWorkspace) { const blocks = this.blocklyWorkspace.getAllBlocks(); let block; @@ -295,11 +347,11 @@ class BlocklyEditor extends React.Component { } } - blocklyCheckBlocks(cb) { + blocklyCheckBlocks(cb: (warningText?: string, badBlock?: BlockSvg) => void): boolean { let warningText; if (!this.blocklyWorkspace || this.blocklyWorkspace.getAllBlocks().length === 0) { cb && cb('no blocks found'); - return; + return false; } let badBlock = this.blocklyGetUnconnectedBlock(); if (badBlock) { @@ -315,7 +367,7 @@ class BlocklyEditor extends React.Component { if (cb) { cb(warningText, badBlock); } else { - this.blocklyBlinkBlock(badBlock); + BlocklyEditor.blocklyBlinkBlock(badBlock); } return false; } @@ -326,20 +378,23 @@ class BlocklyEditor extends React.Component { } // get unconnected block - blocklyGetUnconnectedBlock() { - const blocks = this.blocklyWorkspace.getAllBlocks(); + blocklyGetUnconnectedBlock(): BlockSvg | null { + const blocks: BlockSvg[] | undefined = this.blocklyWorkspace?.getAllBlocks(); let block; - for (let i = 0; (block = blocks[i]); i++) { - const connections = block.getConnections_(true); - let conn; - for (let j = 0; (conn = connections[j]); j++) { - if ( - !conn.sourceBlock_ || - ((conn.type === this.Blockly.INPUT_VALUE || conn.type === this.Blockly.OUTPUT_VALUE) && - !conn.targetConnection && - !conn._optional) - ) { - return block; + if (blocks) { + for (let i = 0; (block = blocks[i]); i++) { + const connections = block.getConnections_(true); + let conn; + for (let j = 0; (conn = connections[j]); j++) { + if ( + !conn.sourceBlock_ || + ((conn.type === BlocklyTS.INPUT_VALUE || conn.type === BlocklyTS.OUTPUT_VALUE) && + !conn.targetConnection && + // @ts-expect-error Check it later + !conn._optional) + ) { + return block; + } } } } @@ -347,55 +402,68 @@ class BlocklyEditor extends React.Component { } // get block with warning - blocklyGetBlockWithWarning() { - const blocks = this.blocklyWorkspace.getAllBlocks(); + blocklyGetBlockWithWarning(): BlockSvg | null { + const blocks = this.blocklyWorkspace?.getAllBlocks(); let block; - for (let i = 0; (block = blocks[i]); i++) { - if (block.warning) { - return block; + if (blocks) { + for (let i = 0; (block = blocks[i]); i++) { + // @ts-expect-error fix later + if (block.warning) { + return block; + } } } return null; } - blocklyCode2JSCode(oneWay) { - let code = this.Blockly.JavaScript.workspaceToCode(this.blocklyWorkspace); + blocklyCode2JSCode(oneWay?: boolean): string { + if (!this.blocklyWorkspace) { + return ''; + } + let code = javascriptGenerator.workspaceToCode(this.blocklyWorkspace); if (!oneWay) { code += '\n'; - const dom = this.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); - const text = this.Blockly.Xml.domToText(dom); + const dom = BlocklyTS.Xml.workspaceToDom(this.blocklyWorkspace); + const text = BlocklyTS.Xml.domToText(dom); code += `//${btoa(encodeURIComponent(text))}`; } return code; } - exportBlocks() { - let exportText; - const selectedBlocks = this.Blockly.getSelected(); + exportBlocks(): void { + if (!this.blocklyWorkspace) { + return; + } + let exportText: string; + const selectedBlocks: BlocklyTS.BlockSvg | null = BlocklyTS.getSelected() as BlocklyTS.BlockSvg | null; if (selectedBlocks) { - const xmlBlock = this.Blockly.Xml.blockToDom(selectedBlocks); - if (this.Blockly.dragMode_ !== this.Blockly.DRAG_FREE) { - this.Blockly.Xml.deleteNext(xmlBlock); + const xmlBlock: Element = BlocklyTS.Xml.blockToDom(selectedBlocks) as Element; + // @ts-expect-error fix later + if (BlocklyTS.dragMode_ !== BlocklyTS.DRAG_FREE) { + BlocklyTS.Xml.deleteNext(xmlBlock); } // Encode start position in XML. const xy = selectedBlocks.getRelativeToSurfaceXY(); - xmlBlock.setAttribute('x', selectedBlocks.RTL ? -xy.x : xy.x); - xmlBlock.setAttribute('y', xy.y); + xmlBlock.setAttribute('x', (selectedBlocks.RTL ? -xy.x : xy.x).toString()); + xmlBlock.setAttribute('y', xy.y.toString()); - exportText = this.Blockly.Xml.domToPrettyText(xmlBlock); + exportText = BlocklyTS.Xml.domToPrettyText(xmlBlock); } else { - const dom = this.Blockly.Xml.workspaceToDom(this.blocklyWorkspace); - exportText = this.Blockly.Xml.domToPrettyText(dom); + const dom = BlocklyTS.Xml.workspaceToDom(this.blocklyWorkspace); + exportText = BlocklyTS.Xml.domToPrettyText(dom); } this.setState({ exportText }); } - importBlocks() { + importBlocks(): void { this.setState({ importText: true }); } - onImportBlocks(xml) { + onImportBlocks(xml: string | undefined): void { + if (!this.blocklyWorkspace) { + return; + } xml = (xml || '').trim(); if (xml) { try { @@ -422,12 +490,14 @@ class BlocklyEditor extends React.Component { xml = xml.replace(/[\n\r]/g, '').replace(/.*<\/variables>/g, ''); window.scripts.loading = true; - const xmlBlocks = this.Blockly.utils.xml.textToDom(xml); + const xmlBlocks = BlocklyTS.utils.xml.textToDom(xml); if (xmlBlocks.nodeName === 'xml') { for (let b = 0; b < xmlBlocks.children.length; b++) { + // @ts-expect-error fix later this.blocklyWorkspace.paste(xmlBlocks.children[b]); } } else { + // @ts-expect-error fix later this.blocklyWorkspace.paste(xmlBlocks); } @@ -435,12 +505,12 @@ class BlocklyEditor extends React.Component { this.onBlocklyChanged(); } catch (e) { - this.setState({ error: { text: e, title: I18n.t('Import error') } }); + this.setState({ error: { text: (e as Error).toString(), title: I18n.t('Import error') } }); } } } - loadCode() { + loadCode(): void { if (!this.blocklyWorkspace) { return; } @@ -450,11 +520,11 @@ class BlocklyEditor extends React.Component { try { const xml = - this.jsCode2Blockly(this.originalCode) || + BlocklyEditor.jsCode2Blockly(this.originalCode) || ''; window.scripts.loading = true; - const dom = this.Blockly.utils.xml.textToDom(xml); - this.Blockly.Xml.domToWorkspace(dom, this.blocklyWorkspace); + const dom = BlocklyTS.utils.xml.textToDom(xml); + BlocklyTS.Xml.domToWorkspace(dom, this.blocklyWorkspace); window.scripts.loading = false; } catch (e) { console.error(e); @@ -463,13 +533,13 @@ class BlocklyEditor extends React.Component { setTimeout(() => (this.ignoreChanges = false), 100); } - onBlocklyChanged() { + onBlocklyChanged(): void { this.blocklyRemoveOrphanedShadows(); this.setState({ changed: true }); this.onChange(); } - async componentDidUpdate() { + async componentDidUpdate(): Promise { if (!this.blockly) { return; } @@ -484,10 +554,11 @@ class BlocklyEditor extends React.Component { window.addEventListener('resize', this.onResizeBind, false); toolboxText = toolboxText || (await this.getToolbox()); - toolboxXml = toolboxXml || this.Blockly.utils.xml.textToDom(toolboxText); + toolboxXml = toolboxXml || BlocklyTS.utils.xml.textToDom(toolboxText); - this.darkTheme = this.Blockly.Theme.defineTheme('dark', { - base: this.Blockly.Themes.Classic, + this.darkTheme = BlocklyTS.Theme.defineTheme('dark', { + name: 'dark', + base: BlocklyTS.Themes.Classic, componentStyles: { workspaceBackgroundColour: '#1e1e1e', toolboxBackgroundColour: 'blackBackground', @@ -500,12 +571,11 @@ class BlocklyEditor extends React.Component { insertionMarkerOpacity: 0.3, scrollbarOpacity: 0.4, cursorColour: '#d0d0d0', - blackBackground: '#333', }, }); // https://developers.google.com/blockly/reference/js/blockly.blocklyoptions_interface.md - this.blocklyWorkspace = this.Blockly.inject(this.blockly, { + this.blocklyWorkspace = BlocklyTS.inject(this.blockly, { renderer: 'thrasos', theme: 'classic', media: 'google-blockly/media/', @@ -541,18 +611,18 @@ class BlocklyEditor extends React.Component { }; // Workaround: Replace procedure category flyout - this.blocklyWorkspace.registerToolboxCategoryCallback('PROCEDURE', this.Blockly.Procedures.flyoutCategoryNew); + this.blocklyWorkspace.registerToolboxCategoryCallback('PROCEDURE', window.Blockly.Procedures.flyoutCategoryNew); // Listen to events on master workspace. this.blocklyWorkspace.addChangeListener(masterEvent => { if (this.someSelected && Date.now() - this.someSelectedTime > 500) { - const allBlocks = this.blocklyWorkspace.getAllBlocks(); + const allBlocks = this.blocklyWorkspace?.getAllBlocks(); this.someSelected = null; - allBlocks.forEach(b => b.removeSelect()); + allBlocks?.forEach(b => b.removeSelect()); } if ( - [this.Blockly.Events.UI, this.Blockly.Events.CREATE, this.Blockly.Events.VIEWPORT_CHANGE].includes( + [BlocklyTS.Events.UI, BlocklyTS.Events.CREATE, BlocklyTS.Events.VIEWPORT_CHANGE].includes( masterEvent.type, ) ) { @@ -578,16 +648,16 @@ class BlocklyEditor extends React.Component { setTimeout(() => this.searchId(), 200); // select found blocks } - updateBackground() { - if (this.state.themeType === 'dark' || this.state.themeType === 'blue') { - this.blocklyWorkspace.setTheme(this.darkTheme); - } else { + updateBackground(): void { + if (this.state.themeType === 'dark') { + this.blocklyWorkspace?.setTheme(this.darkTheme); + } else if (this.blocklyWorkspace) { this.blocklyWorkspace.getThemeManager(); - this.blocklyWorkspace.setTheme(this.Blockly.Themes.Classic); + this.blocklyWorkspace.setTheme(BlocklyTS.Themes.Classic); } } - componentWillUnmount() { + componentWillUnmount(): void { if (!this.blocklyWorkspace) { return; } @@ -598,15 +668,15 @@ class BlocklyEditor extends React.Component { window.removeEventListener('resize', this.onResizeBind); } - onChange() { + onChange(): void { this.originalCode = this.blocklyCode2JSCode(); this.props.onChange && this.props.onChange(this.originalCode); } - async getToolbox(retry) { + async getToolbox(retry?: boolean): Promise { // Interpolate translated messages into toolbox. const el = window.document.getElementById('toolbox'); - let toolboxText = el && el.outerHTML; + let toolboxText = el?.outerHTML; if (!toolboxText) { if (!retry) { return new Promise(resolve => { @@ -619,16 +689,17 @@ class BlocklyEditor extends React.Component { } toolboxText = toolboxText.replace(/{(\w+)}/g, (m, p1) => window.MSG[p1]); - if (this.Blockly.CustomBlocks) { + if (window.Blockly.CustomBlocks) { let blocks = ''; const lang = I18n.getLanguage(); - for (let cb = 0; cb < this.Blockly.CustomBlocks.length; cb++) { - const name = this.Blockly.CustomBlocks[cb]; + for (let cb = 0; cb < window.Blockly.CustomBlocks.length; cb++) { + const name = window.Blockly.CustomBlocks[cb]; // add blocks - blocks += ``; - for (const _b in this.Blockly[name].blocks) { - if (Object.prototype.hasOwnProperty.call(this.Blockly[name].blocks, _b)) { - blocks += this.Blockly[name].blocks[_b]; + const _block: CustomBlock = (window.Blockly as unknown as Record)[name]; + blocks += ``; + for (const _b in _block.blocks) { + if (Object.prototype.hasOwnProperty.call(_block.blocks, _b)) { + blocks += _block.blocks[_b]; } } blocks += ''; @@ -639,7 +710,7 @@ class BlocklyEditor extends React.Component { return toolboxText; } - renderMessageDialog() { + renderMessageDialog(): React.JSX.Element | null { return this.state.message ? ( { if (this.blinkBlock) { - this.blocklyBlinkBlock(this.blinkBlock); + BlocklyEditor.blocklyBlinkBlock(this.blinkBlock); this.blinkBlock = null; } this.setState({ error: '' }); @@ -667,11 +738,11 @@ class BlocklyEditor extends React.Component { ) : null; } - renderExportDialog() { + renderExportDialog(): React.JSX.Element | null { return this.state.exportText ? ( this.setState({ exportText: '' })} text={this.state.exportText} scriptId={this.props.scriptId} @@ -679,11 +750,11 @@ class BlocklyEditor extends React.Component { ) : null; } - renderImportDialog() { + renderImportDialog(): React.JSX.Element | null { return this.state.importText ? ( { + onClose={(text: string | undefined) => { this.setState({ importText: false }); this.onImportBlocks(text); }} @@ -691,11 +762,11 @@ class BlocklyEditor extends React.Component { ) : null; } - render() { + render(): (React.JSX.Element | null)[] | null { if (this.state.languageBlocklyLoaded && this.state.languageOwnLoaded) { this.didUpdate = setTimeout(() => { this.didUpdate = null; - this.componentDidUpdate(); + void this.componentDidUpdate(); }, 100); return [ @@ -722,12 +793,4 @@ class BlocklyEditor extends React.Component { } } -BlocklyEditor.propTypes = { - command: PropTypes.string, - onChange: PropTypes.func, - searchText: PropTypes.string, - scriptId: PropTypes.string, - themeType: PropTypes.string, -}; - export default BlocklyEditor; diff --git a/src-editor/src/Components/Debugger/Editor.jsx b/src-editor/src/Components/Debugger/Editor.tsx similarity index 60% rename from src-editor/src/Components/Debugger/Editor.jsx rename to src-editor/src/Components/Debugger/Editor.tsx index c1cfa2dc3..0014f81a9 100644 --- a/src-editor/src/Components/Debugger/Editor.jsx +++ b/src-editor/src/Components/Debugger/Editor.tsx @@ -1,8 +1,10 @@ import React from 'react'; -import PropTypes from 'prop-types'; import ScriptEditorComponent from '../ScriptEditorVanilaMonaco'; +import type { AdminConnection, ThemeName, ThemeType } from '@iobroker/adapter-react-v5'; -const styles = { +import type { DebuggerLocation, SetBreakpointParameterType } from '@/types'; + +const styles: Record = { editorDiv: { height: '100%', width: '100%', @@ -11,8 +13,27 @@ const styles = { }, }; -class Editor extends React.Component { - constructor(props) { +interface EditorProps { + runningInstances: Record; + socket: AdminConnection; + sourceId: string; + script: string; + scriptName: string; + adapterName: string; + paused: boolean; + breakpoints: SetBreakpointParameterType[]; + location: DebuggerLocation; + themeType: ThemeType; + themeName: ThemeName; + onToggleBreakpoint: (i: number) => void; +} + +interface EditorState { + lines: string[]; +} + +class Editor extends React.Component { + constructor(props: EditorProps) { super(props); this.state = { @@ -20,13 +41,7 @@ class Editor extends React.Component { }; } - editorDidMount(editor, monaco) { - this.monaco = monaco; - this.editor = editor; - editor.focus(); - } - - render() { + render(): React.JSX.Element { return (
= { frameRoot: { @@ -131,12 +131,78 @@ const styles: Record = { }, }; +interface DebugValue { + type: 'function' | 'string' | 'boolean' | 'number' | 'object' | 'undefined' | 'null' | 'bigint' | 'symbol'; + description: string; + value: any; +} +interface DebugVariable { + name: string; + value: DebugValue; +} + interface StackProps { + currentScriptId: string; + mainScriptId: string; + scopes: { + local: { + properties: { + result: DebugVariable[]; + }; + }; + closure: { + properties: { + result: DebugVariable[]; + }; + }; + }; + expressions: DebugVariable[]; + callFrames: CallFrame[]; + currentFrame: number; + onExpressionDelete: (index: number) => void; + onChangeCurrentFrame: (index: number) => void; + onWriteScopeValue: (options: { + variableName: string; + scopeNumber: 0; + newValue: { + value: any; + valueType: + | 'string' + | 'number' + | 'object' + | 'boolean' + | 'undefined' + | 'null' + | 'bigint' + | 'symbol' + | 'function'; + }; + callFrameId: string; + }) => void; + onExpressionAdd: (cb: (index: number, item: DebugVariable) => void) => void; + onExpressionNameUpdate: (index: number, scopeValue: string, cb: () => void) => void; + themeType: ThemeType; +} +interface StackState { + editValue: { + type: 'expression' | 'local' | 'closure'; + valueType: 'function' | 'string' | 'boolean' | 'number' | 'object' | 'undefined' | 'null' | 'bigint' | 'symbol'; + index: number; + name: string; + value: string; + scopeId?: string; + } | null; + callFrames: CallFrame[]; + framesSizes: number[]; } -class Stack extends React.Component { - constructor(props) { +class Stack extends React.Component { + private readonly editRef: React.RefObject; + + private scopeValue: boolean | undefined | number | string | null = null; + + constructor(props: StackProps) { super(props); const framesSizesStr = window.localStorage.getItem('JS.framesSizes'); @@ -144,7 +210,7 @@ class Stack extends React.Component { if (framesSizesStr) { try { framesSizes = JSON.parse(framesSizesStr); - } catch (e) { + } catch { // ignore } } @@ -158,14 +224,16 @@ class Stack extends React.Component { this.editRef = React.createRef(); } - onExpressionNameUpdate() { - this.props.onExpressionNameUpdate(this.state.editValue.index, this.scopeValue, () => { - this.setState({ editValue: null }); - this.scopeValue = null; - }); + onExpressionNameUpdate(): void { + if (this.state.editValue) { + this.props.onExpressionNameUpdate(this.state.editValue.index, this.scopeValue as string, () => { + this.setState({ editValue: null }); + this.scopeValue = null; + }); + } } - renderExpression(item, i) { + renderExpression(item: DebugVariable, i: number): React.JSX.Element { const name = this.state.editValue && this.state.editValue.type === 'expression' && this.state.editValue.index === i ? ( this.state.editValue && this.setState({ editValue: null })} defaultValue={item.name} onKeyUp={e => { - if (e.keyCode === 13) { + if (e.key === 'Enter') { this.onExpressionNameUpdate(); - } else if (e.keyCode === 27) { + } else if (e.key === 'Escape') { this.setState({ editValue: null }); } }} @@ -250,24 +318,21 @@ class Stack extends React.Component { ); } - renderExpressions() { + renderExpressions(): React.JSX.Element[] { return this.props.expressions.map((item, i) => this.renderExpression(item, i)); } - renderOneFrameTitle(frame, i) { + renderOneFrameTitle(frame: CallFrame, i: number): React.JSX.Element | null { if ( this.props.mainScriptId === this.props.currentScriptId && frame.location.scriptId !== this.props.mainScriptId ) { return null; } - const fileName = frame.url - .split('/') - .pop() - .replace(/^script\.js\./, ''); + const fileName = (frame.url.split('/').pop() || '').replace(/^script\.js\./, ''); return ( this.props.onChangeCurrentFrame(i)} dense selected={this.props.currentFrame === i} @@ -287,16 +352,17 @@ class Stack extends React.Component { ); } - formatValue(value, forEdit) { + formatValue(value: DebugValue | null, forEdit?: boolean): React.JSX.Element | string { if (!value) { if (forEdit) { return 'none'; } return none; - } else if (value.type === 'function') { + } + if (value.type === 'function') { const text = value.description ? value.description.length > 100 - ? value.description.substring(0, 100) + '...' + ? `${value.description.substring(0, 100)}...` : value.description : 'function'; if (forEdit) { @@ -310,39 +376,42 @@ class Stack extends React.Component { {text} ); - } else if (value.value === undefined) { + } + if (value.value === undefined) { if (forEdit) { return 'undefined'; } return undefined; - } else if (value.value === null) { + } + if (value.value === null) { if (forEdit) { return 'null'; } return null; - } else if (value.type === 'string') { + } + if (value.type === 'string') { if (forEdit) { return value.value; } - const text = value.value - ? value.value.length > 100 - ? value.value.substring(0, 100) + '...' - : value.value - : ''; + const text = `"${ + value.value ? (value.value.length > 100 ? `${value.value.substring(0, 100)}...` : value.value) : '' + }"`; return ( - "{text}" + {text} ); - } else if (value.type === 'boolean') { + } + if (value.type === 'boolean') { if (forEdit) { return value.value.toString(); } return {value.value.toString()}; - } else if (value.type === 'object') { + } + if (value.type === 'object') { if (forEdit) { return JSON.stringify(value.value); } @@ -361,7 +430,7 @@ class Stack extends React.Component { return value.value.toString(); } - onWriteScopeValue() { + onWriteScopeValue(): void { if (this.scopeValue === 'true') { this.scopeValue = true; } else if (this.scopeValue === 'false') { @@ -370,12 +439,12 @@ class Stack extends React.Component { this.scopeValue = null; } else if (this.scopeValue === 'undefined') { this.scopeValue = undefined; - } else if (parseFloat(this.scopeValue).toString() === this.scopeValue) { + } else if (parseFloat(this.scopeValue as string).toString() === this.scopeValue) { this.scopeValue = parseFloat(this.scopeValue); } this.props.onWriteScopeValue({ - variableName: this.state.editValue.name, + variableName: this.state.editValue?.name || '', scopeNumber: 0, newValue: { value: this.scopeValue, @@ -388,12 +457,12 @@ class Stack extends React.Component { this.scopeValue = null; } - componentDidUpdate() { + componentDidUpdate(): void { //this.editRef.current?.select(); this.editRef.current?.focus(); } - renderScope(scopeId, item, type) { + renderScope(scopeId: string, item: DebugVariable, type: 'local' | 'closure'): React.JSX.Element { const editable = !this.props.currentFrame && item.value && @@ -405,7 +474,7 @@ class Stack extends React.Component { item.value?.value === undefined); const el = - this.state.editValue && this.state.editValue.type === type && this.state.editValue.name === item.name + this.state.editValue?.type === type && this.state.editValue?.name === item.name ? [
, this.state.editValue && this.setState({ editValue: null })} defaultValue={this.formatValue(item.value, true)} onKeyUp={e => { - if (e.keyCode === 13) { + if (e.key === 'Enter') { this.onWriteScopeValue(); - } else if (e.keyCode === 27) { + } else if (e.key === 'Escape') { this.setState({ editValue: null }); } }} @@ -483,6 +553,7 @@ class Stack extends React.Component { editValue: { scopeId, type, + index: 0, valueType: item.value.type, name: item.name, value: item.value.value, @@ -497,19 +568,21 @@ class Stack extends React.Component { ); } - renderScopes(frame) { + renderScopes(frame: CallFrame): React.JSX.Element | null { if (!frame) { return null; } // first local - let result = this.renderExpressions(); + const result: React.JSX.Element[] = this.renderExpressions(); let items = this.props.scopes?.local?.properties?.result.map(item => + // @ts-expect-error fix later this.renderScope(this.props.scopes.id, item, 'local'), ); items && items.forEach(item => result.push(item)); items = this.props.scopes?.closure?.properties?.result.map(item => + // @ts-expect-error fix later this.renderScope(this.props.scopes.id, item, 'closure'), ); items && items.forEach(item => result.push(item)); @@ -521,7 +594,7 @@ class Stack extends React.Component { ); } - render() { + render(): React.JSX.Element { return ( - this.props.onExpressionAdd((i, item) => { + this.props.onExpressionAdd((i: number, item: DebugVariable): void => { this.scopeValue = item.name || ''; this.setState({ editValue: { @@ -573,19 +646,4 @@ class Stack extends React.Component { } } -Stack.propTypes = { - currentScriptId: PropTypes.string, - mainScriptId: PropTypes.string, - scopes: PropTypes.object, - expressions: PropTypes.array, - callFrames: PropTypes.array, - currentFrame: PropTypes.number, - onChangeCurrentFrame: PropTypes.func, - onWriteScopeValue: PropTypes.func, - onExpressionDelete: PropTypes.func, - onExpressionAdd: PropTypes.func, - onExpressionNameUpdate: PropTypes.func, - themeType: PropTypes.string, -}; - export default Stack; diff --git a/src-editor/src/Components/Debugger/index.jsx b/src-editor/src/Components/Debugger/index.jsx index 7a57c042c..c4458902c 100644 --- a/src-editor/src/Components/Debugger/index.jsx +++ b/src-editor/src/Components/Debugger/index.jsx @@ -404,7 +404,7 @@ class Debugger extends React.Component { } }); changed && - window.localStorage.setItem('javascript.tools.bp.' + this.props.src, JSON.stringify(breakpoints)); + window.localStorage.setItem(`javascript.tools.bp.${this.props.src}`, JSON.stringify(breakpoints)); changed && this.setState({ breakpoints }); } else if (data.cmd === 'cb') { const breakpoints = JSON.parse(JSON.stringify(this.state.breakpoints)); diff --git a/src-editor/src/Components/ScriptEditorVanilaMonaco.tsx b/src-editor/src/Components/ScriptEditorVanilaMonaco.tsx index f0cbc93de..1f2913383 100644 --- a/src-editor/src/Components/ScriptEditorVanilaMonaco.tsx +++ b/src-editor/src/Components/ScriptEditorVanilaMonaco.tsx @@ -6,6 +6,7 @@ import { Fab } from '@mui/material'; import { MdGTranslate as IconNoCheck } from 'react-icons/md'; import { type AdminConnection, I18n } from '@iobroker/adapter-react-v5'; +import type { DebuggerLocation, SetBreakpointParameterType } from '@/types'; function isIdOfGlobalScript(id: string): boolean { return /^script\.js\.global\./.test(id); @@ -32,8 +33,8 @@ interface ScriptEditorProps { insert?: string; style?: React.CSSProperties; - breakpoints?: Record[]; - location?: Record; + breakpoints?: SetBreakpointParameterType[]; + location?: DebuggerLocation | null; onToggleBreakpoint?: (lineNumber: number) => void; } @@ -63,14 +64,14 @@ class ScriptEditor extends React.Component private monacoCounter: number = 0; - private location: Record | undefined; + private location: DebuggerLocation | undefined; - private breakpoints: Record[] | undefined; + private breakpoints: SetBreakpointParameterType[] | undefined; private lastSearch: string = ''; // TypeScript declarations - private typings: Record = {}; + private typings: Record = {}; private decorations: monacoEditor.editor.IEditorDecorationsCollection | null = null; @@ -196,7 +197,7 @@ class ScriptEditor extends React.Component setTimeout(() => { this.highlightText(this.state.searchText); - this.location = this.props.location; + this.location = this.props.location || undefined; this.breakpoints = this.props.breakpoints; this.showDecorators(); }); @@ -425,7 +426,7 @@ class ScriptEditor extends React.Component decorations.push({ range: new this.monaco.Range( this.location.lineNumber + 1, - this.location.columnNumber + 1, + (this.location.columnNumber || 0) + 1, this.location.lineNumber + 1, 1000, ), @@ -443,19 +444,17 @@ class ScriptEditor extends React.Component }); } - if (this.breakpoints) { - this.breakpoints.forEach(bp => { - if (this.monaco) { - decorations.push({ - range: new this.monaco.Range(bp.location.lineNumber + 1, 0, bp.location.lineNumber + 1, 100), - options: { - isWholeLine: true, - glyphMarginClassName: this.props.isDark ? 'monacoBreakPointDark' : 'monacoBreakPoint', - }, - }); - } - }); - } + this.breakpoints?.forEach(bp => { + if (this.monaco) { + decorations.push({ + range: new this.monaco.Range(bp.location.lineNumber + 1, 0, bp.location.lineNumber + 1, 100), + options: { + isWholeLine: true, + glyphMarginClassName: this.props.isDark ? 'monacoBreakPointDark' : 'monacoBreakPoint', + }, + }); + } + }); if (this.editor) { this.decorations = this.editor.createDecorationsCollection(decorations); } @@ -530,7 +529,7 @@ class ScriptEditor extends React.Component JSON.stringify(nextProps.location) !== JSON.stringify(this.location) && JSON.stringify(nextProps.breakpoints) !== JSON.stringify(this.breakpoints) ) { - this.location = nextProps.location; + this.location = nextProps.location || undefined; this.breakpoints = nextProps.breakpoints; this.showDecorators(); this.editor && this.location && this.scrollToLineIfNeeded(this.location.lineNumber + 1); @@ -539,7 +538,7 @@ class ScriptEditor extends React.Component this.breakpoints = nextProps.breakpoints; this.showDecorators(); } else if (JSON.stringify(nextProps.location) !== JSON.stringify(this.location)) { - this.location = nextProps.location; + this.location = nextProps.location || undefined; this.showDecorators(); this.editor && this.location && this.scrollToLineIfNeeded(this.location.lineNumber + 1); // this.editor && this.location && this.editor.setPosition(this.location.lineNumber + 1, this.location.columnNumber + 1); diff --git a/src-editor/src/types.d.ts b/src-editor/src/types.d.ts index 5f4cd4fc9..b067caff3 100644 --- a/src-editor/src/types.d.ts +++ b/src-editor/src/types.d.ts @@ -6,3 +6,180 @@ export type LogMessage = { ts: number; severity: ioBroker.LogLevel; }; + +export interface DebuggerLocation { + /** + * Script identifier as reported in the Debugger.scriptParsed. + */ + scriptId: string; + /** + * Line number in the script (0-based). + */ + lineNumber: number; + /** + * Column number in the script (0-based). + */ + columnNumber?: number | undefined; +} + +interface SetBreakpointParameterType { + id: string; + location: DebuggerLocation; + condition?: string | undefined; +} +interface DebuggerPropertyPreview { + /** + * Property name. + */ + name: string; + /** + * Object type. Accessor means that the property itself is an accessor property. + */ + type: string; + /** + * User-friendly property value string. + */ + value?: string | undefined; + /** + * Nested value preview. + */ + valuePreview?: DebuggerObjectPreview | undefined; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; +} + +interface DebuggerObjectPreview { + /** + * Object type. + */ + type: string; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; + /** + * String representation of the object. + */ + description?: string | undefined; + /** + * True iff some of the properties or entries of the original object did not fit. + */ + overflow: boolean; + /** + * List of the properties. + */ + properties: DebuggerPropertyPreview[]; + /** + * List of the entries. Specified for map and set subtype values only. + */ + entries?: DebuggerEntryPreview[] | undefined; +} + +interface DebuggerEntryPreview { + /** + * Preview of the key. Specified for map-like collection entries. + */ + key?: DebuggerObjectPreview | undefined; + /** + * Preview of the value. + */ + value: DebuggerObjectPreview; +} +interface DebuggerCustomPreview { + header: string; + hasBody: boolean; + formatterObjectId: string; + bindRemoteObjectFunctionId: string; + configObjectId?: string | undefined; +} +interface DebuggerRemoteObject { + /** + * Object type. + */ + type: string; + /** + * Object subtype hint. Specified for object type values only. + */ + subtype?: string | undefined; + /** + * Object class (constructor) name. Specified for object type values only. + */ + className?: string | undefined; + /** + * Remote object value in case of primitive values or JSON values (if it was requested). + */ + value?: any; + /** + * Primitive value which can not be JSON-stringified does not have value, but gets this property. + */ + unserializableValue?: string | undefined; + /** + * String representation of the object. + */ + description?: string | undefined; + /** + * Unique object identifier (for non-primitive values). + */ + objectId?: string | undefined; + /** + * Preview containing abbreviated property values. Specified for object type values only. + */ + preview?: DebuggerObjectPreview | undefined; + customPreview?: DebuggerCustomPreview | undefined; +} +interface DebuggerScope { + /** + * Scope type. + */ + type: string; + /** + * Object representing the scope. For global and with scopes it represents the actual object; for the rest of the scopes, it is artificial transient object enumerating scope variables as its properties. + */ + object: DebuggerRemoteObject; + name?: string | undefined; + /** + * Location in the source code where scope starts + */ + startLocation?: DebuggerLocation | undefined; + /** + * Location in the source code where scope ends + */ + endLocation?: DebuggerLocation | undefined; +} + +interface CallFrame { + /** + * Call frame identifier. This identifier is only valid while the virtual machine is paused. + */ + callFrameId: string; + /** + * Name of the JavaScript function called on this call frame. + */ + functionName: string; + /** + * Location in the source code. + */ + functionLocation?: DebuggerLocation | undefined; + /** + * Location in the source code. + */ + location: DebuggerLocation; + /** + * JavaScript script name or url. + */ + url: string; + /** + * Scope chain for this call frame. + */ + scopeChain: DebuggerScope[]; + /** + * this object for this call frame. + */ + this: DebuggerRemoteObject; + /** + * The value being returned, if the function is at return point. + */ + returnValue?: DebuggerRemoteObject | undefined; +}