mirror of
https://github.com/node-red/node-red.git
synced 2023-10-10 13:36:53 +02: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:
commit
42d90542b5
@ -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;
|
||||
|
||||
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>').text(opt.label).appendTo(item);
|
||||
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}
|
||||
})
|
||||
var container = RED.popover.panel(list);
|
||||
var menu = {
|
||||
},
|
||||
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;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user