define(['./scripts.xml', './scripts-add.xml', './script-blockly.xml', './toolbox.xml', './blocks.json', './blocks-lua', './script-view.xml', './script-view-config.xml', './view-schema.json', './script-editor.xml'], function(scriptsTemplate, scriptsAddTemplate, scriptBlocklyTemplate, toolboxXml, blocks, blocksLua, scriptViewTemplate, scriptViewConfigTemplate, scriptsViewConfigSchema, scriptEditorTemplate) { var SCRIPT_PATH = '/engine/scripts/'; var SCRIPT_FILES_PATH = '/engine/scriptFiles/'; function getMatches(s, r) { var matches = []; var m; var re = new RegExp(r, 'g'); while ((m = re.exec(s)) !== null) { matches.push(m[1]); } return matches; } function enumToOptions(e) { return [e.title, e.const]; } function buildOptions(l) { if (isArrayWithItems(l)) { return l.map(enumToOptions); } return [['(no things)', '-empty-']]; } function exportToLua(workspace) { //Blockly.Lua.INFINITE_LOOP_TRAP = 'if(--window.LoopTrap == 0) throw "Infinite loop.";\n'; var lines = [ "local script = ...", "local logger = require('jls.lang.logger')", "local Date = require('jls.util.Date')", "local utils = require('lha.utils')", "" ]; var varRegExp = new RegExp('^[a-zA-Z_][a-zA-Z0-9_]*$'); // Protection against invalid variable names var varNames = workspace.getAllVariables().map(function(v) {return v.name;}).filter(function(n) {return varRegExp.test(n)}); varNames.sort(); if (varNames.length > 0) { lines.push('local ' + varNames.join(', '), ''); } var code = Blockly.Lua.workspaceToCode(workspace); // TODO Find a proper way to extract the function names var names = getMatches(code, /\nfunction ([a-zA-Z][a-zA-Z0-9_]+)\(/g); names.sort(); if (names.length > 0) { lines.push('local ' + names.join(', '), ''); } lines.push(code, ''); return lines.join('\n'); } function exportToXml(workspace) { var xml = Blockly.Xml.workspaceToDom(workspace); // domToPrettyText domToText var xmlText = Blockly.Xml.domToPrettyText(xml); //console.log('scriptsEditor.save()', xmlText); return xmlText; } function exportAs(text, filename, type) { //console.log('exportAs()', text); var blob = new window.Blob([text], {type : (type || 'text/plain')}); var blobUrl = window.URL.createObjectURL(blob); var clickHandler = function() { setTimeout(function() { URL.revokeObjectURL(blobUrl); }, 150); }; const a = document.createElement('a'); a.href = blobUrl; a.download = filename || 'download'; a.addEventListener('click', clickHandler, {capture: false, once: true}); a.click(); //window.open(blobUrl); //window.URL.revokeObjectURL(blobUrl); } function loadBlocklyWorkspace(self) { //console.log('using toolbox', toolboxXml); var workspace = Blockly.inject('scriptsEditorBlocklyDiv', { media: '/static/blockly/media/', toolbox: toolboxXml }); // Registers action buttons workspace.registerButtonCallback('refresh', function() { self.refresh().then(function() { toaster.toast('Refreshed'); }); }); workspace.registerButtonCallback('clear', function() { workspace.clear(); toaster.toast('Cleared'); }); workspace.registerButtonCallback('delete', self.onDelete); workspace.registerButtonCallback('save', self.onSave); workspace.registerButtonCallback('showLua', function() { var win = window.open('', 'Lua-' + self.scriptId, 'popup=yes,scrollbars=yes,resizable=yes,toolbar=no,location=no,directories=no,status=no,menubar=no'); win.document.title = 'Lua ' + self.name; win.document.body.innerHTML = '
' + exportToLua(workspace) + '
'; }); workspace.registerButtonCallback('exportLua', function() { exportAs(exportToLua(workspace), 'lha-script-' + self.name.replace(/\W/g, '-') + '.lua'); }); workspace.registerButtonCallback('exportXml', function() { exportAs(exportToXml(workspace), 'lha-blocks-' + self.name.replace(/\W/g, '-') + '.xml'); }); workspace.registerButtonCallback('importXml', function() { self.$refs.uploadInput.click(); }); return workspace; } function customizeBlockly(blockEnv) { for (var name in blocks) { Blockly.Blocks[name] = (function (name, block) { return { init: function() { //console.log('init block ' + name); var b = deepMap(block, function(v) { if ((typeof v === 'string') && (v.charAt(0) === '$')) { var vv = blockEnv[v.substring(1)] if (vv !== undefined) { return vv; } } return v; }); this.jsonInit(b); } }; })(name, blocks[name]); } for (var name in blocksLua) { Blockly.Lua[name] = blocksLua[name]; } } function onDelete() { var scriptId = this.scriptId; console.log('scriptsEditor.onDelete(), scriptId is "' + scriptId + '"'); if (scriptId) { confirmation.ask('Delete the script?').then(function() { fetch(SCRIPT_PATH + scriptId + '/', { method: 'DELETE' }).then(assertIsOk).then(function() { app.replacePage('scripts'); toaster.toast('Deleted'); }); }); } } function onRename() { var self = this; return fetch(SCRIPT_PATH + this.scriptId + '/name', { method: 'PUT', body: this.newName }).then(assertIsOk).then(function() { self.name = self.newName; self.newName = false; toaster.toast('Renamed'); }); } function onApply() { var scriptId = this.scriptId; return this.onSave().then(function() { return fetch(SCRIPT_PATH + scriptId + '/reload', {method: 'POST'}); }).then(function() { toaster.toast('Script reloaded'); }); } function loadBlockly() { if (typeof Blockly === 'undefined') { return loadScripts( 'static/blockly/blockly_compressed.js', 'static/blockly/msg/js/en.js', 'static/blockly/blocks_compressed.js', 'static/blockly/javascript_compressed.js', 'static/blockly/lua_compressed.js' ); } return Promise.resolve(); } var blockEnv = { lhaDataColor: 38, lhaEventColor: 58, lhaExpressionColor: 78, lhaExperimentalColor: 0, lhaEventNames: [ { const: "-disabled-", title: "never" }, { const: "startup", title: "startup" }, { const: "shutdown", title: "shutdown" }, { const: "poll", title: "polling" }, { const: "data", title: "data" }, { const: "heartbeat", title: "heartbeat" }, { const: "test", title: "testing" } ].map(enumToOptions), getThingPathOptions: [], setThingPathOptions: [], eventThingPathOptions: [] }; var scriptsBlocklyVue = new Vue({ template: scriptBlocklyTemplate, data: { scriptId: '', newName: false, name: '', things: [], workspace: null }, methods: { onShow: function(scriptId) { console.log('scriptsEditor.onShow()'); var self = this; loadBlockly().then(function() { if (self.workspace === null) { self.workspace = loadBlocklyWorkspace(self); customizeBlockly(blockEnv); } return app.getEnumsById(); }).then(function(enumsById) { // Prepare data fields assignMap(blockEnv, { getThingPathOptions: buildOptions(enumsById.readablePropertyPaths), setThingPathOptions: buildOptions(enumsById.writablePropertyPaths), eventThingPathOptions: buildOptions(enumsById.allPropertyPaths) }); }).finally(function() { if (scriptId) { self.scriptId = scriptId; self.refresh(); } }); }, uploadThenSave: function(event) { var input = event.target; if (input.files.length !== 1) { return; } var self = this; fetch(SCRIPT_FILES_PATH + self.scriptId + '/blocks.xml', { method: 'PUT', body: input.files[0] }).then(assertIsOk).then(function() { toaster.toast('Blocks uploaded'); self.refresh(); }); }, onPoll: function() { return fetch('/engine/extensions/' + this.scriptId + '/poll', {method: 'POST'}).then(function() { toaster.toast('Script polled'); }); }, onTest: function() { console.log('Testing script'); return fetch('/engine/extensions/' + this.scriptId + '/test', {method: 'POST'}); }, onLogs: function(logs) { if (logs) { for (var i = 0; i < logs.length; i++) { var log = logs[i]; var msg = 'Engine: ' + log.message switch (log.level) { case 100: console.error(msg); break; case 90: console.warn(msg); break; case 80: console.info(msg); break; case 70: console.log(msg); break; default: console.debug(msg); break; } } } }, onDelete: onDelete, onRename: onRename, onApply: onApply, onSave: function() { var scriptId = this.scriptId; console.log('scriptsEditor.onSave(), scriptId is "' + scriptId + '"'); var workspace = this.workspace; if (!scriptId || !workspace) { return; } // generate var code, xmlText; try { code = exportToLua(workspace); xmlText = exportToXml(workspace); } catch (e) { toaster.toast('Error'); return Promise.reject(e); } // save return Promise.all([ fetch(SCRIPT_FILES_PATH + scriptId + '/blocks.xml', { method: 'PUT', body: xmlText }).then(assertIsOk), fetch(SCRIPT_FILES_PATH + scriptId + '/script.lua', { method: 'PUT', body: code }).then(assertIsOk) ]).then(function() { toaster.toast('Saved'); }); }, refresh: function() { var workspace = this.workspace; if (!workspace) { return Promise.reject('workspace not initialized'); } return Promise.all([ fetch(SCRIPT_FILES_PATH + this.scriptId + '/blocks.xml').then(getResponseText), fetch(SCRIPT_FILES_PATH + this.scriptId + '/manifest.json').then(getResponseJson) ]).then(apply(this, function(xmlText, manifest) { workspace.clear(); var xml = Blockly.Xml.textToDom(xmlText); Blockly.Xml.domToWorkspace(xml, workspace); this.name = manifest.name; })); } } }); var scriptsViewVue = new Vue({ template: scriptViewTemplate, data: { scriptId: '', newName: false, name: '', text: '' }, methods: { onShow: function(scriptId) { if (scriptId) { this.scriptId = scriptId; this.refresh(); } }, onDelete: onDelete, onRename: onRename, onApply: onApply, onSave: function() { console.log('scriptsEditor.onSave(), scriptId is "' + this.scriptId + '"'); if (!this.scriptId) { return; } return fetch(SCRIPT_FILES_PATH + this.scriptId + '/view.xml', {method: 'PUT', body: this.text}).then(assertIsOk).then(function() { toaster.toast('Saved'); }); }, refresh: function() { return Promise.all([ fetch(SCRIPT_FILES_PATH + this.scriptId + '/view.xml').then(assertIsOk).then(getResponseText), fetch(SCRIPT_FILES_PATH + this.scriptId + '/manifest.json').then(assertIsOk).then(getJson) ]).then(apply(this, function(text, manifest) { this.text = text; this.name = manifest.name; })); } } }); var scriptsViewConfigVue = new Vue({ template: scriptViewConfigTemplate, data: { scriptId: '', schema: {}, config: {} }, methods: { onShow: function(scriptId) { this.scriptId = scriptId; return Promise.all([ fetch(SCRIPT_FILES_PATH + this.scriptId + '/config.json').then(getJson), app.getEnumsById() ]).then(apply(this, function(config, enumsById) { //console.info('schema:', populateJsonSchema(scriptsViewConfigSchema, enumsById)); this.schema = populateJsonSchema(scriptsViewConfigSchema, enumsById); this.config = config; })); }, onSave: function() { return putJson(SCRIPT_FILES_PATH + this.scriptId + '/config.json', this.config).then(assertIsOk).then(function() { app.back(); toaster.toast('Saved'); }); } } }); var scriptsEditorVue = new Vue({ template: scriptEditorTemplate, data: { scriptId: '', newName: false, name: '', text: '' }, methods: { onShow: function(scriptId) { if (scriptId) { this.scriptId = scriptId; this.refresh(); } }, onDelete: onDelete, onRename: onRename, onApply: onApply, onSave: function() { var scriptId = this.scriptId; console.log('scriptsEditor.onSave(), scriptId is "' + scriptId + '"'); if (!scriptId) { return; } return fetch(SCRIPT_FILES_PATH + scriptId + '/script.lua', { method: 'PUT', body: this.text }).then(assertIsOk).then(function() { toaster.toast('Saved'); }); }, refresh: function() { return Promise.all([ fetch(SCRIPT_FILES_PATH + this.scriptId + '/script.lua').then(assertIsOk).then(getResponseText), fetch(SCRIPT_FILES_PATH + this.scriptId + '/manifest.json').then(assertIsOk).then(getJson) ]).then(apply(this, function(luaText, manifest) { this.text = luaText; this.name = manifest.name; })); } } }); var scriptsAddVue = new Vue({ template: scriptsAddTemplate, methods: { newScript: function () { fetch(SCRIPT_PATH, {method: 'PUT'}).then(assertIsOk).then(function() { app.replacePage('scripts'); }); }, newBlocks: function () { fetch(SCRIPT_PATH, {method: 'PUT', headers: {'LHA-Name': 'New Blocks'}}).then(assertIsOk).then(getResponseText).then(function(scriptId) { return fetch(SCRIPT_FILES_PATH + scriptId + '/blocks.xml', { method: 'PUT', body: '' }); }).then(function() { app.replacePage('scripts'); }); }, newView: function () { fetch(SCRIPT_PATH, {method: 'PUT', headers: { 'LHA-Name': 'New View', 'LHA-Script': '//web-scripts/view-addon.lua' }}).then(assertIsOk).then(getResponseText).then(function(scriptId) { return Promise.all([ fetch(SCRIPT_FILES_PATH + scriptId + '/view.xml', {method: 'PUT', body: ''}), fetch(SCRIPT_FILES_PATH + scriptId + '/config.json', {method: 'PUT', body: [ '{', ' "id": "view-' + scriptId + '",', ' "title": "View ' + scriptId + '"', '}' ].join('\n')}), fetch(SCRIPT_FILES_PATH + scriptId + '/init.js', {method: 'PUT', body: [ "define(['addon/web-scripts/view-loader.js', './config.json', './view.xml'], function(viewLoader, config, viewXml) {", " viewLoader.load(config, viewXml);", "});" ].join('\n')}) ]); }).then(function() { app.replacePage('scripts'); }); } } }); var scriptsVue = new Vue({ template: scriptsTemplate, data: { scripts: [] }, methods: { onShow: function () { this.scripts = []; var self = this; fetch(SCRIPT_PATH, {headers: {"Accept": 'application/json'}}).then(getResponseJson).then(function(scripts) { self.scripts = scripts; }); }, pollScript: function (script) { fetch('/engine/extensions/' + script.id + '/poll', {method: 'POST'}).then(function() { toaster.toast('Script polled'); }); }, reloadScript: function (script) { fetch(SCRIPT_PATH + script.id + '/reload', {method: 'POST'}).then(function() { toaster.toast('Script reloaded'); }); }, openScript: function (script) { var pageId = 'scriptsEditor'; if (script.hasBlocks) { pageId = 'scriptsBlockly'; } else if (script.hasView) { pageId = 'scriptsView'; } app.toPage(pageId, script.id); }, activateScript: function (script) { //console.log('activateScript()' + script.active); var activate = !script.active; return fetch('/engine/extensions/' + script.id + '/' + (activate ? 'enable' : 'disable'), {method: 'POST'}).then(function() { toaster.toast('Script ' + (activate ? 'enabled' : 'disabled')); }); }, onNew: function() { app.toPage('scripts-add'); } } }); addPageComponent(scriptsAddVue); addPageComponent(scriptsBlocklyVue); addPageComponent(scriptsViewVue); addPageComponent(scriptsViewConfigVue); addPageComponent(scriptsEditorVue); addPageComponent(scriptsVue, 'scroll', true, true); });