mirror of
				https://github.com/node-red/node-red.git
				synced 2025-03-01 10:36:34 +00:00 
			
		
		
		
	Merge pull request #3171 from node-red/auto-complete
Add autoComplete widget and add to TypedInput for msg. props
This commit is contained in:
		| @@ -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", | ||||
|   | ||||
							
								
								
									
										115
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								packages/node_modules/@node-red/editor-client/src/js/ui/common/autoComplete.js
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | ||||
| (function($) { | ||||
|  | ||||
| /** | ||||
|  * Attach to an <input type="text"> 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); | ||||
| @@ -13,6 +13,128 @@ | ||||
|  * 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 = { | ||||
| @@ -123,6 +245,8 @@ RED.popover = (function() { | ||||
|             div.width(width); | ||||
|             if (options.maxWidth) { | ||||
|                 div.css("max-width",options.maxWidth) | ||||
|             } else { | ||||
|                 div.css("max-width", 'auto'); | ||||
|             } | ||||
|  | ||||
|             var targetPos = target[0].getBoundingClientRect(); | ||||
| @@ -340,20 +464,47 @@ RED.popover = (function() { | ||||
|             } | ||||
|             var menuOptions = options.options || []; | ||||
|             var first; | ||||
|             menuOptions.forEach(function(opt) { | ||||
|                 var item = $('<li>').appendTo(list); | ||||
|                 var link = $('<a href="#"></a>').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 = $('<li>').appendTo(list); | ||||
|                         var link = $('<a href="#"></a>').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 +538,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 +562,7 @@ RED.popover = (function() { | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             menu.options(menuOptions); | ||||
|             return menu; | ||||
|         }, | ||||
|         panel: function(content) { | ||||
| @@ -434,7 +591,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(); | ||||
|  | ||||
|   | ||||
| @@ -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 = $('<div/>',{style:"white-space:nowrap; overflow: hidden; flex-grow:1"}); | ||||
|                     $('<span/>').text(pre).appendTo(el); | ||||
|                     $('<span/>',{style:"font-weight: bold"}).text(matchedVal).appendTo(el); | ||||
|                     $('<span/>').text(post).appendTo(el); | ||||
|  | ||||
|                     var element = $('<div>',{style: "display: flex"}); | ||||
|                     el.appendTo(element); | ||||
|                     if (opt.source) { | ||||
|                         $('<div>').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) { | ||||
|   | ||||
| @@ -61,6 +61,7 @@ | ||||
| @import "ui/common/checkboxSet"; | ||||
| @import "ui/common/stack"; | ||||
| @import "ui/common/treeList"; | ||||
| @import "ui/common/autoComplete"; | ||||
|  | ||||
| @import "dragdrop"; | ||||
|  | ||||
|   | ||||
							
								
								
									
										5
									
								
								packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								packages/node_modules/@node-red/editor-client/src/sass/ui/common/autoComplete.scss
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | ||||
| .red-ui-autoComplete-container { | ||||
|     &.red-ui-popover-panel { | ||||
|         border-top: none; | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user