From c0d8f904b3e0a1f3ad7d84899615a6a837a3917b Mon Sep 17 00:00:00 2001 From: Nick O'Leary Date: Tue, 30 Oct 2018 22:18:16 +0000 Subject: [PATCH] Support ctrl-click selection of flow tabs --- .../@node-red/editor-client/src/js/history.js | 11 +- .../editor-client/src/js/ui/clipboard.js | 26 ++- .../editor-client/src/js/ui/common/tabs.js | 138 ++++++++++++-- .../editor-client/src/js/ui/subflow.js | 4 +- .../editor-client/src/js/ui/tab-info.js | 30 +++- .../@node-red/editor-client/src/js/ui/view.js | 169 +++++++++++------- .../editor-client/src/js/ui/workspaces.js | 21 ++- .../editor-client/src/sass/colors.scss | 1 + .../editor-client/src/sass/tabs.scss | 42 ++++- 9 files changed, 343 insertions(+), 99 deletions(-) diff --git a/packages/node_modules/@node-red/editor-client/src/js/history.js b/packages/node_modules/@node-red/editor-client/src/js/history.js index 20e1cb52f..6adfb6d7f 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/history.js +++ b/packages/node_modules/@node-red/editor-client/src/js/history.js @@ -91,12 +91,15 @@ RED.history = (function() { } else if (ev.t == "delete") { if (ev.workspaces) { for (i=0;i 0) { subflow = RED.nodes.subflow(ev.subflowInputs[0].z); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js index 12ccb3cb3..7092b4eba 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/clipboard.js @@ -291,9 +291,18 @@ RED.clipboard = (function() { var flow = ""; var nodes = null; if (type === 'export-range-selected') { - var selection = RED.view.selection(); + var selection = RED.workspaces.selection(); + if (selection.length > 0) { + nodes = []; + selection.forEach(function(n) { + nodes.push(n); + nodes = nodes.concat(RED.nodes.filterNodes({z:n.id})); + }); + } else { + nodes = RED.view.selection().nodes||[]; + } // Don't include the subflow meta-port nodes in the exported selection - nodes = RED.nodes.createExportableNodeSet(selection.nodes.filter(function(n) { return n.type !== 'subflow'})); + nodes = RED.nodes.createExportableNodeSet(nodes.filter(function(n) { return n.type !== 'subflow'})); } else if (type === 'export-range-flow') { var activeWorkspace = RED.workspaces.active(); nodes = RED.nodes.filterNodes({z:activeWorkspace}); @@ -323,12 +332,17 @@ RED.clipboard = (function() { $("#clipboard-dialog-cancel").hide(); $("#clipboard-dialog-copy").hide(); $("#clipboard-dialog-close").hide(); - var selection = RED.view.selection(); - if (selection.nodes) { + var selection = RED.workspaces.selection(); + if (selection.length > 0) { $("#export-range-selected").click(); } else { - $("#export-range-selected").addClass('disabled').removeClass('selected'); - $("#export-range-flow").click(); + selection = RED.view.selection(); + if (selection.nodes) { + $("#export-range-selected").click(); + } else { + $("#export-range-selected").addClass('disabled').removeClass('selected'); + $("#export-range-flow").click(); + } } if (format === "export-format-full") { $("#export-format-full").click(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js index 1e23febaa..1b7d2777a 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/common/tabs.js @@ -161,11 +161,86 @@ RED.tabs = (function() { ul.children().first().addClass("active"); ul.children().addClass("red-ui-tab"); - function onTabClick() { - if (options.onclick) { - options.onclick(tabs[$(this).attr('href').slice(1)]); + function getSelection() { + var selection = ul.find("li.red-ui-tab.selected"); + var selectedTabs = []; + selection.each(function() { + selectedTabs.push(tabs[$(this).find('a').attr('href').slice(1)]) + }) + return selectedTabs; + } + + function selectionChanged() { + options.onselect(getSelection()); + } + + function onTabClick(evt) { + evt.preventDefault(); + var currentTab = ul.find("li.red-ui-tab.active"); + var thisTab = $(this).parent(); + var fireSelectionChanged = false; + if (options.onselect) { + if (evt.metaKey) { + if (thisTab.hasClass("selected")) { + thisTab.removeClass("selected"); + if (thisTab[0] !== currentTab[0]) { + // Deselect background tab + // - don't switch to it + selectionChanged(); + return; + } else { + // Deselect current tab + // - if nothing remains selected, do nothing + // - otherwise switch to first selected tab + var selection = ul.find("li.red-ui-tab.selected"); + if (selection.length === 0) { + selectionChanged(); + return; + } + thisTab = selection.first(); + } + } else { + if (!currentTab.hasClass("selected")) { + var currentTabObj = tabs[currentTab.find('a').attr('href').slice(1)]; + // Auto select current tab + currentTab.addClass("selected"); + } + thisTab.addClass("selected"); + } + fireSelectionChanged = true; + } else if (evt.shiftKey) { + if (currentTab[0] !== thisTab[0]) { + var firstTab,lastTab; + if (currentTab.index() < thisTab.index()) { + firstTab = currentTab; + lastTab = thisTab; + } else { + firstTab = thisTab; + lastTab = currentTab; + } + ul.find("li.red-ui-tab").removeClass("selected"); + firstTab.addClass("selected"); + lastTab.addClass("selected"); + firstTab.nextUntil(lastTab).addClass("selected"); + } + fireSelectionChanged = true; + } else { + var selection = ul.find("li.red-ui-tab.selected"); + if (selection.length > 0) { + selection.removeClass("selected"); + fireSelectionChanged = true; + } + } + } + + var thisTabA = thisTab.find("a"); + if (options.onclick) { + options.onclick(tabs[thisTabA.attr('href').slice(1)]); + } + activateTab(thisTabA); + if (fireSelectionChanged) { + selectionChanged(); } - activateTab($(this)); return false; } @@ -186,7 +261,12 @@ RED.tabs = (function() { } } } - function onTabDblClick() { + function onTabDblClick(evt) { + evt.preventDefault(); + evt.stopPropagation(); + if (evt.metaKey || evt.shiftKey) { + return; + } if (options.ondblclick) { options.ondblclick(tabs[$(this).attr('href').slice(1)]); } @@ -288,23 +368,23 @@ RED.tabs = (function() { currentActiveTabWidth = 0; } } - if (options.collapsible) { - console.log(currentTabWidth); - } + // if (options.collapsible) { + // console.log(currentTabWidth); + // } tabs.css({width:currentTabWidth}); if (tabWidth < 50) { - ul.find(".red-ui-tab-close").hide(); + // ul.find(".red-ui-tab-close").hide(); ul.find(".red-ui-tab-icon").hide(); ul.find(".red-ui-tab-label").css({paddingLeft:Math.min(12,Math.max(0,tabWidth-38))+"px"}) } else { - ul.find(".red-ui-tab-close").show(); + // ul.find(".red-ui-tab-close").show(); ul.find(".red-ui-tab-icon").show(); ul.find(".red-ui-tab-label").css({paddingLeft:""}) } if (currentActiveTabWidth !== 0) { ul.find("li.red-ui-tab.active").css({"width":options.minimumActiveTabWidth}); - ul.find("li.red-ui-tab.active .red-ui-tab-close").show(); + // ul.find("li.red-ui-tab.active .red-ui-tab-close").show(); ul.find("li.red-ui-tab.active .red-ui-tab-icon").show(); ul.find("li.red-ui-tab.active .red-ui-tab-label").css({paddingLeft:""}) } @@ -319,6 +399,13 @@ RED.tabs = (function() { function removeTab(id) { + if (options.onselect) { + var selection = ul.find("li.red-ui-tab.selected"); + if (selection.length > 0) { + selection.removeClass("selected"); + selectionChanged(); + } + } var li = ul.find("a[href='#"+id+"']").parent(); if (li.hasClass("active")) { var tab = li.prev(); @@ -341,14 +428,24 @@ RED.tabs = (function() { return { addTab: function(tab,targetIndex) { + if (options.onselect) { + var selection = ul.find("li.red-ui-tab.selected"); + if (selection.length > 0) { + selection.removeClass("selected"); + selectionChanged(); + } + } tabs[tab.id] = tab; var li = $("
  • ",{class:"red-ui-tab"}); - if (targetIndex === undefined) { - li.appendTo(ul); - } else if (targetIndex === 0) { + if (ul.children().length === 0) { + targetIndex = undefined; + } + if (targetIndex === 0) { li.prependTo(ul); - } else { + } else if (targetIndex > 0) { li.insertAfter(ul.find("li:nth-child("+(targetIndex)+")")); + } else { + li.appendTo(ul); } li.attr('id',"red-ui-tab-"+(tab.id.replace(".","-"))); li.data("tabId",tab.id); @@ -400,15 +497,23 @@ RED.tabs = (function() { } link.on("click",onTabClick); link.on("dblclick",onTabDblClick); + + if (tab.closeable) { + li.addClass("red-ui-tabs-closeable") var closeLink = $("",{href:"#",class:"red-ui-tab-close"}).appendTo(li); closeLink.append(''); - closeLink.on("click",function(event) { event.preventDefault(); removeTab(tab.id); }); } + + var badges = $('').appendTo(li); + if (options.onselect) { + $('').appendTo(badges); + $('').appendTo(badges); + } if (options.onadd) { options.onadd(tab); } @@ -516,6 +621,7 @@ RED.tabs = (function() { tab.find("span.bidiAware").text(label).attr('dir', RED.text.bidi.resolveBaseTextDir(label)); updateTabWidths(); }, + selection: getSelection, order: function(order) { var existingTabOrder = $.makeArray(ul.children().map(function() { return $(this).data('tabId');})); if (existingTabOrder.length !== order.length) { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js index 7eaa9627d..2acbd88b5 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/subflow.js @@ -370,9 +370,7 @@ RED.subflow = (function() { return { nodes:removedNodes, links:removedLinks, - subflow: { - subflow: activeSubflow - } + subflows: [activeSubflow] } } function init() { diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js index d2534538a..31bd2cd24 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/tab-info.js @@ -158,11 +158,37 @@ RED.sidebar.info = (function() { } else if (Array.isArray(node)) { // Multiple things selected // - hide help and info sections + + var types = { + nodes:0, + flows:0, + subflows:0 + } + node.forEach(function(n) { + if (n.type === 'tab') { + types.flows++; + types.nodes += RED.nodes.filterNodes({z:n.id}).length; + } else if (n.type === 'subflow') { + types.subflows++; + } else { + types.nodes++; + } + }); helpSection.container.hide(); infoSection.container.hide(); // - show the count of selected nodes propRow = $(''+RED._("sidebar.info.selection")+"").appendTo(tableBody); - $(propRow.children()[1]).text(RED._("sidebar.info.nodes",{count:node.length})) + + var counts = $('
    ').appendTo($(propRow.children()[1])); + if (types.flows > 0) { + $('
    ').text(RED._("clipboard.flow",{count:types.flows})).appendTo(counts); + } + if (types.subflows > 0) { + $('
    ').text(RED._("clipboard.subflow",{count:types.subflows})).appendTo(counts); + } + if (types.nodes > 0) { + $('
    ').text(RED._("clipboard.node",{count:types.nodes})).appendTo(counts); + } } else { // A single 'thing' selected. @@ -458,6 +484,8 @@ RED.sidebar.info = (function() { } else { refresh(selection.nodes); } + } else if (selection.flows || selection.subflows) { + refresh(selection.flows); } else { var activeWS = RED.workspaces.active(); diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js index 474d9f1a1..7cc283cc1 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/view.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/view.js @@ -325,7 +325,9 @@ RED.view = (function() { mouse_position[0] += scrollDeltaLeft; mouse_position[1] += scrollDeltaTop; } - clearSelection(); + if (RED.workspaces.selection().length === 0) { + clearSelection(); + } RED.nodes.eachNode(function(n) { n.dirty = true; }); @@ -1197,76 +1199,81 @@ RED.view = (function() { function updateSelection() { var selection = {}; - if (moving_set.length > 0) { - selection.nodes = moving_set.map(function(n) { return n.n;}); - } - if (selected_link != null) { - selection.link = selected_link; - } - var activeWorkspace = RED.workspaces.active(); - activeLinks = RED.nodes.filterLinks({ - source:{z:activeWorkspace}, - target:{z:activeWorkspace} - }); - var tabOrder = RED.nodes.getWorkspaceOrder(); - var currentLinks = activeLinks; - var addedLinkLinks = {}; - activeFlowLinks = []; - for (var i=0;i 0) { + selection.nodes = moving_set.map(function(n) { return n.n;}); + } + if (selected_link != null) { + selection.link = selected_link; + } + var activeWorkspace = RED.workspaces.active(); + activeLinks = RED.nodes.filterLinks({ + source:{z:activeWorkspace}, + target:{z:activeWorkspace} + }); + var tabOrder = RED.nodes.getWorkspaceOrder(); + var currentLinks = activeLinks; + var addedLinkLinks = {}; + activeFlowLinks = []; + for (var i=0;i 0) { - activeFlowLinks.push({ - refresh: Math.floor(Math.random()*10000), - node: linkNode, - links: offFlowLinks//offFlows.map(function(i) { return {id:i,links:offFlowLinks[i]};}) }); + var offFlows = Object.keys(offFlowLinks); + // offFlows.sort(function(A,B) { + // return tabOrder.indexOf(A) - tabOrder.indexOf(B); + // }); + if (offFlows.length > 0) { + activeFlowLinks.push({ + refresh: Math.floor(Math.random()*10000), + node: linkNode, + links: offFlowLinks//offFlows.map(function(i) { return {id:i,links:offFlowLinks[i]};}) + }); + } } } + } else { + selection.flows = workspaceSelection; } var selectionJSON = activeWorkspace+":"+JSON.stringify(selection,function(key,value) { - if (key === 'nodes') { + if (key === 'nodes' || key === 'flows') { return value.map(function(n) { return n.id }) } else if (key === 'link') { return value.source.id+":"+value.sourcePort+":"+value.target.id; @@ -1347,7 +1354,45 @@ RED.view = (function() { portLabelHover.remove(); portLabelHover = null; } - if (moving_set.length > 0 || selected_link != null) { + var workspaceSelection = RED.workspaces.selection(); + if (workspaceSelection.length > 0) { + var workspaceCount = 0; + workspaceSelection.forEach(function(ws) { if (ws.type === 'tab') workspaceCount++ }); + if (workspaceCount === RED.workspaces.count()) { + // Cannot delete all workspaces + return; + } + var historyEvent = { + t: 'delete', + dirty: RED.nodes.dirty(), + nodes: [], + links: [], + workspaces: [], + subflows: [] + } + var workspaceOrder = RED.nodes.getWorkspaceOrder().slice(0); + + for (var i=0;i 0 || selected_link != null) { var result; var removedNodes = []; var removedLinks = []; diff --git a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js index 2e8417fa8..bc64e18d0 100644 --- a/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js +++ b/packages/node_modules/@node-red/editor-client/src/js/ui/workspaces.js @@ -22,7 +22,7 @@ RED.workspaces = (function() { function addWorkspace(ws,skipHistoryEntry,targetIndex) { if (ws) { - workspace_tabs.addTab(ws); + workspace_tabs.addTab(ws,targetIndex); workspace_tabs.resize(); } else { var tabId = RED.nodes.id(); @@ -46,6 +46,8 @@ RED.workspaces = (function() { if (workspaceTabCount === 1) { return; } + var workspaceOrder = RED.nodes.getWorkspaceOrder(); + ws._index = workspaceOrder.indexOf(ws.id); removeWorkspace(ws); var historyEvent = RED.nodes.removeWorkspace(ws.id); historyEvent.t = 'delete'; @@ -287,6 +289,20 @@ RED.workspaces = (function() { RED.nodes.dirty(true); setWorkspaceOrder(newOrder); }, + onselect: function(selectedTabs) { + RED.view.select(false) + if (selectedTabs.length === 0) { + // TODO: workspace-toolbar + $("#chart svg").css({"pointer-events":"auto",filter:"none"}) + $("#palette-container").css({"pointer-events":"auto",filter:"none"}) + $(".sidebar-shade").hide(); + } else { + RED.view.select(false) + $("#chart svg").css({"pointer-events":"none",filter:"opacity(60%)"}) + $("#palette-container").css({"pointer-events":"none",filter:"opacity(60%)"}) + $(".sidebar-shade").show(); + } + }, minimumActiveTabWidth: 150, scrollable: true, addButton: "core:add-flow", @@ -366,6 +382,9 @@ RED.workspaces = (function() { active: function() { return activeWorkspace }, + selection: function() { + return workspace_tabs.selection(); + }, show: function(id) { if (!workspace_tabs.contains(id)) { var sf = RED.nodes.subflow(id); diff --git a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss index 4d3ab2033..3066e247c 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/colors.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/colors.scss @@ -35,6 +35,7 @@ $primary-border-color: #bbbbbb; $secondary-border-color: #dddddd; $tab-background-active: #fff; +$tab-background-selected: #f9f9f9; $tab-background-inactive: #f0f0f0; $tab-background-hover: #ddd; diff --git a/packages/node_modules/@node-red/editor-client/src/sass/tabs.scss b/packages/node_modules/@node-red/editor-client/src/sass/tabs.scss index 2dfe511e2..3a0b30a12 100644 --- a/packages/node_modules/@node-red/editor-client/src/sass/tabs.scss +++ b/packages/node_modules/@node-red/editor-client/src/sass/tabs.scss @@ -57,7 +57,14 @@ overflow: hidden; white-space: nowrap; position: relative; - + &.red-ui-tabs-closeable:hover { + .red-ui-tabs-badges { + display: none; + } + .red-ui-tab-close { + display: block; + } + } a.red-ui-tab-label { display: block; font-size: 14px; @@ -97,6 +104,19 @@ opacity: 0.2; } } + &.selected { + &:not(.active) { + background: $tab-background-selected; + } + font-weight: bold; + .red-ui-tabs-badge-selected { + display: inline; + } + .red-ui-tabs-badge-changed { + display: none; + } + + } &:not(.active) a:hover { color: $workspace-button-color-hover; background: $tab-background-hover; @@ -282,11 +302,20 @@ i.red-ui-tab-icon { .red-ui-tabs-badges { position: absolute; - top:2px; - right:2px; + top:0px; + right:0px; + width: 20px; + pointer-events: none; + display: block; + height: 30px; + line-height: 28px; + text-align: center; + padding:0px; + color: #aaa; } -.red-ui-tab-closeable .red-ui-tabs-badges { - right: 22px; + +.red-ui-tabs-badges i { + display: none; } .red-ui-tab.node_changed img.node_changed { @@ -303,13 +332,14 @@ i.red-ui-tab-icon { vertical-align: top; } + .red-ui-tab-close { + display: none; background: $tab-background-inactive; opacity: 0.8; position: absolute; right: 0px; top: 0px; - display: block; width: 20px; height: 30px; line-height: 28px;