diff --git a/.gitignore b/.gitignore index 1f5d3156a..951a0d441 100644 --- a/.gitignore +++ b/.gitignore @@ -147,4 +147,7 @@ docs/.vitepress/cache/ /cypress/fixtures/user-dir/flows.json /cypress/fixtures/user-dir/.flows.json.backup /cypress/screenshots -/cypress/downloads \ No newline at end of file +/cypress/downloads + +# Generated by scripts/build-vue-editor-data.js +/resources/vue-editor/vuetify.json diff --git a/docs/en/nodes/widgets/ui-template.md b/docs/en/nodes/widgets/ui-template.md index 40993cf18..6e4829f11 100644 --- a/docs/en/nodes/widgets/ui-template.md +++ b/docs/en/nodes/widgets/ui-template.md @@ -38,6 +38,16 @@ Provide custom JS and HTML (including any [Vuetify components](https://vuetifyjs +## Code Editor + +When Node-RED uses its default Monaco code editor, the template editor understands Vue templates: + +- Vue syntax is highlighted, including directives (`v-if`, `v-for`, ...), bindings (`:prop`), events (`@click`), slots (`#activator`) and `{{ }}` expressions, which are highlighted as JavaScript. +- Typing `` inside a component, that component's slots (`#`). Props with a fixed set of values, such as `variant`, suggest those values. +- Hovering over a Vuetify component, prop, event or slot shows its type, default and description, with a link to the Vuetify API docs. + +The component information matches the version of Vuetify that ships with the Dashboard and is loaded from Node-RED itself, so it works offline. Editors using ACE keep the plain HTML mode. + ## Writing Custom Widgets UI Template will parse different tags and render them into Dashboard. The available tags are: diff --git a/nodes/plugins/vue-editor.html b/nodes/plugins/vue-editor.html new file mode 100644 index 000000000..57fd7d54e --- /dev/null +++ b/nodes/plugins/vue-editor.html @@ -0,0 +1,97 @@ + diff --git a/nodes/widgets/ui_template.html b/nodes/widgets/ui_template.html index ee3f5f726..f20bdfffc 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, 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 = 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) 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;