From 2df0897cbbb023c1d81cb13d594d63bcce49c0c5 Mon Sep 17 00:00:00 2001 From: Steve-Mcl Date: Sun, 11 Oct 2026 18:38:18 +0100 Subject: [PATCH 1/2] feat: Vue and Vuetify editing in the ui-template code editor The ui-template editor treats a Vue template as plain HTML: no Vue syntax, no help with Vuetify components. This adds a `vue-sfc` language for Node-RED's Monaco editor, shipped as an editor plugin with the Dashboard, so nothing is added to Node-RED core. - Vue highlighting: directives, bindings, events, slots and {{ }} as JavaScript; diff --git a/nodes/widgets/ui_template.html b/nodes/widgets/ui_template.html index ee3f5f726..834506e7e 100644 --- a/nodes/widgets/ui_template.html +++ b/nodes/widgets/ui_template.html @@ -183,6 +183,11 @@ const that = this const $templateScope = $('#node-input-templateScope') + // Monaco gets Vue/Vuetify syntax and completions from the ui-template-vue-editor plugin (nodes/plugins/vue-editor.html). + // ACE, or Monaco without the plugin, keeps the HTML mode. + const vueEditor = RED.plugins.getPlugin('ui-template-vue-editor') + const htmlMode = RED.editor.codeEditor?.editor?.type === 'monaco' && vueEditor?.ensure?.() ? 'ace/mode/vue-sfc' : 'ace/mode/html' + // if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up // as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates) if (RED.nodes.subflow(this.z)) { @@ -239,15 +244,15 @@ break case 'widget:ui': $('#template-row-ui').show() - that.editor.getSession().setMode('ace/mode/html') + that.editor.getSession().setMode(htmlMode) break case 'widget:page': $('#template-row-page').show() - that.editor.getSession().setMode('ace/mode/html') + that.editor.getSession().setMode(htmlMode) break default: $('#template-row-group, #template-row-class').show() - that.editor.getSession().setMode('ace/mode/html') + that.editor.getSession().setMode(htmlMode) break } resize.call(that) @@ -255,7 +260,7 @@ this.editor = RED.editor.createEditor({ id: 'node-input-format-editor', - mode: 'ace/mode/html', + mode: htmlMode, value: $('#node-input-format').val() }) @@ -263,7 +268,7 @@ url: 'uitemplates', // where to get the data from type: 'ui-template', // the type of object the library is for editor: this.editor, // the field name the main text body goes to - mode: 'ace/mode/html', + mode: htmlMode, fields: ['name'] }) @@ -275,7 +280,7 @@ e.preventDefault() const value = that.editor.getValue() RED.editor.editText({ - mode: $templateScope.val() === 'global' ? 'css' : 'html', + mode: $templateScope.val() === 'global' ? 'css' : htmlMode.replace('ace/mode/', ''), value, width: 'Infinity', cursor: that.editor.getCursorPosition(), diff --git a/package.json b/package.json index 025c4e6b1..c761baabc 100644 --- a/package.json +++ b/package.json @@ -31,15 +31,17 @@ ], "files": [ "dist/*", - "nodes/*" + "nodes/*", + "resources/*" ], "scripts": { - "build": "vite build", - "build:dev": "cross-env NODE_ENV=development vite build --mode development", + "build": "npm run build:vue-editor && vite build", + "build:dev": "npm run build:vue-editor && cross-env NODE_ENV=development vite build --mode development", + "build:vue-editor": "node scripts/build-vue-editor-data.js", "cy:open": "cypress open", "cy:run": "cypress run", "cy:server": "node-red -u ./cypress/fixtures/user-dir/ -p 1881", - "dev": "cross-env NODE_ENV=development vite build --mode development --watch", + "dev": "npm run build:vue-editor && cross-env NODE_ENV=development vite build --mode development --watch", "dev:prod": "vite build --watch", "docs:build": "vitepress build docs", "docs:dev": "vitepress dev docs", @@ -157,6 +159,9 @@ "ui-event": "nodes/widgets/ui_event.js", "ui-control": "nodes/widgets/ui_control.js", "ui-progress": "nodes/widgets/ui_progress.js" + }, + "plugins": { + "ui-template-vue-editor": "nodes/plugins/vue-editor.html" } }, "overrides": { diff --git a/resources/vue-editor/vue-sfc.mjs b/resources/vue-editor/vue-sfc.mjs new file mode 100644 index 000000000..d5bbb5882 --- /dev/null +++ b/resources/vue-editor/vue-sfc.mjs @@ -0,0 +1,608 @@ +/* eslint-env browser */ +/** + * Vue single file component support for Node-RED's Monaco code editor, used by the ui-template node. + * + * Loaded on demand by nodes/plugins/vue-editor.html the first time an editor uses the `vue-sfc` language. + * It provides: + * - a Monarch tokenizer for Vue templates (directives, bindings, events, slots and {{ }} interpolation + * are tokenized with JavaScript embedded; diff --git a/nodes/widgets/ui_template.html b/nodes/widgets/ui_template.html index 834506e7e..f20bdfffc 100644 --- a/nodes/widgets/ui_template.html +++ b/nodes/widgets/ui_template.html @@ -184,9 +184,9 @@ const $templateScope = $('#node-input-templateScope') // Monaco gets Vue/Vuetify syntax and completions from the ui-template-vue-editor plugin (nodes/plugins/vue-editor.html). - // ACE, or Monaco without the plugin, keeps the HTML mode. + // ACE, Monaco without the plugin, or an nr-assistant too old for vue-sfc keeps the HTML mode. const vueEditor = RED.plugins.getPlugin('ui-template-vue-editor') - const htmlMode = RED.editor.codeEditor?.editor?.type === 'monaco' && vueEditor?.ensure?.() ? 'ace/mode/vue-sfc' : 'ace/mode/html' + const htmlMode = vueEditor?.useVueSfc?.() ? 'ace/mode/vue-sfc' : 'ace/mode/html' // if this groups parent is a subflow template, set the node-config-input-width and node-config-input-height up // as typedInputs and hide the elementSizer (as it doesn't make sense for subflow templates)