diff --git a/Gruntfile.js b/Gruntfile.js index a5a43d053..83637cbed 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -162,6 +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/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", 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..d66cd26a0 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js @@ -0,0 +1,108 @@ +(function($) { + +/** + * options: + * + * methods: + * + */ + + $.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(); + } + }, + // disable: function(val) { + // if(val === undefined || !!val ) { + // + // } else { + // this.uiSelect.attr("disabled", null); //remove attr + // } + // }, + // enable: function() { + // this.uiSelect.attr("disabled", null); //remove attr + // }, + }); +})(jQuery); 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 c40570626..17969061c 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 @@ -340,20 +340,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(); @@ -387,6 +414,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(); }) @@ -406,6 +438,7 @@ RED.popover = (function() { } } } + menu.options(menuOptions); return menu; }, panel: function(content) { @@ -434,7 +467,7 @@ 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(); 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 41c731e3b..9b0c6f54c 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, @@ -380,6 +460,9 @@ that.element.trigger('paste',evt); }); this.input.on('keydown', function(evt) { + if (that.typeMap[that.propertyType].autoComplete) { + return + } if (evt.keyCode >= 37 && evt.keyCode <= 40) { evt.stopPropagation(); } @@ -795,6 +878,9 @@ } else { this.input.val(this.oldValues.hasOwnProperty("_")?this.oldValues["_"]:(opt.default||"")) } + if (previousType.autoComplete) { + this.input.autoComplete("destroy"); + } } this.propertyType = type; if (this.typeField) { @@ -985,6 +1071,11 @@ } else { this.valueLabelContainer.hide(); this.elementDiv.show(); + if (opt.autoComplete) { + this.input.autoComplete({ + search: opt.autoComplete + }) + } } if (this.optionExpandButton) { if (opt.expand) { diff --git a/packages/node_modules/@node-red/editor-client/src/sass/style.scss b/packages/node_modules/@node-red/editor-client/src/sass/style.scss index 084cda954..7910832ad 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/style.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/style.scss @@ -61,6 +61,7 @@ @import "ui/common/checkboxSet"; @import "ui/common/stack"; @import "ui/common/treeList"; +@import "ui/common/autoComplete"; @import "dragdrop"; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss new file mode 100644 index 000000000..0501bb6a2 --- /dev/null +++ b/packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss @@ -0,0 +1,5 @@ +.red-ui-autoComplete-container { + &.red-ui-popover-panel { + border-top: none; + } +}