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..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/popover.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/popover.js index c40570626..27a454dc6 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,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 = $('