goog.loadJs('common', () => { goog.require('layui'); goog.require('tippy'); goog.require('Base64'); goog.require('Blockly'); goog.require('Mixly.Drag'); goog.require('Mixly.DragV'); goog.require('Mixly.XML'); goog.require('Mixly.Msg'); goog.require('Mixly.Config'); goog.require('Mixly.Env'); goog.require('Mixly.Debug'); goog.require('Mixly.Menu'); goog.require('Mixly.Boards'); goog.require('Mixly.MJson'); goog.require('Mixly.LayerExt'); goog.require('Mixly.HTMLTemplate'); goog.require('Mixly.EditorBlockly'); goog.require('Mixly.EditorCode'); goog.require('Mixly.EditorBase'); goog.provide('Mixly.EditorMix'); const { dropdown } = layui; const { EditorBlockly, EditorCode, EditorBase, Drag, DragV, XML, Msg, Config, Env, Debug, Menu, Boards, MJson, HTMLTemplate, LayerExt } = Mixly; const { BOARD, SOFTWARE } = Config; const { form } = layui; class EditorMix extends EditorBase { static { HTMLTemplate.add( 'html/editor/editor-mix.html', new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-mix.html'))) ); HTMLTemplate.add( 'html/editor/editor-mix-btns.html', new HTMLTemplate(goog.readFileSync(path.join(Env.templatePath, 'html/editor/editor-mix-btns.html'))) ); } #language_ = null; #tabSize_ = null; #$btns_ = null; #temp_ = ''; constructor() { super(); const $content = $(HTMLTemplate.get('html/editor/editor-mix.html').render()); const $btnsContent = $(HTMLTemplate.get('html/editor/editor-mix-btns.html').render({ block: Msg.Lang['editor.block'], mix: Msg.Lang['editor.mix'], code: Msg.Lang['editor.code'] })); this.drag = null; this.#$btns_ = $btnsContent.find('button'); this.setContent($content); this.setBtnsContent($btnsContent); this.addPage($content.find('.editor-blockly'), 'block', new EditorBlockly()); this.addPage($content.find('.editor-code'), 'code', new EditorCode()); } init() { super.init(); this.#addDragEventsListener_(); this.#addBtnEventsListener_(); this.#language_ = this.getDefaultLanguageExt(); this.#tabSize_ = this.getDefaultTabSize(); const codePage = this.getPage('code'); codePage.setLanguage(codePage.getLanguageByExt(this.#language_)); codePage.setTabSize(this.#tabSize_); codePage.setReadOnly(true); this.#py2BlockEditorInit_(); const contextMenu = codePage.getContextMenu(); let codeMenu = contextMenu.getItem('code'); codeMenu.add({ weight: 6, id: 'sep2', data: '---------' }); codeMenu.add({ weight: 7, id: 'block', data: { isHtmlName: false, name: Msg.Lang['editor.contextMenu.exitCodeEditor'], callback: (key, opt) => this.drag.exitfull(Drag.Extend.NEGATIVE) } }); let blockMenu = new Menu(); blockMenu.add({ weight: 0, id: 'copy', data: { isHtmlName: true, name: Menu.getItem(Msg.Lang['editor.contextMenu.copy'], 'Ctrl+C'), callback: (key, opt) => codePage.copy() } }); blockMenu.add({ weight: 1, id: 'sep1', data: '---------' }); blockMenu.add({ weight: 2, id: 'code', data: { isHtmlName: false, name: Msg.Lang['editor.contextMenu.enterCodeEditor'], callback: (key, opt) => this.drag.full(Drag.Extend.NEGATIVE) } }); contextMenu.register('block', blockMenu); contextMenu.offEvent('getMenu'); contextMenu.bind('getMenu', () => { return this.getPageType(); }); } #getCodeExtname_() { let extname = '.c'; const language = BOARD.language.toLowerCase(); switch (language) { case 'python': case 'circuitpython': case 'micropython': extname = '.py'; break; case 'lua': extname = '.lua'; break; case 'javascript': extname = '.js'; break; case 'c/c++': default: extname = '.c'; } return extname; } #py2BlockEditorInit_() { const codePage = this.getPage('code'); if (typeof Sk === 'object' && typeof PythonToBlocks === 'function' && typeof Py2blockEditor === 'function') { const py2blockConverter = new PythonToBlocks(); this.py2BlockEditor = new Py2blockEditor(py2blockConverter, codePage.getEditor()); } } #addCodeChangeEventListener_() { const codePage = this.getPage('code'); codePage.offEvent('change'); codePage.bind('change', () => { this.addDirty(); }); } #addDragEventsListener_() { const blockPage = this.getPage('block'); const codePage = this.getPage('code'); this.drag = new DragV(this.getContent()[0], { min: '200px', full: [true, true], startSize: '100%', startExitFullSize: '70%' }); this.drag.bind('sizeChanged', () => this.resize()); this.drag.bind('onfull', (type) => { this.#$btns_.removeClass('self-adaption-btn'); let $btn = null; switch(type) { case Drag.Extend.POSITIVE: $btn = this.#$btns_.filter('[m-id="block"]'); blockPage.scrollCenter(); break; case Drag.Extend.NEGATIVE: $btn = this.#$btns_.filter('[m-id="code"]'); codePage.setReadOnly(false); codePage.focus(); if (this.py2BlockEditor && BOARD.pythonToBlockly) { this.py2BlockEditor.fromCode = true; } break; } $btn.addClass('self-adaption-btn'); }); this.drag.bind('exitfull', (type) => { this.#$btns_.removeClass('self-adaption-btn'); const $btn = this.#$btns_.filter('[m-id="mixture"]'); $btn.addClass('self-adaption-btn'); switch(type) { case Drag.Extend.NEGATIVE: codePage.setReadOnly(true); if (this.py2BlockEditor && BOARD.pythonToBlockly && typeof this.py2BlockEditor.updateBlock === 'function') { this.py2BlockEditor.updateBlock(); } else { codePage.setValue(blockPage.getValue(), this.#language_); } break; case Drag.Extend.POSITIVE: this.#temp_ = blockPage.getValue(); codePage.setValue(this.#temp_, this.#language_); break; } blockPage.resize(); blockPage.scrollCenter(); }); } #addBtnEventsListener_() { this.#$btns_.on('click', (event) => { const $btn = $(event.currentTarget); const mId = $btn.attr('m-id'); if (mId === 'deps') { return; } if (!$btn.hasClass('self-adaption-btn')) { this.#$btns_.removeClass('self-adaption-btn'); $btn.addClass('self-adaption-btn'); } switch (mId) { case 'block': this.drag.full(Drag.Extend.POSITIVE); break; case 'mixture': this.drag.exitfull(Drag.Extend.POSITIVE); this.drag.exitfull(Drag.Extend.NEGATIVE); break; case 'code': this.drag.full(Drag.Extend.NEGATIVE); break; } }) } getCurrentEditor() { const blockPage = this.getPage('block'); const codePage = this.getPage('code'); if (this.getPageType() === 'code') { return codePage; } else { return blockPage; } } getDefaultLanguageExt() { let ext = '.txt'; let type = (BOARD.language || '').toLowerCase(); switch(type) { case 'python': case 'micropython': case 'circuitpython': ext = '.py'; break; case 'c/c++': ext = '.cpp'; break; case 'javascript': ext = '.js'; break; case 'markdown': ext = '.md'; break; case 'lua': ext = '.lua'; break; default: ext = '.txt'; } return ext; } getDefaultTabSize() { let tabSize = 4; let type = (BOARD.language || '').toLowerCase(); switch(type) { case 'c/c++': case 'markdown': tabSize = 2; break; case 'python': case 'micropython': case 'circuitpython': case 'javascript': default: tabSize = 4; } return tabSize; } getPageType() { if (this.drag.shown !== Drag.Extend.NEGATIVE) { return 'block'; } else { return 'code'; } } undo() { super.undo(); const editor = this.getCurrentEditor(); editor.undo(); } redo() { super.redo(); const editor = this.getCurrentEditor(); editor.redo(); } dispose() { const blockPage = this.getPage('block'); const blocklyWorkspace = blockPage.getEditor(); blocklyWorkspace.removeChangeListener(this.codeChangeListener); this.drag.dispose(); super.dispose(); this.drag = null; } onMounted() { super.onMounted(); const blockPage = this.getPage('block'); const blocklyWorkspace = blockPage.getEditor(); this.codeChangeListener = blocklyWorkspace.addChangeListener((event) => { const blockPage = this.getPage('block'); const codePage = this.getPage('code'); if ( event.isUiEvent || event.type == Blockly.Events.FINISHED_LOADING || blocklyWorkspace.isDragging() ) { return; } this.addDirty(); if (this.drag.shown !== Drag.Extend.BOTH) { return; } const code = blockPage.getCode(); if (this.#temp_ === code) { return; } this.#temp_ = code; codePage.setValue(code, this.#language_); }); this.#addCodeChangeEventListener_(); } onUnmounted() { super.onUnmounted(); const blockPage = this.getPage('block'); const blocklyWorkspace = blockPage.getEditor(); blocklyWorkspace.removeChangeListener(this.codeChangeListener); } setValue(data, ext) { const blockPage = this.getPage('block'); const codePage = this.getPage('code'); switch (ext) { case '.mix': case '.xml': Blockly.Events.disable(); try { data = XML.convert(data, true); data = data.replace(/\\(u[0-9a-fA-F]{4})/g, function (s) { return unescape(s.replace(/\\(u[0-9a-fA-F]{4})/g, '%$1')); }); } catch (error) { Debug.error(error); } this.parseMix($(data), false, false, (message) => { if (message) { switch (message) { case 'USE_CODE': Debug.log('已从code标签中读取代码'); break; case 'USE_INCOMPLETE_BLOCKS': Debug.log('一些块已被忽略'); break; } blockPage.scrollCenter(); Blockly.hideChaff(); } }); Blockly.Events.enable(); break; default: this.drag.full(Drag.Extend.NEGATIVE); this.getPage('code').setValue(data, ext); break; } } getValue() { const blockPage = this.getPage('block'); const codePage = this.getPage('code'); const mix = blockPage.getMix(); const $xml = $(mix.block); const config = Boards.getSelectedBoardConfig(); const boardName = Boards.getSelectedBoardName(); const board = BOARD?.boardType ?? 'default'; let xml = ''; let code = ''; $xml.removeAttr('xmlns') .attr('version', SOFTWARE?.version ?? 'Mixly 2.0') .attr('board', board + '@' + boardName); if (this.drag.shown !== Drag.Extend.NEGATIVE) { $xml.attr('shown', 'block'); code = mix.code; } else { $xml.attr('shown', 'code'); code = codePage.getValue(); } xml = $xml[0].outerHTML; if (config) { xml += `${MJson.stringify(config)}`; } xml += `${Base64.encode(code)}`; return xml; } getCode() { const blockPage = this.getPage('block'); const codePage = this.getPage('code'); if (this.drag.shown !== Drag.Extend.NEGATIVE) { return blockPage.getRawCode(); } else { return codePage.getCode(); } } getMil() { const $mix = $(this.getValue()); let $xml, $config, $code; for (let i = 0; $mix[i]; i++) { switch ($mix[i].nodeName) { case 'XML': $xml = $($mix[i]); break; case 'CONFIG': $config = $($mix[i]); break; case 'CODE': $code = $($mix[i]); break; } } if (!$xml) return ''; $config && $config.remove(); $code && $code.remove(); $xml.attr('type', 'lib'); $xml.find('block,shadow').removeAttr('id varid x y'); const $blocks = $xml.children('block'); let blockXmlList = []; for (let i = 0; $blocks[i]; i++) { const outerHTML = $blocks[i].outerHTML; if (!blockXmlList.includes(outerHTML)) { blockXmlList.push(outerHTML); } else { $blocks[i].remove(); } } return $xml[0].outerHTML; } parseMix(xml, useCode = false, useIncompleteBlocks = false, endFunc = (message) => {}) { const blockPage = this.getPage('block'); const codePage = this.getPage('code'); const mixDom = xml; let xmlDom, configDom, codeDom; for (let i = 0; mixDom[i]; i++) { switch (mixDom[i].nodeName) { case 'XML': xmlDom = $(mixDom[i]); break; case 'CONFIG': configDom = $(mixDom[i]); break; case 'CODE': codeDom = $(mixDom[i]); break; } } if (!xmlDom && !codeDom) { layer.msg(Msg.Lang['editor.invalidData'], { time: 1000 }); return; } for (let i of ['version', 'id', 'type', 'varid', 'name', 'x', 'y', 'items']) { const nowDom = xmlDom.find('*[' + i + ']'); if (nowDom.length) { for (let j = 0; nowDom[j]; j++) { let attr = $(nowDom[j]).attr(i); try { attr = attr.replaceAll('\\\"', ''); } catch (error) { Debug.error(error); } $(nowDom[j]).attr(i, attr); } } } let config, configStr = configDom && configDom.html(); config = configStr? MJson.parse(configStr) : {}; let boardName = xmlDom.attr('board') ?? ''; blockPage.getEditor().clear(); Boards.setSelectedBoard(boardName, config); let code = codeDom ? codeDom.html() : ''; if (Base64.isValid(code)) { code = Base64.decode(code); } else { try { code = util.unescape(code); code = code.replace(/(_E[0-9A-F]{1}_[0-9A-F]{2}_[0-9A-F]{2})+/g, function (s) { try { return decodeURIComponent(s.replace(/_/g, '%')); } catch (error) { return s; } }); } catch (error) { Debug.error(error); } } if (useCode) { if (!codeDom) { layer.msg(Msg.Lang['editor.invalidData'], { time: 1000 }); return; } this.drag.full(Drag.Extend.NEGATIVE); // 完全显示代码编辑器 codePage.setValue(code, this.#language_); endFunc('USE_CODE'); return; } const blockDom = mixDom.find('block'); const shadowDom = mixDom.find('shadow'); blockDom.removeAttr('id varid'); shadowDom.removeAttr('id varid'); let blocks = []; let undefinedBlocks = []; for (let i = 0; blockDom[i]; i++) { const blockType = $(blockDom[i]).attr('type'); if (blockType && !blocks.includes(blockType)) blocks.push(blockType); } for (let i = 0; shadowDom[i]; i++) { const shadowType = $(shadowDom[i]).attr('type'); if (shadowType && !blocks.includes(shadowType)) blocks.push(shadowType); } const blocklyGenerator = Blockly.generator; for (let i of blocks) { if (Blockly.Blocks[i] && blocklyGenerator.forBlock[i]) { continue; } undefinedBlocks.push(i); } if (undefinedBlocks.length) { this.showParseMixErrorDialog(mixDom, undefinedBlocks, endFunc); return; } Blockly.Xml.clearWorkspaceAndLoadFromXml(xmlDom[0], blockPage.getEditor()); blockPage.getEditor().scrollCenter(); Blockly.hideChaff(); if (!useIncompleteBlocks && codeDom && xmlDom.attr('shown') === 'code') { this.drag.full(Drag.Extend.NEGATIVE); // 完全显示代码编辑器 codePage.setValue(code, this.#language_); endFunc(); return; } this.drag.full(Drag.Extend.POSITIVE); // 完全显示块编辑器 if (useIncompleteBlocks) endFunc('USE_INCOMPLETE_BLOCKS'); else endFunc(); } showParseMixErrorDialog(xml, undefinedBlocks, endFunc = () => {}) { const { PARSE_MIX_ERROR_DIV } = XML.TEMPLATE_STR; const renderStr = XML.render(PARSE_MIX_ERROR_DIV, { text: undefinedBlocks.join('
'), btn1Name: Msg.Lang['editor.cancel'], btn2Name: Msg.Lang['editor.ignoreBlocks'], btn3Name: Msg.Lang['editor.loadCode'] }) LayerExt.open({ title: Msg.Lang['editor.parseMixErrorInfo'], id: 'parse-mix-error-layer', area: ['50%', '250px'], max: ['500px', '250px'], min: ['350px', '100px'], shade: LayerExt.SHADE_ALL, content: renderStr, borderRadius: '5px', success: (layero, index) => { $('#parse-mix-error-layer').css('overflow', 'hidden'); form.render(null, 'parse-mix-error-filter'); layero.find('button').click((event) => { layer.close(index); const mId = $(event.currentTarget).attr('m-id'); switch (mId) { case '0': break; case '1': for (let i of undefinedBlocks) { xml.find('*[type='+i+']').remove(); } this.parseMix(xml, false, true, endFunc); break; case '2': this.parseMix(xml, true, false, endFunc); break; } }); } }); } } Mixly.EditorMix = EditorMix; });