diff --git a/CHANGELOG.md b/CHANGELOG.md index d442405b4..1c6ee44c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,93 @@ +#### 2.1.0: Milestone Release + +Editor + + - Position popover properly on a scrolled page + - Fixes from 2.1.0-beta.2 (#3202) @knolleary + +Nodes + +- Link Out: Fix saving link out node links (#3201) @knolleary + - Switch: Refix #3170 - copy switch rule type when adding new rule + - TCP Request: Add string option to TCP request node output (#3204) @dceejay + +#### 2.1.0-beta.2: Beta Release + +Editor + + - Fix switching projects (#3199) @knolleary + - Use locale setting when installing/enabling node (#3198) @knolleary + - Do not show projects-wecome dialog until welcome tour completes (#3197) @knolleary + - Fix converting selection to subflow (#3196) @knolleary + - Avoid conflicts with native browser cmd-ctrl type shortcuts (#3195) @knolleary + - Ensure message tools stay attached to top-level entry in Debug/Context (#3186) @knolleary + - Ensure tab state updates properly when toggling enable state (#3175) @knolleary + - Improve handling of long labels in TreeList (#3176) @knolleary + - Shift-click tab scroll arrows to jump to start/end (#3177) @knolleary + +Runtime + + - Update package dependencies + - Update to latest node-red-admin + +Nodes + + - Dynamic MQTT connections (#3189) + - Link: Filter out Link Out Return nodes in Link In edit dialog Fixes #3187 + - Link: Fix link call label (#3200) @knolleary + - Debug: Redesign debug filter options and make them persistant (#3183) @knolleary + - Inject: Widen Inject interval box for >1 digit (#3184) @knolleary + - Switch: Fix rule focus when switch 'otherwise' rule is used (#3185) @knolleary + +#### 2.1.0-beta.1: Beta Release + +Editor + + - Add Tour Guide component (#3136) @knolleary + - Allow tabs to be hidden (#3120) @knolleary + - Add align actions to editor (#3110) @knolleary + - Add support of environment variable for tab & group (#3112) @HiroyasuNishiyama + - Add autoComplete widget and add to TypedInput for msg. props (#3171) @knolleary + - Render node documentation to node-red style guide when written in markdown. (#3169) @Steve-Mcl + - Allow colouring of tab icon svg (#3140) @harmonic7 + - Restore tab selection after merging conflicts (#3151) @GerwinvBeek + - Fix serving of theme files on Windows (#3154) @knolleary + - Ensure config-node select inherits width properly from input (#3155) @knolleary + - Do better remembering TypedInput values whilst switching types (#3159) @knolleary + - Update monaco to 0.28.1 (#3153) @knolleary + - Improve themeing of tourGuide (#3161) @knolleary + - Allow a node to specify a filter for the config nodes it can pick from (#3160) @knolleary + - Allow RED.notify.update to modify any notification setting (#3163) @knolleary + - Fix typo in ko editor.json Fixes #3119 + - Improve RED.actions api to ensure actions cannot be overridden + - Ensure treeList row has suitable min-height when no content Fixes #3109 + - Refactor edit dialogs to use separate edit panes + - Ensure type select button is not focussable when TypedInput only has one type + - Place close tab link in front of fade + +Runtime + + - Improve error reporting with oauth login strategies (#3148) @knolleary + - Add allowUpdate feature to externalModules.palette (#3143) @knolleary + - Improve node install error reporting (#3158) @knolleary + - Improve unit test coverage (#3168) @knolleary + - Allow coreNodesDir to be set to false (#3149) @hardillb + - Update package dependencies + - uncaughtException debug improvements (#3146) @renatojuniorrs + +Nodes + + - Change: Add option to deep-clone properties in Change node (#3156) @knolleary + - Delay: Add push to front of rate limit queue. (#3069) @dceejay + - File: Add paletteLabel to file nodes to make read/write more obvious (#3157) @knolleary + - HTTP Request: Extend HTTP request node to log detailed timing information (#3116) @k-toumura + - HTTP Response: Fix sizing of HTTP Response header fields (#3164) @knolleary + - Join: Support for msg.restartTimeout (#3121) @magma1447 + - Link Call: Add Link Call node (#3152) @knolleary + - Switch: Copy previous rule type when adding rule to switch node (#3170) @knolleary + - Delay node: add option to send intermediate messages on separate output (#3166) @knolleary + - Typo in http request set method translation (#3173) @mailsvb + #### 2.0.6: Maintenance Release Editor diff --git a/Gruntfile.js b/Gruntfile.js index df9939a8c..83637cbed 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -162,7 +162,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/common/stack.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js", "packages/node_modules/@node-red/editor-client/src/js/ui/common/toggleButton.js", - "packages/node_modules/@node-red/editor-client/src/js/ui/common/colorPicker.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js", "packages/node_modules/@node-red/editor-client/src/js/ui/actions.js", "packages/node_modules/@node-red/editor-client/src/js/ui/deploy.js", "packages/node_modules/@node-red/editor-client/src/js/ui/diff.js", @@ -182,6 +182,7 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/tab-context.js", "packages/node_modules/@node-red/editor-client/src/js/ui/palette-editor.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editor.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/editors/panes/*.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editors/*.js", "packages/node_modules/@node-red/editor-client/src/js/ui/editors/code-editors/*.js", "packages/node_modules/@node-red/editor-client/src/js/ui/event-log.js", @@ -199,7 +200,8 @@ module.exports = function(grunt) { "packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectSettings.js", "packages/node_modules/@node-red/editor-client/src/js/ui/projects/projectUserSettings.js", "packages/node_modules/@node-red/editor-client/src/js/ui/projects/tab-versionControl.js", - "packages/node_modules/@node-red/editor-client/src/js/ui/touch/radialMenu.js" + "packages/node_modules/@node-red/editor-client/src/js/ui/touch/radialMenu.js", + "packages/node_modules/@node-red/editor-client/src/js/ui/tour/*.js" ], dest: "packages/node_modules/@node-red/editor-client/public/red/red.js" }, @@ -326,6 +328,12 @@ module.exports = function(grunt) { ], tasks: ['jsonlint:keymaps','copy:build'] }, + tours: { + files: [ + 'packages/node_modules/@node-red/editor-client/src/tours/**/*.js' + ], + tasks: ['copy:build'] + }, misc: { files: [ 'CHANGELOG.md' @@ -423,6 +431,12 @@ module.exports = function(grunt) { src: '**', expand: true, dest: 'packages/node_modules/@node-red/editor-client/public/vendor/ace/' + }, + { + cwd: 'packages/node_modules/@node-red/editor-client/src/tours', + src: '**', + expand: true, + dest: 'packages/node_modules/@node-red/editor-client/public/red/tours/' } ] } diff --git a/package.json b/package.json index d52055bc4..fa4eb9543 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red", - "version": "2.0.6", + "version": "2.1.0", "description": "Low-code programming for event-driven applications", "homepage": "http://nodered.org", "license": "Apache-2.0", @@ -26,9 +26,9 @@ } ], "dependencies": { - "acorn": "8.4.1", - "acorn-walk": "8.1.1", - "ajv": "8.6.2", + "acorn": "8.5.0", + "acorn-walk": "8.2.0", + "ajv": "8.6.3", "async-mutex": "0.3.2", "basic-auth": "2.0.1", "bcryptjs": "2.4.3", @@ -40,7 +40,7 @@ "cookie-parser": "1.4.5", "cors": "2.8.5", "cronosjs": "1.7.1", - "denque": "1.5.0", + "denque": "2.0.1", "express": "4.17.1", "express-session": "1.17.2", "form-data": "4.0.0", @@ -48,9 +48,9 @@ "fs.notify": "0.0.4", "got": "11.8.2", "hash-sum": "2.0.0", - "hpagent": "^0.1.2", + "hpagent": "0.1.2", "https-proxy-agent": "5.0.0", - "i18next": "20.3.2", + "i18next": "21.3.1", "iconv-lite": "0.6.3", "is-utf8": "0.2.1", "js-yaml": "3.14.1", @@ -64,18 +64,18 @@ "mqtt": "4.2.8", "multer": "1.4.3", "mustache": "4.2.0", - "node-red-admin": "^2.2.0", + "node-red-admin": "^2.2.1", "nopt": "5.0.0", "oauth2orize": "1.11.0", "on-headers": "1.0.2", - "passport": "0.4.1", + "passport": "0.5.0", "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", "raw-body": "2.4.1", "semver": "7.3.5", "tar": "6.1.11", "tough-cookie": "4.0.0", - "uglify-js": "3.14.1", + "uglify-js": "3.14.2", "uuid": "8.3.2", "ws": "7.5.1", "xml2js": "0.4.23" @@ -84,7 +84,7 @@ "bcrypt": "5.0.1" }, "devDependencies": { - "dompurify": "2.3.1", + "dompurify": "2.3.3", "grunt": "1.4.1", "grunt-chmod": "~1.1.1", "grunt-cli": "~1.4.3", @@ -93,7 +93,7 @@ "grunt-contrib-compress": "2.0.0", "grunt-contrib-concat": "~1.0.1", "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-jshint": "3.0.0", + "grunt-contrib-jshint": "3.1.1", "grunt-contrib-uglify": "5.0.1", "grunt-contrib-watch": "~1.1.0", "grunt-jsdoc": "2.4.1", @@ -104,16 +104,16 @@ "grunt-sass": "~3.1.0", "grunt-simple-mocha": "~0.4.1", "grunt-simple-nyc": "^3.0.1", - "i18next-http-backend": "1.2.6", + "i18next-http-backend": "1.3.1", "jquery-i18next": "1.2.1", "jsdoc-nr-template": "github:node-red/jsdoc-nr-template", - "marked": "2.1.3", + "marked": "3.0.7", "minami": "1.2.3", - "mocha": "9.1.1", + "mocha": "9.1.2", "node-red-node-test-helper": "^0.2.7", - "nodemon": "2.0.12", + "nodemon": "2.0.13", "proxy": "^1.0.2", - "sass": "1.39.0", + "sass": "1.43.2", "should": "13.2.3", "sinon": "11.1.2", "stoppable": "^1.1.0", diff --git a/packages/node_modules/@node-red/editor-api/lib/auth/index.js b/packages/node_modules/@node-red/editor-api/lib/auth/index.js index 5552301d0..f32f6d0d6 100644 --- a/packages/node_modules/@node-red/editor-api/lib/auth/index.js +++ b/packages/node_modules/@node-red/editor-api/lib/auth/index.js @@ -199,8 +199,12 @@ function genericStrategy(adminApp,strategy) { passport.use(new strategy.strategy(options, verify)); adminApp.get('/auth/strategy', - passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }), - completeGenerateStrategyAuth + passport.authenticate(strategy.name, {session:false, + failureMessage: true, + failureRedirect: settings.httpAdminRoot + }), + completeGenerateStrategyAuth, + handleStrategyError ); var callbackMethodFunc = adminApp.get; @@ -208,8 +212,13 @@ function genericStrategy(adminApp,strategy) { callbackMethodFunc = adminApp.post; } callbackMethodFunc.call(adminApp,'/auth/strategy/callback', - passport.authenticate(strategy.name, {session:false, failureRedirect: settings.httpAdminRoot }), - completeGenerateStrategyAuth + passport.authenticate(strategy.name, { + session:false, + failureMessage: true, + failureRedirect: settings.httpAdminRoot + }), + completeGenerateStrategyAuth, + handleStrategyError ); } @@ -219,6 +228,13 @@ function completeGenerateStrategyAuth(req,res) { // Successful authentication, redirect home. res.redirect(settings.httpAdminRoot + '?access_token='+tokens.accessToken); } +function handleStrategyError(err, req, res, next) { + if (res.headersSent) { + return next(err) + } + log.audit({event: "auth.login.fail.oauth",error:err.toString()}); + res.redirect(settings.httpAdminRoot + '?session_message='+err.toString()); +} module.exports = { init: init, diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js index 3e7b0de87..6d1c62e11 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/sshkeys.js @@ -18,14 +18,6 @@ var apiUtils = require("../util"); var express = require("express"); var runtimeAPI; -function getUsername(userObj) { - var username = '__default'; - if ( userObj && userObj.name ) { - username = userObj.name; - } - return username; -} - module.exports = { init: function(_runtimeAPI) { runtimeAPI = _runtimeAPI; diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js index 2651660bf..b3aa0be83 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/theme.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/theme.js @@ -24,15 +24,18 @@ var defaultContext = { page: { title: "Node-RED", favicon: "favicon.ico", - tabicon: "red/images/node-red-icon-black.svg" + tabicon: { + icon: "red/images/node-red-icon-black.svg", + colour: "#8f0000" + } }, header: { title: "Node-RED", image: "red/images/node-red.svg" }, asset: { - red: (process.env.NODE_ENV == "development")? "red/red.js":"red/red.min.js", - main: (process.env.NODE_ENV == "development")? "red/main.js":"red/main.min.js", + red: "red/red.min.js", + main: "red/main.min.js", vendorMonaco: "" } }; @@ -74,7 +77,7 @@ function serveFilesFromTheme(themeValue, themeApp, directory, baseDirectory) { let fullPath = array[i]; if (baseDirectory) { fullPath = path.resolve(baseDirectory,array[i]); - if (fullPath.indexOf(baseDirectory) !== 0) { + if (fullPath.indexOf(path.resolve(baseDirectory)) !== 0) { continue; } } @@ -91,6 +94,10 @@ module.exports = { init: function(settings, _runtimeAPI) { runtimeAPI = _runtimeAPI; themeContext = clone(defaultContext); + if (process.env.NODE_ENV == "development") { + themeContext.asset.red = "red/red.js"; + themeContext.asset.main = "red/main.js"; + } themeSettings = null; theme = settings.editorTheme || {}; themeContext.asset.vendorMonaco = ((theme.codeEditor || {}).lib === "monaco") ? "vendor/monaco/monaco-bootstrap.js" : ""; @@ -123,9 +130,13 @@ module.exports = { } if (theme.page.tabicon) { - url = serveFile(themeApp,"/tabicon/",theme.page.tabicon) + let icon = theme.page.tabicon.icon || theme.page.tabicon + url = serveFile(themeApp,"/tabicon/", icon) if (url) { - themeContext.page.tabicon = url; + themeContext.page.tabicon.icon = url; + } + if (theme.page.tabicon.colour) { + themeContext.page.tabicon.colour = theme.page.tabicon.colour } } @@ -246,6 +257,9 @@ module.exports = { theme.page = theme.page || {_:{}} theme.page._.scripts = scriptFiles.concat(theme.page._.scripts || []) } + if(theme.codeEditor) { + theme.codeEditor.options = Object.assign({}, themePlugin.monacoOptions, theme.codeEditor.options); + } } activeThemeInitialised = true; } diff --git a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js index 5fff30725..998816f5e 100644 --- a/packages/node_modules/@node-red/editor-api/lib/editor/ui.js +++ b/packages/node_modules/@node-red/editor-api/lib/editor/ui.js @@ -91,7 +91,16 @@ module.exports = { }, editor: async function(req,res) { - res.send(Mustache.render(editorTemplate,await theme.context())); + + let sessionMessages; + if (req.session && req.session.messages) { + sessionMessages = JSON.stringify(req.session.messages); + delete req.session.messages + } + res.send(Mustache.render(editorTemplate,{ + sessionMessages, + ...await theme.context() + })); }, editorResources: express.static(path.join(editorClientDir,'public')) }; diff --git a/packages/node_modules/@node-red/editor-api/package.json b/packages/node_modules/@node-red/editor-api/package.json index 73f512538..fcadd851d 100644 --- a/packages/node_modules/@node-red/editor-api/package.json +++ b/packages/node_modules/@node-red/editor-api/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-api", - "version": "2.0.6", + "version": "2.1.0", "license": "Apache-2.0", "main": "./lib/index.js", "repository": { @@ -16,8 +16,8 @@ } ], "dependencies": { - "@node-red/util": "2.0.6", - "@node-red/editor-client": "2.0.6", + "@node-red/util": "2.1.0", + "@node-red/editor-client": "2.1.0", "bcryptjs": "2.4.3", "body-parser": "1.19.0", "clone": "2.1.2", @@ -31,7 +31,7 @@ "oauth2orize": "1.11.0", "passport-http-bearer": "1.0.1", "passport-oauth2-client-password": "0.1.2", - "passport": "0.4.1", + "passport": "0.5.0", "ws": "7.5.1" }, "optionalDependencies": { diff --git a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json index 3f1e32588..0e60d17e3 100755 --- a/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json +++ b/packages/node_modules/@node-red/editor-client/locales/en-US/editor.json @@ -53,8 +53,15 @@ "confirmDelete": "Confirm delete", "delete": "Are you sure you want to delete '__label__'?", "dropFlowHere": "Drop the flow here", - "addFlow": "Add Flow", - "listFlows": "List Flows", + "addFlow": "Add flow", + "addFlowToRight": "Add flow to the right", + "hideFlow": "Hide flow", + "hideOtherFlows": "Hide other flows", + "showAllFlows": "Show all flows", + "hideAllFlows": "Hide all flows", + "showLastHiddenFlow": "Show last hidden flow", + "listFlows": "List flows", + "listSubflows": "List subflows", "status": "Status", "enabled": "Enabled", "disabled":"Disabled", @@ -105,6 +112,7 @@ "editPalette":"Manage palette", "other": "Other", "showTips": "Show tips", + "showWelcomeTours": "Show guided tours for new versions", "help": "Node-RED website", "projects": "Projects", "projects-new": "New", @@ -116,7 +124,20 @@ "groupSelection": "Group selection", "ungroupSelection": "Ungroup selection", "groupMergeSelection": "Merge selection", - "groupRemoveSelection": "Remove from group" + "groupRemoveSelection": "Remove from group", + "arrange":"Arrange", + "alignLeft":"Align to left", + "alignCenter":"Align to center", + "alignRight":"Align to right", + "alignTop":"Align to top", + "alignMiddle":"Align to middle", + "alignBottom":"Align to bottom", + "distributeHorizontally":"Distribute horizontally", + "distributeVertically":"Distribute vertically", + "moveToBack":"Move to back", + "moveToFront":"Move to front", + "moveBackwards":"Move backwards", + "moveForwards":"Move forwards" } }, "actions": { @@ -450,8 +471,9 @@ "unassigned": "Unassigned", "global": "global", "workspace": "workspace", - "selectAll": "Select all nodes", - "selectAllConnected": "Select all connected nodes", + "selectAll": "Select all", + "selectNone": "Select none", + "selectAllConnected": "Select connected", "addRemoveNode": "Add/remove node from selection", "editSelected": "Edit selected node", "deleteSelected": "Delete selected nodes or link", @@ -464,7 +486,10 @@ "copyNode": "Copy selected nodes", "cutNode": "Cut selected nodes", "pasteNode": "Paste nodes", - "undoChange": "Undo the last change performed", + "copyGroupStyle": "Copy group style", + "pasteGroupStyle": "Paste group style", + "undoChange": "Undo", + "redoChange": "Redo", "searchBox": "Open search box", "managePalette": "Manage palette", "actionList":"Action list" @@ -519,7 +544,8 @@ "nodeEnabled_plural": "Nodes enabled:", "nodeDisabled": "Node disabled:", "nodeDisabled_plural": "Nodes disabled:", - "nodeUpgraded": "Node module __module__ upgraded to version __version__" + "nodeUpgraded": "Node module __module__ upgraded to version __version__", + "unknownNodeRegistered": "Error loading node: " }, "editor": { "title": "Manage palette", @@ -1108,6 +1134,10 @@ "preview": "UI Preview", "defaultValue": "Default value" }, + "tourGuide": { + "start": "Start", + "next": "Next" + }, "languages" : { "de": "German", "en-US": "English", diff --git a/packages/node_modules/@node-red/editor-client/package.json b/packages/node_modules/@node-red/editor-client/package.json index 932acedf7..04fe24fee 100644 --- a/packages/node_modules/@node-red/editor-client/package.json +++ b/packages/node_modules/@node-red/editor-client/package.json @@ -1,6 +1,6 @@ { "name": "@node-red/editor-client", - "version": "2.0.6", + "version": "2.1.0", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/packages/node_modules/@node-red/editor-client/src/images/node-red-256.svg b/packages/node_modules/@node-red/editor-client/src/images/node-red-256.svg new file mode 100644 index 000000000..a8bb17861 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/images/node-red-256.svg @@ -0,0 +1 @@ + diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index 338d955e1..256500200 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -558,11 +558,22 @@ RED.history = (function() { } else if (ev.t == "reorder") { inverseEv = { t: 'reorder', - order: RED.nodes.getWorkspaceOrder(), dirty: RED.nodes.dirty() }; - if (ev.order) { - RED.workspaces.order(ev.order); + if (ev.workspaces) { + inverseEv.workspaces = { + from: ev.workspaces.to, + to: ev.workspaces.from + } + RED.workspaces.order(ev.workspaces.from); + } + if (ev.nodes) { + inverseEv.nodes = { + z: ev.nodes.z, + from: ev.nodes.to, + to: ev.nodes.from + } + RED.nodes.setNodeOrder(ev.nodes.z,ev.nodes.from); } } else if (ev.t == "createGroup") { inverseEv = { @@ -658,6 +669,8 @@ RED.history = (function() { push: function(ev) { undoHistory.push(ev); redoHistory = []; + RED.menu.setDisabled("menu-item-edit-undo", false); + RED.menu.setDisabled("menu-item-edit-redo", true); }, pop: function() { var ev = undoHistory.pop(); @@ -665,6 +678,8 @@ RED.history = (function() { if (rev) { redoHistory.push(rev); } + RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0); + RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0); }, peek: function() { return undoHistory[undoHistory.length-1]; @@ -672,6 +687,8 @@ RED.history = (function() { clear: function() { undoHistory = []; redoHistory = []; + RED.menu.setDisabled("menu-item-edit-undo", true); + RED.menu.setDisabled("menu-item-edit-redo", true); }, redo: function() { var ev = redoHistory.pop(); @@ -681,6 +698,8 @@ RED.history = (function() { undoHistory.push(uev); } } + RED.menu.setDisabled("menu-item-edit-undo", undoHistory.length === 0); + RED.menu.setDisabled("menu-item-edit-redo", redoHistory.length === 0); } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/i18n.js b/packages/node_modules/@node-red/editor-client/src/js/i18n.js index b92fc06b9..028321ce9 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/i18n.js +++ b/packages/node_modules/@node-red/editor-client/src/js/i18n.js @@ -27,6 +27,7 @@ RED.i18n = (function() { apiRootUrl = options.apiRootUrl||""; var preferredLanguage = localStorage.getItem("editor-language") || detectLanguage(); var opts = { + compatibilityJSON: 'v3', backend: { loadPath: apiRootUrl+'locales/__ns__?lng=__lng__', }, diff --git a/packages/node_modules/@node-red/editor-client/src/js/keymap.json b/packages/node_modules/@node-red/editor-client/src/js/keymap.json index c323b3e64..766f6bd9f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/keymap.json +++ b/packages/node_modules/@node-red/editor-client/src/js/keymap.json @@ -25,7 +25,9 @@ "ctrl-alt-o": "core:open-project", "ctrl-g v": "core:show-version-control-tab", "ctrl-shift-l": "core:show-event-log", - "ctrl-shift-p":"core:show-action-list" + "ctrl-shift-p":"core:show-action-list", + "alt-w": "core:hide-flow", + "alt-shift-w": "core:show-last-hidden-flow" }, "red-ui-sidebar-node-config": { "backspace": "core:delete-config-selection", @@ -77,6 +79,15 @@ "right": "core:go-to-nearest-node-on-right", "left": "core:go-to-nearest-node-on-left", "up": "core:go-to-nearest-node-above", - "down": "core:go-to-nearest-node-below" + "down": "core:go-to-nearest-node-below", + "alt-a g": "core:align-selection-to-grid", + "alt-a l": "core:align-selection-to-left", + "alt-a r": "core:align-selection-to-right", + "alt-a t": "core:align-selection-to-top", + "alt-a b": "core:align-selection-to-bottom", + "alt-a m": "core:align-selection-to-middle", + "alt-a c": "core:align-selection-to-center", + "alt-a h": "core:distribute-selection-horizontally", + "alt-a v": "core:distribute-selection-vertically" } } diff --git a/packages/node_modules/@node-red/editor-client/src/js/nodes.js b/packages/node_modules/@node-red/editor-client/src/js/nodes.js index ee3542eee..76f544396 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/nodes.js +++ b/packages/node_modules/@node-red/editor-client/src/js/nodes.js @@ -16,8 +16,6 @@ RED.nodes = (function() { var node_defs = {}; - var nodes = {}; - var nodeTabMap = {}; var linkTabMap = {}; var configNodes = {}; @@ -41,6 +39,7 @@ RED.nodes = (function() { RED.events.emit("workspace:dirty",{dirty:dirty}); } + // The registry holds information about all node types. var registry = (function() { var moduleList = {}; var nodeList = []; @@ -53,7 +52,8 @@ RED.nodes = (function() { defaults: { label: {value:""}, disabled: {value: false}, - info: {value: ""} + info: {value: ""}, + env: {value: []} } }; @@ -142,9 +142,21 @@ RED.nodes = (function() { RED.events.emit("registry:node-set-disabled",ns); }, registerNodeType: function(nt,def) { - nodeDefinitions[nt] = def; - def.type = nt; if (nt.substring(0,8) != "subflow:") { + if (!nodeSets[typeToId[nt]]) { + var error = ""; + var fullType = nt; + if (RED._loadingModule) { + fullType = "["+RED._loadingModule+"] "+nt; + if (nodeSets[RED._loadingModule]) { + error = nodeSets[RED._loadingModule].err || ""; + } else { + error = "Unknown error"; + } + } + RED.notify(RED._("palette.event.unknownNodeRegistered",{type:fullType, error:error}), "error"); + return; + } def.set = nodeSets[typeToId[nt]]; nodeSets[typeToId[nt]].added = true; nodeSets[typeToId[nt]].enabled = true; @@ -167,9 +179,13 @@ RED.nodes = (function() { } return result; } - // TODO: too tightly coupled into palette UI } + + def.type = nt; + nodeDefinitions[nt] = def; + + if (def.defaults) { for (var d in def.defaults) { if (def.defaults.hasOwnProperty(d)) { @@ -209,6 +225,285 @@ RED.nodes = (function() { return exports; })(); + // allNodes holds information about the Flow nodes. + var allNodes = (function() { + var nodes = {}; + var tabMap = {}; + var api = { + addTab: function(id) { + tabMap[id] = []; + }, + hasTab: function(z) { + return tabMap.hasOwnProperty(z) + }, + removeTab: function(id) { + delete tabMap[id]; + }, + addNode: function(n) { + nodes[n.id] = n; + if (tabMap.hasOwnProperty(n.z)) { + tabMap[n.z].push(n); + } else { + console.warn("Node added to unknown tab/subflow:",n); + tabMap["_"] = tabMap["_"] || []; + tabMap["_"].push(n); + } + }, + removeNode: function(n) { + delete nodes[n.id] + if (tabMap.hasOwnProperty(n.z)) { + var i = tabMap[n.z].indexOf(n); + if (i > -1) { + tabMap[n.z].splice(i,1); + } + } + }, + hasNode: function(id) { + return nodes.hasOwnProperty(id); + }, + getNode: function(id) { + return nodes[id] + }, + moveNode: function(n, newZ) { + api.removeNode(n); + n.z = newZ; + api.addNode(n) + }, + moveNodesForwards: function(nodes) { + var result = []; + if (!Array.isArray(nodes)) { + nodes = [nodes] + } + // Can only do this for nodes on the same tab. + // Use nodes[0] to get the z + var tabNodes = tabMap[nodes[0].z]; + var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + var moved = new Set(); + for (var i = tabNodes.length-1; i >= 0; i--) { + if (toMove.size === 0) { + break; + } + var n = tabNodes[i]; + if (toMove.has(n)) { + // This is a node to move. + if (i < tabNodes.length-1 && !moved.has(tabNodes[i+1])) { + // Remove from current position + tabNodes.splice(i,1); + // Add it back one position higher + tabNodes.splice(i+1,0,n); + n._reordered = true; + result.push(n); + } + toMove.delete(n); + moved.add(n); + } + } + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); + } + return result; + }, + moveNodesBackwards: function(nodes) { + var result = []; + if (!Array.isArray(nodes)) { + nodes = [nodes] + } + // Can only do this for nodes on the same tab. + // Use nodes[0] to get the z + var tabNodes = tabMap[nodes[0].z]; + var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + var moved = new Set(); + for (var i = 0; i < tabNodes.length; i++) { + if (toMove.size === 0) { + break; + } + var n = tabNodes[i]; + if (toMove.has(n)) { + // This is a node to move. + if (i > 0 && !moved.has(tabNodes[i-1])) { + // Remove from current position + tabNodes.splice(i,1); + // Add it back one position lower + tabNodes.splice(i-1,0,n); + n._reordered = true; + result.push(n); + } + toMove.delete(n); + moved.add(n); + } + } + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); + } + return result; + }, + moveNodesToFront: function(nodes) { + var result = []; + if (!Array.isArray(nodes)) { + nodes = [nodes] + } + // Can only do this for nodes on the same tab. + // Use nodes[0] to get the z + var tabNodes = tabMap[nodes[0].z]; + var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + var target = tabNodes.length-1; + for (var i = tabNodes.length-1; i >= 0; i--) { + if (toMove.size === 0) { + break; + } + var n = tabNodes[i]; + if (toMove.has(n)) { + // This is a node to move. + if (i < target) { + // Remove from current position + tabNodes.splice(i,1); + tabNodes.splice(target,0,n); + n._reordered = true; + result.push(n); + } + target--; + toMove.delete(n); + } + } + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); + } + return result; + }, + moveNodesToBack: function(nodes) { + var result = []; + if (!Array.isArray(nodes)) { + nodes = [nodes] + } + // Can only do this for nodes on the same tab. + // Use nodes[0] to get the z + var tabNodes = tabMap[nodes[0].z]; + var toMove = new Set(nodes.filter(function(n) { return n.type !== "group" && n.type !== "subflow" })); + var target = 0; + for (var i = 0; i < tabNodes.length; i++) { + if (toMove.size === 0) { + break; + } + var n = tabNodes[i]; + if (toMove.has(n)) { + // This is a node to move. + if (i > target) { + // Remove from current position + tabNodes.splice(i,1); + // Add it back one position lower + tabNodes.splice(target,0,n); + n._reordered = true; + result.push(n); + } + target++; + toMove.delete(n); + } + } + if (result.length > 0) { + RED.events.emit('nodes:reorder',{ + z: nodes[0].z, + nodes: result + }); + } + return result; + }, + getNodes: function(z) { + return tabMap[z]; + }, + clear: function() { + nodes = {}; + tabMap = {}; + }, + eachNode: function(cb) { + var nodeList,i,j; + for (i in subflows) { + if (subflows.hasOwnProperty(i)) { + nodeList = tabMap[i]; + for (j = 0; j < nodeList.length; j++) { + if (cb(nodeList[j]) === false) { + return; + } + } + } + } + for (i = 0; i < workspacesOrder.length; i++) { + nodeList = tabMap[workspacesOrder[i]]; + for (j = 0; j < nodeList.length; j++) { + if (cb(nodeList[j]) === false) { + return; + } + } + } + // Flow nodes that do not have a valid tab/subflow + if (tabMap["_"]) { + nodeList = tabMap["_"]; + for (j = 0; j < nodeList.length; j++) { + if (cb(nodeList[j]) === false) { + return; + } + } + } + }, + filterNodes: function(filter) { + var result = []; + var searchSet = null; + var doZFilter = false; + if (filter.hasOwnProperty("z")) { + if (tabMap.hasOwnProperty(filter.z)) { + searchSet = tabMap[filter.z]; + } else { + doZFilter = true; + } + } + var objectLookup = false; + if (searchSet === null) { + searchSet = Object.keys(nodes); + objectLookup = true; + } + + + for (var n=0;ndiv:not(.node-input-env-container-row)"); - var height = size.height; - // for (var i=0; idiv.node-input-env-container-row"); - // height -= (parseInt(editorRow.css("marginTop"))+parseInt(editorRow.css("marginBottom"))); - $("ol.red-ui-editor-subflow-env-list").editableList('height',height); + if (this.type === 'subflow') { + $("#node-input-env-container").editableList('height',size.height - 80); + } }, set:{ module: "node-red" @@ -618,27 +912,24 @@ RED.nodes = (function() { function removeSubflow(sf) { if (subflows[sf.id]) { delete subflows[sf.id]; - delete nodeTabMap[sf.id]; + allNodes.removeTab(sf.id); registry.removeNodeType("subflow:"+sf.id); RED.events.emit("subflows:remove",sf); } } function subflowContains(sfid,nodeid) { - for (var i in nodes) { - if (nodes.hasOwnProperty(i)) { - var node = nodes[i]; - if (node.z === sfid) { - var m = /^subflow:(.+)$/.exec(node.type); - if (m) { - if (m[1] === nodeid) { - return true; - } else { - var result = subflowContains(m[1],nodeid); - if (result) { - return true; - } - } + var sfNodes = allNodes.getNodes(sfid); + for (var i = 0; i 0) { + node.credentials = credentialSet; + } + } + } return node; } /** @@ -710,7 +1024,7 @@ RED.nodes = (function() { } if (n.type === 'tab') { - return convertWorkspace(n); + return convertWorkspace(n, { credentials: exportCreds }); } var node = {}; node.id = n.id; @@ -739,8 +1053,10 @@ RED.nodes = (function() { } if (exportCreds) { var credentialSet = {}; - if (/^subflow:/.test(node.type) && n.credentials) { - // A subflow instance node can have arbitrary creds + if ((/^subflow:/.test(node.type) || + (node.type === "group")) && + n.credentials) { + // A subflow instance/group node can have arbitrary creds for (var sfCred in n.credentials) { if (n.credentials.hasOwnProperty(sfCred)) { if (!n.credentials._ || @@ -824,8 +1140,8 @@ RED.nodes = (function() { } } if ((!n._def.defaults || !n._def.defaults.hasOwnProperty("l")) && n.hasOwnProperty('l')) { - var isLink = /^link (in|out)$/.test(node.type); - if (isLink == n.l) { + var showLabel = n._def.hasOwnProperty("showLabel")?n._def.showLabel:true; + if (showLabel != n.l) { node.l = n.l; } } @@ -934,11 +1250,15 @@ RED.nodes = (function() { function createExportableSubflow(id) { var sf = getSubflow(id); - var nodeSet = [sf]; - var sfNodeIds = Object.keys(nodeTabMap[sf.id]||{}); - for (var i=0, l=sfNodeIds.length; i 0) { + RED.workspaces.show(RED.nodes.getWorkspaceOrder()[0]) + } } catch(err) { console.warn(err); RED.notify( @@ -267,7 +271,7 @@ var RED = (function() { }); } - function completeLoad() { + function completeLoad(showProjectWelcome) { var persistentNotifications = {}; RED.comms.subscribe("notification/#",function(topic,msg) { var parts = topic.split("/"); @@ -477,8 +481,17 @@ var RED = (function() { RED.nodes.addNodeSet(m); addedTypes = addedTypes.concat(m.types); RED.i18n.loadNodeCatalog(id, function() { - $.get('nodes/'+id, function(data) { - appendNodeConfig(data); + var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage(); + $.ajax({ + headers: { + "Accept":"text/html", + "Accept-Language": lang + }, + cache: false, + url: 'nodes/'+id, + success: function(data) { + appendNodeConfig(data); + } }); }); }); @@ -505,10 +518,19 @@ var RED = (function() { typeList = "
  • "+msg.types.map(RED.utils.sanitize).join("
  • ")+"
"; RED.notify(RED._("palette.event.nodeEnabled", {count:msg.types.length})+typeList,"success"); } else { - $.get('nodes/'+msg.id, function(data) { - appendNodeConfig(data); - typeList = "
  • "+msg.types.map(RED.utils.sanitize).join("
  • ")+"
"; - RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success"); + var lang = localStorage.getItem("editor-language")||RED.i18n.detectLanguage(); + $.ajax({ + headers: { + "Accept":"text/html", + "Accept-Language": lang + }, + cache: false, + url: 'nodes/'+msg.id, + success: function(data) { + appendNodeConfig(data); + typeList = "
  • "+msg.types.map(RED.utils.sanitize).join("
  • ")+"
"; + RED.notify(RED._("palette.event.nodeAdded", {count:msg.types.length})+typeList,"success"); + } }); } } @@ -535,19 +557,24 @@ var RED = (function() { setTimeout(function() { loader.end(); + checkFirstRun(function() { + if (showProjectWelcome) { + RED.projects.showStartup(); + } + }); },100); } - function showAbout() { - $.get('red/about', function(data) { - // data will be strictly markdown. Any HTML should be escaped. - data = RED.utils.sanitize(data); - var aboutHeader = '
'+ - ''+ - '
'; - - RED.sidebar.help.set(aboutHeader+RED.utils.renderMarkdown(data)); - }); + function checkFirstRun(done) { + if (RED.settings.theme("tours") === false) { + done(); + return; + } + if (!RED.settings.get("editor.view.view-show-welcome-tours", true)) { + done(); + return; + } + RED.actions.invoke("core:show-welcome-tour", RED.settings.get("editor.tours.welcome"), done); } function buildMainMenu() { @@ -559,6 +586,22 @@ var RED = (function() { {id:"menu-item-projects-settings",label:RED._("menu.label.projects-settings"),disabled:false,onselect:"core:show-project-settings"} ]}); } + menuOptions.push({id:"menu-item-edit-menu", label:"Edit", options: [ + {id: "menu-item-edit-undo", label:RED._("keyboard.undoChange"), disabled: true, onselect: "core:undo"}, + {id: "menu-item-edit-redo", label:RED._("keyboard.redoChange"), disabled: true, onselect: "core:redo"}, + null, + {id: "menu-item-edit-cut", label:RED._("keyboard.cutNode"), onselect: "core:cut-selection-to-internal-clipboard"}, + {id: "menu-item-edit-copy", label:RED._("keyboard.copyNode"), onselect: "core:copy-selection-to-internal-clipboard"}, + {id: "menu-item-edit-paste", label:RED._("keyboard.pasteNode"), disabled: true, onselect: "core:paste-from-internal-clipboard"}, + null, + {id: "menu-item-edit-copy-group-style", label:RED._("keyboard.copyGroupStyle"), onselect: "core:copy-group-style"}, + {id: "menu-item-edit-paste-group-style", label:RED._("keyboard.pasteGroupStyle"), disabled: true, onselect: "core:paste-group-style"}, + null, + {id: "menu-item-edit-select-all", label:RED._("keyboard.selectAll"), onselect: "core:select-all-nodes"}, + {id: "menu-item-edit-select-connected", label:RED._("keyboard.selectAllConnected"), onselect: "core:select-connected-nodes"}, + {id: "menu-item-edit-select-none", label:RED._("keyboard.selectNone"), onselect: "core:select-none"} + ]}); + menuOptions.push({id:"menu-item-view-menu",label:RED._("menu.label.view.view"),options:[ {id:"menu-item-palette",label:RED._("menu.label.palette.show"),toggle:true,onselect:"core:toggle-palette", selected: true}, {id:"menu-item-sidebar",label:RED._("menu.label.sidebar.show"),toggle:true,onselect:"core:toggle-sidebar", selected: true}, @@ -566,6 +609,25 @@ var RED = (function() { {id:"menu-item-action-list",label:RED._("keyboard.actionList"),onselect:"core:show-action-list"}, null ]}); + + menuOptions.push({id:"menu-item-arrange-menu", label:RED._("menu.label.arrange"), options: [ + {id: "menu-item-view-tools-move-to-back", label:RED._("menu.label.moveToBack"), disabled: true, onselect: "core:move-selection-to-back"}, + {id: "menu-item-view-tools-move-to-front", label:RED._("menu.label.moveToFront"), disabled: true, onselect: "core:move-selection-to-front"}, + {id: "menu-item-view-tools-move-backwards", label:RED._("menu.label.moveBackwards"), disabled: true, onselect: "core:move-selection-backwards"}, + {id: "menu-item-view-tools-move-forwards", label:RED._("menu.label.moveForwards"), disabled: true, onselect: "core:move-selection-forwards"}, + null, + {id: "menu-item-view-tools-align-left", label:RED._("menu.label.alignLeft"), disabled: true, onselect: "core:align-selection-to-left"}, + {id: "menu-item-view-tools-align-center", label:RED._("menu.label.alignCenter"), disabled: true, onselect: "core:align-selection-to-center"}, + {id: "menu-item-view-tools-align-right", label:RED._("menu.label.alignRight"), disabled: true, onselect: "core:align-selection-to-right"}, + null, + {id: "menu-item-view-tools-align-top", label:RED._("menu.label.alignTop"), disabled: true, onselect: "core:align-selection-to-top"}, + {id: "menu-item-view-tools-align-middle", label:RED._("menu.label.alignMiddle"), disabled: true, onselect: "core:align-selection-to-middle"}, + {id: "menu-item-view-tools-align-bottom", label:RED._("menu.label.alignBottom"), disabled: true, onselect: "core:align-selection-to-bottom"}, + null, + {id: "menu-item-view-tools-distribute-horizontally", label:RED._("menu.label.distributeHorizontally"), disabled: true, onselect: "core:distribute-selection-horizontally"}, + {id: "menu-item-view-tools-distribute-veritcally", label:RED._("menu.label.distributeVertically"), disabled: true, onselect: "core:distribute-selection-vertically"} + ]}); + menuOptions.push(null); if (RED.settings.theme("menu.menu-item-import-library", true)) { menuOptions.push({id: "menu-item-import", label: RED._("menu.label.import"), onselect: "core:show-import-dialog"}); @@ -626,7 +688,6 @@ var RED = (function() { RED.user.init(); RED.notifications.init(); RED.library.init(); - RED.keyboard.init(); RED.palette.init(); RED.eventLog.init(); @@ -655,16 +716,13 @@ var RED = (function() { RED.deploy.init(RED.settings.theme("deployButton",null)); - buildMainMenu(); + RED.keyboard.init(buildMainMenu); RED.nodes.init(); RED.comms.connect(); $("#red-ui-main-container").show(); - - RED.actions.add("core:show-about", showAbout); - loadPluginList(); } diff --git a/packages/node_modules/@node-red/editor-client/src/js/settings.js b/packages/node_modules/@node-red/editor-client/src/js/settings.js index 39f372974..1c6513c43 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/settings.js +++ b/packages/node_modules/@node-red/editor-client/src/js/settings.js @@ -19,7 +19,6 @@ RED.settings = (function () { var loadedSettings = {}; var userSettings = {}; - var settingsDirty = false; var pendingSave; var hasLocalStorage = function () { @@ -220,6 +219,16 @@ RED.settings = (function () { return defaultValue; } } + function getLocal(key) { + return localStorage.getItem(key) + } + function setLocal(key, value) { + localStorage.setItem(key, value); + } + function removeLocal(key) { + localStorage.removeItem(key) + } + return { init: init, @@ -228,6 +237,9 @@ RED.settings = (function () { set: set, get: get, remove: remove, - theme: theme + theme: theme, + setLocal: setLocal, + getLocal: getLocal, + removeLocal: removeLocal } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/actions.js b/packages/node_modules/@node-red/editor-client/src/js/ui/actions.js index 8ebc97cda..c7c5dd950 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/actions.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/actions.js @@ -4,6 +4,12 @@ RED.actions = (function() { } function addAction(name,handler) { + if (typeof handler !== 'function') { + throw new Error("Action handler not a function"); + } + if (actions[name]) { + throw new Error("Cannot override existing action"); + } actions[name] = handler; } function removeAction(name) { @@ -12,9 +18,11 @@ RED.actions = (function() { function getAction(name) { return actions[name]; } - function invokeAction(name,args) { + function invokeAction() { + var args = Array.prototype.slice.call(arguments); + var name = args.shift(); if (actions.hasOwnProperty(name)) { - actions[name](args); + actions[name].apply(null, args); } } function listActions() { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index 485b43d98..b38017c60 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -498,6 +498,13 @@ RED.clipboard = (function() { $("#red-ui-clipboard-dialog-import-text").on("keyup", validateImport); $("#red-ui-clipboard-dialog-import-text").on('paste',function() { setTimeout(validateImport,10)}); + if (RED.workspaces.active() === 0) { + $("#red-ui-clipboard-dialog-import-opt-current").addClass('disabled').removeClass("selected"); + $("#red-ui-clipboard-dialog-import-opt-new").addClass("selected"); + } else { + $("#red-ui-clipboard-dialog-import-opt-current").removeClass('disabled').addClass("selected"); + $("#red-ui-clipboard-dialog-import-opt-new").removeClass("selected"); + } $("#red-ui-clipboard-dialog-import-opt > a").on("click", function(evt) { evt.preventDefault(); if ($(this).hasClass('disabled') || $(this).hasClass('selected')) { @@ -611,9 +618,6 @@ RED.clipboard = (function() { activeLibraries[tabId] = browser; }) - - - $("#red-ui-clipboard-dialog-tab-library-name").on("keyup", validateExportFilename); $("#red-ui-clipboard-dialog-tab-library-name").on('paste',function() { setTimeout(validateExportFilename,10)}); $("#red-ui-clipboard-dialog-export").button("enable"); @@ -636,7 +640,6 @@ RED.clipboard = (function() { label: RED._("editor.types.json") }); - var previewList = $("#red-ui-clipboard-dialog-export-tab-clipboard-preview-list").css({position:"absolute",top:0,right:0,bottom:0,left:0}).treeList({ data: [] }) @@ -738,16 +741,22 @@ RED.clipboard = (function() { $("#red-ui-clipboard-dialog-export").hide(); $("#red-ui-clipboard-dialog-import-conflict").hide(); - var selection = RED.workspaces.selection(); - if (selection.length > 0) { - $("#red-ui-clipboard-dialog-export-rng-selected").trigger("click"); + if (RED.workspaces.active() === 0) { + $("#red-ui-clipboard-dialog-export-rng-selected").addClass('disabled').removeClass('selected'); + $("#red-ui-clipboard-dialog-export-rng-flow").addClass('disabled').removeClass('selected'); + $("#red-ui-clipboard-dialog-export-rng-full").trigger("click"); } else { - selection = RED.view.selection(); - if (selection.nodes) { + var selection = RED.workspaces.selection(); + if (selection.length > 0) { $("#red-ui-clipboard-dialog-export-rng-selected").trigger("click"); } else { - $("#red-ui-clipboard-dialog-export-rng-selected").addClass('disabled').removeClass('selected'); - $("#red-ui-clipboard-dialog-export-rng-flow").trigger("click"); + selection = RED.view.selection(); + if (selection.nodes) { + $("#red-ui-clipboard-dialog-export-rng-selected").trigger("click"); + } else { + $("#red-ui-clipboard-dialog-export-rng-selected").addClass('disabled').removeClass('selected'); + $("#red-ui-clipboard-dialog-export-rng-flow").trigger("click"); + } } } if (format === "red-ui-clipboard-dialog-export-fmt-full") { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js new file mode 100644 index 000000000..6f1ac1aa1 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js @@ -0,0 +1,115 @@ +(function($) { + +/** + * Attach to an to provide auto-complete + * + * $("#node-red-text").autoComplete({ + * search: function(value) { return ['a','b','c'] } + * }) + * + * options: + * + * search : function(value, [done]) + * A function that is passed the current contents of the input whenever + * it changes. + * The function must either return auto-complete options, or pass them + * to the optional 'done' parameter. + * If the function signature includes 'done', it must be used + * + * The auto-complete options should be an array of objects in the form: + * { + * value: String : the value to insert if selected + * label: String|DOM Element : the label to display in the dropdown. + * } + * + */ + + $.widget( "nodered.autoComplete", { + _create: function() { + var that = this; + this.completionMenuShown = false; + this.options.search = this.options.search || function() { return [] } + this.element.addClass("red-ui-autoComplete") + this.element.on("keydown.red-ui-autoComplete", function(evt) { + if ((evt.keyCode === 13 || evt.keyCode === 9) && that.completionMenuShown) { + var opts = that.menu.options(); + that.element.val(opts[0].value); + that.menu.hide(); + evt.preventDefault(); + } + }) + this.element.on("keyup.red-ui-autoComplete", function(evt) { + if (evt.keyCode === 13 || evt.keyCode === 9 || evt.keyCode === 27) { + // ENTER / TAB / ESCAPE + return + } + if (evt.keyCode === 8 || evt.keyCode === 46) { + // Delete/Backspace + if (!that.completionMenuShown) { + return; + } + } + that._updateCompletions(this.value); + }); + }, + _showCompletionMenu: function(completions) { + if (this.completionMenuShown) { + return; + } + this.menu = RED.popover.menu({ + tabSelect: true, + width: 300, + maxHeight: 200, + class: "red-ui-autoComplete-container", + options: completions, + onselect: (opt) => { this.element.val(opt.value); this.element.focus() }, + onclose: () => { this.completionMenuShown = false; delete this.menu; this.element.focus()} + }); + this.menu.show({ + target: this.element + }) + this.completionMenuShown = true; + }, + _updateCompletions: function(val) { + var that = this; + if (val.trim() === "") { + if (this.completionMenuShown) { + this.menu.hide(); + } + return; + } + function displayResults(completions,requestId) { + if (requestId && requestId !== that.pendingRequest) { + // This request has been superseded + return + } + if (!completions || completions.length === 0) { + if (that.completionMenuShown) { + that.menu.hide(); + } + return + } + if (that.completionMenuShown) { + that.menu.options(completions); + } else { + that._showCompletionMenu(completions); + } + } + if (this.options.search.length === 2) { + var requestId = 1+Math.floor(Math.random()*10000); + this.pendingRequest = requestId; + this.options.search(val,function(completions) { displayResults(completions,requestId);}) + } else { + displayResults(this.options.search(val)) + } + }, + _destroy: function() { + this.element.removeClass("red-ui-autoComplete") + this.element.off("keydown.red-ui-autoComplete") + this.element.off("keyup.red-ui-autoComplete") + if (this.completionMenuShown) { + this.menu.hide(); + } + } + }); +})(jQuery); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js index 7ebe659c9..417189b33 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/menu.js @@ -82,12 +82,19 @@ RED.menu = (function() { linkContent += ''+opt.label+''+ ''+opt.sublabel+'' } else { - linkContent += ''+opt.label+'' + linkContent += ''+opt.label+'' } linkContent += ''; var link = $(linkContent).appendTo(item); + opt.link = link; + if (typeof opt.onselect === 'string') { + var shortcut = RED.keyboard.getShortcut(opt.onselect); + if (shortcut && shortcut.key) { + opt.shortcutSpan = $(''+RED.keyboard.formatKey(shortcut.key, true)+'').appendTo(link.find(".red-ui-menu-label")); + } + } menuItems[opt.id] = opt; @@ -276,6 +283,22 @@ RED.menu = (function() { } } + function refreshShortcuts() { + for (var id in menuItems) { + if (menuItems.hasOwnProperty(id)) { + var opt = menuItems[id]; + if (typeof opt.onselect === "string" && opt.shortcutSpan) { + opt.shortcutSpan.remove(); + delete opt.shortcutSpan; + var shortcut = RED.keyboard.getShortcut(opt.onselect); + if (shortcut && shortcut.key) { + opt.shortcutSpan = $(''+RED.keyboard.formatKey(shortcut.key, true)+'').appendTo(opt.link.find(".red-ui-menu-label")); + } + } + } + } + } + return { init: createMenu, setSelected: setSelected, @@ -284,6 +307,7 @@ RED.menu = (function() { setDisabled: setDisabled, addItem: addItem, removeItem: removeItem, - setAction: setAction + setAction: setAction, + refreshShortcuts: refreshShortcuts } })(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index 8d7553069..f060af37d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js @@ -13,24 +13,138 @@ * See the License for the specific language governing permissions and * limitations under the License. **/ +/* + * RED.popover.create(options) - create a popover callout box + * RED.popover.tooltip(target,content, action) - add a tooltip to an element + * RED.popover.menu(options) - create a dropdown menu + * RED.popover.panel(content) - create a dropdown container element + */ + + +/* + * RED.popover.create(options) + * + * options + * - target : DOM element - the element to target with the popover + * - direction : string - position of the popover relative to target + * 'top', 'right'(default), 'bottom', 'left', 'inset-[top,right,bottom,left]' + * - trigger : string - what triggers the popover to be displayed + * 'hover' - display when hovering the target + * 'click' - display when target is clicked + * 'modal' - hmm not sure, need to find where we use that mode + * - content : string|function - contents of the popover. If a string, handled + * as raw HTML, so take care. + * If a function, can return a String to be added + * as text (not HTML), or a DOM element to append + * - delay : object - sets show/hide delays after mouseover/out events + * { show: 750, hide: 50 } + * - autoClose : number - delay before closing the popover in some cases + * if trigger is click - delay after mouseout + * else if trigger not hover/modal - delay after showing + * - width : number - width of popover, default 'auto' + * - maxWidth : number - max width of popover, default 'auto' + * - size : string - scale of popover. 'default', 'small' + * - offset : number - px offset from target + * - tooltip : boolean - if true, clicking on popover closes it + * - class : string - optional css class to apply to popover + * - interactive : if trigger is 'hover' and this is set to true, allow the mouse + * to move over the popover without hiding it. + * + * Returns the popover object with the following properties/functions: + * properties: + * - element : DOM element - the popover dom element + * functions: + * - setContent(content) - change the popover content. This only works if the + * popover is not currently displayed. It does not + * change the content of a visible popover. + * - open(instant) - show the popover. If 'instant' is true, don't fade in + * - close(instant) - hide the popover. If 'instant' is true, don't fade out + * - move(options) - move the popover. The options parameter can take many + * of the options detailed above including: + * target,direction,content,width,offset + * Other settings probably won't work because we haven't needed to change them + */ + +/* + * RED.popover.tooltip(target,content, action) + * + * - target : DOM element - the element to apply the tooltip to + * - content : string - the text of the tooltip + * - action : string - *optional* the name of an Action this tooltip is tied to + * For example, it 'target' is a button that triggers a particular action. + * The tooltip will include the keyboard shortcut for the action + * if one is defined + * + */ + +/* + * RED.popover.menu(options) + * + * options + * - options : array - list of menu options - see below for format + * - width : number - width of the menu. Default: 'auto' + * - class : string - class to apply to the menu container + * - maxHeight : number - maximum height of menu before scrolling items. Default: none + * - onselect : function(item) - called when a menu item is selected, if that item doesn't + * have its own onselect function + * - onclose : function(cancelled) - called when the menu is closed + * - disposeOnClose : boolean - by default, the menu is discarded when it closes + * and mustbe rebuilt to redisplay. Setting this to 'false' + * keeps the menu on the DOM so it can be shown again. + * + * Menu Options array: + * [ + * label : string|DOM element - the label of the item. Can be custom DOM element + * onselect : function - called when the item is selected + * ] + * + * Returns the menu object with the following functions: + * + * - options([menuItems]) - if menuItems is undefined, returns the current items. + * otherwise, sets the current menu items + * - show(opts) - shows the menu. `opts` is an object of options. See RED.popover.panel.show(opts) + * for the full list of options. In most scenarios, this just needs: + * - target : DOM element - the element to display the menu below + * - hide(cancelled) - hide the menu + */ + +/* + * RED.popover.panel(content) + * Create a UI panel that can be displayed relative to any target element. + * Handles auto-closing when mouse clicks outside the panel + * + * - 'content' - DOM element to display in the panel + * + * Returns the panel object with the following functions: + * + * properties: + * - container : DOM element - the panel element + * + * functions: + * - show(opts) - show the panel. + * opts: + * - onclose : function - called when the panel closes + * - closeButton : DOM element - if the panel is closeable by a click of a button, + * by providing a reference to it here, we can + * handle the events properly to hide the panel + * - target : DOM element - the element to display the panel relative to + * - align : string - should the panel align to the left or right edge of target + * default: 'right' + * - offset : Array - px offset to apply from the target. [width, height] + * - dispose : boolean - whether the panel should be removed from DOM when hidden + * default: true + * - hide(dispose) - hide the panel. + */ RED.popover = (function() { var deltaSizes = { "default": { - top: 10, - topTop: 30, - leftRight: 17, - leftLeft: 25, - leftBottom: 8, - leftTop: 11 + x: 12, + y: 12 }, "small": { - top: 6, - topTop: 20, - leftRight: 8, - leftLeft: 26, - leftBottom: 8, - leftTop: 9 + x:8, + y:8 } } function createPopover(options) { @@ -41,7 +155,9 @@ RED.popover = (function() { var delay = options.delay || { show: 750, hide: 50 }; var autoClose = options.autoClose; var width = options.width||"auto"; + var maxWidth = options.maxWidth; var size = options.size||"default"; + var popupOffset = options.offset || 0; if (!deltaSizes[size]) { throw new Error("Invalid RED.popover size value:",size); } @@ -49,6 +165,8 @@ RED.popover = (function() { var timer = null; var active; var div; + var contentDiv; + var currentStyle; var openPopup = function(instant) { if (active) { @@ -58,6 +176,10 @@ RED.popover = (function() { return; } div = $('
'); + if (options.class) { + div.addClass(options.class); + } + contentDiv = $('
').appendTo(div); if (size !== "default") { div.addClass("red-ui-popover-size-"+size); } @@ -67,71 +189,23 @@ RED.popover = (function() { return; } if (typeof result === 'string') { - div.text(result); + contentDiv.text(result); } else { - div.append(result); + contentDiv.append(result); } } else { - div.html(content); - } - if (width !== "auto") { - div.width(width); + contentDiv.html(content); } div.appendTo("body"); - var targetPos = target.offset(); - var targetWidth = target.outerWidth(); - var targetHeight = target.outerHeight(); - var divHeight = div.height(); - var divWidth = div.width(); - var paddingRight = 10; + movePopup({target,direction,width,maxWidth}); - var viewportTop = $(window).scrollTop(); - var viewportLeft = $(window).scrollLeft(); - var viewportBottom = viewportTop + $(window).height(); - var viewportRight = viewportLeft + $(window).width(); - var top = 0; - var left = 0; - var d = direction; - if (d === 'right') { - top = targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top; - left = targetPos.left+targetWidth+deltaSizes[size].leftRight; - } else if (d === 'left') { - top = targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top; - left = targetPos.left-deltaSizes[size].leftLeft-divWidth; - } else if (d === 'bottom') { - top = targetPos.top+targetHeight+deltaSizes[size].top; - left = targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftBottom; - if (left < 0) { - d = "right"; - top = targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top; - left = targetPos.left+targetWidth+deltaSizes[size].leftRight; - } else if (left+divWidth+paddingRight > viewportRight) { - d = "left"; - top = targetPos.top+targetHeight/2-divHeight/2-deltaSizes[size].top; - left = targetPos.left-deltaSizes[size].leftLeft-divWidth; - if (top+divHeight+targetHeight/2 + 5 > viewportBottom) { - top -= (top+divHeight+targetHeight/2 - viewportBottom + 5) - } - } else if (top+divHeight > viewportBottom) { - d = 'top'; - top = targetPos.top-deltaSizes[size].topTop-divHeight; - left = targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftTop; - } - } else if (d === 'top') { - top = targetPos.top-deltaSizes[size].topTop-divHeight; - left = targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftTop; - if (top < 0) { - d = 'bottom'; - top = targetPos.top+targetHeight+deltaSizes[size].top; - left = targetPos.left+targetWidth/2-divWidth/2 - deltaSizes[size].leftBottom; - } - } - div.addClass('red-ui-popover-'+d).css({top: top, left: left}); if (existingPopover) { existingPopover.close(true); } - target.data("red-ui-popover",res) + if (options.trigger !== 'manual') { + target.data("red-ui-popover",res) + } if (options.tooltip) { div.on("mousedown", function(evt) { closePopup(true); @@ -161,6 +235,104 @@ RED.popover = (function() { } } } + var movePopup = function(options) { + target = options.target || target; + direction = options.direction || direction || "right"; + popupOffset = options.offset || popupOffset; + var transition = options.transition; + + var width = options.width||"auto"; + div.width(width); + if (options.maxWidth) { + div.css("max-width",options.maxWidth) + } else { + div.css("max-width", 'auto'); + } + + var targetPos = target[0].getBoundingClientRect(); + var targetHeight = targetPos.height; + var targetWidth = targetPos.width; + + var divHeight = div.outerHeight(); + var divWidth = div.outerWidth(); + var paddingRight = 10; + + var viewportTop = $(window).scrollTop(); + var viewportLeft = $(window).scrollLeft(); + var viewportBottom = viewportTop + $(window).height(); + var viewportRight = viewportLeft + $(window).width(); + var top = 0; + var left = 0; + if (direction === 'right') { + top = targetPos.top+targetHeight/2-divHeight/2; + left = targetPos.left+targetWidth+deltaSizes[size].x+popupOffset; + } else if (direction === 'left') { + top = targetPos.top+targetHeight/2-divHeight/2; + left = targetPos.left-deltaSizes[size].x-divWidth-popupOffset; + } else if (direction === 'bottom') { + top = targetPos.top+targetHeight+deltaSizes[size].y+popupOffset; + left = targetPos.left+targetWidth/2-divWidth/2; + if (left < 0) { + direction = "right"; + top = targetPos.top+targetHeight/2-divHeight/2; + left = targetPos.left+targetWidth+deltaSizes[size].x+popupOffset; + } else if (left+divWidth+paddingRight > viewportRight) { + direction = "left"; + top = targetPos.top+targetHeight/2-divHeight/2; + left = targetPos.left-deltaSizes[size].x-divWidth-popupOffset; + if (top+divHeight+targetHeight/2 + 5 > viewportBottom) { + top -= (top+divHeight+targetHeight/2 - viewportBottom + 5) + } + } else if (top+divHeight > viewportBottom) { + direction = 'top'; + top = targetPos.top-deltaSizes[size].y-divHeight-popupOffset; + left = targetPos.left+targetWidth/2-divWidth/2; + } + } else if (direction === 'top') { + top = targetPos.top-deltaSizes[size].y-divHeight-popupOffset; + left = targetPos.left+targetWidth/2-divWidth/2; + if (top < 0) { + direction = 'bottom'; + top = targetPos.top+targetHeight+deltaSizes[size].y+popupOffset; + left = targetPos.left+targetWidth/2-divWidth/2; + } + } else if (/inset/.test(direction)) { + top = targetPos.top + targetHeight/2 - divHeight/2; + left = targetPos.left + targetWidth/2 - divWidth/2; + + if (/bottom/.test(direction)) { + top = targetPos.top + targetHeight - divHeight-popupOffset; + } + if (/top/.test(direction)) { + top = targetPos.top+popupOffset; + } + if (/left/.test(direction)) { + left = targetPos.left+popupOffset; + } + if (/right/.test(direction)) { + left = targetPos.left + targetWidth - divWidth-popupOffset; + } + } + if (currentStyle) { + div.removeClass(currentStyle); + } + if (transition) { + div.css({ + "transition": "0.6s ease", + "transition-property": "top,left,right,bottom" + }) + } + currentStyle = 'red-ui-popover-'+direction; + div.addClass(currentStyle).css({top: top, left: left}); + if (transition) { + setTimeout(function() { + div.css({ + "transition": "none" + }); + },600); + } + + } var closePopup = function(instant) { $(document).off('mousedown.red-ui-popover'); if (!active) { @@ -236,8 +408,10 @@ RED.popover = (function() { },autoClose); } var res = { + get element() { return div }, setContent: function(_content) { content = _content; + return res; }, open: function (instant) { @@ -249,6 +423,10 @@ RED.popover = (function() { active = false; closePopup(instant); return res; + }, + move: function(options) { + movePopup(options); + return } } return res; @@ -258,18 +436,17 @@ RED.popover = (function() { return { create: createPopover, tooltip: function(target,content, action) { - var label = content; - if (action) { - label = function() { - var label = content; + var label = function() { + var label = content; + if (action) { var shortcut = RED.keyboard.getShortcut(action); if (shortcut && shortcut.key) { label = $(''+content+' '+RED.keyboard.formatKey(shortcut.key, true)+''); } - return label; } + return label; } - return RED.popover.create({ + var popover = RED.popover.create({ tooltip: true, target:target, trigger: "hover", @@ -278,6 +455,14 @@ RED.popover = (function() { content: label, delay: { show: 750, hide: 50 } }); + popover.setContent = function(newContent) { + content = newContent; + } + popover.setAction = function(newAction) { + action = newAction; + } + return popover; + }, menu: function(options) { var list = $('
    '); @@ -286,20 +471,47 @@ RED.popover = (function() { } var menuOptions = options.options || []; var first; - menuOptions.forEach(function(opt) { - var item = $('
  • ').appendTo(list); - var link = $('').text(opt.label).appendTo(item); - link.on("click", function(evt) { - evt.preventDefault(); - if (opt.onselect) { - opt.onselect(); - } - menu.hide(); - }) - if (!first) { first = link} - }) + var container = RED.popover.panel(list); + if (options.width) { + container.container.width(options.width); + } + if (options.class) { + container.container.addClass(options.class); + } + if (options.maxHeight) { + container.container.css({ + "max-height": options.maxHeight, + "overflow-y": 'auto' + }) + } var menu = { + options: function(opts) { + if (opts === undefined) { + return menuOptions + } + menuOptions = opts || []; + list.empty(); + menuOptions.forEach(function(opt) { + var item = $('
  • ').appendTo(list); + var link = $('').appendTo(item); + if (typeof opt.label == "string") { + link.text(opt.label) + } else if (opt.label){ + opt.label.appendTo(link); + } + link.on("click", function(evt) { + evt.preventDefault(); + if (opt.onselect) { + opt.onselect(); + } else if (options.onselect) { + options.onselect(opt); + } + menu.hide(); + }) + if (!first) { first = link} + }) + }, show: function(opts) { $(document).on("keydown.red-ui-menu", function(evt) { var currentItem = list.find(":focus").parent(); @@ -333,6 +545,11 @@ RED.popover = (function() { // ESCAPE evt.preventDefault(); menu.hide(true); + } else if (evt.keyCode === 9 && options.tabSelect) { + // TAB - with tabSelect enabled + evt.preventDefault(); + currentItem.find("a").trigger("click"); + } evt.stopPropagation(); }) @@ -352,6 +569,7 @@ RED.popover = (function() { } } } + menu.options(menuOptions); return menu; }, panel: function(content) { @@ -380,12 +598,12 @@ RED.popover = (function() { var pos = target.offset(); var targetWidth = target.width(); - var targetHeight = target.height(); + var targetHeight = target.outerHeight(); var panelHeight = panel.height(); var panelWidth = panel.width(); var top = (targetHeight+pos.top) + offset[1]; - if (top+panelHeight > $(window).height()) { + if (top+panelHeight-$(document).scrollTop() > $(window).height()) { top -= (top+panelHeight)-$(window).height() + 5; } if (top < 0) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js index 41c6e704c..f3327c2e6 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js @@ -38,6 +38,7 @@ RED.tabs = (function() { if (options.vertical) { wrapper.addClass("red-ui-tabs-vertical"); } + if (options.addButton) { wrapper.addClass("red-ui-tabs-add"); var addButton = $('
    ').appendTo(wrapper); @@ -75,6 +76,8 @@ RED.tabs = (function() { }); } if (options.searchButton) { + // This is soft-deprecated as we don't use this in the core anymore + // We no use the `menu` option to provide a drop-down list of actions wrapper.addClass("red-ui-tabs-search"); var searchButton = $('').appendTo(wrapper); searchButton.find('a').on("click", function(evt) { @@ -94,6 +97,50 @@ RED.tabs = (function() { } } + if (options.menu) { + wrapper.addClass("red-ui-tabs-menu"); + var menuButton = $('
    ').appendTo(wrapper); + var menuButtonLink = menuButton.find('a') + var menuOpen = false; + var menu; + menuButtonLink.on("click", function(evt) { + evt.stopPropagation(); + evt.preventDefault(); + if (menuOpen) { + menu.remove(); + menuOpen = false; + return; + } + menuOpen = true; + var menuOptions = []; + if (typeof options.searchButton === 'function') { + menuOptions = options.menu() + } else if (Array.isArray(options.menu)) { + menuOptions = options.menu; + } + menu = RED.menu.init({options: menuOptions}); + menu.attr("id",options.id+"-menu"); + menu.css({ + position: "absolute" + }) + menu.appendTo("body"); + var elementPos = menuButton.offset(); + menu.css({ + top: (elementPos.top+menuButton.height()-2)+"px", + left: (elementPos.left - menu.width() + menuButton.width())+"px" + }) + $(".red-ui-menu.red-ui-menu-dropdown").hide(); + $(document).on("click.red-ui-tabmenu", function(evt) { + $(document).off("click.red-ui-tabmenu"); + menuOpen = false; + menu.remove(); + }); + menu.show(); + }) + } + + + var scrollLeft; var scrollRight; @@ -117,9 +164,9 @@ RED.tabs = (function() { } }) scrollLeft = $('
    ').appendTo(wrapper).find("a"); - scrollLeft.on('mousedown',function(evt) { scrollEventHandler(evt,'-=150') }).on('click',function(evt){ evt.preventDefault();}); + scrollLeft.on('mousedown',function(evt) {scrollEventHandler(evt, evt.shiftKey?('-='+scrollContainer.scrollLeft()):'-=150') }).on('click',function(evt){ evt.preventDefault();}); scrollRight = $('
    ').appendTo(wrapper).find("a"); - scrollRight.on('mousedown',function(evt) { scrollEventHandler(evt,'+=150') }).on('click',function(evt){ evt.preventDefault();}); + scrollRight.on('mousedown',function(evt) { scrollEventHandler(evt,evt.shiftKey?('+='+(scrollContainer[0].scrollWidth - scrollContainer.width()-scrollContainer.scrollLeft())):'+=150') }).on('click',function(evt){ evt.preventDefault();}); } if (options.collapsible) { @@ -337,6 +384,12 @@ RED.tabs = (function() { if (link.length === 0) { return; } + if (link.parent().hasClass("hide-tab")) { + link.parent().removeClass("hide-tab").removeClass("hide"); + if (options.onshow) { + options.onshow(tabs[link.attr('href').slice(1)]) + } + } if (!link.parent().hasClass("active")) { ul.children().removeClass("active"); ul.children().css({"transition": "width 100ms"}); @@ -362,13 +415,13 @@ RED.tabs = (function() { } } function activatePreviousTab() { - var previous = ul.find("li.active").prev(); + var previous = findPreviousVisibleTab(); if (previous.length > 0) { activateTab(previous.find("a")); } } function activateNextTab() { - var next = ul.find("li.active").next(); + var next = findNextVisibleTab(); if (next.length > 0) { activateTab(next.find("a")); } @@ -378,7 +431,9 @@ RED.tabs = (function() { if (options.vertical) { return; } - var tabs = ul.find("li.red-ui-tab"); + var allTabs = ul.find("li.red-ui-tab"); + var tabs = allTabs.filter(":not(.hide-tab)"); + var hiddenTabs = allTabs.filter(".hide-tab"); var width = wrapper.width(); var tabCount = tabs.length; var tabWidth; @@ -446,6 +501,7 @@ RED.tabs = (function() { // } tabs.css({width:currentTabWidth}); + hiddenTabs.css({width:"0px"}); if (tabWidth < 50) { // ul.find(".red-ui-tab-close").hide(); ul.find(".red-ui-tab-icon").hide(); @@ -486,24 +542,104 @@ RED.tabs = (function() { } var li = ul.find("a[href='#"+id+"']").parent(); if (li.hasClass("active")) { - var tab = li.prev(); + var tab = findPreviousVisibleTab(li); if (tab.length === 0) { - tab = li.next(); + tab = findNextVisibleTab(li); + } + if (tab.length > 0) { + activateTab(tab.find("a")); + } else { + if (options.onchange) { + options.onchange(null); + } } - activateTab(tab.find("a")); } - li.remove(); - if (tabs[id].pinned) { - pinnedTabsCount--; + + li.one("transitionend", function(evt) { + li.remove(); + if (tabs[id].pinned) { + pinnedTabsCount--; + } + if (options.onremove) { + options.onremove(tabs[id]); + } + delete tabs[id]; + updateTabWidths(); + if (collapsibleMenu) { + collapsibleMenu.remove(); + collapsibleMenu = null; + } + }) + li.addClass("hide-tab"); + li.width(0); + } + + function findPreviousVisibleTab(li) { + if (!li) { + li = ul.find("li.active").parent(); } - if (options.onremove) { - options.onremove(tabs[id]); + var previous = li.prev(); + while(previous.length > 0 && previous.hasClass("hide-tab")) { + previous = previous.prev(); } - delete tabs[id]; - updateTabWidths(); - if (collapsibleMenu) { - collapsibleMenu.remove(); - collapsibleMenu = null; + return previous; + } + function findNextVisibleTab(li) { + if (!li) { + li = ul.find("li.active").parent(); + } + var next = ul.find("li.active").next(); + while(next.length > 0 && next.hasClass("hide-tab")) { + next = next.next(); + } + return next; + } + function showTab(id) { + if (tabs[id]) { + var li = ul.find("a[href='#"+id+"']").parent(); + if (li.hasClass("hide-tab")) { + li.removeClass("hide-tab").removeClass("hide"); + if (ul.find("li.red-ui-tab:not(.hide-tab)").length === 1) { + activateTab(li.find("a")) + } + updateTabWidths(); + if (options.onshow) { + options.onshow(tabs[id]) + } + } + } + } + function hideTab(id) { + if (tabs[id]) { + var li = ul.find("a[href='#"+id+"']").parent(); + if (!li.hasClass("hide-tab")) { + if (li.hasClass("active")) { + var tab = findPreviousVisibleTab(li); + if (tab.length === 0) { + tab = findNextVisibleTab(li); + } + if (tab.length > 0) { + activateTab(tab.find("a")); + } else { + if (options.onchange) { + options.onchange(null); + } + } + } + li.removeClass("active"); + li.one("transitionend", function(evt) { + li.addClass("hide"); + updateTabWidths(); + if (options.onhide) { + options.onhide(tabs[id]) + } + setTimeout(function() { + updateScroll() + },200) + }) + li.addClass("hide-tab"); + li.css({width:0}) + } } } @@ -663,7 +799,6 @@ RED.tabs = (function() { link.on("click", function(evt) { evt.preventDefault(); }) link.on("dblclick", function(evt) { evt.stopPropagation(); evt.preventDefault(); }) - $('').appendTo(li); if (tab.closeable) { @@ -675,6 +810,15 @@ RED.tabs = (function() { removeTab(tab.id); }); } + if (tab.hideable) { + li.addClass("red-ui-tabs-closeable") + var closeLink = $("",{href:"#",class:"red-ui-tab-close"}).appendTo(li); + closeLink.append(''); + closeLink.on("click",function(event) { + event.preventDefault(); + hideTab(tab.id); + }); + } var badges = $('').appendTo(li); if (options.onselect) { @@ -682,11 +826,11 @@ RED.tabs = (function() { $('').appendTo(badges); } + link.attr("title",tab.label); if (options.onadd) { options.onadd(tab); } - link.attr("title",tab.label); if (ul.find("li.red-ui-tab").length == 1) { activateTab(link); } @@ -787,7 +931,7 @@ RED.tabs = (function() { previousTab: activatePreviousTab, resize: updateTabWidths, count: function() { - return ul.find("li.red-ui-tab").length; + return ul.find("li.red-ui-tab:not(.hide)").length; }, activeIndex: function() { return ul.find("li.active").index() @@ -795,6 +939,9 @@ RED.tabs = (function() { contains: function(id) { return ul.find("a[href='#"+id+"']").length > 0; }, + showTab: showTab, + hideTab: hideTab, + renameTab: function(id,label) { tabs[id].label = label; var tab = ul.find("a[href='#"+id+"']"); @@ -802,7 +949,20 @@ RED.tabs = (function() { tab.find("span.red-ui-text-bidi-aware").text(label).attr('dir', RED.text.bidi.resolveBaseTextDir(label)); updateTabWidths(); }, + listTabs: function() { + return $.makeArray(ul.children().map(function() { return $(this).data('tabId');})); + }, selection: getSelection, + clearSelection: function() { + if (options.onselect) { + var selection = ul.find("li.red-ui-tab.selected"); + if (selection.length > 0) { + selection.removeClass("selected"); + selectionChanged(); + } + } + + }, order: function(order) { preferredOrder = order; var existingTabOrder = $.makeArray(ul.children().map(function() { return $(this).data('tabId');})); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js index 6619ad76e..d4523f88f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/treeList.js @@ -24,6 +24,9 @@ * - rootSortable: boolean - if 'sortable' is set, then setting this to * false, prevents items being sorted to the * top level of the tree + * - autoSelect: boolean - default true - triggers item selection when navigating + * list by keyboard. If the list has checkboxed items + * you probably want to set this to false * * methods: * - data(items) - clears existing items and replaces with new data @@ -41,6 +44,7 @@ * sublabel: 'Local', // a sub-label for the item * icon: 'fa fa-rocket', // (optional) icon for the item * checkbox: true/false, // (optional) if present, display checkbox accordingly + * radio: 'group-name', // (optional) if present, display radio box - using group-name to set radio group * selected: true/false, // (optional) whether the item is selected or not * children: [] | function(done,item) // (optional) an array of child items, or a function * // that will call the `done` callback with an array @@ -49,6 +53,7 @@ * deferBuild: true/false, // don't build any ui elements for the item's children * until it is expanded by the user. * element: // custom dom element to use for the item - ignored if `label` is set + * collapsible: true/false, // prevent a parent item from being collapsed. default true. * } * ] * @@ -89,77 +94,99 @@ $.widget( "nodered.treeList", { _create: function() { var that = this; - + var autoSelect = true; + if (that.options.autoSelect === false) { + autoSelect = false; + } this.element.addClass('red-ui-treeList'); this.element.attr("tabIndex",0); var wrapper = $('
    ',{class:'red-ui-treeList-container'}).appendTo(this.element); this.element.on('keydown', function(evt) { - var selected = that._topList.find(".selected").parent().data('data'); - if (!selected && (evt.keyCode === 40 || evt.keyCode === 38)) { - that.select(that._data[0]); + var focussed = that._topList.find(".focus").parent().data('data'); + if (!focussed && (evt.keyCode === 40 || evt.keyCode === 38)) { + if (that._data[0]) { + if (autoSelect) { + that.select(that._data[0]); + } else { + that._topList.find(".focus").removeClass("focus") + } + that._data[0].treeList.label.addClass('focus') + } return; } var target; switch(evt.keyCode) { + case 32: // SPACE case 13: // ENTER + if (evt.altKey || evt.ctrlKey || evt.metaKey || evt.shiftKey) { + return + } evt.preventDefault(); evt.stopPropagation(); - if (selected.children) { - if (selected.treeList.container.hasClass("expanded")) { - selected.treeList.collapse() + if (focussed.checkbox) { + focussed.treeList.checkbox.trigger("click"); + } else if (focussed.radio) { + focussed.treeList.radio.trigger("click"); + } else if (focussed.children) { + if (focussed.treeList.container.hasClass("expanded")) { + focussed.treeList.collapse() } else { - selected.treeList.expand() + focussed.treeList.expand() } } else { - that._trigger("confirm",null,selected) + that._trigger("confirm",null,focussed) } - break; case 37: // LEFT evt.preventDefault(); evt.stopPropagation(); - if (selected.children&& selected.treeList.container.hasClass("expanded")) { - selected.treeList.collapse() - } else if (selected.parent) { - target = selected.parent; + if (focussed.children&& focussed.treeList.container.hasClass("expanded")) { + focussed.treeList.collapse() + } else if (focussed.parent) { + target = focussed.parent; } break; case 38: // UP evt.preventDefault(); evt.stopPropagation(); - target = that._getPreviousSibling(selected); + target = that._getPreviousSibling(focussed); if (target) { target = that._getLastDescendant(target); } - if (!target && selected.parent) { - target = selected.parent; + if (!target && focussed.parent) { + target = focussed.parent; } break; case 39: // RIGHT evt.preventDefault(); evt.stopPropagation(); - if (selected.children) { - if (!selected.treeList.container.hasClass("expanded")) { - selected.treeList.expand() + if (focussed.children) { + if (!focussed.treeList.container.hasClass("expanded")) { + focussed.treeList.expand() } } break case 40: //DOWN evt.preventDefault(); evt.stopPropagation(); - if (selected.children && Array.isArray(selected.children) && selected.children.length > 0 && selected.treeList.container.hasClass("expanded")) { - target = selected.children[0]; + if (focussed.children && Array.isArray(focussed.children) && focussed.children.length > 0 && focussed.treeList.container.hasClass("expanded")) { + target = focussed.children[0]; } else { - target = that._getNextSibling(selected); - while (!target && selected.parent) { - selected = selected.parent; - target = that._getNextSibling(selected); + target = that._getNextSibling(focussed); + while (!target && focussed.parent) { + focussed = focussed.parent; + target = that._getNextSibling(focussed); } } break } if (target) { - that.select(target); + if (autoSelect) { + that.select(target); + } else { + that._topList.find(".focus").removeClass("focus") + } + target.treeList.label.addClass('focus') } }); this._data = []; @@ -462,6 +489,9 @@ container.addClass("expanded"); } item.treeList.collapse = function() { + if (item.collapsible === false) { + return + } if (!item.children) { return; } @@ -532,10 +562,11 @@ }).appendTo(label) } - var labelPaddingWidth = (item.gutter?item.gutter.width()+2:0)+(depth*20); - // var labelPaddingWidth = (item.gutter ? item.gutter[0].offsetWidth + 2 : 0) + (depth * 20) + // var labelPaddingWidth = (item.gutter?item.gutter.width()+2:0)+(depth*20); + var labelPaddingWidth = (item.gutter ? item.gutter[0].offsetWidth + 2 : 0) + (depth * 20) item.treeList.labelPadding = $('').css({ display: "inline-block", + "flex-shrink": 0, width: labelPaddingWidth+'px' }).appendTo(label); @@ -581,7 +612,7 @@ // Already a parent because we've got the angle-right icon return; } - $('').appendTo(treeListIcon); + $('').toggleClass("hide",item.collapsible === false).appendTo(treeListIcon); treeListIcon.on("click.red-ui-treeList-expand", function(e) { e.stopPropagation(); e.preventDefault(); @@ -632,6 +663,46 @@ label.on("click", function(e) { e.stopPropagation(); cb.trigger("click"); + that._topList.find(".focus").removeClass("focus") + label.addClass('focus') + }) + } + item.treeList.select = function(v) { + if (v !== item.selected) { + cb.trigger("click"); + } + } + item.treeList.checkbox = cb; + selectWrapper.appendTo(label) + } else if (item.radio) { + var selectWrapper = $(''); + var cb = $('').prop('name', item.radio).prop('checked',item.selected).appendTo(selectWrapper); + cb.on('click', function(e) { + e.stopPropagation(); + }); + cb.on('change', function(e) { + item.selected = this.checked; + that._selected.forEach(function(selectedItem) { + if (selectedItem.radio === item.radio) { + selectedItem.treeList.label.removeClass("selected"); + selectedItem.selected = false; + that._selected.delete(selectedItem); + } + }) + if (item.selected) { + that._selected.add(item); + } else { + that._selected.delete(item); + } + label.toggleClass("selected",this.checked); + that._trigger("select",e,item); + }) + if (!item.children) { + label.on("click", function(e) { + e.stopPropagation(); + cb.trigger("click"); + that._topList.find(".focus").removeClass("focus") + label.addClass('focus') }) } item.treeList.select = function(v) { @@ -640,6 +711,7 @@ } } selectWrapper.appendTo(label) + item.treeList.radio = cb; } else { label.on("click", function(e) { if (!that.options.multi) { @@ -647,10 +719,14 @@ } label.addClass("selected"); that._selected.add(item); + that._topList.find(".focus").removeClass("focus") + label.addClass('focus') that._trigger("select",e,item) }) label.on("dblclick", function(e) { + that._topList.find(".focus").removeClass("focus") + label.addClass('focus') if (!item.children) { that._trigger("confirm",e,item); } @@ -798,6 +874,9 @@ if (item.treeList.label) { item.treeList.label.addClass("selected"); } + + that._topList.find(".focus").removeClass("focus"); + if (triggerEvent !== false) { this._trigger("select",null,item) } @@ -805,6 +884,9 @@ clearSelection: function() { this._selected.forEach(function(item) { item.selected = false; + if (item.treeList.checkbox) { + item.treeList.checkbox.prop('checked',false) + } if (item.treeList.label) { item.treeList.label.removeClass("selected") } diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js index 0401be1b9..ba7e48900 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/typedInput.js @@ -53,8 +53,88 @@ } return icon; } + + var autoComplete = function(options) { + return function(val) { + var matches = []; + options.forEach(opt => { + let v = opt.value; + var i = v.toLowerCase().indexOf(val.toLowerCase()); + if (i > -1) { + var pre = v.substring(0,i); + var matchedVal = v.substring(i,i+val.length); + var post = v.substring(i+val.length) + + var el = $('
    ',{style:"white-space:nowrap; overflow: hidden; flex-grow:1"}); + $('').text(pre).appendTo(el); + $('',{style:"font-weight: bold"}).text(matchedVal).appendTo(el); + $('').text(post).appendTo(el); + + var element = $('
    ',{style: "display: flex"}); + el.appendTo(element); + if (opt.source) { + $('
    ').css({ + "font-size": "0.8em" + }).text(opt.source.join(",")).appendTo(element); + } + + matches.push({ + value: v, + label: element, + i:i + }) + } + }) + matches.sort(function(A,B){return A.i-B.i}) + return matches; + } + } + + // This is a hand-generated list of completions for the core nodes (based on the node help html). + var msgCompletions = [ + { value: "payload" }, + { value: "req", source: ["http in"]}, + { value: "req.body", source: ["http in"]}, + { value: "req.headers", source: ["http in"]}, + { value: "req.query", source: ["http in"]}, + { value: "req.params", source: ["http in"]}, + { value: "req.cookies", source: ["http in"]}, + { value: "req.files", source: ["http in"]}, + { value: "complete", source: ["join"] }, + { value: "contentType", source: ["mqtt"] }, + { value: "cookies", source: ["http in","http request"] }, + { value: "correlationData", source: ["mqtt"] }, + { value: "delay", source: ["delay","trigger"] }, + { value: "encoding", source: ["file"] }, + { value: "error", source: ["catch"] }, + { value: "filename", source: ["file","file in"] }, + { value: "flush", source: ["delay"] }, + { value: "followRedirects", source: ["http request"] }, + { value: "headers", source: ["http in"," http request"] }, + { value: "kill", source: ["exec"] }, + { value: "messageExpiryInterval", source: ["mqtt"] }, + { value: "method", source: ["http-request"] }, + { value: "options", source: ["xml"] }, + { value: "parts", source: ["split","join"] }, + { value: "pid", source: ["exec"] }, + { value: "qos", source: ["mqtt"] }, + { value: "rate", source: ["delay"] }, + { value: "rejectUnauthorized", source: ["http request"] }, + { value: "requestTimeout", source: ["http request"] }, + { value: "reset", source: ["delay","trigger","join","rbe"] }, + { value: "responseTopic", source: ["mqtt"] }, + { value: "restartTimeout", source: ["join"] }, + { value: "retain", source: ["mqtt"] }, + { value: "select", source: ["html"] }, + { value: "statusCode", source: ["http in"] }, + { value: "template", source: ["template"] }, + { value: "toFront", source: ["delay"] }, + { value: "topic", source: ["inject","mqtt","rbe"] }, + { value: "url", source: ["http request"] }, + { value: "userProperties", source: ["mqtt"] } + ] var allOptions = { - msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression}, + msg: {value:"msg",label:"msg.",validate:RED.utils.validatePropertyExpression, autoComplete: autoComplete(msgCompletions)}, flow: {value:"flow",label:"flow.",hasValue:true, options:[], validate:RED.utils.validatePropertyExpression, @@ -265,6 +345,47 @@ } } }; + + // For a type with options, check value is a valid selection + // If !opt.multiple, returns the valid option object + // if opt.multiple, returns an array of valid option objects + // If not valid, returns null; + + function isOptionValueValid(opt, currentVal) { + if (!opt.multiple) { + for (var i=0;i= 37 && evt.keyCode <= 40) { evt.stopPropagation(); } @@ -428,6 +554,7 @@ this.optionExpandButton = $('').appendTo(this.uiSelect); this.optionExpandButtonIcon = $('').appendTo(this.optionExpandButton); + this.type(this.options.default||this.typeList[0].value); }catch(err) { console.log(err.stack); @@ -579,7 +706,7 @@ var height = relativeTo.height(); var menuHeight = menu.height(); var top = (height+pos.top); - if (top+menuHeight > $(window).height()) { + if (top+menuHeight-$(document).scrollTop() > $(window).height()) { top -= (top+menuHeight)-$(window).height()+5; } if (top < 0) { @@ -688,8 +815,10 @@ }); if (this.typeList.length < 2) { this.selectTrigger.attr("tabindex", -1) + this.selectTrigger.on("mousedown.red-ui-typedInput-focus-block", function(evt) { evt.preventDefault(); }) } else { this.selectTrigger.attr("tabindex", 0) + this.selectTrigger.off("mousedown.red-ui-typedInput-focus-block") } this.selectTrigger.toggleClass("disabled", this.typeList.length === 1); this.selectTrigger.find(".fa-caret-down").toggle(this.typeList.length > 1) @@ -703,6 +832,11 @@ this.propertyType = null; this.type(currentType); } + if (this.typeList.length === 1 && !this.typeList[0].icon && (!this.typeList[0].label || this.typeList[0].showLabel === false)) { + this.selectTrigger.hide() + } else { + this.selectTrigger.show() + } }, width: function(desiredWidth) { this.uiWidth = desiredWidth; @@ -765,8 +899,51 @@ return this.propertyType; } else { var that = this; + var previousValue = null; var opt = this.typeMap[type]; if (opt && this.propertyType !== type) { + // If previousType is !null, then this is a change of the type, rather than the initialisation + var previousType = this.typeMap[this.propertyType]; + var typeChanged = !!previousType; + previousValue = this.input.val(); + + if (typeChanged) { + if (previousType.options && opt.hasValue !== true) { + this.oldValues[previousType.value] = previousValue; + } else if (previousType.hasValue === false) { + this.oldValues[previousType.value] = previousValue; + } else { + this.oldValues["_"] = previousValue; + } + if ((opt.options && opt.hasValue !== true) || opt.hasValue === false) { + if (this.oldValues.hasOwnProperty(opt.value)) { + this.input.val(this.oldValues[opt.value]); + } else { + // No old value for the option type. + // It is possible code has called 'value' then 'type' + // to set the selected option. This is what the Inject/Switch/Change + // nodes did before 2.1. + // So we need to be careful to not reset the value if it is a valid option. + var validOptions = isOptionValueValid(opt,previousValue); + if (previousValue && validOptions) { + this.input.val(previousValue); + } else { + if (typeof opt.default === "string") { + this.input.val(opt.default); + } else if (Array.isArray(opt.default)) { + this.input.val(opt.default.join(",")) + } else { + this.input.val(""); + } + } + } + } else { + this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||"")) + } + if (previousType.autoComplete) { + this.input.autoComplete("destroy"); + } + } this.propertyType = type; if (this.typeField) { this.typeField.val(type); @@ -836,22 +1013,12 @@ var op; if (!opt.hasValue) { - var validValue = false; - var currentVal = this.input.val(); + // Check the value is valid for the available options + var validValues = isOptionValueValid(opt,this.input.val()); if (!opt.multiple) { - for (var i=0;i
    ").css({display:'inline-block',position:'relative'}); - var selectWrap = $("
    ").css({position:'absolute',left:0,right:'40px'}).appendTo(outerWrap); - var select = $('').appendTo(selectWrap); - - outerWrap.width(newWidth).height(input.height()); - if (outerWrap.width() === 0) { - outerWrap.width("70%"); - } + var outerWrap = $("
    ").css({ + width: newWidth, + display:'inline-flex' + }); + var select = $('').appendTo(outerWrap); input.replaceWith(outerWrap); // set the style attr directly - using width() on FF causes a value of 114%... - select.attr('style',"width:100%"); - updateConfigNodeSelect(property,type,node[property],prefix); + select.css({ + 'flex-grow': 1 + }); + updateConfigNodeSelect(property,type,node[property],prefix,filter); $('
    ') - .css({position:'absolute',right:0,top:0}) + .css({"margin-left":"10px"}) .appendTo(outerWrap); $('#'+prefix+'-lookup-'+property).on("click", function(e) { - showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix); + showEditConfigNodeDialog(property,type,select.find(":selected").val(),prefix,node); e.preventDefault(); }); var label = ""; @@ -309,7 +304,7 @@ RED.editor = (function() { } button.on("click", function(e) { - showEditConfigNodeDialog(property,type,input.val()||"_ADD_",prefix); + showEditConfigNodeDialog(property,type,input.val()||"_ADD_",prefix,node); e.preventDefault(); }); } @@ -398,74 +393,107 @@ RED.editor = (function() { } } - /** - * Update the node credentials from the edit form - * @param node - the node containing the credentials - * @param credDefinition - definition of the credentials - * @param prefix - prefix of the input fields - * @return {boolean} whether anything has changed - */ - function updateNodeCredentials(node, credDefinition, prefix) { - var changed = false; - if (!node.credentials) { - node.credentials = {_:{}}; - } else if (!node.credentials._) { - node.credentials._ = {}; - } - - for (var cred in credDefinition) { - if (credDefinition.hasOwnProperty(cred)) { - var input = $("#" + prefix + '-' + cred); - if (input.length > 0) { - var value = input.val(); - if (credDefinition[cred].type == 'password') { - node.credentials['has_' + cred] = (value !== ""); - if (value == '__PWRD__') { - continue; - } - changed = true; - - } - node.credentials[cred] = value; - if (value != node.credentials._[cred]) { - changed = true; - } - } - } - } - return changed; - } - /** * Prepare all of the editor dialog fields + * @param trayBody - the tray body to populate + * @param nodeEditPanes - array of edit pane ids to add to the dialog * @param node - the node being edited * @param definition - the node definition * @param prefix - the prefix to use in the input element ids (node-input|node-config-input) + * @param default - the id of the tab to show by default */ - function prepareEditDialog(node,definition,prefix,done) { - for (var d in definition.defaults) { - if (definition.defaults.hasOwnProperty(d)) { - if (definition.defaults[d].type) { - if (!definition.defaults[d]._type.array) { - var configTypeDef = RED.nodes.getType(definition.defaults[d].type); - if (configTypeDef && configTypeDef.category === 'config') { - if (configTypeDef.exclusive) { - prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix); - } else { - prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix); - } - } else { - console.log("Unknown type:", definition.defaults[d].type); - preparePropertyEditor(node,d,prefix,definition.defaults); - } - } - } else { - preparePropertyEditor(node,d,prefix,definition.defaults); - } - attachPropertyChangeHandler(node,definition.defaults,d,prefix); - } - } + function prepareEditDialog(trayBody, nodeEditPanes, node, definition, prefix, defaultTab, done) { + var finishedBuilding = false; var completePrepare = function() { + + var editorTabEl = $('
      ').appendTo(trayBody); + var editorContent = $('
      ').appendTo(trayBody); + + var editorTabs = RED.tabs.create({ + element:editorTabEl, + onchange:function(tab) { + editorContent.children().hide(); + tab.content.show(); + if (tab.onchange) { + tab.onchange.call(tab); + } + if (finishedBuilding) { + RED.tray.resize(); + } + }, + collapsible: true, + menu: false + }); + + var activeEditPanes = []; + + nodeEditPanes = nodeEditPanes.slice(); + for (var i in filteredEditPanes) { + if (filteredEditPanes.hasOwnProperty(i)) { + if (filteredEditPanes[i](node)) { + nodeEditPanes.push(i); + } + } + } + + nodeEditPanes.forEach(function(id) { + try { + var editPaneDefinition = editPanes[id]; + if (editPaneDefinition) { + if (typeof editPaneDefinition === 'function') { + editPaneDefinition = editPaneDefinition.call(editPaneDefinition, node); + } + var editPaneContent = $('
      ', {class:"red-ui-tray-content"}).appendTo(editorContent).hide(); + editPaneDefinition.create.call(editPaneDefinition,editPaneContent); + var editTab = { + id: id, + label: editPaneDefinition.label, + name: editPaneDefinition.name, + iconClass: editPaneDefinition.iconClass, + content: editPaneContent, + onchange: function() { + if (editPaneDefinition.show) { + editPaneDefinition.show.call(editPaneDefinition) + } + } + } + editorTabs.addTab(editTab); + activeEditPanes.push(editPaneDefinition); + } else { + console.warn("Unregisted edit pane:",id) + } + } catch(err) { + console.log(id,err); + } + }); + + for (var d in definition.defaults) { + if (definition.defaults.hasOwnProperty(d)) { + if (definition.defaults[d].type) { + if (!definition.defaults[d]._type.array) { + var configTypeDef = RED.nodes.getType(definition.defaults[d].type); + if (configTypeDef && configTypeDef.category === 'config') { + if (configTypeDef.exclusive) { + prepareConfigNodeButton(node,d,definition.defaults[d].type,prefix); + } else { + prepareConfigNodeSelect(node,d,definition.defaults[d].type,prefix,definition.defaults[d].filter); + } + } else { + console.log("Unknown type:", definition.defaults[d].type); + preparePropertyEditor(node,d,prefix,definition.defaults); + } + } + } else { + preparePropertyEditor(node,d,prefix,definition.defaults); + } + attachPropertyChangeHandler(node,definition.defaults,d,prefix); + } + } + + if (!/^subflow:/.test(definition.type)) { + populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); + } + if (definition.oneditprepare) { try { definition.oneditprepare.call(node); @@ -474,6 +502,7 @@ RED.editor = (function() { console.log(err.stack); } } + // Now invoke any change handlers added to the fields - passing true // to prevent full node validation from being triggered each time for (var d in definition.defaults) { @@ -503,22 +532,27 @@ RED.editor = (function() { } } validateNodeEditor(node,prefix); + finishedBuilding = true; + if (defaultTab) { + editorTabs.activateTab(defaultTab); + } if (done) { - done(); + done(activeEditPanes); } } - if (definition.credentials || /^subflow:/.test(definition.type)) { + if (definition.credentials || /^subflow:/.test(definition.type) || node.type === "group" || node.type === "tab") { if (node.credentials) { populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); completePrepare(); } else { - getNodeCredentials(node.type, node.id, function(data) { + var nodeType = node.type; + if (/^subflow:/.test(nodeType)) { + nodeType = "subflow" + } + getNodeCredentials(nodeType, node.id, function(data) { if (data) { node.credentials = data; node.credentials._ = $.extend(true,{},data); - if (!/^subflow:/.test(definition.type)) { - populateCredentialsInputs(node, definition.credentials, node.credentials, prefix); - } } completePrepare(); }); @@ -598,13 +632,6 @@ RED.editor = (function() { $(this).attr("data-i18n",keys.join(";")); }); - if (type === "subflow-template") { - // This is the 'edit properties' dialog for a subflow template - // TODO: this needs to happen later in the dialog open sequence - // so that credentials can be loaded prior to building the form - RED.subflow.buildEditForm(type,node); - } - // Add dummy fields to prevent 'Enter' submitting the form in some // cases, and also prevent browser auto-fill of password // - the elements cannot be hidden otherwise Chrome will ignore them. @@ -617,506 +644,151 @@ RED.editor = (function() { return dialogForm; } - function refreshLabelForm(container,node) { - - var inputPlaceholder = node._def.inputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel"); - var outputPlaceholder = node._def.outputLabels?RED._("editor.defaultLabel"):RED._("editor.noDefaultLabel"); - - var inputsDiv = $("#red-ui-editor-node-label-form-inputs"); - var outputsDiv = $("#red-ui-editor-node-label-form-outputs"); - - var inputCount; - var formInputs = $("#node-input-inputs").val(); - if (formInputs === undefined) { - if (node.type === 'subflow') { - inputCount = node.in.length; - } else { - inputCount = node.inputs || node._def.inputs || 0; - } - } else { - inputCount = Math.min(1,Math.max(0,parseInt(formInputs))); - if (isNaN(inputCount)) { - inputCount = 0; - } - } - - var children = inputsDiv.children(); - var childCount = children.length; - if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) { - childCount--; - } - - if (childCount < inputCount) { - if (childCount === 0) { - // remove the 'none' placeholder - $(children[0]).remove(); - } - for (i = childCount;i inputCount) { - for (i=inputCount;i B.__label__) { + return 1; + } + return 0; + } + + function updateConfigNodeSelect(name,type,value,prefix,filter) { + // if prefix is null, there is no config select to update + if (prefix) { + var button = $("#"+prefix+"-edit-"+name); + if (button.length) { + if (value) { + button.text(RED._("editor.configEdit")); } else { - row.detach(); + button.text(RED._("editor.configAdd")); } - if (outputMap[p] !== -1) { - outputCount++; - rows.push({i:parseInt(outputMap[p]),r:row}); - } - }); - rows.sort(function(A,B) { - return A.i-B.i; - }) - rows.forEach(function(r,i) { - r.r.find("label").text((i+1)+"."); - r.r.appendTo(outputsDiv); - }) - if (rows.length === 0) { - buildLabelRow("output",i,"").appendTo(outputsDiv); + $("#"+prefix+"-"+name).val(value); } else { - } - } else { - outputCount = Math.max(0,parseInt(formOutputs)); - } - children = outputsDiv.children(); - childCount = children.length; - if (childCount === 1 && $(children[0]).hasClass('red-ui-editor-node-label-form-none')) { - childCount--; - } - if (childCount < outputCount) { - if (childCount === 0) { - // remove the 'none' placeholder - $(children[0]).remove(); - } - for (i = childCount;i outputCount) { - for (i=outputCount;i',{class:"red-ui-editor-node-label-form-row"}); - if (type === undefined) { - $('').text(RED._("editor.noDefaultLabel")).appendTo(result); - result.addClass("red-ui-editor-node-label-form-none"); - } else { - result.addClass(""); - var id = "red-ui-editor-node-label-form-"+type+"-"+index; - $('