From e3e0378857f04d12d94860bf8859a00fb63f43fd Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 11 Jun 2019 14:44:44 +0100 Subject: [PATCH] Add visual json editor --- .../editor-client/locales/en-US/editor.json | 11 +- .../src/js/ui/common/editableList.js | 14 +- .../src/js/ui/common/treeList.js | 344 +++++++++--- .../src/js/ui/common/typedInput.js | 13 + .../editor-client/src/js/ui/editors/json.js | 520 +++++++++++++++++- .../editor-client/src/sass/editor.scss | 111 ++++ 6 files changed, 923 insertions(+), 90 deletions(-) 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 a591ee14a..37e51adf1 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 @@ -739,7 +739,16 @@ }, "jsonEditor": { "title": "JSON editor", - "format": "format JSON" + "format": "format JSON", + "rawMode": "Edit JSON", + "uiMode": "Visual editor", + "insertAbove": "Insert above", + "insertBelow": "Insert below", + "addItem": "Add item", + "copyPath": "Copy path to item", + "expandItems": "Expand items", + "collapseItems": "Collapse items", + "duplicate": "Duplicate" }, "markdownEditor": { "title": "Markdown editor", diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js index 0464866f7..a41ee423d 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/editableList.js @@ -185,6 +185,7 @@ } }, _destroy: function() { + this.topContainer.remove(); }, _refreshFilter: function() { var that = this; @@ -230,7 +231,7 @@ this.uiHeight = desiredHeight; this._resize(); }, - addItem: function(data) { + insertItemAt: function(data,index) { var that = this; data = data || {}; var li = $('
  • '); @@ -248,7 +249,13 @@ }); } if (!added) { - li.appendTo(this.element); + if (index <= 0) { + li.prependTo(this.element); + } else if (index > that.element.children().length-1) { + li.appendTo(this.element); + } else { + li.insertBefore(this.element.children().eq(index)); + } } var row = $('
    ').addClass("red-ui-editableList-item-content").appendTo(li); row.data('data',data); @@ -293,6 +300,9 @@ },0); } }, + addItem: function(data) { + this.insertItemAt(data,this.element.children().length) + }, addItems: function(items) { for (var i=0; i").css({width: "100%", height: "100%"}).treeList({data:[...]}) * treeList.on('treelistselect', function(e,item) { console.log(item)}) * treeList.treeList('data',[ ... ] ) * + * + * After `data` has been added to the tree, each item is augmented the following + * properties and functions: + * + * item.parent - set to the parent item + * item.treeList.container + * item.treeList.label - the label element for the item + * item.treeList.depth - the depth in the tree (0 == root) + * item.treeList.parentList - the editableList instance this item is in + * item.treeList.remove() - removes the item from the tree + * item.treeList.makeLeaf(detachChildElements) - turns an element with children into a leaf node, + * removing the UI decoration etc. + * detachChildElements - any children with custom + * elements will be detached rather than removed + * so jQuery event handlers are preserved in case + * the child elements need to be reattached later + * item.treeList.makeParent(children) - turns an element into a parent node, adding the necessary + * UI decoration. + * item.treeList.insertChildAt(newItem,position,select) - adds a child item an the specified position. + * Optionally selects the item after adding. + * item.treeList.addChild(newItem,select) - appends a child item. + * Optionally selects the item after adding. + * item.treeList.expand(done) - expands the parent item to show children. Optional 'done' callback. + * item.treeList.collapse() - collapse the parent item to hide children. + * + * + * + * */ $.widget( "nodered.treeList", { @@ -116,20 +152,30 @@ }); this._data = []; - this._topList = $('
      ').css({ + this._topList = $('
        ').css({ position:'absolute', top: 0, left:0, right:0, bottom:0 - }).appendTo(wrapper).editableList({ + }).appendTo(wrapper); + + var topListOptions = { addButton: false, scrollOnAdd: false, height: '100%', addItem: function(container,i,item) { that._addSubtree(that._topList,container,item,0); } - }) + }; + if (this.options.rootSortable !== false && !!this.options.sortable) { + topListOptions.sortable = this.options.sortable; + topListOptions.connectWith = '.red-ui-treeList-sortable'; + this._topList.addClass('red-ui-treeList-sortable'); + } + this._topList.editableList(topListOptions) + + if (this.options.data) { this.data(this.options.data); } @@ -171,23 +217,82 @@ }, _addChildren: function(container,parent,children,depth) { var that = this; - var subtree = $('
          ').appendTo(container).editableList({ + var subtree = $('
            ').appendTo(container).editableList({ + connectWith: ".red-ui-treeList-sortable", + sortable: that.options.sortable, addButton: false, scrollOnAdd: false, height: 'auto', addItem: function(container,i,item) { that._addSubtree(subtree,container,item,depth+1); + }, + sortItems: function(data) { + var children = []; + var reparented = []; + data.each(function() { + var child = $(this).data('data'); + children.push(child); + var evt = that._fixDepths(parent,child); + if (evt) { + reparented.push(evt); + } + }) + if (Array.isArray(parent.children)) { + parent.children = children; + } + reparented.forEach(function(evt) { + that._trigger("changeparent",null,evt); + }); + that._trigger("sort",null,parent); } }); + if (!!that.options.sortable) { + subtree.addClass('red-ui-treeList-sortable'); + } for (var i=0;i",{class:"red-ui-treeList-label"}).appendTo(container); item.treeList.label = label; if (item.class) { label.addClass(item.class); } - label.css({ - paddingLeft: (depth*15)+'px' - }) + if (item.gutter) { + item.gutter.css({ + position: 'absolute' + }).appendTo(label) + + } + var labelPaddingWidth = (item.gutter?item.gutter.width()+2:0)+(depth*20); + item.treeList.labelPadding = $('').css({ + display: "inline-block", + width: labelPaddingWidth+'px' + }).appendTo(label); + label.on('mouseover',function(e) { that._trigger('itemmouseover',e,item); }) label.on('mouseout',function(e) { that._trigger('itemmouseout',e,item); }) label.on('mouseenter',function(e) { that._trigger('itemmouseenter',e,item); }) label.on('mouseleave',function(e) { that._trigger('itemmouseleave',e,item); }) - if (item.children) { - item.treeList.addChild = function(newItem,select) { - item.treeList.childList.editableList('addItem',newItem) - newItem.parent = item; - item.children.push(newItem); - if (select) { - setTimeout(function() { - that.select(newItem) - },100); - } + item.treeList.makeLeaf = function(detachChildElements) { + if (!treeListIcon.children().length) { + // Already a leaf + return } - item.treeList.expand = function(done) { - if (container.hasClass("expanded")) { - done && done(); - return; - } - if (!container.hasClass("built") && typeof item.children === 'function') { - container.addClass('built'); - var childrenAdded = false; - var spinner; - var startTime = 0; - item.children(function(children) { - childrenAdded = true; - item.treeList.childList = that._addChildren(container,item,children,depth).hide(); - var delta = Date.now() - startTime; - if (delta < 400) { - setTimeout(function() { - item.treeList.childList.slideDown('fast'); - if (spinner) { - spinner.remove(); - } - },400-delta); - } else { - item.treeList.childList.slideDown('fast'); - if (spinner) { - spinner.remove(); + if (detachChildElements && item.children) { + var detachChildren = function(item) { + if (item.children) { + item.children.forEach(function(child) { + if (child.element) { + child.element.detach(); } - } - done && done(); - that._trigger("childrenloaded",null,item) - },item); - if (!childrenAdded) { - startTime = Date.now(); - spinner = $('
            ').css({ - "background-position": (35+depth*15)+'px 50%' - }).appendTo(container); + if (child.gutter) { + child.gutter.detach(); + } + detachChildren(child); + }); } - - } else { - item.treeList.childList.slideDown('fast'); - done && done(); } - container.addClass("expanded"); + detachChildren(item); } - item.treeList.collapse = function() { - item.treeList.childList.slideUp('fast'); - container.removeClass("expanded"); + treeListIcon.empty(); + if (!item.deferBuild) { + item.treeList.childList.remove(); + delete item.treeList.childList; } - - $('').appendTo(label); + label.off("click.red-ui-treeList-expand"); + treeListIcon.off("click.red-ui-treeList-expand"); + delete item.children; + container.removeClass("expanded"); + } + item.treeList.makeParent = function(children) { + if (treeListIcon.children().length) { + // Already a parent because we've got the angle-right icon + return; + } + $('').appendTo(treeListIcon); + treeListIcon.on("click.red-ui-treeList-expand", function(e) { + e.stopPropagation(); + e.preventDefault(); + if (container.hasClass("expanded")) { + item.treeList.collapse(); + } else { + item.treeList.expand(); + } + }); // $('').appendTo(label); - label.on("click", function(e) { + label.on("click.red-ui-treeList-expand", function(e) { if (container.hasClass("expanded")) { if (item.hasOwnProperty('selected') || label.hasClass("selected")) { item.treeList.collapse(); @@ -285,9 +383,97 @@ item.treeList.expand(); } }) - } else { - $('').appendTo(label); + if (!item.children) { + item.children = children||[]; + item.treeList.childList = that._addChildren(container,item,item.children,depth).hide(); + } } + item.treeList.insertChildAt = function(newItem,position,select) { + newItem.parent = item; + item.children.splice(position,0,newItem); + + if (!item.deferBuild) { + item.treeList.childList.editableList('insertItemAt',newItem,position) + if (select) { + setTimeout(function() { + that.select(newItem) + },100); + } + that._trigger("sort",null,item); + } + } + item.treeList.addChild = function(newItem,select) { + item.treeList.insertChildAt(newItem,item.children.length,select); + } + item.treeList.expand = function(done) { + if (!item.children) { + return; + } + if (container.hasClass("expanded")) { + done && done(); + return; + } + if (!container.hasClass("built") && (item.deferBuild || typeof item.children === 'function')) { + container.addClass('built'); + var childrenAdded = false; + var spinner; + var startTime = 0; + var completeBuild = function(children) { + childrenAdded = true; + item.treeList.childList = that._addChildren(container,item,children,depth).hide(); + var delta = Date.now() - startTime; + if (delta < 400) { + setTimeout(function() { + item.treeList.childList.slideDown('fast'); + if (spinner) { + spinner.remove(); + } + },400-delta); + } else { + item.treeList.childList.slideDown('fast'); + if (spinner) { + spinner.remove(); + } + } + done && done(); + that._trigger("childrenloaded",null,item) + } + if (typeof item.children === 'function') { + item.children(completeBuild,item); + } else { + delete item.deferBuild; + completeBuild(item.children); + } + if (!childrenAdded) { + startTime = Date.now(); + spinner = $('
            ').css({ + "background-position": (35+depth*20)+'px 50%' + }).appendTo(container); + } + + } else { + if (that._loadingData) { + item.treeList.childList.show(); + } else { + item.treeList.childList.slideDown('fast'); + } + done && done(); + } + container.addClass("expanded"); + } + item.treeList.collapse = function() { + if (!item.children) { + return; + } + item.treeList.childList.slideUp('fast'); + container.removeClass("expanded"); + } + + var treeListIcon = $('').appendTo(label); + if (item.children) { + item.treeList.makeParent(); + } + if (item.hasOwnProperty('selected')) { var selectWrapper = $('').appendTo(label); var cb = $('').prop('checked',item.selected).appendTo(selectWrapper); @@ -326,19 +512,22 @@ if (item.icon) { $('').appendTo(label); } - if (item.label || item.sublabel) { - if (item.label) { + if (item.hasOwnProperty('label') || item.hasOwnProperty('sublabel')) { + if (item.hasOwnProperty('label')) { $('').text(item.label).appendTo(label); } - if (item.sublabel) { + if (item.hasOwnProperty('sublabel')) { $('').text(item.sublabel).appendTo(label); } } else if (item.element) { $(item.element).appendTo(label); + $(item.element).css({ + width: "calc(100% - "+(labelPaddingWidth+20+(item.icon?20:0))+"px)" + }) } if (item.children) { - if (Array.isArray(item.children)) { + if (Array.isArray(item.children) && !item.deferBuild) { item.treeList.childList = that._addChildren(container,item,item.children,depth).hide(); } if (item.expanded) { @@ -350,12 +539,17 @@ this._topList.editableList('empty'); }, data: function(items) { + var that = this; if (items !== undefined) { this._data = items; this._topList.editableList('empty'); + this._loadingData = true; for (var i=0; i= 37 && evt.keyCode <= 40) { + evt.stopPropagation(); + } }) this.selectTrigger.on("click", function(event) { event.preventDefault(); + event.stopPropagation(); that._showTypeMenu(); }); this.selectTrigger.on('keydown',function(evt) { @@ -259,6 +265,7 @@ // Down that._showTypeMenu(); } + evt.stopPropagation(); }).on('focus', function() { that.uiSelect.addClass('red-ui-typedInput-focus'); }) @@ -271,12 +278,14 @@ }); this.optionSelectTrigger.on("click", function(event) { event.preventDefault(); + event.stopPropagation(); that._showOptionSelectMenu(); }).on('keydown', function(evt) { if (evt.keyCode === 40) { // Down that._showOptionSelectMenu(); } + evt.stopPropagation(); }).on('blur', function() { that.uiSelect.removeClass('red-ui-typedInput-focus'); }).on('focus', function() { @@ -357,6 +366,7 @@ op.on("click", function(event) { event.preventDefault(); + event.stopPropagation(); callback(opt.value); that._hideMenu(menu); }); @@ -376,9 +386,11 @@ // UP $(this).children(":focus").prev().trigger("focus"); } else if (evt.keyCode === 27) { + // ESCAPE evt.preventDefault(); that._hideMenu(menu); } + evt.stopPropagation(); }) @@ -516,6 +528,7 @@ this.optionMenu.remove(); } this.menu.remove(); + this.uiSelect.remove(); }, types: function(types) { var that = this; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js index 7655e991c..26b84ecaa 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/editors/json.js @@ -16,7 +16,419 @@ (function() { - var template = ''; + // var template = ''; + var template = ''; + + var activeTab; + + function insertNewItem(parent,index,copyIndex) { + var newValue = ""; + + if (parent.children.length > 0) { + switch (parent.children[Math.max(0,Math.min(parent.children.length-1,copyIndex))].type) { + case 'string': newValue = ""; break; + case 'number': newValue = 0; break; + case 'boolean': newValue = true; break; + case 'null': newValue = null; break; + case 'object': newValue = {}; break; + case 'array': newValue = []; break; + } + } + var newKey; + if (parent.type === 'array') { + newKey = parent.children.length; + } else { + var usedKeys = {}; + parent.children.forEach(function(child) { usedKeys[child.key] = true }) + var keyRoot = "item"; + var keySuffix = 2; + newKey = keyRoot; + while(usedKeys[newKey]) { + newKey = keyRoot+"-"+(keySuffix++); + } + } + var newItem = handleItem(newKey,newValue,parent.depth+1,parent); + parent.treeList.insertChildAt(newItem, index, true); + parent.treeList.expand(); + } + function showObjectMenu(button,item) { + var elementPos = button.offset(); + var options = []; + if (item.parent) { + options.push({id:"red-ui-editor-type-json-menu-insert-above", icon:"fa fa-toggle-up", label:RED._('jsonEditor.insertAbove'),onselect:function(){ + var index = item.parent.children.indexOf(item); + insertNewItem(item.parent,index,index); + }}); + options.push({id:"red-ui-editor-type-json-menu-insert-below", icon:"fa fa-toggle-down", label:RED._('jsonEditor.insertBelow'),onselect:function(){ + var index = item.parent.children.indexOf(item)+1; + insertNewItem(item.parent,index,index-1); + }}); + } + if (item.type === 'array' || item.type === 'object') { + options.push({id:"red-ui-editor-type-json-menu-add-child", icon:"fa fa-plus", label:RED._('jsonEditor.addItem'),onselect:function(){ + insertNewItem(item,item.children.length,item.children.length-1); + }}); + } + if (item.parent) { + options.push({id:"red-ui-editor-type-json-menu-copy-path", icon:"fa fa-terminal", label:RED._('jsonEditor.copyPath'),onselect:function(){ + var i = item; + var path = ""; + var newPath; + while(i.parent) { + if (i.parent.type === "array") { + newPath = "["+i.key+"]"; + } else { + if (/^[a-zA-Z_$][0-9a-zA-Z_$]*$/.test(i.key)) { + newPath = i.key; + } else { + newPath = "[\""+i.key.replace(/"/,"\\\"")+"\"]" + } + } + path = newPath+(path.length>0 && path[0] !== "["?".":"")+path; + i = i.parent; + } + RED.clipboard.copyText(path,item.element,"clipboard.copyMessagePath"); + }}); + + options.push({id:"red-ui-editor-type-json-menu-duplicate", icon:"fa fa-copy", label:RED._("jsonEditor.duplicate"),onselect:function(){ + var newKey = item.key; + if (item.parent.type === 'array') { + newKey = parent.children.length; + } else { + var m = /^(.*?)(-(\d+))?$/.exec(newKey); + var usedKeys = {}; + item.parent.children.forEach(function(child) { usedKeys[child.key] = true }) + var keyRoot = m[1]; + var keySuffix = 2; + if (m[3] !== undefined) { + keySuffix = parseInt(m[3]); + } + newKey = keyRoot; + while(usedKeys[newKey]) { + newKey = keyRoot+"-"+(keySuffix++); + } + } + var newItem = handleItem(newKey,convertToObject(item),item.parent.depth+1,item.parent); + var index = item.parent.children.indexOf(item)+1; + + item.parent.treeList.insertChildAt(newItem, index, true); + item.parent.treeList.expand(); + }}); + + options.push({id:"red-ui-editor-type-json-menu-delete", icon:"fa fa-times", label:RED._('common.label.delete'),onselect:function(){ + item.treeList.remove(); + }}); + } + if (item.type === 'array' || item.type === 'object') { + options.push(null) + options.push({id:"red-ui-editor-type-json-menu-expand-children",icon:"fa fa-angle-double-down", label:RED._('jsonEditor.expandItems'),onselect:function(){ + item.treeList.expand(); + item.children.forEach(function(child) { + child.treeList.expand(); + }) + }}); + options.push({id:"red-ui-editor-type-json-menu-collapse-children",icon:"fa fa-angle-double-up", label:RED._('jsonEditor.collapseItems'),onselect:function(){ + item.children.forEach(function(child) { + child.treeList.collapse(); + }) + }}); + } + + var menuOptionMenu = RED.menu.init({ + id:"red-ui-editor-type-json-menu", + options: options + }); + menuOptionMenu.css({ + position: "absolute" + }) + menuOptionMenu.on('mouseleave', function(){ $(this).hide() }); + menuOptionMenu.on('mouseup', function() { $(this).hide() }); + menuOptionMenu.appendTo("body"); + var top = elementPos.top; + var height = menuOptionMenu.height(); + var winHeight = $(window).height(); + if (top+height > winHeight) { + top -= (top+height)-winHeight + 20; + } + menuOptionMenu.css({ + top: top+"px", + left: elementPos.left+"px" + }) + menuOptionMenu.show(); + } + + function parseObject(obj,depth,parent) { + var result = []; + for (var prop in obj) { + if (obj.hasOwnProperty(prop)) { + result.push(handleItem(prop,obj[prop],depth,parent)); + } + } + return result; + } + function parseArray(obj,depth,parent) { + var result = []; + var l = obj.length; + for (var i=0;i'); + if (key != null) { + item.key = key; + var keyText; + if (typeof key === 'string') { + keyText = '"'+key+'"'; + } else { + keyText = key; + } + var keyLabel = $('').text(keyText).appendTo(container); + keyLabel.addClass('red-ui-debug-msg-type-'+(typeof key)); + if (parent && parent.type === "array") { + keyLabel.addClass("red-ui-editor-type-json-editor-label-array-key") + } + + keyLabel.on("click", function(evt) { + if (item.parent.type === 'array') { + return; + } + evt.preventDefault(); + evt.stopPropagation(); + var w = Math.max(150,keyLabel.width()); + var keyInput = $('').css({width:w+"px"}).val(""+item.key).insertAfter(keyLabel).typedInput({types:['str']}); + $(document).on("mousedown.nr-ui-json-editor", function(evt) { + var typedInputElement = keyInput.next(".red-ui-typedInput-container")[0]; + var target = evt.target; + while (target.nodeName !== 'BODY' && target !== typedInputElement && !$(target).hasClass("red-ui-typedInput-options")) { + target = target.parentElement; + } + if (target.nodeName === 'BODY') { + var newKey = keyInput.typedInput("value"); + item.key = newKey; + var keyText; + if (typeof newKey === 'string') { + keyText = '"'+newKey+'"'; + } else { + keyText = newKey; + } + keyLabel.text(keyText); + keyInput.remove(); + keyLabel.show(); + $(document).off("mousedown.nr-ui-json-editor"); + $(document).off("keydown.nr-ui-json-editor"); + } + }); + $(document).on("keydown.nr-ui-json-editor",function(evt) { + if (evt.keyCode === 27) { + // Escape + keyInput.remove(); + keyLabel.show(); + $(document).off("mousedown.nr-ui-json-editor"); + $(document).off("keydown.nr-ui-json-editor"); + } + }); + keyLabel.hide(); + }); + $('').text(" : ").appendTo(container); + } + + if (Array.isArray(val)) { + item.expanded = depth < 2; + item.type = "array"; + item.deferBuild = depth >= 2; + item.children = parseArray(val,depth+1,item); + } else if (val !== null && item.type === "object") { + item.expanded = depth < 2; + item.children = parseObject(val,depth+1,item); + item.deferBuild = depth >= 2; + } else { + item.value = val; + if (val === null) { + item.type = 'null' + } + } + + var valType; + var valValue = ""; + var valClass; + switch(item.type) { + case 'string': valType = 'str'; valValue = '"'+item.value+'"'; valClass = "red-ui-debug-msg-type-string"; break; + case 'number': valType = 'num'; valValue = item.value; valClass = "red-ui-debug-msg-type-number";break; + case 'boolean': valType = 'bool'; valValue = item.value; valClass = "red-ui-debug-msg-type-other";break; + case 'null': valType = item.type; valValue = item.type; valClass = "red-ui-debug-msg-type-null";break; + case 'object': + valType = item.type; + valValue = item.type;//+"{"+item.children.length+"}"; + valClass = "red-ui-debug-msg-type-meta"; + break; + case 'array': + valType = item.type; + valValue = item.type+"["+item.children.length+"]"; + valClass = "red-ui-debug-msg-type-meta"; + break; + } + // + var orphanedChildren; + var valueLabel = $('').addClass(valClass).text(valValue).appendTo(container); + valueLabel.on("click", function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (valType === 'str') { + valValue = valValue.substring(1,valValue.length-1); + } else if (valType === 'array') { + valValue = ""; + } else if (valType === 'object') { + valValue = ""; + } + var w = Math.max(150,valueLabel.width()); + var val = $('').css({width:w+"px"}).val(""+valValue).insertAfter(valueLabel).typedInput({ + types:[ + 'str','num','bool', + {value:"null",label:"null",hasValue:false}, + {value:"array",label:"array",hasValue:false}, + {value:"object",label:"object",hasValue:false} + ], + default: valType + }); + $(document).on("mousedown.nr-ui-json-editor", function(evt) { + var typedInputElement = val.next(".red-ui-typedInput-container")[0]; + var target = evt.target; + while (target.nodeName !== 'BODY' && target !== typedInputElement && !$(target).hasClass("red-ui-typedInput-options")) { + target = target.parentElement; + } + if (target.nodeName === 'BODY') { + valType = val.typedInput("type"); + valValue = val.typedInput("value"); + if (valType === 'num') { + valValue = valValue.trim(); + if (isNaN(valValue)) { + valType = 'str'; + } else if (valValue === "") { + valValue = 0; + } + } + item.value = valValue; + var valClass; + switch(valType) { + case 'str': item.children && (orphanedChildren = item.children); item.treeList.makeLeaf(true); item.type = "string"; valClass = "red-ui-debug-msg-type-string"; valValue = '"'+valValue+'"'; break; + case 'num': item.children && (orphanedChildren = item.children); item.treeList.makeLeaf(true); item.type = "number"; valClass = "red-ui-debug-msg-type-number"; break; + case 'bool': item.children && (orphanedChildren = item.children); item.treeList.makeLeaf(true); item.type = "boolean"; valClass = "red-ui-debug-msg-type-other"; item.value = (valValue === "true"); break; + case 'null': item.children && (orphanedChildren = item.children); item.treeList.makeLeaf(true); item.type = "null"; valClass = "red-ui-debug-msg-type-null"; item.value = valValue = "null"; break; + case 'object': + item.treeList.makeParent(orphanedChildren); + item.type = "object"; + valClass = "red-ui-debug-msg-type-meta"; + item.value = valValue = "object"; + item.children.forEach(function(child,i) { + if (child.hasOwnProperty('_key')) { + child.key = child._key; + delete child._key; + var keyText; + var keyLabel = child.element.find(".red-ui-editor-type-json-editor-label-key"); + keyLabel.removeClass("red-ui-editor-type-json-editor-label-array-key"); + if (typeof child.key === 'string') { + keyText = '"'+child.key+'"'; + keyLabel.addClass('red-ui-debug-msg-type-string'); + keyLabel.removeClass('red-ui-debug-msg-type-number'); + } else { + keyText = child.key; + keyLabel.removeClass('red-ui-debug-msg-type-string'); + keyLabel.addClass('red-ui-debug-msg-type-number'); + } + keyLabel.text(keyText); + } + }) + break; + case 'array': + item.treeList.makeParent(orphanedChildren); + item.type = "array"; + valClass = "red-ui-debug-msg-type-meta"; + item.value = valValue = "array["+(item.children.length)+"]"; + item.children.forEach(function(child,i) { + child._key = child.key; + child.key = i; + child.element.find(".red-ui-editor-type-json-editor-label-key") + .addClass("red-ui-editor-type-json-editor-label-array-key") + .text(""+child.key) + .removeClass('red-ui-debug-msg-type-string') + .addClass('red-ui-debug-msg-type-number'); + }) + break; + } + valueLabel.text(valValue).removeClass().addClass("red-ui-editor-type-json-editor-label-value "+valClass); + val.remove(); + valueLabel.show(); + $(document).off("mousedown.nr-ui-json-editor"); + $(document).off("keydown.nr-ui-json-editor"); + } + }) + + $(document).on("keydown.nr-ui-json-editor",function(evt) { + if (evt.keyCode === 27) { + // Escape + val.remove(); + valueLabel.show(); + if (valType === 'str') { + valValue = '"'+valValue+'"'; + } + $(document).off("mousedown.nr-ui-json-editor"); + $(document).off("keydown.nr-ui-json-editor"); + } + }); + valueLabel.hide(); + }) + item.gutter = $(''); + + if (parent) {//red-ui-editor-type-json-editor-item-handle + $('').appendTo(item.gutter); + } else { + $('').appendTo(item.gutter); + } + $('').appendTo(item.gutter).on("click", function(evt) { + evt.preventDefault(); + evt.stopPropagation(); + showObjectMenu($(this), item); + }); + item.element = container; + return item; + } + function convertToObject(item) { + var element; + switch (item.type) { + case 'string': element = item.value; break; + case 'number': element = Number(item.value); break; + case 'boolean': element = item.value; break; + case 'null': element = null; break; + case 'object': + element = {}; + item.children.forEach(function(child) { + element[child.key] = convertToObject(child); + }) + break; + case 'array': + element = item.children.map(function(child) { + return convertToObject(child); + }) + break; + } + return element; + } var definition = { show: function(options) { @@ -41,9 +453,11 @@ return false; } } + var rootNode; + var trayOptions = { title: options.title, - width: "inherit", + width: options.width||700, buttons: [ { id: "node-dialog-cancel", @@ -60,25 +474,60 @@ if (options.requireValid && !checkValid()) { return; } - onComplete(expressionEditor.getValue()); + var result; + if (activeTab === "json-ui") { + if (rootNode) { + result = JSON.stringify(convertToObject(rootNode),null,4); + } else { + result = expressionEditor.getValue(); + } + } else if (activeTab === "json-raw") { + result = expressionEditor.getValue(); + } + onComplete && onComplete(result); RED.tray.close(); } } ], resize: function(dimensions) { - var rows = $("#dialog-form>div:not(.node-text-editor-row)"); - var editorRow = $("#dialog-form>div.node-text-editor-row"); - var height = $("#dialog-form").height(); - for (var i=0;i').appendTo(container).treeList({ + rootSortable: false, + sortable: ".red-ui-editor-type-json-editor-item-handle", + }).on("treelistchangeparent", function(event, evt) { + if (evt.old.type === 'array') { + evt.old.element.find(".red-ui-editor-type-json-editor-label-type").text("array["+evt.old.children.length+"]"); + } + if (evt.item.parent.type === 'array') { + evt.item.parent.element.find(".red-ui-editor-type-json-editor-label-type").text("array["+evt.item.parent.children.length+"]"); + } + }).on("treelistsort", function(event, item) { + item.children.forEach(function(child,i) { + if (item.type === 'array') { + child.key = i; + child.element.find(".red-ui-editor-type-json-editor-label-key") + .text(child.key) + .removeClass('red-ui-debug-msg-type-string') + .addClass('red-ui-debug-msg-type-number'); + } else { + child.element.find(".red-ui-editor-type-json-editor-label-key") + .text('"'+child.key+'"') + .removeClass('red-ui-debug-msg-type-number') + .addClass('red-ui-debug-msg-type-string'); + } + }) + }); + + expressionEditor = RED.editor.createEditor({ id: 'node-input-json', value: "", @@ -103,9 +552,56 @@ expressionEditor.getSession().setValue(v||"",-1); }); dialogForm.i18n(); + + var finishedBuild = false; + var tabs = RED.tabs.create({ + element: $("#red-ui-editor-type-json-tabs"), + onchange:function(tab) { + activeTab = tab.id; + $(".red-ui-editor-type-json-tab-content").hide(); + if (finishedBuild) { + if (tab.id === "json-raw") { + if (rootNode) { + var result = JSON.stringify(convertToObject(rootNode),null,4); + expressionEditor.getSession().setValue(result||"",-1); + } + + } else if (tab.id === "json-ui") { + var raw = expressionEditor.getValue().trim() ||"{}"; + try { + var parsed = JSON.parse(raw); + rootNode = handleItem(null,parsed,0,null); + rootNode.class = "red-ui-editor-type-json-root-node" + list.treeList('data',[rootNode]); + } catch(err) { + rootNode = null; + list.treeList('data',[{ + label: "Invalid JSON: "+err.toString() + }]); + } + } + } + tab.content.show(); + trayOptions.resize(); + } + }) + + tabs.addTab({ + id: 'json-raw', + label: RED._('jsonEditor.rawMode'), + content: $("#red-ui-editor-type-json-tab-raw") + }); + tabs.addTab({ + id: 'json-ui', + label: RED._('jsonEditor.uiMode'), + content: $("#red-ui-editor-type-json-tab-ui") + }); + finishedBuild = true; + + }, close: function() { - expressionEditor.destroy(); + // expressionEditor.destroy(); if (options.onclose) { options.onclose(); } diff --git a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss index d3f729a96..83c72b07b 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/editor.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/editor.scss @@ -461,3 +461,114 @@ button.red-ui-button-small margin: 2px; } } + + +.red-ui-editor input.red-ui-editor-type-json-editor-key { + width: 150px; +} + +.red-ui-editor-type-json-editor { + height: calc(100% - 10px); + .red-ui-treeList-container { + background: $secondary-background; + } + .red-ui-treeList-label { + padding-top: 0; + padding-bottom: 0; + white-space: nowrap; + min-height: 35px; + .red-ui-treeList-icon:before { + content:''; + display: inline-block; + height: 35px; + vertical-align: middle; + } + > span, > span > span { + vertical-align: middle; + } + &:hover, &:hover .red-ui-treeList-sublabel-text { + background: $secondary-background-disabled; + .red-ui-editor-type-json-editor-item-gutter { + > span, > button { + display: inline-block; + } + } + } + &.selected { + .red-ui-editor-type-json-editor-item-gutter { + background: $secondary-background-hover; + } + &:hover { + .red-ui-editor-type-json-editor-item-gutter { + background: $secondary-background-selected; + } + } + } + &.red-ui-editor-type-json-root-node { + .red-ui-editor-type-json-editor-item-gutter { + > span, > button { + display: inline-block; + } + } + } + } +} +.red-ui-editor-type-json-editor-controls { + height: 34px; + line-height: 34px; + display: none; +} +.red-ui-editor-type-json-editor-key { + width: 100px; +} +.red-ui-editor-type-json-editor-label { + display: inline-block; + white-space: pre-wrap; +} +.red-ui-editor-type-json-editor-label-value { + min-width: 200px; +} +.red-ui-editor-type-json-editor-label-value, +.red-ui-editor-type-json-editor-label-key { + display: inline-block; + box-sizing: border-box; + min-height: 34px; + line-height: 30px; + padding: 0 2px; + border: 2px solid rgba(0,0,0,0); + border-radius: 3px; + &:not(.red-ui-editor-type-json-editor-label-array-key):hover { + border-color: $list-item-background-hover; + border-style: dashed; + } +} +.red-ui-editor-type-json-editor-item-gutter { + width: 48px; + padding-left: 4px; + + height: 100%; + line-height: 35px; + color: $tertiary-text-color; + background: $secondary-background-disabled; + > span { + display: inline-block; + height: 35px; + line-height: 35px; + width: 20px; + text-align:center; + } + > span, > button { + display: none; + } +} + + + + +.red-ui-editor-type-json-editor-item-handle { + cursor: move; +} +.red-ui-editor-type-json-tab-content { + position: relative; + height: calc(100% - 40px); +}