@node-red/editor-client 4.1.2 → 5.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/public/red/red.js CHANGED
@@ -878,7 +878,7 @@ var RED = (function() {
878
878
  RED.user.init();
879
879
  RED.notifications.init();
880
880
  RED.library.init();
881
- RED.palette.init();
881
+ RED.sidebar.init();
882
882
  RED.eventLog.init();
883
883
 
884
884
  if (RED.settings.get('externalModules.palette.allowInstall', true) !== false) {
@@ -887,7 +887,6 @@ var RED = (function() {
887
887
  console.log("Palette editor disabled");
888
888
  }
889
889
 
890
- RED.sidebar.init();
891
890
 
892
891
  if (RED.settings.theme("projects.enabled",false)) {
893
892
  RED.projects.init();
@@ -904,23 +903,35 @@ var RED = (function() {
904
903
  RED.diagnostics.init();
905
904
  RED.diff.init();
906
905
 
907
-
908
906
  RED.deploy.init(RED.settings.theme("deployButton",null));
907
+ RED.keyboard.init(() => {
908
+ buildMainMenu();
909
909
 
910
- RED.keyboard.init(buildMainMenu);
911
- RED.envVar.init();
910
+ // Register the core set of sidebar panels now the menu is ready to receive items
911
+ RED.palette.init();
912
+ RED.sidebar.info.init();
913
+ RED.sidebar.info.outliner.init();
914
+ RED.sidebar.help.init();
915
+ RED.sidebar.config.init();
916
+ RED.sidebar.context.init();
917
+ // hide sidebar at start if screen rather narrow...
918
+ if ($("#red-ui-editor").width() < 600) { RED.menu.setSelected("menu-item-sidebar", false); }
912
919
 
913
- RED.nodes.init();
914
- RED.runtime.init()
920
+ RED.envVar.init();
915
921
 
916
- if (RED.settings.theme("multiplayer.enabled",false)) {
917
- RED.multiplayer.init()
918
- }
919
- RED.comms.connect();
922
+ RED.nodes.init();
923
+ RED.runtime.init()
924
+
925
+ if (RED.settings.theme("multiplayer.enabled",false)) {
926
+ RED.multiplayer.init()
927
+ }
928
+ RED.comms.connect();
920
929
 
921
- $("#red-ui-main-container").show();
930
+ $("#red-ui-main-container").show();
931
+ RED.events.emit("sidebar:resize")
922
932
 
923
- loadPluginList();
933
+ loadPluginList();
934
+ });
924
935
  }
925
936
 
926
937
 
@@ -929,13 +940,18 @@ var RED = (function() {
929
940
  var logo = $('<span class="red-ui-header-logo"></span>').appendTo(header);
930
941
  $('<ul class="red-ui-header-toolbar hide"></ul>').appendTo(header);
931
942
  $('<div id="red-ui-header-shade" class="hide"></div>').appendTo(header);
932
- $('<div id="red-ui-main-container" class="red-ui-sidebar-closed hide">'+
943
+ $('<div id="red-ui-main-container">'+
944
+ '<div id="red-ui-sidebar-left"></div>'+
933
945
  '<div id="red-ui-workspace"></div>'+
934
- '<div id="red-ui-editor-stack" tabindex="-1"></div>'+
935
- '<div id="red-ui-palette"></div>'+
936
946
  '<div id="red-ui-sidebar"></div>'+
937
- '<div id="red-ui-sidebar-separator"></div>'+
947
+ '<div id="red-ui-editor-stack" tabindex="-1"></div>'+
948
+ // '<div id="red-ui-palette"></div>'+
938
949
  '</div>').appendTo(options.target);
950
+
951
+ // Don't use the `hide` class on this container, as the show reverts it to block rather
952
+ // than the expected flex. So hide via jQuery as it'll track the show state internally.
953
+ options.target.find('#red-ui-main-container').hide()
954
+
939
955
  $('<div id="red-ui-editor-plugin-configs"></div>').appendTo(options.target);
940
956
  $('<div id="red-ui-editor-node-configs"></div>').appendTo(options.target);
941
957
  $('<div id="red-ui-full-shade" class="hide"></div>').appendTo(options.target);
@@ -13203,7 +13219,7 @@ RED.menu = (function() {
13203
13219
  } else {
13204
13220
  for (var i=0;i<groupItems.length;i++) {
13205
13221
  var groupItem = groupItems[i];
13206
- var label = $(groupItem).find(".red-ui-menu-label").html();
13222
+ var label = $(groupItem).find(".red-ui-menu-label span").text();
13207
13223
  if (opt.label < label) {
13208
13224
  $(groupItem).before(item);
13209
13225
  break;
@@ -13293,6 +13309,13 @@ RED.panels = (function() {
13293
13309
  $(children[1]).addClass("red-ui-panel");
13294
13310
 
13295
13311
  var separator = $('<div class="red-ui-panels-separator"></div>').insertAfter(children[0]);
13312
+ if (options.invisibleSeparator) {
13313
+ if (!vertical) {
13314
+ throw new Error("invisibleSeparator option is only valid for vertical panels");
13315
+ }
13316
+ separator.addClass("red-ui-panels-separator-invisible");
13317
+ $('<div class="red-ui-panels-separator-handle"></div>').appendTo(separator)
13318
+ }
13296
13319
  var startPosition;
13297
13320
  var panelSizes = [];
13298
13321
  var modifiedSizes = false;
@@ -14869,7 +14892,6 @@ RED.tabs = (function() {
14869
14892
  ul.find("li.red-ui-tab.active .red-ui-tab-label").css({paddingLeft:""})
14870
14893
  }
14871
14894
  }
14872
-
14873
14895
  }
14874
14896
 
14875
14897
  ul.find("li.red-ui-tab a")
@@ -15369,7 +15391,8 @@ RED.tabs = (function() {
15369
15391
  pinnedButtons["__menu__"].appendTo(collapsedButtonsRow);
15370
15392
  updateTabWidths();
15371
15393
  }
15372
- }
15394
+ },
15395
+ container: wrapper
15373
15396
  }
15374
15397
  return tabAPI;
15375
15398
  }
@@ -17932,13 +17955,13 @@ RED.deploy = (function() {
17932
17955
  $("#red-ui-header-shade").show();
17933
17956
  $("#red-ui-editor-shade").show();
17934
17957
  $("#red-ui-palette-shade").show();
17935
- $("#red-ui-sidebar-shade").show();
17958
+ $(".red-ui-sidebar-shade").show();
17936
17959
  }
17937
17960
  function shadeHide() {
17938
17961
  $("#red-ui-header-shade").hide();
17939
17962
  $("#red-ui-editor-shade").hide();
17940
17963
  $("#red-ui-palette-shade").hide();
17941
- $("#red-ui-sidebar-shade").hide();
17964
+ $(".red-ui-sidebar-shade").hide();
17942
17965
  }
17943
17966
  function deployButtonSetBusy(){
17944
17967
  $(".red-ui-deploy-button-content").css('opacity',0);
@@ -19752,11 +19775,11 @@ RED.diagnostics = (function () {
19752
19775
  diffTable.finish();
19753
19776
  diffTable.list.show();
19754
19777
  },300);
19755
- $("#red-ui-sidebar-shade").show();
19778
+ $(".red-ui-sidebar-shade").show();
19756
19779
  },
19757
19780
  close: function() {
19758
19781
  diffVisible = false;
19759
- $("#red-ui-sidebar-shade").hide();
19782
+ $(".red-ui-sidebar-shade").hide();
19760
19783
 
19761
19784
  },
19762
19785
  show: function() {
@@ -22683,11 +22706,29 @@ RED.view = (function() {
22683
22706
  node_height = 30,
22684
22707
  dblClickInterval = 650;
22685
22708
 
22709
+ var cancelInProgressAnimation = null; // For smooth zoom animation
22710
+
22686
22711
  var touchLongPressTimeout = 1000,
22687
22712
  startTouchDistance = 0,
22688
22713
  startTouchCenter = [],
22689
22714
  moveTouchCenter = [],
22690
- touchStartTime = 0;
22715
+ touchStartTime = 0,
22716
+ gesture = {};
22717
+
22718
+ var spacebarPressed = false;
22719
+
22720
+ // Momentum scrolling state
22721
+ var scrollVelocity = { x: 0, y: 0 };
22722
+ var lastScrollTime = 0;
22723
+ var lastScrollPos = { x: 0, y: 0 };
22724
+ var scrollAnimationId = null;
22725
+ var momentumActive = false;
22726
+
22727
+ // Bounce effect parameters
22728
+ var BOUNCE_DAMPING = 0.6;
22729
+ var BOUNCE_TENSION = 0.3;
22730
+ var MIN_VELOCITY = 0.5;
22731
+ var FRICTION = 0.95;
22691
22732
 
22692
22733
  var workspaceScrollPositions = {};
22693
22734
 
@@ -22964,6 +23005,24 @@ RED.view = (function() {
22964
23005
  function init() {
22965
23006
 
22966
23007
  chart = $("#red-ui-workspace-chart");
23008
+
23009
+ // Add invisible spacer div to ensure scrollable area matches canvas dimensions
23010
+ // At minimum zoom with "cover" behavior, SVG may be smaller than viewport in one dimension
23011
+ // This spacer forces the browser to calculate scrollWidth/Height based on full canvas size
23012
+ // Browser's maxScroll = scrollWidth - viewport will then correctly show canvas edges
23013
+ var scrollSpacer = $('<div>')
23014
+ .css({
23015
+ position: 'absolute',
23016
+ top: 0,
23017
+ left: 0,
23018
+ width: space_width + 'px',
23019
+ height: space_height + 'px',
23020
+ pointerEvents: 'none',
23021
+ visibility: 'hidden'
23022
+ })
23023
+ .attr('id', 'red-ui-workspace-scroll-spacer')
23024
+ .appendTo(chart);
23025
+
22967
23026
  chart.on('contextmenu', function(evt) {
22968
23027
  if (RED.view.DEBUG) {
22969
23028
  console.warn("contextmenu", { mouse_mode, event: d3.event });
@@ -23008,8 +23067,9 @@ RED.view = (function() {
23008
23067
  lasso.remove();
23009
23068
  lasso = null;
23010
23069
  }
23011
- } else if (mouse_mode === RED.state.PANNING && d3.event.buttons !== 4) {
23012
- resetMouseVars();
23070
+ } else if (mouse_mode === RED.state.PANNING) {
23071
+ // ensure the cursor is set to grab when re-entering the canvas while panning
23072
+ outer.style('cursor', 'grabbing');
23013
23073
  } else if (slicePath) {
23014
23074
  if (d3.event.buttons !== 2) {
23015
23075
  slicePath.remove();
@@ -23026,11 +23086,15 @@ RED.view = (function() {
23026
23086
  if (RED.touch.radialMenu.active()) {
23027
23087
  return;
23028
23088
  }
23089
+ // End gesture when touches end
23090
+ RED.view.zoomAnimator.endGesture();
23029
23091
  canvasMouseUp.call(this);
23030
23092
  })
23031
23093
  .on("touchcancel", function() {
23032
23094
  if (RED.view.DEBUG) { console.warn("eventLayer.touchcancel", mouse_mode); }
23033
23095
  d3.event.preventDefault();
23096
+ // End gesture when touches are cancelled
23097
+ RED.view.zoomAnimator.endGesture();
23034
23098
  canvasMouseUp.call(this);
23035
23099
  })
23036
23100
  .on("touchstart", function() {
@@ -23056,6 +23120,20 @@ RED.view = (function() {
23056
23120
  touch1["pageY"]+(a/2)
23057
23121
  ]
23058
23122
  startTouchDistance = Math.sqrt((a*a)+(b*b));
23123
+
23124
+ // Store initial scale for ratio-based zoom calculation
23125
+ gesture = {
23126
+ initialScale: scaleFactor,
23127
+ initialDistance: startTouchDistance,
23128
+ mode: null // Will be determined on first significant move
23129
+ };
23130
+
23131
+ // Start gesture with fixed focal point (store in workspace coordinates)
23132
+ var focalPoint = [
23133
+ (touch0["pageX"] + touch1["pageX"]) / 2 - offset.left,
23134
+ (touch0["pageY"] + touch1["pageY"]) / 2 - offset.top
23135
+ ];
23136
+ RED.view.zoomAnimator.startGesture(focalPoint, scaleFactor, scrollPos, scaleFactor);
23059
23137
  } else {
23060
23138
  var obj = d3.select(document.body);
23061
23139
  touch0 = d3.event.touches.item(0);
@@ -23105,33 +23183,93 @@ RED.view = (function() {
23105
23183
  var offset = chart.offset();
23106
23184
  var scrollPos = [chart.scrollLeft(),chart.scrollTop()];
23107
23185
  var moveTouchDistance = Math.sqrt((a*a)+(b*b));
23108
- var touchCenter = [
23109
- touch1["pageX"]+(b/2),
23110
- touch1["pageY"]+(a/2)
23186
+
23187
+ // Calculate center point of two fingers
23188
+ var currentTouchCenter = [
23189
+ (touch0["pageX"] + touch1["pageX"]) / 2,
23190
+ (touch0["pageY"] + touch1["pageY"]) / 2
23111
23191
  ];
23112
23192
 
23113
23193
  if (!isNaN(moveTouchDistance)) {
23114
- oldScaleFactor = scaleFactor;
23115
- scaleFactor = Math.min(2,Math.max(0.3, scaleFactor + (Math.floor(((moveTouchDistance*100)-(startTouchDistance*100)))/10000)));
23116
-
23117
- var deltaTouchCenter = [ // Try to pan whilst zooming - not 100%
23118
- startTouchCenter[0]*(scaleFactor-oldScaleFactor),//-(touchCenter[0]-moveTouchCenter[0]),
23119
- startTouchCenter[1]*(scaleFactor-oldScaleFactor) //-(touchCenter[1]-moveTouchCenter[1])
23120
- ];
23121
-
23122
- startTouchDistance = moveTouchDistance;
23123
- moveTouchCenter = touchCenter;
23194
+ // Determine gesture mode on first significant movement
23195
+ if (!gesture.mode) {
23196
+ var distanceChange = Math.abs(moveTouchDistance - startTouchDistance);
23197
+ var centerChange = moveTouchCenter ?
23198
+ Math.sqrt(Math.pow(currentTouchCenter[0] - moveTouchCenter[0], 2) +
23199
+ Math.pow(currentTouchCenter[1] - moveTouchCenter[1], 2)) : 0;
23200
+
23201
+ // Lock into zoom mode if distance changes significantly (>10px)
23202
+ // Lock into pan mode if center moves significantly (>5px) without distance change
23203
+ if (distanceChange > 10) {
23204
+ gesture.mode = 'zoom';
23205
+ } else if (centerChange > 5) {
23206
+ gesture.mode = 'pan';
23207
+ }
23208
+ }
23209
+
23210
+ // Once mode is determined, stay in that mode for the entire gesture
23211
+ if (gesture.mode === 'zoom') {
23212
+ oldScaleFactor = scaleFactor;
23213
+ // Use smooth ratio-based scaling for natural pinch-to-zoom
23214
+ var zoomRatio = moveTouchDistance / startTouchDistance;
23215
+ var minZoom = calculateMinZoom();
23216
+ var newScaleFactor = Math.min(RED.view.zoomConstants.MAX_ZOOM,
23217
+ Math.max(minZoom, gesture.initialScale * zoomRatio));
23218
+
23219
+ // Use gesture state management to maintain fixed focal point
23220
+ var gestureState = RED.view.zoomAnimator.updateGesture(newScaleFactor);
23221
+
23222
+ // Only call zoomView if scale is actually changing (not at limits)
23223
+ if (Math.abs(scaleFactor - newScaleFactor) >= 0.001) {
23224
+ // Get focal point converted back to current screen coordinates
23225
+ var currentScrollPos = [chart.scrollLeft(), chart.scrollTop()];
23226
+ var focalPoint = RED.view.zoomAnimator.getGestureFocalPoint(currentScrollPos, scaleFactor);
23227
+
23228
+ if (focalPoint) {
23229
+ // Use the fixed focal point from gesture start (converted from workspace coords)
23230
+ zoomView(newScaleFactor, focalPoint);
23231
+ } else {
23232
+ // Fallback to current behavior if gesture not active
23233
+ var touchCenter = [
23234
+ touch1["pageX"]+(b/2),
23235
+ touch1["pageY"]+(a/2)
23236
+ ];
23237
+ var pinchCenter = [
23238
+ touchCenter[0] - offset.left,
23239
+ touchCenter[1] - offset.top
23240
+ ];
23241
+ zoomView(newScaleFactor, pinchCenter);
23242
+ }
23243
+ }
23244
+ } else if (gesture.mode === 'pan' || !gesture.mode) {
23245
+ // Two-finger pan: allow immediate panning even if mode not determined
23246
+ // Clear touchStartTime to prevent issues with next gesture
23247
+ if (touchStartTime) {
23248
+ clearTimeout(touchStartTime);
23249
+ touchStartTime = null;
23250
+ }
23251
+ if (moveTouchCenter) {
23252
+ var dx = currentTouchCenter[0] - moveTouchCenter[0];
23253
+ var dy = currentTouchCenter[1] - moveTouchCenter[1];
23254
+
23255
+ // Pan the canvas
23256
+ var currentScroll = [chart.scrollLeft(), chart.scrollTop()];
23257
+ chart.scrollLeft(currentScroll[0] - dx);
23258
+ chart.scrollTop(currentScroll[1] - dy);
23259
+ RED.events.emit("view:navigate");
23260
+ }
23261
+ // Update the center for next move
23262
+ moveTouchCenter = currentTouchCenter;
23263
+ }
23124
23264
 
23125
- chart.scrollLeft(scrollPos[0]+deltaTouchCenter[0]);
23126
- chart.scrollTop(scrollPos[1]+deltaTouchCenter[1]);
23127
- redraw();
23265
+ // Don't update startTouchDistance - keep initial distance for ratio calculation
23128
23266
  }
23129
23267
  }
23130
23268
  d3.event.preventDefault();
23131
23269
  });
23132
-
23133
-
23134
- const handleAltToggle = (event) => {
23270
+
23271
+ const handleChartKeyboardEvents = (event) => {
23272
+ // Handle Alt toggle for pulling nodes out of groups
23135
23273
  if (mouse_mode === RED.state.MOVING_ACTIVE && event.key === 'Alt' && groupAddParentGroup) {
23136
23274
  RED.nodes.group(groupAddParentGroup).dirty = true
23137
23275
  for (let n = 0; n<movingSet.length(); n++) {
@@ -23150,10 +23288,67 @@ RED.view = (function() {
23150
23288
  }
23151
23289
  }
23152
23290
  RED.view.redraw()
23291
+ } else if (event.keyCode === 32 || event.key === ' ') {
23292
+ if (mouse_mode === RED.state.PANNING) {
23293
+ // Already in panning mode - just prevent the event default handler
23294
+ event.preventDefault()
23295
+ event.stopPropagation()
23296
+ if (event.type === 'keyup' && spacebarPressed) {
23297
+ spacebarPressed = false
23298
+ }
23299
+ } else if (mouse_mode === RED.state.DEFAULT) {
23300
+ // Handle spacebar for panning
23301
+ event.preventDefault();
23302
+ event.stopPropagation();
23303
+ if (event.type === "keydown" && !spacebarPressed) {
23304
+ spacebarPressed = true;
23305
+ // Change cursor to grab hand when spacebar is pressed
23306
+ outer.style('cursor', 'grab');
23307
+ } else if (event.type === "keyup" && spacebarPressed) {
23308
+ spacebarPressed = false;
23309
+ // Revert cursor when spacebar is released
23310
+ outer.style('cursor', '');
23311
+ }
23312
+ }
23153
23313
  }
23154
23314
  }
23155
- document.addEventListener("keyup", handleAltToggle)
23156
- document.addEventListener("keydown", handleAltToggle)
23315
+ chart.on("keydown", handleChartKeyboardEvents)
23316
+ chart.on("keyup", handleChartKeyboardEvents)
23317
+
23318
+ // // // Window-level keyup listener to catch spacebar release when cursor is outside canvas
23319
+ // // function handleWindowSpacebarUp(e) {
23320
+ // // if ((e.keyCode === 32 || e.key === ' ') && spacebarPressed) {
23321
+ // // spacebarPressed = false;
23322
+ // // // Revert cursor when spacebar is released outside canvas
23323
+ // // outer.style('cursor', '');
23324
+ // // e.preventDefault();
23325
+ // // e.stopPropagation();
23326
+ // // }
23327
+ // // }
23328
+ // // chart.on("keyup", handleSpacebarToggle)
23329
+ // // chart.on("keydown", handleSpacebarToggle)
23330
+ // // Additional window-level keyup listener to ensure spacebar state is cleared
23331
+ // // when cursor leaves canvas area while spacebar is held
23332
+ // window.addEventListener("keyup", handleWindowSpacebarUp)
23333
+
23334
+ // // Reset spacebar state when window loses focus to prevent stuck state
23335
+ // window.addEventListener("blur", function() {
23336
+ // if (spacebarPressed) {
23337
+ // spacebarPressed = false;
23338
+ // // Revert cursor when window loses focus
23339
+ // outer.style('cursor', '');
23340
+ // }
23341
+ // })
23342
+
23343
+ // Recalculate minimum zoom when window resizes
23344
+ $(window).on("resize.red-ui-view", function() {
23345
+ // Recalculate minimum zoom to ensure canvas fits in viewport
23346
+ var newMinZoom = calculateMinZoom();
23347
+ // If current zoom is below new minimum, adjust it
23348
+ if (scaleFactor < newMinZoom) {
23349
+ zoomView(newMinZoom);
23350
+ }
23351
+ })
23157
23352
 
23158
23353
  // Workspace Background
23159
23354
  eventLayer.append("svg:rect")
@@ -23245,22 +23440,194 @@ RED.view = (function() {
23245
23440
  '<button class="red-ui-footer-button" id="red-ui-view-zoom-out"><i class="fa fa-minus"></i></button>'+
23246
23441
  '<button class="red-ui-footer-button" id="red-ui-view-zoom-zero"><i class="fa fa-circle-o"></i></button>'+
23247
23442
  '<button class="red-ui-footer-button" id="red-ui-view-zoom-in"><i class="fa fa-plus"></i></button>'+
23443
+ '<button class="red-ui-footer-button" id="red-ui-view-zoom-fit"><i class="fa fa-compress"></i></button>'+
23248
23444
  '</span>')
23249
23445
  })
23250
23446
 
23251
- $("#red-ui-view-zoom-out").on("click", zoomOut);
23447
+ $("#red-ui-view-zoom-out").on("click", function() { zoomOut(); });
23252
23448
  RED.popover.tooltip($("#red-ui-view-zoom-out"),RED._('actions.zoom-out'),'core:zoom-out');
23253
23449
  $("#red-ui-view-zoom-zero").on("click", zoomZero);
23254
23450
  RED.popover.tooltip($("#red-ui-view-zoom-zero"),RED._('actions.zoom-reset'),'core:zoom-reset');
23255
- $("#red-ui-view-zoom-in").on("click", zoomIn);
23451
+ $("#red-ui-view-zoom-in").on("click", function() { zoomIn(); });
23256
23452
  RED.popover.tooltip($("#red-ui-view-zoom-in"),RED._('actions.zoom-in'),'core:zoom-in');
23257
- chart.on("DOMMouseScroll mousewheel", function (evt) {
23258
- if ( evt.altKey ) {
23453
+ $("#red-ui-view-zoom-fit").on("click", zoomToFitAll);
23454
+ RED.popover.tooltip($("#red-ui-view-zoom-fit"),RED._('actions.zoom-fit'),'core:zoom-fit');
23455
+ // Legacy mouse wheel handler - disabled in favor of modern wheel event
23456
+ // chart.on("DOMMouseScroll mousewheel", function (evt) {
23457
+ // if ( evt.altKey || spacebarPressed ) {
23458
+ // evt.preventDefault();
23459
+ // evt.stopPropagation();
23460
+ // // Get cursor position relative to the chart
23461
+ // var offset = chart.offset();
23462
+ // var cursorPos = [
23463
+ // evt.originalEvent.pageX - offset.left,
23464
+ // evt.originalEvent.pageY - offset.top
23465
+ // ];
23466
+ // var move = -(evt.originalEvent.detail) || evt.originalEvent.wheelDelta;
23467
+ // if (move <= 0) { zoomOut(cursorPos); }
23468
+ // else { zoomIn(cursorPos); }
23469
+ // }
23470
+ // });
23471
+
23472
+ // Modern wheel event handler for better trackpad support (pinch-to-zoom) and momentum
23473
+ var momentumTimer = null;
23474
+ var trackpadGestureTimer = null;
23475
+ var lastWheelEventTime = 0;
23476
+ var wheelEventContinuityThreshold = 100; // Events within 100ms are same gesture
23477
+ var gestureEndThreshold = 500; // 500ms+ gap means gesture ended
23478
+
23479
+ // Prevent browser zoom on non-canvas areas
23480
+ document.addEventListener("wheel", function(e) {
23481
+ if (e.ctrlKey && !e.target.closest('#red-ui-workspace-chart')) {
23482
+ e.preventDefault();
23483
+ }
23484
+ }, { passive: false });
23485
+
23486
+ chart.on("wheel", function(evt) {
23487
+ if (mouse_mode === RED.state.PANNING) {
23488
+ // Ignore wheel events while panning
23489
+ return;
23490
+ }
23491
+ // ctrlKey is set during pinch gestures on trackpads
23492
+ if (evt.ctrlKey || evt.altKey || spacebarPressed) {
23259
23493
  evt.preventDefault();
23260
23494
  evt.stopPropagation();
23261
- var move = -(evt.originalEvent.detail) || evt.originalEvent.wheelDelta;
23262
- if (move <= 0) { zoomOut(); }
23263
- else { zoomIn(); }
23495
+
23496
+ var currentTime = Date.now();
23497
+ var timeSinceLastEvent = currentTime - lastWheelEventTime;
23498
+
23499
+ // Get cursor position relative to the chart
23500
+ var offset = chart.offset();
23501
+ var cursorPos = [
23502
+ evt.originalEvent.pageX - offset.left,
23503
+ evt.originalEvent.pageY - offset.top
23504
+ ];
23505
+ var delta = evt.originalEvent.deltaY;
23506
+
23507
+ // For trackpad pinch (Ctrl+wheel), use smooth proportional zoom
23508
+ if (evt.ctrlKey && !evt.altKey && !spacebarPressed) {
23509
+ // Detect input device: trackpad has small deltas, mouse wheel has large deltas
23510
+ var isTrackpadInput = Math.abs(delta) < 50;
23511
+ // Invert delta: spreading fingers (negative deltaY) should zoom in
23512
+ var scaleDelta = RED.view.zoomAnimator.calculateZoomDelta(scaleFactor, -delta, isTrackpadInput);
23513
+ var minZoom = calculateMinZoom();
23514
+ var newScale = Math.min(RED.view.zoomConstants.MAX_ZOOM,
23515
+ Math.max(minZoom, scaleFactor + scaleDelta));
23516
+
23517
+ // Session-based gesture tracking:
23518
+ // - If no active gesture OR gap > gestureEndThreshold, start new gesture
23519
+ // - If gap < wheelEventContinuityThreshold, continue current gesture
23520
+ // - If gap between continuity and end threshold, keep current gesture but don't update focal point
23521
+
23522
+ if (!RED.view.zoomAnimator.isGestureActive() || timeSinceLastEvent > gestureEndThreshold) {
23523
+ // Start new gesture session - store focal point in workspace coordinates
23524
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
23525
+ RED.view.zoomAnimator.startGesture(cursorPos, scaleFactor, scrollPos, scaleFactor);
23526
+ } else if (timeSinceLastEvent <= wheelEventContinuityThreshold) {
23527
+ // Events are continuous - this is the same gesture, focal point remains locked
23528
+ // No need to update focal point
23529
+ }
23530
+ // For gaps between continuity and end threshold, keep existing gesture state
23531
+
23532
+ // Update gesture with new scale, maintaining locked focal point
23533
+ RED.view.zoomAnimator.updateGesture(newScale);
23534
+ // Only call zoomView if scale is actually changing (not at limits)
23535
+ if (Math.abs(scaleFactor - newScale) >= 0.001) {
23536
+ // Get focal point converted back to current screen coordinates
23537
+ var currentScrollPos = [chart.scrollLeft(), chart.scrollTop()];
23538
+ var focalPoint = RED.view.zoomAnimator.getGestureFocalPoint(currentScrollPos, scaleFactor);
23539
+ zoomView(newScale, focalPoint); // Direct call, no animation
23540
+ }
23541
+
23542
+ // Update last event time for continuity tracking
23543
+ lastWheelEventTime = currentTime;
23544
+
23545
+ // Reset gesture timeout - end gesture when no more events come in for gestureEndThreshold
23546
+ if (trackpadGestureTimer) {
23547
+ clearTimeout(trackpadGestureTimer);
23548
+ }
23549
+ trackpadGestureTimer = setTimeout(function() {
23550
+ RED.view.zoomAnimator.endGesture();
23551
+ trackpadGestureTimer = null;
23552
+ // Store zoom level when gesture completes
23553
+ if (RED.settings.get("editor.view.view-store-zoom")) {
23554
+ RED.settings.setLocal('zoom-level', scaleFactor.toFixed(1));
23555
+ }
23556
+ }, gestureEndThreshold); // Use 500ms timeout for gesture end detection
23557
+ } else {
23558
+ // Regular Alt+scroll or Space+scroll - use smooth zoom without animation
23559
+ // Detect input device: trackpad has small deltas, mouse wheel has large deltas
23560
+ var isTrackpadInput = Math.abs(delta) < 50;
23561
+ var scaleDelta = RED.view.zoomAnimator.calculateZoomDelta(scaleFactor, -delta, isTrackpadInput);
23562
+ var minZoom = calculateMinZoom();
23563
+ var newScale = Math.min(RED.view.zoomConstants.MAX_ZOOM,
23564
+ Math.max(minZoom, scaleFactor + scaleDelta));
23565
+
23566
+ // Use gesture tracking for stable focal point like trackpad pinch
23567
+ if (!RED.view.zoomAnimator.isGestureActive() || timeSinceLastEvent > gestureEndThreshold) {
23568
+ // Start new gesture session - store focal point in workspace coordinates
23569
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
23570
+ RED.view.zoomAnimator.startGesture(cursorPos, scaleFactor, scrollPos, scaleFactor);
23571
+ } else if (timeSinceLastEvent <= wheelEventContinuityThreshold) {
23572
+ // Events are continuous - same gesture, focal point remains locked
23573
+ }
23574
+
23575
+ // Update gesture with new scale, maintaining locked focal point
23576
+ RED.view.zoomAnimator.updateGesture(newScale);
23577
+
23578
+ // Only zoom if scale is actually changing
23579
+ if (Math.abs(scaleFactor - newScale) >= 0.001) {
23580
+ // Get focal point converted back to current screen coordinates
23581
+ var currentScrollPos = [chart.scrollLeft(), chart.scrollTop()];
23582
+ var focalPoint = RED.view.zoomAnimator.getGestureFocalPoint(currentScrollPos, scaleFactor);
23583
+ zoomView(newScale, focalPoint);
23584
+ }
23585
+
23586
+ // Update last event time for continuity tracking
23587
+ lastWheelEventTime = currentTime;
23588
+
23589
+ // Reset gesture timeout
23590
+ if (trackpadGestureTimer) {
23591
+ clearTimeout(trackpadGestureTimer);
23592
+ }
23593
+ trackpadGestureTimer = setTimeout(function() {
23594
+ RED.view.zoomAnimator.endGesture();
23595
+ trackpadGestureTimer = null;
23596
+ // Store zoom level when gesture completes
23597
+ if (RED.settings.get("editor.view.view-store-zoom")) {
23598
+ RED.settings.setLocal('zoom-level', scaleFactor.toFixed(1));
23599
+ }
23600
+ }, gestureEndThreshold);
23601
+ }
23602
+ } else {
23603
+ // Regular scroll - prevent default and manually handle both axes
23604
+ evt.preventDefault();
23605
+ evt.stopPropagation();
23606
+
23607
+ // Apply scroll deltas directly to both axes
23608
+ var deltaX = evt.originalEvent.deltaX;
23609
+ var deltaY = evt.originalEvent.deltaY;
23610
+
23611
+ chart.scrollLeft(chart.scrollLeft() + deltaX);
23612
+ chart.scrollTop(chart.scrollTop() + deltaY);
23613
+
23614
+ // Emit navigate event for minimap
23615
+ RED.events.emit("view:navigate");
23616
+
23617
+ // Track velocity and apply momentum
23618
+ handleScroll();
23619
+
23620
+ // Cancel previous momentum timer
23621
+ if (momentumTimer) {
23622
+ clearTimeout(momentumTimer);
23623
+ }
23624
+
23625
+ // Start momentum after scroll stops
23626
+ momentumTimer = setTimeout(function() {
23627
+ if (Math.abs(scrollVelocity.x) > MIN_VELOCITY || Math.abs(scrollVelocity.y) > MIN_VELOCITY) {
23628
+ startMomentumScroll();
23629
+ }
23630
+ }, 100);
23264
23631
  }
23265
23632
  });
23266
23633
 
@@ -23431,6 +23798,12 @@ RED.view = (function() {
23431
23798
  });
23432
23799
  chart.on("blur", function() {
23433
23800
  $("#red-ui-workspace-tabs").removeClass("red-ui-workspace-focussed");
23801
+ // Reset spacebar state when chart loses focus to prevent stuck state
23802
+ if (spacebarPressed) {
23803
+ spacebarPressed = false;
23804
+ // Revert cursor when chart loses focus
23805
+ outer.style('cursor', '');
23806
+ }
23434
23807
  });
23435
23808
 
23436
23809
  RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection);
@@ -23495,6 +23868,7 @@ RED.view = (function() {
23495
23868
  RED.actions.add("core:zoom-in",zoomIn);
23496
23869
  RED.actions.add("core:zoom-out",zoomOut);
23497
23870
  RED.actions.add("core:zoom-reset",zoomZero);
23871
+ RED.actions.add("core:zoom-fit",zoomToFitAll);
23498
23872
  RED.actions.add("core:enable-selected-nodes", function() { setSelectedNodeState(false)});
23499
23873
  RED.actions.add("core:disable-selected-nodes", function() { setSelectedNodeState(true)});
23500
23874
 
@@ -23610,6 +23984,9 @@ RED.view = (function() {
23610
23984
  RED.settings.setLocal('scroll-positions', JSON.stringify(workspaceScrollPositions) )
23611
23985
  }
23612
23986
  chart.on("scroll", function() {
23987
+ // Track scroll velocity for momentum
23988
+ handleScroll();
23989
+
23613
23990
  if (RED.settings.get("editor.view.view-store-position")) {
23614
23991
  if (onScrollTimer) {
23615
23992
  clearTimeout(onScrollTimer)
@@ -23887,12 +24264,26 @@ RED.view = (function() {
23887
24264
  return;
23888
24265
  }
23889
24266
 
24267
+ // Spacebar + left click for panning
24268
+ if (spacebarPressed && d3.event.button === 0) {
24269
+ d3.event.preventDefault();
24270
+ d3.event.stopPropagation();
24271
+ mouse_mode = RED.state.PANNING;
24272
+ mouse_position = [d3.event.pageX,d3.event.pageY]
24273
+ scroll_position = [chart.scrollLeft(),chart.scrollTop()];
24274
+ // Change cursor to grabbing while actively panning
24275
+ outer.style('cursor', 'grabbing');
24276
+ return;
24277
+ }
24278
+
23890
24279
  if (d3.event.button === 1) {
23891
24280
  // Middle Click pan
23892
24281
  d3.event.preventDefault();
23893
24282
  mouse_mode = RED.state.PANNING;
23894
24283
  mouse_position = [d3.event.pageX,d3.event.pageY]
23895
24284
  scroll_position = [chart.scrollLeft(),chart.scrollTop()];
24285
+ // Change cursor to grabbing while actively panning
24286
+ outer.style('cursor', 'grabbing');
23896
24287
  return;
23897
24288
  }
23898
24289
  if (d3.event.button === 2) {
@@ -24424,6 +24815,30 @@ RED.view = (function() {
24424
24815
  redraw();
24425
24816
  }
24426
24817
 
24818
+ function startPanning () {
24819
+
24820
+ }
24821
+ window.addEventListener('mousemove', windowMouseMove)
24822
+ window.addEventListener('touchmove', windowMouseMove)
24823
+
24824
+ function windowMouseMove (event) {
24825
+ if (mouse_mode === RED.state.PANNING) {
24826
+ let pos = [event.pageX, event.pageY]
24827
+ if (event.touches) {
24828
+ let touch0 = event.touches.item(0)
24829
+ pos = [touch0.pageX, touch0.pageY]
24830
+ }
24831
+ const deltaPos = [
24832
+ mouse_position[0]-pos[0],
24833
+ mouse_position[1]-pos[1]
24834
+ ]
24835
+ chart.scrollLeft(scroll_position[0]+deltaPos[0])
24836
+ chart.scrollTop(scroll_position[1]+deltaPos[1])
24837
+ RED.events.emit("view:navigate");
24838
+ return
24839
+ }
24840
+ }
24841
+
24427
24842
  function canvasMouseMove() {
24428
24843
  var i;
24429
24844
  var node;
@@ -24438,18 +24853,8 @@ RED.view = (function() {
24438
24853
  //console.log(d3.mouse(this),container.offsetWidth,container.offsetHeight,container.scrollLeft,container.scrollTop);
24439
24854
 
24440
24855
  if (mouse_mode === RED.state.PANNING) {
24441
- var pos = [d3.event.pageX,d3.event.pageY];
24442
- if (d3.event.touches) {
24443
- var touch0 = d3.event.touches.item(0);
24444
- pos = [touch0.pageX, touch0.pageY];
24445
- }
24446
- var deltaPos = [
24447
- mouse_position[0]-pos[0],
24448
- mouse_position[1]-pos[1]
24449
- ];
24450
-
24451
- chart.scrollLeft(scroll_position[0]+deltaPos[0])
24452
- chart.scrollTop(scroll_position[1]+deltaPos[1])
24856
+ // A window-level handler is used for panning so the mouse can leave the confines of the chart
24857
+ // but continue panning
24453
24858
  return
24454
24859
  }
24455
24860
 
@@ -24793,6 +25198,12 @@ RED.view = (function() {
24793
25198
  }
24794
25199
  if (mouse_mode === RED.state.PANNING) {
24795
25200
  resetMouseVars();
25201
+ // Revert to grab cursor if spacebar still held, otherwise clear cursor
25202
+ if (spacebarPressed) {
25203
+ outer.style('cursor', 'grab');
25204
+ } else {
25205
+ outer.style('cursor', '');
25206
+ }
24796
25207
  return
24797
25208
  }
24798
25209
  if (mouse_mode === RED.state.SELECTING_NODE) {
@@ -25124,39 +25535,454 @@ RED.view = (function() {
25124
25535
 
25125
25536
  }
25126
25537
 
25127
- function zoomIn() {
25128
- if (scaleFactor < 2) {
25129
- zoomView(scaleFactor+0.1);
25538
+ function calculateMinZoom() {
25539
+ // Calculate the minimum zoom to ensure canvas always fills the viewport (no empty space)
25540
+ var viewportWidth = chart.width();
25541
+ var viewportHeight = chart.height();
25542
+
25543
+ // Canvas is 8000x8000, calculate zoom to cover viewport
25544
+ var zoomToFitWidth = viewportWidth / space_width;
25545
+ var zoomToFitHeight = viewportHeight / space_height;
25546
+
25547
+ // Use the LARGER zoom to ensure canvas covers entire viewport (no empty space visible)
25548
+ var calculatedMinZoom = Math.max(zoomToFitWidth, zoomToFitHeight);
25549
+
25550
+ // Return the larger of the calculated min or the configured min
25551
+ // This ensures canvas always fills the viewport
25552
+ return Math.max(calculatedMinZoom, RED.view.zoomConstants.MIN_ZOOM);
25553
+ }
25554
+
25555
+ // Track focal point for sequential button/hotkey zoom operations
25556
+ // Store in workspace coordinates so it remains valid after viewport shifts
25557
+ var buttonZoomWorkspaceCenter = null;
25558
+ var buttonZoomTimeout = null;
25559
+ var BUTTON_ZOOM_FOCAL_TIMEOUT = 1000; // ms - time to keep same focal point
25560
+
25561
+ function zoomIn(focalPoint) {
25562
+ if (scaleFactor < RED.view.zoomConstants.MAX_ZOOM) {
25563
+ var useFocalPoint = null;
25564
+
25565
+ // If focalPoint is explicitly provided (e.g., from wheel/pinch), use it directly
25566
+ if (focalPoint) {
25567
+ useFocalPoint = focalPoint;
25568
+ } else {
25569
+ // For button/hotkey zoom, maintain the same workspace center across sequential zooms
25570
+ if (!buttonZoomWorkspaceCenter) {
25571
+ // First button zoom - calculate and store workspace center
25572
+ var screenSize = [chart.width(), chart.height()];
25573
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
25574
+ // Convert viewport center to workspace coordinates
25575
+ buttonZoomWorkspaceCenter = [
25576
+ (scrollPos[0] + screenSize[0]/2) / scaleFactor,
25577
+ (scrollPos[1] + screenSize[1]/2) / scaleFactor
25578
+ ];
25579
+ }
25580
+
25581
+ // ALWAYS use viewport center as focal point (fixed screen position)
25582
+ // The stored workspace center will be kept at this screen position
25583
+ var screenSize = [chart.width(), chart.height()];
25584
+ useFocalPoint = [screenSize[0]/2, screenSize[1]/2];
25585
+
25586
+ // Reset timeout
25587
+ clearTimeout(buttonZoomTimeout);
25588
+ buttonZoomTimeout = setTimeout(function() {
25589
+ buttonZoomWorkspaceCenter = null;
25590
+ }, BUTTON_ZOOM_FOCAL_TIMEOUT);
25591
+ }
25592
+
25593
+ animatedZoomView(scaleFactor + RED.view.zoomConstants.ZOOM_STEP, useFocalPoint, buttonZoomWorkspaceCenter);
25594
+ }
25595
+ }
25596
+ function zoomOut(focalPoint) {
25597
+ var minZoom = calculateMinZoom();
25598
+ if (scaleFactor > minZoom) {
25599
+ var useFocalPoint = null;
25600
+
25601
+ if (focalPoint) {
25602
+ useFocalPoint = focalPoint;
25603
+ } else {
25604
+ if (!buttonZoomWorkspaceCenter) {
25605
+ var screenSize = [chart.width(), chart.height()];
25606
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
25607
+ buttonZoomWorkspaceCenter = [
25608
+ (scrollPos[0] + screenSize[0]/2) / scaleFactor,
25609
+ (scrollPos[1] + screenSize[1]/2) / scaleFactor
25610
+ ];
25611
+ }
25612
+
25613
+ // ALWAYS use viewport center as focal point (fixed screen position)
25614
+ var screenSize = [chart.width(), chart.height()];
25615
+ useFocalPoint = [screenSize[0]/2, screenSize[1]/2];
25616
+
25617
+ clearTimeout(buttonZoomTimeout);
25618
+ buttonZoomTimeout = setTimeout(function() {
25619
+ buttonZoomWorkspaceCenter = null;
25620
+ }, BUTTON_ZOOM_FOCAL_TIMEOUT);
25621
+ }
25622
+
25623
+ animatedZoomView(Math.max(scaleFactor - RED.view.zoomConstants.ZOOM_STEP, minZoom), useFocalPoint, buttonZoomWorkspaceCenter);
25130
25624
  }
25131
25625
  }
25132
- function zoomOut() {
25133
- if (scaleFactor > 0.3) {
25134
- zoomView(scaleFactor-0.1);
25626
+ function zoomZero() {
25627
+ // Reset button zoom focal point for zoom reset
25628
+ clearTimeout(buttonZoomTimeout);
25629
+ buttonZoomWorkspaceCenter = null;
25630
+ animatedZoomView(1);
25631
+ }
25632
+
25633
+ function zoomToFitAll() {
25634
+ // Refresh active nodes to ensure we have the latest
25635
+ updateActiveNodes();
25636
+
25637
+ // Get all nodes in active workspace
25638
+ if (!activeNodes || activeNodes.length === 0) {
25639
+ return; // No nodes to fit
25640
+ }
25641
+
25642
+ // Calculate bounding box of all nodes
25643
+ var minX = Infinity, minY = Infinity;
25644
+ var maxX = -Infinity, maxY = -Infinity;
25645
+
25646
+ activeNodes.forEach(function(node) {
25647
+ var nodeLeft = node.x - node.w / 2;
25648
+ var nodeRight = node.x + node.w / 2;
25649
+ var nodeTop = node.y - node.h / 2;
25650
+ var nodeBottom = node.y + node.h / 2;
25651
+
25652
+ minX = Math.min(minX, nodeLeft);
25653
+ maxX = Math.max(maxX, nodeRight);
25654
+ minY = Math.min(minY, nodeTop);
25655
+ maxY = Math.max(maxY, nodeBottom);
25656
+ });
25657
+
25658
+ // Add padding around nodes for visual breathing room
25659
+ var padding = 80;
25660
+ minX -= padding;
25661
+ minY -= padding;
25662
+ maxX += padding;
25663
+ maxY += padding;
25664
+
25665
+ // Calculate dimensions of bounding box
25666
+ var boundingWidth = maxX - minX;
25667
+ var boundingHeight = maxY - minY;
25668
+
25669
+ // Get viewport dimensions
25670
+ var viewportWidth = chart.width();
25671
+ var viewportHeight = chart.height();
25672
+
25673
+ // Calculate zoom level that fits bounding box in viewport
25674
+ var zoomX = viewportWidth / boundingWidth;
25675
+ var zoomY = viewportHeight / boundingHeight;
25676
+ var targetZoom = Math.min(zoomX, zoomY);
25677
+
25678
+ // Respect minimum and maximum zoom limits
25679
+ var minZoom = calculateMinZoom();
25680
+ targetZoom = Math.max(minZoom, Math.min(RED.view.zoomConstants.MAX_ZOOM, targetZoom));
25681
+
25682
+ // Calculate center point of bounding box in workspace coordinates
25683
+ var centerX = (minX + maxX) / 2;
25684
+ var centerY = (minY + maxY) / 2;
25685
+
25686
+ // Reset button zoom focal point for zoom-to-fit
25687
+ clearTimeout(buttonZoomTimeout);
25688
+ buttonZoomWorkspaceCenter = null;
25689
+
25690
+ // Pass the bounding box center as workspace center
25691
+ // This ensures the nodes are centered in viewport after zoom
25692
+ var focalPoint = [viewportWidth / 2, viewportHeight / 2];
25693
+
25694
+ // If zoom level won't change significantly, animate just the pan
25695
+ if (Math.abs(scaleFactor - targetZoom) < 0.01) {
25696
+ var targetScrollLeft = centerX * scaleFactor - viewportWidth / 2;
25697
+ var targetScrollTop = centerY * scaleFactor - viewportHeight / 2;
25698
+
25699
+ // Calculate pan distance to determine duration (match zoom animation logic)
25700
+ var startScrollLeft = chart.scrollLeft();
25701
+ var startScrollTop = chart.scrollTop();
25702
+ var panDistance = Math.sqrt(
25703
+ Math.pow(targetScrollLeft - startScrollLeft, 2) +
25704
+ Math.pow(targetScrollTop - startScrollTop, 2)
25705
+ );
25706
+
25707
+ // Use similar duration calculation as zoom: scale with distance
25708
+ // Normalize by viewport diagonal for consistent feel
25709
+ var viewportDiagonal = Math.sqrt(viewportWidth * viewportWidth + viewportHeight * viewportHeight);
25710
+ var relativeDistance = panDistance / viewportDiagonal;
25711
+ // Duration scales with distance, matching zoom animation feel
25712
+ var duration = Math.max(200, Math.min(350, relativeDistance * RED.view.zoomConstants.DEFAULT_ZOOM_DURATION * 4));
25713
+
25714
+ RED.view.zoomAnimator.easeToValuesRAF({
25715
+ fromValues: {
25716
+ scrollLeft: startScrollLeft,
25717
+ scrollTop: startScrollTop
25718
+ },
25719
+ toValues: {
25720
+ scrollLeft: targetScrollLeft,
25721
+ scrollTop: targetScrollTop
25722
+ },
25723
+ duration: duration,
25724
+ onStep: function(values) {
25725
+ chart.scrollLeft(values.scrollLeft);
25726
+ chart.scrollTop(values.scrollTop);
25727
+ },
25728
+ onStart: function() {
25729
+ RED.events.emit("view:navigate");
25730
+ }
25731
+ });
25732
+ } else {
25733
+ animatedZoomView(targetZoom, focalPoint, [centerX, centerY]);
25135
25734
  }
25136
25735
  }
25137
- function zoomZero() { zoomView(1); }
25736
+
25138
25737
  function searchFlows() { RED.actions.invoke("core:search", $(this).data("term")); }
25139
25738
  function searchPrev() { RED.actions.invoke("core:search-previous"); }
25140
25739
  function searchNext() { RED.actions.invoke("core:search-next"); }
25141
25740
 
25142
25741
 
25143
- function zoomView(factor) {
25742
+ function zoomView(factor, focalPoint) {
25743
+ // Early return if scale factor isn't actually changing
25744
+ // This prevents focal point shifts when at zoom limits
25745
+ if (Math.abs(scaleFactor - factor) < 0.001) {
25746
+ return;
25747
+ }
25748
+ // Make scale 1 'sticky'
25749
+ if (Math.abs(1.0 - factor) < 0.02) {
25750
+ factor = 1
25751
+ }
25752
+
25753
+ console.log(factor)
25144
25754
  var screenSize = [chart.width(),chart.height()];
25145
25755
  var scrollPos = [chart.scrollLeft(),chart.scrollTop()];
25146
- var center = [(scrollPos[0] + screenSize[0]/2)/scaleFactor,(scrollPos[1] + screenSize[1]/2)/scaleFactor];
25756
+ var oldScaleFactor = scaleFactor;
25757
+
25758
+ // Calculate workspace coordinates of the point that should remain fixed
25759
+ var center;
25760
+ if (focalPoint) {
25761
+ // focalPoint is in screen coordinates, convert to workspace coordinates
25762
+ center = [(scrollPos[0] + focalPoint[0])/oldScaleFactor, (scrollPos[1] + focalPoint[1])/oldScaleFactor];
25763
+ } else {
25764
+ // Default to viewport center in workspace coordinates
25765
+ center = [(scrollPos[0] + screenSize[0]/2)/oldScaleFactor, (scrollPos[1] + screenSize[1]/2)/oldScaleFactor];
25766
+ }
25767
+
25147
25768
  scaleFactor = factor;
25148
- var newCenter = [(scrollPos[0] + screenSize[0]/2)/scaleFactor,(scrollPos[1] + screenSize[1]/2)/scaleFactor];
25149
- var delta = [(newCenter[0]-center[0])*scaleFactor,(newCenter[1]-center[1])*scaleFactor]
25150
- chart.scrollLeft(scrollPos[0]-delta[0]);
25151
- chart.scrollTop(scrollPos[1]-delta[1]);
25769
+
25770
+ // Calculate new scroll position to keep the center point at the same screen position
25771
+ if (focalPoint) {
25772
+ // Keep the focal point at the same screen position
25773
+ chart.scrollLeft(center[0] * scaleFactor - focalPoint[0]);
25774
+ chart.scrollTop(center[1] * scaleFactor - focalPoint[1]);
25775
+ } else {
25776
+ // Keep viewport center on the same workspace coordinates
25777
+ var newScrollLeft = center[0] * scaleFactor - screenSize[0]/2;
25778
+ var newScrollTop = center[1] * scaleFactor - screenSize[1]/2;
25779
+ chart.scrollLeft(newScrollLeft);
25780
+ chart.scrollTop(newScrollTop);
25781
+ }
25152
25782
 
25153
25783
  RED.view.navigator.resize();
25154
25784
  redraw();
25785
+ RED.events.emit("view:navigate");
25155
25786
  if (RED.settings.get("editor.view.view-store-zoom")) {
25156
25787
  RED.settings.setLocal('zoom-level', factor.toFixed(1))
25157
25788
  }
25158
25789
  }
25159
25790
 
25791
+ function animatedZoomView(targetFactor, focalPoint, workspaceCenter) {
25792
+ // Cancel any in-progress animation
25793
+ if (cancelInProgressAnimation) {
25794
+ cancelInProgressAnimation();
25795
+ cancelInProgressAnimation = null;
25796
+ }
25797
+
25798
+ // Calculate the actual minimum zoom to fit canvas
25799
+ var minZoom = calculateMinZoom();
25800
+
25801
+ // Clamp target factor to valid range
25802
+ targetFactor = Math.max(minZoom,
25803
+ Math.min(RED.view.zoomConstants.MAX_ZOOM, targetFactor));
25804
+
25805
+ // If we're already at the target, no need to animate
25806
+ // Use a more tolerant threshold to account for floating-point precision
25807
+ if (Math.abs(scaleFactor - targetFactor) < 0.01) {
25808
+ return;
25809
+ }
25810
+ // Make scale 1 'sticky'
25811
+ if (Math.abs(1.0 - targetFactor) < 0.02) {
25812
+ targetFactor = 1
25813
+ }
25814
+
25815
+
25816
+ var startFactor = scaleFactor;
25817
+ var screenSize = [chart.width(), chart.height()];
25818
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
25819
+
25820
+ // Calculate the focal point in workspace coordinates (will remain constant)
25821
+ var center;
25822
+ if (workspaceCenter) {
25823
+ // Use the provided workspace center directly (for button zoom focal point locking)
25824
+ center = workspaceCenter;
25825
+ } else if (focalPoint) {
25826
+ // focalPoint is in screen coordinates, convert to workspace coordinates
25827
+ center = [(scrollPos[0] + focalPoint[0])/scaleFactor, (scrollPos[1] + focalPoint[1])/scaleFactor];
25828
+ } else {
25829
+ // Default to viewport center
25830
+ center = [(scrollPos[0] + screenSize[0]/2)/scaleFactor, (scrollPos[1] + screenSize[1]/2)/scaleFactor];
25831
+ }
25832
+
25833
+ // Calculate duration based on relative zoom change to maintain consistent velocity
25834
+ // Use logarithmic scaling since zoom feels exponential to the user
25835
+ var zoomRatio = targetFactor / startFactor;
25836
+ var logChange = Math.abs(Math.log(zoomRatio));
25837
+ // Scale duration more aggressively: multiply by 2 for stronger effect
25838
+ // At extreme zoom levels, animation will be noticeably longer
25839
+ var duration = Math.max(200, Math.min(350, logChange / 0.693 * RED.view.zoomConstants.DEFAULT_ZOOM_DURATION * 2));
25840
+
25841
+ // Start the animation
25842
+ cancelInProgressAnimation = RED.view.zoomAnimator.easeToValuesRAF({
25843
+ fromValues: {
25844
+ zoom: startFactor
25845
+ },
25846
+ toValues: {
25847
+ zoom: targetFactor
25848
+ },
25849
+ duration: duration,
25850
+ interpolateValue: true, // Use exponential interpolation for zoom
25851
+ onStep: function(values) {
25852
+ var currentFactor = values.zoom;
25853
+ scaleFactor = currentFactor;
25854
+
25855
+ // Calculate new scroll position to maintain focal point
25856
+ var currentScreenSize = [chart.width(), chart.height()];
25857
+ var newScrollPos;
25858
+
25859
+ if (focalPoint) {
25860
+ // Keep the focal point at the same screen position
25861
+ newScrollPos = [
25862
+ center[0] * scaleFactor - focalPoint[0],
25863
+ center[1] * scaleFactor - focalPoint[1]
25864
+ ];
25865
+ } else {
25866
+ // Keep viewport center steady
25867
+ newScrollPos = [
25868
+ center[0] * scaleFactor - currentScreenSize[0]/2,
25869
+ center[1] * scaleFactor - currentScreenSize[1]/2
25870
+ ];
25871
+ }
25872
+
25873
+ chart.scrollLeft(newScrollPos[0]);
25874
+ chart.scrollTop(newScrollPos[1]);
25875
+
25876
+ // During animation, only update the scale transform, not the full redraw
25877
+ // This is much more performant with many nodes
25878
+ eventLayer.attr("transform", "scale(" + scaleFactor + ")");
25879
+ outer.attr("width", space_width * scaleFactor).attr("height", space_height * scaleFactor);
25880
+ RED.view.navigator.resize();
25881
+ },
25882
+ onStart: function() {
25883
+ // Show minimap when zoom animation starts
25884
+ RED.events.emit("view:navigate");
25885
+ },
25886
+ onEnd: function() {
25887
+ cancelInProgressAnimation = null;
25888
+ // Ensure scaleFactor is exactly the target to prevent precision issues
25889
+ scaleFactor = targetFactor;
25890
+ // Full redraw at the end to ensure everything is correct
25891
+ redraw();
25892
+ if (RED.settings.get("editor.view.view-store-zoom")) {
25893
+ RED.settings.setLocal('zoom-level', targetFactor.toFixed(1));
25894
+ }
25895
+ },
25896
+ onCancel: function() {
25897
+ cancelInProgressAnimation = null;
25898
+ // Ensure scaleFactor is set to current target on cancel
25899
+ scaleFactor = targetFactor;
25900
+ }
25901
+ });
25902
+ }
25903
+
25904
+ // Momentum scrolling functions
25905
+ function startMomentumScroll() {
25906
+ if (scrollAnimationId) {
25907
+ cancelAnimationFrame(scrollAnimationId);
25908
+ }
25909
+ momentumActive = true;
25910
+ animateMomentumScroll();
25911
+ }
25912
+
25913
+ function animateMomentumScroll() {
25914
+ if (!momentumActive) {
25915
+ return;
25916
+ }
25917
+
25918
+ var scrollX = chart.scrollLeft();
25919
+ var scrollY = chart.scrollTop();
25920
+ var maxScrollX = chart[0].scrollWidth - chart.width();
25921
+ var maxScrollY = chart[0].scrollHeight - chart.height();
25922
+
25923
+ // Apply friction
25924
+ scrollVelocity.x *= FRICTION;
25925
+ scrollVelocity.y *= FRICTION;
25926
+
25927
+ // Check for edges and apply bounce
25928
+ var newScrollX = scrollX + scrollVelocity.x;
25929
+ var newScrollY = scrollY + scrollVelocity.y;
25930
+
25931
+ // Bounce effect at edges
25932
+ if (newScrollX < 0) {
25933
+ newScrollX = 0;
25934
+ scrollVelocity.x = -scrollVelocity.x * BOUNCE_DAMPING;
25935
+ } else if (newScrollX > maxScrollX) {
25936
+ newScrollX = maxScrollX;
25937
+ scrollVelocity.x = -scrollVelocity.x * BOUNCE_DAMPING;
25938
+ }
25939
+
25940
+ if (newScrollY < 0) {
25941
+ newScrollY = 0;
25942
+ scrollVelocity.y = -scrollVelocity.y * BOUNCE_DAMPING;
25943
+ } else if (newScrollY > maxScrollY) {
25944
+ newScrollY = maxScrollY;
25945
+ scrollVelocity.y = -scrollVelocity.y * BOUNCE_DAMPING;
25946
+ }
25947
+
25948
+ // Apply new scroll position
25949
+ chart.scrollLeft(newScrollX);
25950
+ chart.scrollTop(newScrollY);
25951
+
25952
+ // Stop if velocity is too small
25953
+ if (Math.abs(scrollVelocity.x) < MIN_VELOCITY && Math.abs(scrollVelocity.y) < MIN_VELOCITY) {
25954
+ momentumActive = false;
25955
+ scrollVelocity.x = 0;
25956
+ scrollVelocity.y = 0;
25957
+ } else {
25958
+ scrollAnimationId = requestAnimationFrame(animateMomentumScroll);
25959
+ }
25960
+ }
25961
+
25962
+ function handleScroll() {
25963
+ var now = Date.now();
25964
+ var scrollX = chart.scrollLeft();
25965
+ var scrollY = chart.scrollTop();
25966
+
25967
+ if (lastScrollTime) {
25968
+ var dt = now - lastScrollTime;
25969
+ if (dt > 0 && dt < 100) { // Only calculate velocity for recent scrolls
25970
+ scrollVelocity.x = (scrollX - lastScrollPos.x) / dt * 16; // Normalize to 60fps
25971
+ scrollVelocity.y = (scrollY - lastScrollPos.y) / dt * 16;
25972
+ }
25973
+ }
25974
+
25975
+ lastScrollTime = now;
25976
+ lastScrollPos.x = scrollX;
25977
+ lastScrollPos.y = scrollY;
25978
+
25979
+ // Cancel any ongoing momentum animation
25980
+ if (scrollAnimationId) {
25981
+ cancelAnimationFrame(scrollAnimationId);
25982
+ scrollAnimationId = null;
25983
+ }
25984
+ }
25985
+
25160
25986
  function selectNone() {
25161
25987
  if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) {
25162
25988
  return;
@@ -25861,6 +26687,9 @@ RED.view = (function() {
25861
26687
 
25862
26688
  function portMouseDown(d,portType,portIndex, evt) {
25863
26689
  if (RED.view.DEBUG) { console.warn("portMouseDown", mouse_mode,d,portType,portIndex); }
26690
+ if (spacebarPressed) {
26691
+ return
26692
+ }
25864
26693
  clearSuggestedFlow();
25865
26694
  RED.contextMenu.hide();
25866
26695
  evt = evt || d3.event;
@@ -26290,6 +27119,9 @@ RED.view = (function() {
26290
27119
  (d3.event || event).stopPropagation();
26291
27120
  return;
26292
27121
  }
27122
+ if (spacebarPressed) {
27123
+ return
27124
+ }
26293
27125
  clearTimeout(portLabelHoverTimeout);
26294
27126
  var active = (mouse_mode!=RED.state.JOINING && mouse_mode != RED.state.QUICK_JOINING) || // Not currently joining - all ports active
26295
27127
  (
@@ -26446,6 +27278,9 @@ RED.view = (function() {
26446
27278
  }
26447
27279
  function nodeMouseDown(d) {
26448
27280
  if (RED.view.DEBUG) { console.warn("nodeMouseDown", mouse_mode,d); }
27281
+ if (spacebarPressed) {
27282
+ return
27283
+ }
26449
27284
  clearSuggestedFlow()
26450
27285
  focusView();
26451
27286
  RED.contextMenu.hide();
@@ -26624,6 +27459,9 @@ RED.view = (function() {
26624
27459
 
26625
27460
  function nodeMouseOver(d) {
26626
27461
  if (RED.view.DEBUG) { console.warn("nodeMouseOver", mouse_mode,d); }
27462
+ if (spacebarPressed) {
27463
+ return
27464
+ }
26627
27465
  if (mouse_mode === 0 || mouse_mode === RED.state.SELECTING_NODE) {
26628
27466
  if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions && selectNodesOptions.filter) {
26629
27467
  if (selectNodesOptions.filter(d)) {
@@ -26797,6 +27635,9 @@ RED.view = (function() {
26797
27635
  if (RED.view.DEBUG) {
26798
27636
  console.warn("groupMouseDown", { mouse_mode, point: mouse, event: d3.event });
26799
27637
  }
27638
+ if (spacebarPressed) {
27639
+ return
27640
+ }
26800
27641
  RED.contextMenu.hide();
26801
27642
  focusView();
26802
27643
  if (d3.event.button === 1) {
@@ -27256,6 +28097,15 @@ RED.view = (function() {
27256
28097
  eventLayer.attr("transform","scale("+scaleFactor+")");
27257
28098
  outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor);
27258
28099
 
28100
+ // Update scroll spacer to match scaled canvas size
28101
+ // This ensures scrollable area = canvas area
28102
+ // Browser calculates maxScroll = scrollWidth - viewport, which correctly
28103
+ // allows scrolling to see the far edges of canvas without going beyond
28104
+ $('#red-ui-workspace-scroll-spacer').css({
28105
+ width: (space_width * scaleFactor) + 'px',
28106
+ height: (space_height * scaleFactor) + 'px'
28107
+ });
28108
+
27259
28109
  // Don't bother redrawing nodes if we're drawing links
27260
28110
  if (forceFullRedraw || showAllLinkPorts !== -1 || mouse_mode != RED.state.JOINING) {
27261
28111
  forceFullRedraw = false
@@ -29616,7 +30466,7 @@ RED.view = (function() {
29616
30466
  selectNodes: function(options) {
29617
30467
  $("#red-ui-workspace-tabs-shade").show();
29618
30468
  $("#red-ui-palette-shade").show();
29619
- $("#red-ui-sidebar-shade").show();
30469
+ $(".red-ui-sidebar-shade").show();
29620
30470
  $("#red-ui-header-shade").show();
29621
30471
  $("#red-ui-workspace").addClass("red-ui-workspace-select-mode");
29622
30472
 
@@ -29638,7 +30488,7 @@ RED.view = (function() {
29638
30488
  clearSelection();
29639
30489
  $("#red-ui-workspace-tabs-shade").hide();
29640
30490
  $("#red-ui-palette-shade").hide();
29641
- $("#red-ui-sidebar-shade").hide();
30491
+ $(".red-ui-sidebar-shade").hide();
29642
30492
  $("#red-ui-header-shade").hide();
29643
30493
  $("#red-ui-workspace").removeClass("red-ui-workspace-select-mode");
29644
30494
  resetMouseVars();
@@ -29707,7 +30557,283 @@ RED.view = (function() {
29707
30557
  applySuggestedFlow
29708
30558
  };
29709
30559
  })();
29710
- ;RED.view.annotations = (function() {
30560
+ ;/**
30561
+ * Zoom configuration constants
30562
+ */
30563
+ RED.view.zoomConstants = {
30564
+ // Zoom limits
30565
+ MIN_ZOOM: 0.05, // Default minimum, will be dynamically calculated to fit canvas
30566
+ MAX_ZOOM: 2.0,
30567
+
30568
+ // Zoom step for keyboard/button controls
30569
+ ZOOM_STEP: 0.2,
30570
+
30571
+ // Animation settings
30572
+ DEFAULT_ZOOM_DURATION: 125, // ms, faster animation
30573
+
30574
+ // Gesture thresholds
30575
+ PINCH_THRESHOLD: 10, // minimum pixel movement to trigger zoom
30576
+
30577
+ // Momentum and friction for smooth scrolling
30578
+ FRICTION: 0.92,
30579
+ BOUNCE_DAMPING: 0.6
30580
+ };;/**
30581
+ * Copyright JS Foundation and other contributors, http://js.foundation
30582
+ *
30583
+ * Licensed under the Apache License, Version 2.0 (the "License");
30584
+ * you may not use this file except in compliance with the License.
30585
+ * You may obtain a copy of the License at
30586
+ *
30587
+ * http://www.apache.org/licenses/LICENSE-2.0
30588
+ *
30589
+ * Unless required by applicable law or agreed to in writing, software
30590
+ * distributed under the License is distributed on an "AS IS" BASIS,
30591
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30592
+ * See the License for the specific language governing permissions and
30593
+ * limitations under the License.
30594
+ **/
30595
+
30596
+ RED.view.zoomAnimator = (function() {
30597
+
30598
+ /**
30599
+ * Easing function for smooth deceleration
30600
+ * Creates natural-feeling animation curves
30601
+ * @param {number} t - Progress from 0 to 1
30602
+ * @returns {number} - Eased value from 0 to 1
30603
+ */
30604
+ function easeOut(t) {
30605
+ // Cubic ease-out for smooth deceleration
30606
+ return 1 - Math.pow(1 - t, 3);
30607
+ }
30608
+
30609
+ /**
30610
+ * Animate values using requestAnimationFrame with easing
30611
+ * Based on Excalidraw's implementation for smooth zoom transitions
30612
+ *
30613
+ * @param {Object} options - Animation options
30614
+ * @param {Object} options.fromValues - Starting values object
30615
+ * @param {Object} options.toValues - Target values object
30616
+ * @param {Function} options.onStep - Callback for each animation frame
30617
+ * @param {number} [options.duration=250] - Animation duration in ms
30618
+ * @param {Function} [options.interpolateValue] - Custom interpolation function
30619
+ * @param {Function} [options.onStart] - Animation start callback
30620
+ * @param {Function} [options.onEnd] - Animation end callback
30621
+ * @param {Function} [options.onCancel] - Animation cancel callback
30622
+ * @returns {Function} - Cancel function to stop animation
30623
+ */
30624
+ function easeToValuesRAF(options) {
30625
+ const {
30626
+ fromValues,
30627
+ toValues,
30628
+ onStep,
30629
+ duration = 250,
30630
+ interpolateValue,
30631
+ onStart,
30632
+ onEnd,
30633
+ onCancel
30634
+ } = options;
30635
+
30636
+ let startTime = null;
30637
+ let animationId = null;
30638
+ let cancelled = false;
30639
+
30640
+ function step(timestamp) {
30641
+ if (cancelled) {
30642
+ return;
30643
+ }
30644
+
30645
+ if (!startTime) {
30646
+ startTime = timestamp;
30647
+ if (onStart) {
30648
+ onStart();
30649
+ }
30650
+ }
30651
+
30652
+ const elapsed = timestamp - startTime;
30653
+ const progress = Math.min(elapsed / duration, 1);
30654
+ const easedProgress = easeOut(progress);
30655
+
30656
+ const interpolatedValues = {};
30657
+
30658
+ for (const key in fromValues) {
30659
+ if (fromValues.hasOwnProperty(key)) {
30660
+ const from = fromValues[key];
30661
+ const to = toValues[key];
30662
+
30663
+ if (interpolateValue && key === 'zoom') {
30664
+ // Special interpolation for zoom to feel more natural
30665
+ // Exponential interpolation preserves relative zoom feel
30666
+ interpolatedValues[key] = from * Math.pow(to / from, easedProgress);
30667
+ } else {
30668
+ // Linear interpolation for other values
30669
+ interpolatedValues[key] = from + (to - from) * easedProgress;
30670
+ }
30671
+ }
30672
+ }
30673
+
30674
+ onStep(interpolatedValues);
30675
+
30676
+ if (progress < 1) {
30677
+ animationId = requestAnimationFrame(step);
30678
+ } else {
30679
+ if (onEnd) {
30680
+ onEnd();
30681
+ }
30682
+ }
30683
+ }
30684
+
30685
+ animationId = requestAnimationFrame(step);
30686
+
30687
+ // Return cancel function
30688
+ return function cancel() {
30689
+ cancelled = true;
30690
+ if (animationId) {
30691
+ cancelAnimationFrame(animationId);
30692
+ }
30693
+ if (onCancel) {
30694
+ onCancel();
30695
+ }
30696
+ };
30697
+ }
30698
+
30699
+ /**
30700
+ * Calculate smooth zoom delta with acceleration
30701
+ * Provides consistent zoom speed regardless of input device
30702
+ *
30703
+ * @param {number} currentScale - Current zoom scale
30704
+ * @param {number} delta - Input delta (wheel, gesture, etc)
30705
+ * @param {boolean} isTrackpad - Whether input is from trackpad
30706
+ * @returns {number} - Calculated zoom delta
30707
+ */
30708
+ function calculateZoomDelta(currentScale, delta, isTrackpad) {
30709
+ // Normalize delta across different input devices
30710
+ let normalizedDelta = delta;
30711
+
30712
+ if (isTrackpad) {
30713
+ // Trackpad deltas are typically smaller and more frequent
30714
+ normalizedDelta = delta * 0.005; // Reduced from 0.01 for gentler zoom
30715
+ } else {
30716
+ // Mouse wheel deltas are larger and less frequent
30717
+ // Reduce zoom out speed more than zoom in
30718
+ normalizedDelta = delta > 0 ? 0.06 : -0.08; // Reduced from 0.1, asymmetric for gentler zoom out
30719
+ }
30720
+
30721
+ // Apply gentler acceleration based on current zoom level
30722
+ // Less aggressive acceleration to prevent rapid zoom out
30723
+ const acceleration = Math.max(0.7, Math.min(1.1, 1 / currentScale)); // Reduced from 0.5-1.2 to 0.7-1.1
30724
+
30725
+ return normalizedDelta * acceleration;
30726
+ }
30727
+
30728
+ /**
30729
+ * Gesture state management for consistent focal points
30730
+ */
30731
+ const gestureState = {
30732
+ active: false,
30733
+ initialFocalPoint: null, // Will store workspace coordinates
30734
+ initialScale: 1,
30735
+ currentScale: 1,
30736
+ lastDistance: 0,
30737
+ scrollPosAtStart: null, // Store initial scroll position
30738
+ scaleFatorAtStart: 1 // Store initial scale factor
30739
+ };
30740
+
30741
+ /**
30742
+ * Start a zoom gesture with fixed focal point
30743
+ * @param {Array} focalPoint - [x, y] coordinates of focal point in workspace
30744
+ * @param {number} scale - Initial scale value
30745
+ * @param {Array} scrollPos - Current scroll position [x, y]
30746
+ * @param {number} currentScaleFactor - Current scale factor for coordinate conversion
30747
+ */
30748
+ function startGesture(focalPoint, scale, scrollPos, currentScaleFactor) {
30749
+ gestureState.active = true;
30750
+ // Store the focal point in workspace coordinates for stability
30751
+ // This ensures the point remains fixed even if scroll changes due to canvas edge constraints
30752
+ if (focalPoint && scrollPos && currentScaleFactor) {
30753
+ gestureState.initialFocalPoint = [
30754
+ (scrollPos[0] + focalPoint[0]) / currentScaleFactor,
30755
+ (scrollPos[1] + focalPoint[1]) / currentScaleFactor
30756
+ ];
30757
+ gestureState.scrollPosAtStart = [...scrollPos];
30758
+ gestureState.scaleFatorAtStart = currentScaleFactor;
30759
+ } else {
30760
+ gestureState.initialFocalPoint = focalPoint ? [...focalPoint] : null;
30761
+ }
30762
+ gestureState.initialScale = scale;
30763
+ gestureState.currentScale = scale;
30764
+ return gestureState;
30765
+ }
30766
+
30767
+ /**
30768
+ * Update gesture maintaining fixed focal point
30769
+ * @param {number} newScale - New scale value
30770
+ * @returns {Object} - Gesture state with fixed focal point
30771
+ */
30772
+ function updateGesture(newScale) {
30773
+ if (!gestureState.active) {
30774
+ return null;
30775
+ }
30776
+
30777
+ gestureState.currentScale = newScale;
30778
+
30779
+ return {
30780
+ scale: newScale,
30781
+ focalPoint: gestureState.initialFocalPoint,
30782
+ active: gestureState.active
30783
+ };
30784
+ }
30785
+
30786
+ /**
30787
+ * End the current gesture
30788
+ */
30789
+ function endGesture() {
30790
+ gestureState.active = false;
30791
+ gestureState.initialFocalPoint = null;
30792
+ gestureState.lastDistance = 0;
30793
+ }
30794
+
30795
+ /**
30796
+ * Check if a gesture is currently active
30797
+ */
30798
+ function isGestureActive() {
30799
+ return gestureState.active;
30800
+ }
30801
+
30802
+ /**
30803
+ * Get the fixed focal point for the current gesture
30804
+ * @param {Array} currentScrollPos - Current scroll position [x, y]
30805
+ * @param {number} currentScaleFactor - Current scale factor
30806
+ * @returns {Array} - Focal point in screen coordinates or null
30807
+ */
30808
+ function getGestureFocalPoint(currentScrollPos, currentScaleFactor) {
30809
+ if (!gestureState.initialFocalPoint) {
30810
+ return null;
30811
+ }
30812
+
30813
+ // If we stored workspace coordinates, convert back to screen coordinates
30814
+ if (gestureState.scrollPosAtStart && currentScrollPos && currentScaleFactor) {
30815
+ // Convert workspace coordinates back to current screen coordinates
30816
+ return [
30817
+ gestureState.initialFocalPoint[0] * currentScaleFactor - currentScrollPos[0],
30818
+ gestureState.initialFocalPoint[1] * currentScaleFactor - currentScrollPos[1]
30819
+ ];
30820
+ }
30821
+
30822
+ return gestureState.initialFocalPoint;
30823
+ }
30824
+
30825
+ return {
30826
+ easeOut: easeOut,
30827
+ easeToValuesRAF: easeToValuesRAF,
30828
+ calculateZoomDelta: calculateZoomDelta,
30829
+ gestureState: gestureState,
30830
+ startGesture: startGesture,
30831
+ updateGesture: updateGesture,
30832
+ endGesture: endGesture,
30833
+ isGestureActive: isGestureActive,
30834
+ getGestureFocalPoint: getGestureFocalPoint
30835
+ };
30836
+ })();;RED.view.annotations = (function() {
29711
30837
 
29712
30838
  var annotations = {};
29713
30839
 
@@ -29907,139 +31033,205 @@ RED.view = (function() {
29907
31033
  **/
29908
31034
 
29909
31035
 
29910
- RED.view.navigator = (function() {
29911
-
29912
- var nav_scale = 50;
29913
- var nav_width = 8000/nav_scale;
29914
- var nav_height = 8000/nav_scale;
29915
-
29916
- var navContainer;
29917
- var navBox;
29918
- var navBorder;
29919
- var navVis;
29920
- var scrollPos;
29921
- var scaleFactor;
29922
- var chartSize;
29923
- var dimensions;
29924
- var isDragging;
29925
- var isShowing = false;
29926
-
29927
- function refreshNodes() {
29928
- if (!isShowing) {
29929
- return;
29930
- }
29931
- var navNode = navVis.selectAll(".red-ui-navigator-node").data(RED.view.getActiveNodes(),function(d){return d.id});
29932
- navNode.exit().remove();
29933
- navNode.enter().insert("rect")
29934
- .attr('class','red-ui-navigator-node')
29935
- .attr("pointer-events", "none");
29936
- navNode.each(function(d) {
29937
- d3.select(this).attr("x",function(d) { return (d.x-d.w/2)/nav_scale })
29938
- .attr("y",function(d) { return (d.y-d.h/2)/nav_scale })
29939
- .attr("width",function(d) { return Math.max(9,d.w/nav_scale) })
29940
- .attr("height",function(d) { return Math.max(3,d.h/nav_scale) })
29941
- .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def);})
29942
- });
29943
- }
29944
- function onScroll() {
29945
- if (!isDragging) {
29946
- resizeNavBorder();
29947
- }
29948
- }
29949
- function resizeNavBorder() {
29950
- if (navBorder) {
29951
- scaleFactor = RED.view.scale();
29952
- chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
29953
- scrollPos = [$("#red-ui-workspace-chart").scrollLeft(),$("#red-ui-workspace-chart").scrollTop()];
29954
- navBorder.attr('x',scrollPos[0]/nav_scale)
29955
- .attr('y',scrollPos[1]/nav_scale)
29956
- .attr('width',chartSize[0]/nav_scale/scaleFactor)
29957
- .attr('height',chartSize[1]/nav_scale/scaleFactor)
29958
- }
29959
- }
29960
- function toggle() {
29961
- if (!isShowing) {
29962
- isShowing = true;
29963
- $("#red-ui-view-navigate").addClass("selected");
29964
- resizeNavBorder();
29965
- refreshNodes();
29966
- $("#red-ui-workspace-chart").on("scroll",onScroll);
29967
- navContainer.fadeIn(200);
29968
- } else {
29969
- isShowing = false;
29970
- navContainer.fadeOut(100);
29971
- $("#red-ui-workspace-chart").off("scroll",onScroll);
29972
- $("#red-ui-view-navigate").removeClass("selected");
29973
- }
29974
- }
29975
-
29976
- return {
29977
- init: function() {
29978
-
29979
- $(window).on("resize", resizeNavBorder);
29980
- RED.events.on("sidebar:resize",resizeNavBorder);
29981
- RED.actions.add("core:toggle-navigator",toggle);
29982
- var hideTimeout;
29983
-
29984
- navContainer = $('<div>').css({
29985
- "position":"absolute",
29986
- "bottom":$("#red-ui-workspace-footer").height(),
29987
- "right":0,
29988
- zIndex: 1
29989
- }).appendTo("#red-ui-workspace").hide();
29990
-
29991
- navBox = d3.select(navContainer[0])
29992
- .append("svg:svg")
29993
- .attr("width", nav_width)
29994
- .attr("height", nav_height)
29995
- .attr("pointer-events", "all")
29996
- .attr("id","red-ui-navigator-canvas")
29997
-
29998
- navBox.append("rect").attr("x",0).attr("y",0).attr("width",nav_width).attr("height",nav_height).style({
29999
- fill:"none",
30000
- stroke:"none",
30001
- pointerEvents:"all"
30002
- }).on("mousedown", function() {
30003
- // Update these in case they have changed
30004
- scaleFactor = RED.view.scale();
30005
- chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
30006
- dimensions = [chartSize[0]/nav_scale/scaleFactor, chartSize[1]/nav_scale/scaleFactor];
30007
- var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
30008
- var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
30009
- navBorder.attr('x',newX).attr('y',newY);
30010
- isDragging = true;
30011
- $("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
30012
- $("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
30013
- }).on("mousemove", function() {
30014
- if (!isDragging) { return }
30015
- if (d3.event.buttons === 0) {
30016
- isDragging = false;
30017
- return;
30018
- }
30019
- var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
30020
- var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
30021
- navBorder.attr('x',newX).attr('y',newY);
30022
- $("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
30023
- $("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
30024
- }).on("mouseup", function() {
30025
- isDragging = false;
30026
- })
30027
-
30028
- navBorder = navBox.append("rect").attr("class","red-ui-navigator-border")
30029
-
30030
- navVis = navBox.append("svg:g")
30031
-
30032
- RED.statusBar.add({
30033
- id: "view-navigator",
30034
- align: "right",
30035
- element: $('<button class="red-ui-footer-button-toggle single" id="red-ui-view-navigate"><i class="fa fa-map-o"></i></button>')
30036
- })
31036
+ RED.view.navigator = (function() {
31037
+ var nav_scale = 50;
31038
+ var nav_width = 8000/nav_scale;
31039
+ var nav_height = 8000/nav_scale;
31040
+ var navContainer;
31041
+ var navBox;
31042
+ var navBorder;
31043
+ var navVis;
31044
+ var scrollPos;
31045
+ var scaleFactor;
31046
+ var chartSize;
31047
+ var dimensions;
31048
+ var isDragging;
31049
+ var isShowing = false;
31050
+ var toggleTimeout;
31051
+ var autoHideTimeout;
31052
+ var isManuallyToggled = false;
31053
+ var isTemporaryShow = false;
31054
+ function refreshNodes() {
31055
+ if (!isShowing) {
31056
+ return;
31057
+ }
31058
+ var navNode = navVis.selectAll(".red-ui-navigator-node").data(RED.view.getActiveNodes(),function(d){return d.id});
31059
+ navNode.exit().remove();
31060
+ navNode.enter().insert("rect")
31061
+ .attr('class','red-ui-navigator-node')
31062
+ .attr("pointer-events", "none");
31063
+ navNode.each(function(d) {
31064
+ d3.select(this).attr("x",function(d) { return (d.x-d.w/2)/nav_scale })
31065
+ .attr("y",function(d) { return (d.y-d.h/2)/nav_scale })
31066
+ .attr("width",function(d) { return Math.max(9,d.w/nav_scale) })
31067
+ .attr("height",function(d) { return Math.max(3,d.h/nav_scale) })
31068
+ .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def);})
31069
+ });
31070
+ }
31071
+ function onScroll() {
31072
+ if (!isDragging) {
31073
+ resizeNavBorder();
31074
+ }
31075
+ }
31076
+ function resizeNavBorder() {
31077
+ if (navBorder) {
31078
+ scaleFactor = RED.view.scale();
31079
+ chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
31080
+ scrollPos = [$("#red-ui-workspace-chart").scrollLeft(),$("#red-ui-workspace-chart").scrollTop()];
31081
+ // Convert scroll position (in scaled pixels) to workspace coordinates, then to minimap coordinates
31082
+ // scrollPos is in scaled canvas pixels, divide by scaleFactor to get workspace coords
31083
+ if (chartSize[0] > 0 && chartSize[1] > 0) {
31084
+ navBorder.attr('x',scrollPos[0]/scaleFactor/nav_scale)
31085
+ .attr('y',scrollPos[1]/scaleFactor/nav_scale)
31086
+ .attr('width',chartSize[0]/nav_scale/scaleFactor)
31087
+ .attr('height',chartSize[1]/nav_scale/scaleFactor)
31088
+ }
31089
+ }
31090
+ }
31091
+ function show () {
31092
+ if (!isShowing) {
31093
+ isShowing = true;
31094
+ clearTimeout(autoHideTimeout);
31095
+ $("#red-ui-view-navigate").addClass("selected");
31096
+ resizeNavBorder();
31097
+ refreshNodes();
31098
+ $("#red-ui-workspace-chart").on("scroll",onScroll);
31099
+ navContainer.addClass('red-ui-navigator-container');
31100
+ navContainer.show();
31101
+ clearTimeout(toggleTimeout)
31102
+ toggleTimeout = setTimeout(function() {
31103
+ navContainer.addClass('red-ui-navigator-visible');
31104
+ }, 10);
31105
+ }
31106
+ }
31107
+ function hide () {
31108
+ if (isShowing) {
31109
+ isShowing = false;
31110
+ isTemporaryShow = false;
31111
+ isManuallyToggled = false;
31112
+ clearTimeout(autoHideTimeout);
31113
+ navContainer.removeClass('red-ui-navigator-visible');
31114
+ clearTimeout(toggleTimeout)
31115
+ toggleTimeout = setTimeout(function() {
31116
+ navContainer.hide();
31117
+ }, 300);
31118
+ $("#red-ui-workspace-chart").off("scroll",onScroll);
31119
+ $("#red-ui-view-navigate").removeClass("selected");
31120
+ }
31121
+ }
31122
+ function toggle() {
31123
+ if (!isShowing) {
31124
+ isManuallyToggled = true;
31125
+ show()
31126
+ } else {
31127
+ isManuallyToggled = false;
31128
+ hide()
31129
+ }
31130
+ }
31131
+ function setupAutoHide () {
31132
+ clearTimeout(autoHideTimeout);
31133
+ autoHideTimeout = setTimeout(function() {
31134
+ hide()
31135
+ }, 2000)
31136
+ }
31137
+ function showTemporary() {
31138
+ if (!isManuallyToggled) {
31139
+ isTemporaryShow = true
31140
+ clearTimeout(autoHideTimeout);
31141
+ show()
31142
+ setupAutoHide()
31143
+ }
31144
+ }
31145
+ return {
31146
+ init: function() {
31147
+ $(window).on("resize", resizeNavBorder);
31148
+ RED.events.on("sidebar:resize",resizeNavBorder);
31149
+ RED.actions.add("core:toggle-navigator",toggle);
31150
+ navContainer = $('<div>').css({
31151
+ "position":"absolute",
31152
+ "bottom":$("#red-ui-workspace-footer").height() + 2,
31153
+ "right": 4,
31154
+ zIndex: 1
31155
+ }).addClass('red-ui-navigator-container').appendTo("#red-ui-workspace").hide();
31156
+ navBox = d3.select(navContainer[0])
31157
+ .append("svg:svg")
31158
+ .attr("width", nav_width)
31159
+ .attr("height", nav_height)
31160
+ .attr("pointer-events", "all")
31161
+ .attr("id","red-ui-navigator-canvas")
31162
+ navVis = navBox.append("svg:g")
31163
+
31164
+ navBox.append("rect").attr("x",0).attr("y",0).attr("width",nav_width).attr("height",nav_height).style({
31165
+ fill:"none",
31166
+ stroke:"none",
31167
+ pointerEvents:"all"
31168
+ }).on("mousedown", function() {
31169
+ // Update these in case they have changed
31170
+ scaleFactor = RED.view.scale();
31171
+ chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
31172
+ dimensions = [chartSize[0]/nav_scale/scaleFactor, chartSize[1]/nav_scale/scaleFactor];
31173
+ var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
31174
+ var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
31175
+ navBorder.attr('x',newX).attr('y',newY);
31176
+ isDragging = true;
31177
+ $("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
31178
+ $("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
31179
+ }).on("mousemove", function() {
31180
+ if (!isDragging) { return }
31181
+ if (d3.event.buttons === 0) {
31182
+ isDragging = false;
31183
+ return;
31184
+ }
31185
+ var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
31186
+ var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
31187
+ navBorder.attr('x',newX).attr('y',newY);
31188
+ $("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
31189
+ $("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
31190
+ }).on("mouseup", function() {
31191
+ isDragging = false;
31192
+ }).on("mouseenter", function () {
31193
+ if (isTemporaryShow) {
31194
+ // If user hovers over the minimap while it's temporarily shown, keep it shown
31195
+ clearTimeout(autoHideTimeout);
31196
+ }
31197
+ }).on("mouseleave", function () {
31198
+ if (isTemporaryShow) {
31199
+ // Restart the auto-hide timer after mouse leaves the minimap
31200
+ setupAutoHide()
31201
+ }
31202
+ })
31203
+ navBorder = navBox.append("rect").attr("class","red-ui-navigator-border")
31204
+ RED.statusBar.add({
31205
+ id: "view-navigator",
31206
+ align: "right",
31207
+ element: $('<button class="red-ui-footer-button-toggle single" id="red-ui-view-navigate"><i class="fa fa-map-o"></i></button>')
31208
+ })
30037
31209
 
30038
31210
  $("#red-ui-view-navigate").on("click", function(evt) {
30039
31211
  evt.preventDefault();
30040
31212
  toggle();
30041
31213
  })
30042
31214
  RED.popover.tooltip($("#red-ui-view-navigate"),RED._('actions.toggle-navigator'),'core:toggle-navigator');
31215
+
31216
+ // Listen for canvas interactions to show minimap temporarily
31217
+ // Only show on actual pan/zoom navigation, not selection changes
31218
+ // RED.events.on("view:navigate", function() {
31219
+ // showTemporary();
31220
+ // });
31221
+
31222
+ // Show minimap briefly when workspace changes (includes initial load)
31223
+ // RED.events.on("workspace:change", function(event) {
31224
+ // // Only show if there's an active workspace with nodes
31225
+ // if (event.workspace && RED.nodes.getWorkspaceOrder().length > 0) {
31226
+ // // Small delay to ensure nodes are rendered
31227
+ // setTimeout(function() {
31228
+ // var activeNodes = RED.nodes.filterNodes({z: event.workspace});
31229
+ // if (activeNodes.length > 0) {
31230
+ // showTemporary();
31231
+ // }
31232
+ // }, 100);
31233
+ // }
31234
+ // });
30043
31235
  },
30044
31236
  refresh: refreshNodes,
30045
31237
  resize: resizeNavBorder,
@@ -31507,19 +32699,65 @@ RED.view.tools = (function() {
31507
32699
  * limitations under the License.
31508
32700
  **/
31509
32701
  RED.sidebar = (function() {
32702
+ const sidebars = {
32703
+ primary: {
32704
+ id: 'primary',
32705
+ direction: 'right',
32706
+ menuToggle: 'menu-item-sidebar',
32707
+ minimumWidth: 180,
32708
+ maximumWidth: 800,
32709
+ defaultWidth: 300,
32710
+ defaultTopHeight: 0.7
32711
+ },
32712
+ secondary: {
32713
+ id: 'secondary',
32714
+ direction: 'left',
32715
+ menuToggle: 'menu-item-palette',
32716
+ minimumWidth: 180,
32717
+ maximumWidth: 800,
32718
+ // Make LH side slightly narrower by default as its the palette that doesn't require a lot of width
32719
+ defaultWidth: 210,
32720
+ defaultTopHeight: 1
32721
+ }
32722
+ }
32723
+ const defaultSidebarConfiguration = {
32724
+ primary: [ ['info','help','config','context'], ['debug'] ],
32725
+ secondary: [ ['explorer','palette'], [] ]
32726
+ }
32727
+
32728
+ const knownTabs = {};
32729
+
32730
+ function exportSidebarState () {
32731
+ const state = {
32732
+ primary: [[], []],
32733
+ secondary: [[], []]
32734
+ }
32735
+ function getTabButtons(tabBar) {
32736
+ const result = []
32737
+ tabBar.children('button').each(function() {
32738
+ const tabId = $(this).attr('data-tab-id');
32739
+ if (tabId) {
32740
+ result.push(tabId);
32741
+ }
32742
+ })
32743
+ return result
32744
+ }
32745
+ state.primary[0] = getTabButtons(sidebars.primary.tabBars.top.container);
32746
+ state.primary[1] = getTabButtons(sidebars.primary.tabBars.bottom.container);
32747
+ state.secondary[0] = getTabButtons(sidebars.secondary.tabBars.top.container);
32748
+ state.secondary[1] = getTabButtons(sidebars.secondary.tabBars.bottom.container);
32749
+
32750
+ RED.settings.set('editor.sidebar.state', state)
32751
+ }
31510
32752
 
31511
- //$('#sidebar').tabs();
31512
- var sidebar_tabs;
31513
- var knownTabs = {};
31514
32753
 
31515
32754
  // We store the current sidebar tab id in localStorage as 'last-sidebar-tab'
31516
32755
  // This is restored when the editor is reloaded.
31517
- // We use sidebar_tabs.onchange to update localStorage. However that will
32756
+ // We use sidebars.primary.tabs.onchange to update localStorage. However that will
31518
32757
  // also get triggered when the first tab gets added to the tabs - typically
31519
32758
  // the 'info' tab. So we use the following variable to store the retrieved
31520
32759
  // value from localStorage before we start adding the actual tabs
31521
- var lastSessionSelectedTab = null;
31522
-
32760
+ let lastSessionSelectedTabs = {}
31523
32761
 
31524
32762
  function addTab(title,content,closeable,visible) {
31525
32763
  var options;
@@ -31536,10 +32774,55 @@ RED.sidebar = (function() {
31536
32774
  } else if (typeof title === "object") {
31537
32775
  options = title;
31538
32776
  }
32777
+ options.target = options.target || 'primary';
32778
+ let targetTabButtonIndex = -1 // Append to end by default
32779
+
32780
+ // Check the saved sidebar state to see if this tab should be added to the primary or secondary sidebar
32781
+ let savedState = RED.settings.get('editor.sidebar.state', defaultSidebarConfiguration)
32782
+ if (true || typeof savedState.primary[0] === 'string' || typeof savedState.secondary[0] === 'string') {
32783
+ // This is a beta.0 format. Reset it for beta.1
32784
+ savedState = defaultSidebarConfiguration
32785
+ RED.settings.set('editor.sidebar.state', savedState)
32786
+ }
32787
+ let targetSidebar = null
32788
+ let targetSection = null
32789
+ if (savedState) {
32790
+ let sidebarState
32791
+ if (savedState.secondary[0].includes(options.id)) {
32792
+ options.target = 'secondary'
32793
+ sidebarState = savedState.secondary[0]
32794
+ targetSidebar = sidebars.secondary
32795
+ targetSection = 'top'
32796
+ } else if (savedState.secondary[1].includes(options.id)) {
32797
+ options.target = 'secondary'
32798
+ sidebarState = savedState.secondary[1]
32799
+ targetSidebar = sidebars.secondary
32800
+ targetSection = 'bottom'
32801
+ } else if (savedState.primary[0].includes(options.id)) {
32802
+ options.target = 'primary'
32803
+ sidebarState = savedState.primary[0]
32804
+ targetSidebar = sidebars.primary
32805
+ targetSection = 'top'
32806
+ } else if (savedState.primary[1].includes(options.id)) {
32807
+ options.target = 'primary'
32808
+ sidebarState = savedState.primary[1]
32809
+ targetSidebar = sidebars.primary
32810
+ targetSection = 'bottom'
32811
+ }
32812
+ if (targetSidebar) {
32813
+ // This tab was found in the saved sidebar state. Now find the target position for the tab button
32814
+ targetTabButtonIndex = sidebarState.indexOf(options.id)
32815
+ }
32816
+ }
32817
+
32818
+
32819
+ targetSidebar = targetSidebar || (options.target === 'secondary' ? sidebars.secondary : sidebars.primary);
32820
+ targetSection = targetSection || 'top'
32821
+ options.targetSection = targetSection;
31539
32822
 
31540
32823
  delete options.closeable;
31541
32824
 
31542
- options.wrapper = $('<div>',{style:"height:100%"}).appendTo("#red-ui-sidebar-content")
32825
+ options.wrapper = $('<div>',{style:"height:100%"}).appendTo(targetSidebar.sections[targetSection].content)
31543
32826
  options.wrapper.append(options.content);
31544
32827
  options.wrapper.hide();
31545
32828
 
@@ -31548,11 +32831,12 @@ RED.sidebar = (function() {
31548
32831
  }
31549
32832
 
31550
32833
  if (options.toolbar) {
31551
- $("#red-ui-sidebar-footer").append(options.toolbar);
32834
+ targetSidebar.sections[targetSection].footer.append(options.toolbar);
31552
32835
  $(options.toolbar).hide();
31553
32836
  }
31554
32837
  var id = options.id;
31555
32838
 
32839
+ // console.log('menu', options.id, options.name)
31556
32840
  RED.menu.addItem("menu-item-view-menu",{
31557
32841
  id:"menu-item-view-menu-"+options.id,
31558
32842
  label:options.name,
@@ -31565,208 +32849,543 @@ RED.sidebar = (function() {
31565
32849
  options.iconClass = options.iconClass || "fa fa-square-o"
31566
32850
 
31567
32851
  knownTabs[options.id] = options;
32852
+ options.tabButton = $('<button></button>')
32853
+ // Insert the tab button at the correct index
32854
+ if (targetTabButtonIndex === -1) {
32855
+ // Append to end
32856
+ targetSidebar.tabBars[targetSection].addButton(options.tabButton)
32857
+ } else {
32858
+ // Insert before the item at targetTabButtonIndex
32859
+ targetSidebar.tabBars[targetSection].addButton(options.tabButton, targetTabButtonIndex)
32860
+ }
32861
+ options.tabButton.attr('data-tab-id', options.id)
31568
32862
 
31569
- if (options.visible !== false) {
31570
- sidebar_tabs.addTab(knownTabs[options.id]);
32863
+ options.tabButtonTooltip = RED.popover.tooltip(options.tabButton, options.name, options.action);
32864
+ if (options.icon) {
32865
+ $('<i>',{class: 'red-ui-sidebar-tab-icon', style:"mask-image: url("+options.icon+"); -webkit-mask-image: url("+options.icon+");"}).appendTo(options.tabButton);
32866
+ } else if (options.iconClass) {
32867
+ $('<i>',{class:options.iconClass}).appendTo(options.tabButton);
31571
32868
  }
32869
+ options.tabButton.on('mouseup', function(evt) {
32870
+ if (draggingTabButton) {
32871
+ draggingTabButton = false
32872
+ return
32873
+ }
32874
+ const targetSidebar = options.target === 'secondary' ? sidebars.secondary : sidebars.primary;
32875
+ //
32876
+ if (targetSidebar.tabBars[options.targetSection].active === options.id && RED.menu.isSelected(targetSidebar.menuToggle)) {
32877
+ RED.menu.setSelected(targetSidebar.menuToggle, false);
32878
+ } else {
32879
+ RED.sidebar.show(options.id)
32880
+ }
32881
+ })
32882
+ if (targetSidebar.sections[targetSection].content.children().length === 1) {
32883
+ RED.sidebar.show(options.id)
32884
+ }
32885
+ targetSidebar.resizeSidebarTabBar()
31572
32886
  }
31573
32887
 
31574
32888
  function removeTab(id) {
31575
- sidebar_tabs.removeTab(id);
31576
- $(knownTabs[id].wrapper).remove();
31577
- if (knownTabs[id].footer) {
31578
- knownTabs[id].footer.remove();
32889
+ if (knownTabs[id]) {
32890
+ const targetSidebar = knownTabs[id].target === 'secondary' ? sidebars.secondary : sidebars.primary;
32891
+ $(knownTabs[id].wrapper).remove();
32892
+ if (knownTabs[id].footer) {
32893
+ knownTabs[id].footer.remove();
32894
+ }
32895
+ targetSidebar.tabBar.find('button[data-tab-id="'+id+'"]').remove()
32896
+ RED.menu.removeItem("menu-item-view-menu-"+id);
32897
+ if (knownTabs[id].onremove) {
32898
+ knownTabs[id].onremove.call(knownTabs[id]);
32899
+ }
32900
+ delete knownTabs[id];
32901
+ const firstTab = targetSidebar.tabBar.find('button').first().attr('data-tab-id');
32902
+ if (firstTab) {
32903
+ RED.sidebar.show(firstTab);
32904
+ }
31579
32905
  }
31580
- delete knownTabs[id];
31581
- RED.menu.removeItem("menu-item-view-menu-"+id);
31582
32906
  }
31583
32907
 
31584
- var sidebarSeparator = {};
31585
- sidebarSeparator.dragging = false;
32908
+ function moveTab(id, srcSidebar, srcPosition, targetSidebar, targetPosition) {
32909
+ const options = knownTabs[id];
32910
+ options.target = targetSidebar.id;
32911
+ options.targetSection = targetPosition;
31586
32912
 
31587
- function setupSidebarSeparator() {
31588
- $("#red-ui-sidebar-separator").draggable({
31589
- axis: "x",
31590
- start:function(event,ui) {
31591
- sidebarSeparator.closing = false;
31592
- sidebarSeparator.opening = false;
31593
- var winWidth = $("#red-ui-editor").width();
31594
- sidebarSeparator.start = ui.position.left;
31595
- sidebarSeparator.chartWidth = $("#red-ui-workspace").width();
31596
- sidebarSeparator.chartRight = winWidth-$("#red-ui-workspace").width()-$("#red-ui-workspace").offset().left-2;
31597
- sidebarSeparator.dragging = true;
31598
-
31599
- if (!RED.menu.isSelected("menu-item-sidebar")) {
31600
- sidebarSeparator.opening = true;
31601
- var newChartRight = 7;
31602
- $("#red-ui-sidebar").addClass("closing");
31603
- $("#red-ui-workspace").css("right",newChartRight);
31604
- $("#red-ui-editor-stack").css("right",newChartRight+1);
31605
- $("#red-ui-sidebar").width(0);
31606
- RED.menu.setSelected("menu-item-sidebar",true);
31607
- RED.events.emit("sidebar:resize");
31608
- }
31609
- sidebarSeparator.width = $("#red-ui-sidebar").width();
31610
- },
31611
- drag: function(event,ui) {
31612
- var d = ui.position.left-sidebarSeparator.start;
31613
- var newSidebarWidth = sidebarSeparator.width-d;
31614
- if (sidebarSeparator.opening) {
31615
- newSidebarWidth -= 3;
31616
- }
32913
+ $(options.wrapper).appendTo(targetSidebar.sections[targetPosition].content);
32914
+ if (options.toolbar) {
32915
+ targetSidebar.sections[targetPosition].footer.append(options.toolbar);
32916
+ }
32917
+ // Reset the tooltip so its left/right direction is recalculated
32918
+ options.tabButtonTooltip.delete()
32919
+ options.tabButtonTooltip = RED.popover.tooltip(options.tabButton, options.name, options.action);
31617
32920
 
31618
- if (newSidebarWidth > 150) {
31619
- if (sidebarSeparator.chartWidth+d < 200) {
31620
- ui.position.left = 200+sidebarSeparator.start-sidebarSeparator.chartWidth;
31621
- d = ui.position.left-sidebarSeparator.start;
31622
- newSidebarWidth = sidebarSeparator.width-d;
31623
- }
31624
- }
32921
+ if (targetSidebar.sections[targetPosition].content.children().length === 1) {
32922
+ RED.sidebar.show(options.id)
32923
+ }
32924
+ if (srcSidebar.sections[srcPosition].content.children().length === 0 && srcPosition === 'bottom') {
32925
+ srcSidebar.hideSection(srcPosition)
32926
+ } else if (targetSidebar.sections[targetPosition].hidden) {
32927
+ targetSidebar.showSection(targetPosition)
32928
+ }
32929
+ }
31625
32930
 
31626
- if (newSidebarWidth < 150) {
31627
- if (!sidebarSeparator.closing) {
31628
- $("#red-ui-sidebar").addClass("closing");
31629
- sidebarSeparator.closing = true;
31630
- }
31631
- if (!sidebarSeparator.opening) {
31632
- newSidebarWidth = 150;
31633
- ui.position.left = sidebarSeparator.width-(150 - sidebarSeparator.start);
31634
- d = ui.position.left-sidebarSeparator.start;
32931
+ let draggingTabButton = false
32932
+
32933
+ function setupSidebarTabs(sidebar) {
32934
+ const tabBar = $('<div class="red-ui-sidebar-tab-bar"></div>').addClass('red-ui-sidebar-' + sidebar.direction);
32935
+ if (sidebar.direction === 'right') {
32936
+ tabBar.insertAfter(sidebar.container);
32937
+ } else if (sidebar.direction === 'left') {
32938
+ tabBar.insertBefore(sidebar.container);
32939
+ }
32940
+
32941
+ // TODO: make this an API object, not just a jQuery object
32942
+ sidebar.tabBars = {
32943
+ top: setupTabSection(sidebar, tabBar, 'top'),
32944
+ bottom: setupTabSection(sidebar, tabBar, 'bottom')
32945
+ }
32946
+ sidebar.tabBar = sidebar.tabBars.top.container;
32947
+ sidebar.resizeSidebarTabBar = function () {
32948
+ sidebar.tabBars.top.resizeSidebarTabBar();
32949
+ sidebar.tabBars.bottom.resizeSidebarTabBar();
32950
+ }
32951
+ sidebar.hideSection = function (position) {
32952
+ sidebar.sections.bottom.container.hide()
32953
+ sidebar.sections.bottom.hidden = true
32954
+ sidebar.sections.top.container.css('flex-grow', '1')
32955
+ sidebar.tabBars.top.container.css('flex-grow', '1')
32956
+ sidebar.tabBars.bottom.container.css('flex-grow', '0')
32957
+ sidebar.tabBars.bottom.container.css('height', '60px')
32958
+
32959
+ sidebar.resizeSidebar()
32960
+ }
32961
+ sidebar.showSection = function (position) {
32962
+ sidebar.sections.bottom.container.show()
32963
+ sidebar.sections.bottom.hidden = false
32964
+ sidebar.sections.top.container.css('flex-grow', '0')
32965
+ sidebar.sections.top.container.css('height', '70%')
32966
+ sidebar.tabBars.top.container.css('flex-grow', '')
32967
+ sidebar.tabBars.bottom.container.css('flex-grow', '')
32968
+ sidebar.tabBars.bottom.container.css('height', '')
32969
+
32970
+ sidebar.resizeSidebar()
32971
+ }
32972
+ }
32973
+
32974
+ function setupTabSection(sidebar, tabBar, position) {
32975
+ const tabBarButtonsContainer = $('<div class="red-ui-sidebar-tab-bar-buttons"></div>').appendTo(tabBar);
32976
+ const tabOverflowButton = $('<button class="red-ui-sidebar-tab-bar-overflow-button"><i class="fa fa-ellipsis-h"></i></button>').appendTo(tabBarButtonsContainer);
32977
+ tabOverflowButton.hide()
32978
+ tabOverflowButton.on('click', function(evt) {
32979
+ try {
32980
+ const menuOptions = []
32981
+ tabBarButtonsContainer.find('button:not(.red-ui-sidebar-tab-bar-overflow-button)').each(function () {
32982
+ if ($(this).is(':visible')) {
32983
+ return
32984
+ }
32985
+ const tabId = $(this).attr('data-tab-id')
32986
+ const tabOptions = knownTabs[tabId]
32987
+ menuOptions.push({
32988
+ label: tabOptions.name,
32989
+ onselect: function() {
32990
+ RED.sidebar.show(tabId)
31635
32991
  }
31636
- } else if (newSidebarWidth > 150 && (sidebarSeparator.closing || sidebarSeparator.opening)) {
31637
- sidebarSeparator.closing = false;
31638
- $("#red-ui-sidebar").removeClass("closing");
32992
+ })
32993
+ })
32994
+ if (menuOptions.length === 0) {
32995
+ return
32996
+ }
32997
+ const menu = RED.menu.init({ options: menuOptions });
32998
+ menu.attr("id",sidebar.container.attr('id')+"-menu");
32999
+ menu.css({
33000
+ position: "absolute"
33001
+ })
33002
+ menu.appendTo("body");
33003
+ var elementPos = tabOverflowButton.offset();
33004
+ menu.css({
33005
+ top: (elementPos.top+tabOverflowButton.height()- menu.height() - 10)+"px",
33006
+ left: (elementPos.left - menu.width() - 3)+"px"
33007
+ })
33008
+ $(".red-ui-menu.red-ui-menu-dropdown").hide();
33009
+ setTimeout(() => {
33010
+ $(document).on("click.red-ui-sidebar-tabmenu", function(evt) {
33011
+ $(document).off("click.red-ui-sidebar-tabmenu");
33012
+ menu.remove();
33013
+ });
33014
+ }, 0)
33015
+ menu.show();
33016
+ } catch (err) {
33017
+ console.log(err)
33018
+ }
33019
+ })
33020
+ tabBarButtonsContainer.data('sidebar', sidebar.id)
33021
+ tabBarButtonsContainer.data('sidebar-position', position)
33022
+ tabBarButtonsContainer.sortable({
33023
+ distance: 10,
33024
+ cancel: false,
33025
+ items: "button:not(.red-ui-sidebar-tab-bar-overflow-button)",
33026
+ placeholder: "red-ui-sidebar-tab-bar-button-placeholder",
33027
+ connectWith: ".red-ui-sidebar-tab-bar-buttons",
33028
+ start: function(event, ui) {
33029
+ const tabId = ui.item.attr('data-tab-id');
33030
+ const options = knownTabs[tabId];
33031
+ options.tabButtonTooltip.delete()
33032
+ draggingTabButton = true
33033
+ tabBar.css('z-index','inherit')
33034
+ $(".red-ui-sidebar-tab-bar").addClass("red-ui-sidebar-dragging-tab");
33035
+ },
33036
+ stop: function(event, ui) {
33037
+ // Restore the tooltip
33038
+ const tabId = ui.item.attr('data-tab-id');
33039
+ const options = knownTabs[tabId];
33040
+ options.tabButtonTooltip.delete()
33041
+ options.tabButtonTooltip = RED.popover.tooltip(options.tabButton, options.name, options.action);
33042
+ // Save the sidebar state
33043
+ exportSidebarState()
33044
+ tabBar.css('z-index','')
33045
+ $(".red-ui-sidebar-tab-bar").removeClass("red-ui-sidebar-dragging-tab");
33046
+ },
33047
+ receive: function(event, ui) {
33048
+ // Tab has been moved from one sidebar to another
33049
+ const src = sidebars[ui.sender.data('sidebar')]
33050
+ const dest = sidebars[$(this).data('sidebar')]
33051
+ const srcPosition = ui.sender.data('sidebar-position')
33052
+ const destPosition = $(this).data('sidebar-position')
33053
+ const tabId = ui.item.attr('data-tab-id');
33054
+ moveTab(tabId, src, srcPosition, dest, destPosition);
33055
+ if (ui.item.hasClass('selected')) {
33056
+ const firstTab = src.tabBars[srcPosition].container.find('button').first().attr('data-tab-id');
33057
+ if (firstTab) {
33058
+ RED.sidebar.show(firstTab);
31639
33059
  }
33060
+ }
33061
+ src.resizeSidebarTabBar();
33062
+ dest.resizeSidebarTabBar();
31640
33063
 
31641
- var newChartRight = sidebarSeparator.chartRight-d;
31642
- $("#red-ui-workspace").css("right",newChartRight);
31643
- $("#red-ui-editor-stack").css("right",newChartRight+1);
31644
- $("#red-ui-sidebar").width(newSidebarWidth);
33064
+ RED.sidebar.show(tabId)
33065
+ }
33066
+ })
31645
33067
 
31646
- sidebar_tabs.resize();
31647
- RED.events.emit("sidebar:resize");
31648
- },
31649
- stop:function(event,ui) {
31650
- sidebarSeparator.dragging = false;
31651
- if (sidebarSeparator.closing) {
31652
- $("#red-ui-sidebar").removeClass("closing");
31653
- RED.menu.setSelected("menu-item-sidebar",false);
31654
- if ($("#red-ui-sidebar").width() < 180) {
31655
- $("#red-ui-sidebar").width(180);
31656
- $("#red-ui-workspace").css("right",187);
31657
- $("#red-ui-editor-stack").css("right",188);
31658
- }
31659
- }
31660
- $("#red-ui-sidebar-separator").css("left","auto");
31661
- $("#red-ui-sidebar-separator").css("right",($("#red-ui-sidebar").width()+2)+"px");
33068
+ let hasHidden = false
33069
+ const resizeSidebarTabBar = function () {
33070
+ let tabBarButtonsBottom = tabBarButtonsContainer.position().top + tabBarButtonsContainer.outerHeight();
33071
+ const buttonHeight = tabOverflowButton.outerHeight()
33072
+ // Find the last visible button
33073
+ let bottomButton = tabBarButtonsContainer.children(":visible").last()
33074
+ if (bottomButton.length === 0) {
33075
+ // Nothing visible - bail out
33076
+ return
33077
+ }
33078
+ if (tabBarButtonsBottom < bottomButton.position().top + buttonHeight * 1.5) {
33079
+ tabOverflowButton.show()
33080
+ let tabOverflowButtonBottom = tabOverflowButton.position().top + buttonHeight * 1.5;
33081
+ while (tabBarButtonsBottom < tabOverflowButtonBottom) {
33082
+ const lastVisible = tabBarButtonsContainer.children(':not(".red-ui-sidebar-tab-bar-overflow-button"):visible').last()
33083
+ if (lastVisible.length === 0) {
33084
+ // Nothing left to hide
33085
+ break
33086
+ }
33087
+ lastVisible.hide()
33088
+ tabOverflowButtonBottom = tabOverflowButton.position().top + buttonHeight * 1.5;
33089
+ }
33090
+ } else {
33091
+ const hiddenChildren = tabBarButtonsContainer.children(':not(".red-ui-sidebar-tab-bar-overflow-button"):hidden')
33092
+ if (hiddenChildren.length > 0) {
33093
+ // We may be able to show some more buttons
33094
+ let tabOverflowButtonBottom = tabOverflowButton.position().top + buttonHeight * 2;
33095
+ let shownCount = 0
33096
+ while (tabBarButtonsBottom > tabOverflowButtonBottom + buttonHeight) {
33097
+ const firstHidden = tabBarButtonsContainer.children(':not(".red-ui-sidebar-tab-bar-overflow-button"):hidden').first()
33098
+ if (firstHidden.length === 0) {
33099
+ // Nothing left to show
33100
+ break
33101
+ }
33102
+ firstHidden.show()
33103
+ shownCount++
33104
+ tabOverflowButtonBottom = tabOverflowButton.position().top + buttonHeight * 2;
33105
+ }
33106
+ if (hiddenChildren.length - shownCount <= 0) {
33107
+ // We were able to show all of the hidden buttons
33108
+ // so hide the overflow button again
33109
+ tabOverflowButton.hide()
33110
+ }
33111
+ }
33112
+ }
33113
+ }
33114
+ return {
33115
+ container: tabBarButtonsContainer,
33116
+ addButton: function(button, position) {
33117
+ if (position === undefined || position >= tabBarButtonsContainer.children().length) {
33118
+ button.insertBefore(tabOverflowButton);
33119
+ } else {
33120
+ button.insertBefore(tabBarButtonsContainer.children().eq(position));
33121
+ }
33122
+ },
33123
+ clearSelected: function() {
33124
+ tabBarButtonsContainer.children('button').removeClass('selected')
33125
+ },
33126
+ resizeSidebarTabBar
33127
+ }
33128
+ }
33129
+ function setupSidebarSeparator(sidebar) {
33130
+ const separator = $('<div class="red-ui-sidebar-separator"></div>');
33131
+ separator.attr('id', sidebar.container.attr('id') + '-separator')
33132
+ $('<div class="red-ui-sidebar-shade hide"></div>').appendTo(separator);
33133
+ $('<div class="red-ui-sidebar-separator-handle"></div>').appendTo(separator);
33134
+ let scaleFactor = 1;
33135
+ if (sidebar.direction === 'right') {
33136
+ separator.insertBefore(sidebar.container);
33137
+ } else if (sidebar.direction === 'left') {
33138
+ scaleFactor = -1;
33139
+ separator.insertAfter(sidebar.container);
33140
+ }
33141
+ // Track sidebar state whilst dragging
33142
+ const sidebarSeparator = {}
33143
+ separator.draggable({
33144
+ axis: "x",
33145
+ start:function(event,ui) {
33146
+ sidebarSeparator.closing = false;
33147
+ sidebarSeparator.opening = false;
33148
+ // var winWidth = $("#red-ui-editor").width();
33149
+ sidebarSeparator.start = ui.position.left;
33150
+ sidebarSeparator.width = sidebar.container.width();
33151
+ sidebarSeparator.chartWidth = $("#red-ui-workspace").width();
33152
+ sidebarSeparator.dragging = true;
33153
+
33154
+ if (!RED.menu.isSelected(sidebar.menuToggle)) {
33155
+ sidebarSeparator.opening = true;
33156
+ sidebar.container.width(0);
33157
+ RED.menu.setSelected(sidebar.menuToggle,true);
31662
33158
  RED.events.emit("sidebar:resize");
31663
33159
  }
31664
- });
33160
+ sidebarSeparator.width = sidebar.container.width();
33161
+ },
33162
+ drag: function(event,ui) {
33163
+ var d = scaleFactor * (ui.position.left-sidebarSeparator.start);
31665
33164
 
31666
- var sidebarControls = $('<div class="red-ui-sidebar-control-right"><i class="fa fa-chevron-right"</div>').appendTo($("#red-ui-sidebar-separator"));
31667
- sidebarControls.on("click", function() {
31668
- sidebarControls.hide();
31669
- RED.menu.toggleSelected("menu-item-sidebar");
31670
- })
31671
- $("#red-ui-sidebar-separator").on("mouseenter", function() {
31672
- if (!sidebarSeparator.dragging) {
31673
- if (RED.menu.isSelected("menu-item-sidebar")) {
31674
- sidebarControls.find("i").addClass("fa-chevron-right").removeClass("fa-chevron-left");
33165
+ var newSidebarWidth = sidebarSeparator.width - d;
33166
+ if (newSidebarWidth > sidebar.maximumWidth) {
33167
+ newSidebarWidth = sidebar.maximumWidth;
33168
+ d = sidebarSeparator.width - sidebar.maximumWidth;
33169
+ ui.position.left = sidebarSeparator.start + scaleFactor * d;
33170
+ }
33171
+
33172
+ if (newSidebarWidth > sidebar.minimumWidth) {
33173
+ if (sidebarSeparator.chartWidth + d < 200) {
33174
+ // Chart is now too small, but we have room to resize the sidebar
33175
+ d += (200 - (sidebarSeparator.chartWidth + d));
33176
+ newSidebarWidth = sidebarSeparator.width - d;
33177
+ ui.position.left = sidebarSeparator.start + scaleFactor * d;
33178
+ }
33179
+ } else if (newSidebarWidth < sidebar.minimumWidth) {
33180
+ if (newSidebarWidth > 100) {
33181
+ newSidebarWidth = sidebar.minimumWidth
33182
+ sidebarSeparator.closing = false
33183
+ } else {
33184
+ newSidebarWidth = 0
33185
+ sidebarSeparator.closing = true
33186
+ }
31675
33187
  } else {
31676
- sidebarControls.find("i").removeClass("fa-chevron-right").addClass("fa-chevron-left");
33188
+ sidebarSeparator.closing = false
31677
33189
  }
31678
- sidebarControls.toggle("slide", { direction: "right" }, 200);
31679
- }
31680
- })
31681
- $("#red-ui-sidebar-separator").on("mouseleave", function() {
31682
- if (!sidebarSeparator.dragging) {
31683
- sidebarControls.stop(false,true);
31684
- sidebarControls.hide();
33190
+ sidebar.container.width(newSidebarWidth);
33191
+ ui.position.left -= scaleFactor * d
33192
+
33193
+ // sidebar.tabs.resize();
33194
+ RED.events.emit("sidebar:resize");
33195
+ },
33196
+ stop:function(event,ui) {
33197
+ sidebarSeparator.dragging = false;
33198
+ if (sidebarSeparator.closing) {
33199
+ sidebar.container.removeClass("closing");
33200
+ if (sidebar.menuToggle) {
33201
+ RED.menu.setSelected(sidebar.menuToggle,false);
33202
+ }
33203
+ sidebar.container.hide()
33204
+ sidebar.separator.hide()
33205
+ if (sidebar.container.width() < sidebar.minimumWidth) {
33206
+ sidebar.container.width(sidebar.defaultWidth);
33207
+ }
33208
+ }
33209
+ RED.events.emit("sidebar:resize");
31685
33210
  }
31686
33211
  });
33212
+ return separator
31687
33213
  }
31688
33214
 
31689
- function toggleSidebar(state) {
33215
+ function toggleSidebar(sidebar, state) {
31690
33216
  if (!state) {
31691
- $("#red-ui-main-container").addClass("red-ui-sidebar-closed");
33217
+ sidebar.container.hide()
33218
+ sidebar.separator.hide()
33219
+ sidebar.tabBars.top.clearSelected()
33220
+ sidebar.tabBars.bottom.clearSelected()
31692
33221
  } else {
31693
- $("#red-ui-main-container").removeClass("red-ui-sidebar-closed");
31694
- sidebar_tabs.resize();
33222
+ sidebar.container.show()
33223
+ sidebar.separator.show()
33224
+ if (sidebar.tabBars.top.active) {
33225
+ sidebar.tabBars.top.container.find('button[data-tab-id="'+sidebar.tabBars.top.active+'"]').addClass('selected')
33226
+ }
33227
+ if (sidebar.tabBars.bottom.active) {
33228
+ sidebar.tabBars.bottom.container.find('button[data-tab-id="'+sidebar.tabBars.bottom.active+'"]').addClass('selected')
33229
+ }
31695
33230
  }
31696
33231
  RED.events.emit("sidebar:resize");
31697
33232
  }
31698
33233
 
31699
33234
  function showSidebar(id, skipShowSidebar) {
31700
33235
  if (id === ":first") {
31701
- id = lastSessionSelectedTab || RED.settings.get("editor.sidebar.order",["info", "help", "version-control", "debug"])[0]
33236
+ // Show the last selected tab for each sidebar
33237
+ Object.keys(sidebars).forEach(function(sidebarKey) {
33238
+ const sidebar = sidebars[sidebarKey];
33239
+ let lastTabId = lastSessionSelectedTabs[sidebarKey];
33240
+ if (!lastTabId) {
33241
+ lastTabId = sidebar.tabBars.top.container.children('button').first().attr('data-tab-id');
33242
+ }
33243
+ showSidebar(lastTabId, true)
33244
+ })
33245
+ return
31702
33246
  }
31703
33247
  if (id) {
31704
- if (!containsTab(id) && knownTabs[id]) {
31705
- sidebar_tabs.addTab(knownTabs[id]);
31706
- }
31707
- sidebar_tabs.activateTab(id);
31708
- if (!skipShowSidebar && !RED.menu.isSelected("menu-item-sidebar")) {
31709
- RED.menu.setSelected("menu-item-sidebar",true);
33248
+ const tabOptions = knownTabs[id];
33249
+ if (tabOptions) {
33250
+ const targetSidebar = tabOptions.target === 'secondary' ? sidebars.secondary : sidebars.primary;
33251
+ const targetSection = tabOptions.targetSection || 'top'
33252
+ targetSidebar.sections[targetSection].content.children().hide();
33253
+ targetSidebar.sections[targetSection].footer.children().hide();
33254
+ if (tabOptions.onchange) {
33255
+ tabOptions.onchange.call(tabOptions);
33256
+ }
33257
+ $(tabOptions.wrapper).show();
33258
+ if (tabOptions.toolbar) {
33259
+ $(tabOptions.toolbar).show();
33260
+ }
33261
+ RED.settings.setLocal("last-sidebar-tab-" + targetSidebar.id, tabOptions.id)
33262
+ // TODO: find which tabBar the button is in
33263
+ targetSidebar.tabBars[targetSection].clearSelected()
33264
+ targetSidebar.tabBars[targetSection].container.find('button[data-tab-id="'+id+'"]').addClass('selected')
33265
+ targetSidebar.tabBars[targetSection].active = id
33266
+
33267
+ if (!skipShowSidebar && !RED.menu.isSelected(targetSidebar.menuToggle)) {
33268
+ RED.menu.setSelected(targetSidebar.menuToggle,true);
33269
+ }
31710
33270
  }
31711
33271
  }
31712
33272
  }
31713
33273
 
31714
33274
  function containsTab(id) {
31715
- return sidebar_tabs.contains(id);
31716
- }
31717
-
31718
- function init () {
31719
- setupSidebarSeparator();
31720
- sidebar_tabs = RED.tabs.create({
31721
- element: $('<ul id="red-ui-sidebar-tabs"></ul>').appendTo("#red-ui-sidebar"),
31722
- onchange:function(tab) {
31723
- $("#red-ui-sidebar-content").children().hide();
31724
- $("#red-ui-sidebar-footer").children().hide();
31725
- if (tab.onchange) {
31726
- tab.onchange.call(tab);
31727
- }
31728
- $(tab.wrapper).show();
31729
- if (tab.toolbar) {
31730
- $(tab.toolbar).show();
31731
- }
31732
- RED.settings.setLocal("last-sidebar-tab", tab.id)
33275
+ return sidebars.primary.tabs.contains(id);
33276
+ }
33277
+
33278
+ function setupSidebar(sidebar) {
33279
+ // Get the appropriate height for the sidebar - as the sidebar will be hidden at this point in time, we need to use
33280
+ // the main-container height as a decent proxy
33281
+ const sidebarHeight = $("#red-ui-main-container").height();
33282
+ sidebar.container.addClass("red-ui-sidebar").addClass('red-ui-sidebar-' + sidebar.direction);
33283
+ sidebar.container.width(sidebar.defaultWidth);
33284
+ if (sidebar.direction === 'right') {
33285
+ $('<div>',{class:"red-ui-sidebar-shade hide"}).css("z-index", 0).appendTo(sidebar.container);
33286
+ }
33287
+ sidebar.sections = {};
33288
+ sidebar.sections.top = {}
33289
+ sidebar.sections.top.container = $('<div class="red-ui-sidebar-section red-ui-sidebar-section-top"></div>').appendTo(sidebar.container);
33290
+ sidebar.sections.top.content = $('<div class="red-ui-sidebar-content"></div>').appendTo(sidebar.sections.top.container);
33291
+ sidebar.sections.top.footer = $('<div class="red-ui-sidebar-footer"></div>').appendTo(sidebar.sections.top.container);
33292
+ sidebar.sectionsSeparator = $('<div class="red-ui-sidebar-tab-bar-separator"><div class="red-ui-sidebar-separator-handle"></div></div>').appendTo(sidebar.container);
33293
+ sidebar.sections.bottom = {}
33294
+ sidebar.sections.bottom.container = $('<div class="red-ui-sidebar-section red-ui-sidebar-section-bottom"></div>').appendTo(sidebar.container);
33295
+ sidebar.sections.bottom.content = $('<div class="red-ui-sidebar-content"></div>').appendTo(sidebar.sections.bottom.container);
33296
+ sidebar.sections.bottom.footer = $('<div class="red-ui-sidebar-footer"></div>').appendTo(sidebar.sections.bottom.container);
33297
+
33298
+ let startPosition
33299
+ let startTopSectionHeight
33300
+ let startTopTabSectionHeight
33301
+ let startSidebarHeight
33302
+ sidebar.sectionsSeparator.draggable({
33303
+ axis: "y",
33304
+ containment: sidebar.container,
33305
+ scroll: false,
33306
+ start:function(event,ui) {
33307
+ startPosition = ui.position.top
33308
+ startTopSectionHeight = sidebar.sections.top.container.outerHeight()
33309
+ startTopTabSectionHeight = sidebar.tabBars.top.container.outerHeight()
33310
+ startSidebarHeight = sidebar.container.height()
31733
33311
  },
31734
- onremove: function(tab) {
31735
- $(tab.wrapper).hide();
31736
- if (tab.onremove) {
31737
- tab.onremove.call(tab);
33312
+ drag: function(event,ui) {
33313
+ const delta = ui.position.top - startPosition
33314
+ const newTopHeight = startTopSectionHeight + delta
33315
+ const newBottomHeight = startSidebarHeight - newTopHeight
33316
+ if (newTopHeight < 100 || newBottomHeight < 100) {
33317
+ ui.position.top += delta
33318
+ return
31738
33319
  }
33320
+ sidebar.sections.top.container.outerHeight(startTopSectionHeight + delta)
33321
+ sidebar.tabBars.top.container.outerHeight(startTopTabSectionHeight + delta)
33322
+ ui.position.top -= delta
33323
+ sidebar.resizeSidebar()
31739
33324
  },
31740
- // minimumActiveTabWidth: 70,
31741
- collapsible: true,
31742
- onreorder: function(order) {
31743
- RED.settings.set("editor.sidebar.order",order);
31744
- },
31745
- order: RED.settings.get("editor.sidebar.order",["info", "help", "version-control", "debug"])
31746
- // scrollable: true
33325
+ stop:function(event,ui) {
33326
+ }
31747
33327
  });
31748
33328
 
31749
- $('<div id="red-ui-sidebar-content"></div>').appendTo("#red-ui-sidebar");
31750
- $('<div id="red-ui-sidebar-footer" class="red-ui-component-footer"></div>').appendTo("#red-ui-sidebar");
31751
- $('<div id="red-ui-sidebar-shade" class="hide"></div>').appendTo("#red-ui-sidebar");
33329
+ // sidebar.shade = $('<div class="red-ui-sidebar-shade hide"></div>').appendTo(sidebar.container);
33330
+
33331
+ sidebar.separator = setupSidebarSeparator(sidebar);
33332
+ setupSidebarTabs(sidebar)
33333
+ sidebar.resizeSidebar = function () {
33334
+ // Resize sidebar sections as needed
33335
+ const topSectionHeight = sidebar.sections.top.container.outerHeight()
33336
+ if (!sidebar.sections.bottom.hidden) {
33337
+ const bottomSectionHeight = sidebar.sections.bottom.container.outerHeight()
33338
+
33339
+ // Shrink the top section if the bottom section is too small
33340
+ if (bottomSectionHeight < 90 && topSectionHeight > 90) {
33341
+ sidebar.sections.top.container.outerHeight(topSectionHeight - (90 - bottomSectionHeight));
33342
+ }
33343
+ sidebar.tabBars.top.container.height(sidebar.sections.top.container.outerHeight())
33344
+ // } else {
33345
+ // sidebar.tabBars.top.container.height(sidebar.sections.top.container.outerHeight() - 60)
33346
+ }
33347
+ // Trigger a resize of the tab bars to handle overflow
33348
+ sidebar.resizeSidebarTabBar()
33349
+ RED.events.emit("sidebar:resize");
33350
+
33351
+ }
33352
+ $(window).on("resize", sidebar.resizeSidebar)
33353
+ if (sidebar.defaultTopHeight > 0) {
33354
+ if (sidebar.defaultTopHeight === 1) {
33355
+ sidebar.hideSection('bottom')
33356
+ } else {
33357
+ sidebar.sections.top.container.outerHeight(sidebarHeight * sidebar.defaultTopHeight);
33358
+ }
33359
+ }
33360
+ sidebar.resizeSidebar()
33361
+
33362
+ }
33363
+ function init () {
33364
+ sidebars.primary.container = $("#red-ui-sidebar");
33365
+ setupSidebar(sidebars.primary)
33366
+ sidebars.secondary.container = $("#red-ui-sidebar-left");
33367
+ setupSidebar(sidebars.secondary)
31752
33368
 
31753
33369
  RED.actions.add("core:toggle-sidebar",function(state){
31754
33370
  if (state === undefined) {
31755
- RED.menu.toggleSelected("menu-item-sidebar");
33371
+ RED.menu.toggleSelected(sidebars.primary.menuToggle);
31756
33372
  } else {
31757
- toggleSidebar(state);
33373
+ toggleSidebar(sidebars.primary, state);
33374
+ }
33375
+ });
33376
+ RED.actions.add("core:toggle-palette", function(state) {
33377
+ if (state === undefined) {
33378
+ RED.menu.toggleSelected(sidebars.secondary.menuToggle);
33379
+ } else {
33380
+ toggleSidebar(sidebars.secondary, state);
31758
33381
  }
31759
33382
  });
31760
- RED.popover.tooltip($("#red-ui-sidebar-separator").find(".red-ui-sidebar-control-right"),RED._("keyboard.toggleSidebar"),"core:toggle-sidebar");
31761
-
31762
- lastSessionSelectedTab = RED.settings.getLocal("last-sidebar-tab")
31763
33383
 
31764
- RED.sidebar.info.init();
31765
- RED.sidebar.help.init();
31766
- RED.sidebar.config.init();
31767
- RED.sidebar.context.init();
31768
- // hide info bar at start if screen rather narrow...
31769
- if ($("#red-ui-editor").width() < 600) { RED.menu.setSelected("menu-item-sidebar",false); }
33384
+ // Remember the last selected tab for each sidebar before
33385
+ // the tabs are readded causing the state to get updated
33386
+ Object.keys(sidebars).forEach(function(sidebarKey) {
33387
+ lastSessionSelectedTabs[sidebarKey] = RED.settings.getLocal("last-sidebar-tab-" + sidebarKey)
33388
+ })
31770
33389
  }
31771
33390
 
31772
33391
  return {
@@ -31814,7 +33433,6 @@ RED.palette = (function() {
31814
33433
  ];
31815
33434
 
31816
33435
  var categoryContainers = {};
31817
- var sidebarControls;
31818
33436
 
31819
33437
  let paletteState = { filter: "", collapsed: [] };
31820
33438
 
@@ -32091,6 +33709,7 @@ RED.palette = (function() {
32091
33709
  width: "300px",
32092
33710
  content: "hi",
32093
33711
  delay: { show: 750, hide: 50 }
33712
+ // direction: "left"
32094
33713
  });
32095
33714
 
32096
33715
  d.data('popover',popover);
@@ -32107,13 +33726,15 @@ RED.palette = (function() {
32107
33726
  var paletteWidth;
32108
33727
  var paletteTop;
32109
33728
  var dropEnabled;
33729
+ let chartOffset;
32110
33730
  $(d).draggable({
32111
33731
  helper: 'clone',
32112
33732
  appendTo: '#red-ui-editor',
32113
33733
  revert: 'invalid',
32114
33734
  revertDuration: 200,
32115
33735
  containment:'#red-ui-main-container',
32116
- start: function() {
33736
+ start: function(e, ui) {
33737
+ ui.helper.css('z-index', 1000);
32117
33738
  dropEnabled = !(RED.nodes.workspace(RED.workspaces.active())?.locked);
32118
33739
  paletteWidth = $("#red-ui-palette").width();
32119
33740
  paletteTop = $("#red-ui-palette").parent().position().top + $("#red-ui-palette-container").position().top;
@@ -32122,6 +33743,7 @@ RED.palette = (function() {
32122
33743
  if (activeGroup) {
32123
33744
  document.getElementById("group_select_"+activeGroup.id).classList.add("red-ui-flow-group-active-hovered");
32124
33745
  }
33746
+ chartOffset = chart.offset()
32125
33747
  RED.view.focus();
32126
33748
  },
32127
33749
  stop: function() {
@@ -32139,10 +33761,13 @@ RED.palette = (function() {
32139
33761
  },
32140
33762
  drag: function(e,ui) {
32141
33763
  var paletteNode = getPaletteNode(nt);
32142
- ui.originalPosition.left = paletteNode.offset().left;
33764
+ // console.log(ui.originalPosition.left, paletteNode.offset().left)
33765
+ // ui.originalPosition.left = paletteNode.offset().left;
33766
+ // console.log(paletteNode.offset())
32143
33767
  if (dropEnabled) {
32144
- mouseX = ui.position.left - paletteWidth + (ui.helper.width()/2) + chart.scrollLeft();
32145
- mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop() + 10;
33768
+ mouseX = ui.offset.left - chartOffset.left + (ui.helper.width()/2) + chart.scrollLeft();
33769
+ mouseY = ui.offset.top - chartOffset.top + (ui.helper.height()/2) + chart.scrollTop() + 10;
33770
+ // console.log(mouseX, mouseY)
32146
33771
  if (!groupTimer) {
32147
33772
  groupTimer = setTimeout(function() {
32148
33773
  var mx = mouseX / RED.view.scale();
@@ -32388,11 +34013,24 @@ RED.palette = (function() {
32388
34013
 
32389
34014
  function init() {
32390
34015
 
34016
+ const content = $('<div id="red-ui-palette" class="red-ui-sidebar-tab-content">')
34017
+ const toolbar = $('<div></div>');
34018
+ RED.sidebar.addTab({
34019
+ target: 'secondary',
34020
+ id: "palette",
34021
+ label: "Palette",
34022
+ name: "Palette",
34023
+ icon: "red/images/subflow_tab.svg",
34024
+ content,
34025
+ toolbar,
34026
+ pinned: true,
34027
+ enableOnEdit: false
34028
+ });
34029
+
32391
34030
  $('<img src="red/images/spin.svg" class="red-ui-palette-spinner hide"/>').appendTo("#red-ui-palette");
32392
34031
  $('<div id="red-ui-palette-search" class="red-ui-palette-search hide"><input type="text" data-i18n="[placeholder]palette.filter"></input></div>').appendTo("#red-ui-palette");
32393
34032
  $('<div id="red-ui-palette-container" class="red-ui-palette-scroll hide"></div>').appendTo("#red-ui-palette");
32394
- $('<div class="red-ui-component-footer"></div>').appendTo("#red-ui-palette");
32395
- $('<div id="red-ui-palette-shade" class="hide"></div>').appendTo("#red-ui-palette");
34033
+ // $('<div id="red-ui-palette-shade" class="hide"></div>').appendTo("#red-ui-palette");
32396
34034
 
32397
34035
  $("#red-ui-palette > .red-ui-palette-spinner").show();
32398
34036
 
@@ -32451,19 +34089,6 @@ RED.palette = (function() {
32451
34089
  }
32452
34090
  });
32453
34091
 
32454
- sidebarControls = $('<div class="red-ui-sidebar-control-left"><i class="fa fa-chevron-left"></i></div>').appendTo($("#red-ui-palette"));
32455
- RED.popover.tooltip(sidebarControls,RED._("keyboard.togglePalette"),"core:toggle-palette");
32456
-
32457
- sidebarControls.on("click", function() {
32458
- RED.menu.toggleSelected("menu-item-palette");
32459
- })
32460
- $("#red-ui-palette").on("mouseenter", function() {
32461
- sidebarControls.toggle("slide", { direction: "left" }, 200);
32462
- })
32463
- $("#red-ui-palette").on("mouseleave", function() {
32464
- sidebarControls.stop(false,true);
32465
- sidebarControls.hide();
32466
- })
32467
34092
  var userCategories = [];
32468
34093
  if (RED.settings.paletteCategories) {
32469
34094
  userCategories = RED.settings.paletteCategories;
@@ -32485,7 +34110,7 @@ RED.palette = (function() {
32485
34110
  }
32486
34111
  });
32487
34112
 
32488
- var paletteFooterButtons = $('<span class="button-group"></span>').appendTo("#red-ui-palette .red-ui-component-footer");
34113
+ var paletteFooterButtons = $('<span class="button-group"></span>').appendTo(toolbar);
32489
34114
  var paletteCollapseAll = $('<button type="button" class="red-ui-footer-button"><i class="fa fa-angle-double-up"></i></button>').appendTo(paletteFooterButtons);
32490
34115
  paletteCollapseAll.on("click", function(e) {
32491
34116
  e.preventDefault();
@@ -32508,13 +34133,7 @@ RED.palette = (function() {
32508
34133
  });
32509
34134
  RED.popover.tooltip(paletteExpandAll,RED._('palette.actions.expand-all'));
32510
34135
 
32511
- RED.actions.add("core:toggle-palette", function(state) {
32512
- if (state === undefined) {
32513
- RED.menu.toggleSelected("menu-item-palette");
32514
- } else {
32515
- togglePalette(state);
32516
- }
32517
- });
34136
+
32518
34137
 
32519
34138
  try {
32520
34139
  paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
@@ -32532,18 +34151,6 @@ RED.palette = (function() {
32532
34151
  }, 10000)
32533
34152
  }
32534
34153
 
32535
- function togglePalette(state) {
32536
- if (!state) {
32537
- $("#red-ui-main-container").addClass("red-ui-palette-closed");
32538
- sidebarControls.hide();
32539
- sidebarControls.find("i").addClass("fa-chevron-right").removeClass("fa-chevron-left");
32540
- } else {
32541
- $("#red-ui-main-container").removeClass("red-ui-palette-closed");
32542
- sidebarControls.find("i").removeClass("fa-chevron-right").addClass("fa-chevron-left");
32543
- }
32544
- setTimeout(function() { $(window).trigger("resize"); } ,200);
32545
- }
32546
-
32547
34154
  function getCategories() {
32548
34155
  var categories = [];
32549
34156
  $("#red-ui-palette-container .red-ui-palette-category").each(function(i,d) {
@@ -32624,10 +34231,6 @@ RED.sidebar.info = (function() {
32624
34231
 
32625
34232
  var stackContainer = $("<div>",{class:"red-ui-sidebar-info-stack"}).appendTo(content);
32626
34233
 
32627
- var outlinerPanel = $("<div>").css({
32628
- "overflow": "hidden",
32629
- "height": "calc(70%)"
32630
- }).appendTo(stackContainer);
32631
34234
  var propertiesPanel = $("<div>").css({
32632
34235
  "overflow":"hidden",
32633
34236
  "height":"100%",
@@ -32643,7 +34246,7 @@ RED.sidebar.info = (function() {
32643
34246
 
32644
34247
  propertiesPanelHeaderCopyLink = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-link"></button>').css({
32645
34248
  position: 'absolute',
32646
- top: '12px',
34249
+ top: '6px',
32647
34250
  right: '32px'
32648
34251
  }).on("click", function(evt) {
32649
34252
  RED.actions.invoke('core:copy-item-url',selectedObject)
@@ -32652,7 +34255,7 @@ RED.sidebar.info = (function() {
32652
34255
 
32653
34256
  propertiesPanelHeaderHelp = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-book"></button>').css({
32654
34257
  position: 'absolute',
32655
- top: '12px',
34258
+ top: '6px',
32656
34259
  right: '56px'
32657
34260
  }).on("click", function(evt) {
32658
34261
  evt.preventDefault();
@@ -32665,7 +34268,7 @@ RED.sidebar.info = (function() {
32665
34268
 
32666
34269
  propertiesPanelHeaderReveal = $('<button type="button" class="red-ui-button red-ui-button-small"><i class="fa fa-search"></button>').css({
32667
34270
  position: 'absolute',
32668
- top: '12px',
34271
+ top: '6px',
32669
34272
  right: '8px'
32670
34273
  }).on("click", function(evt) {
32671
34274
  evt.preventDefault();
@@ -32684,13 +34287,13 @@ RED.sidebar.info = (function() {
32684
34287
  }).appendTo(propertiesPanel);
32685
34288
 
32686
34289
 
32687
- panels = RED.panels.create({container: stackContainer})
32688
- panels.ratio(0.6);
32689
- RED.sidebar.info.outliner.build().appendTo(outlinerPanel);
32690
-
34290
+ // panels = RED.panels.create({container: stackContainer})
34291
+ // panels.ratio(0.6);
34292
+ // RED.sidebar.info.outliner.build().appendTo(outlinerPanel);
32691
34293
 
32692
34294
  RED.sidebar.addTab({
32693
34295
  id: "info",
34296
+ // target: "secondary",
32694
34297
  label: RED._("sidebar.info.label"),
32695
34298
  name: RED._("sidebar.info.name"),
32696
34299
  iconClass: "fa fa-info",
@@ -32729,6 +34332,8 @@ RED.sidebar.info = (function() {
32729
34332
  tips.stop();
32730
34333
  }
32731
34334
 
34335
+ resizeStack();
34336
+
32732
34337
  }
32733
34338
 
32734
34339
  function show() {
@@ -33866,9 +35471,30 @@ RED.sidebar.info = (function() {
33866
35471
  function onSelectionChanged(selection) {
33867
35472
  // treeList.treeList('clearSelection');
33868
35473
  }
35474
+ function show() {
35475
+ RED.sidebar.show("explorer");
35476
+ }
35477
+ function init() {
35478
+ const content = build()
35479
+
35480
+ RED.actions.add("core:show-explorer-tab",show);
35481
+
35482
+
35483
+ RED.sidebar.addTab({
35484
+ id: "explorer",
35485
+ // target: "secondary",
35486
+ label: 'Explorer', // RED._("sidebar.info.label"),
35487
+ name: 'Explorer', //RED._("sidebar.info.name"),
35488
+ icon: "red/images/explorer.svg",
35489
+ action:"core:show-explorer-tab",
35490
+ content: content,
35491
+ pinned: true,
35492
+ enableOnEdit: true
35493
+ });
35494
+ }
33869
35495
 
33870
35496
  return {
33871
- build: build,
35497
+ init,
33872
35498
  search: function(val) {
33873
35499
  searchInput.searchBox('value',val)
33874
35500
  },
@@ -45508,14 +47134,14 @@ RED.eventLog = (function() {
45508
47134
  }
45509
47135
  function showTray(options) {
45510
47136
  var el = $('<div class="red-ui-tray"></div>');
45511
- // `editor-tray-header` is deprecated - use red-ui-tray-body instead
45512
- var header = $('<div class="red-ui-tray-header editor-tray-header"></div>').appendTo(el);
45513
- var bodyWrapper = $('<div class="red-ui-tray-body-wrapper"></div>').appendTo(el);
45514
- // `editor-tray-body` is deprecated - use red-ui-tray-body instead
45515
- var body = $('<div class="red-ui-tray-body editor-tray-body"></div>').appendTo(bodyWrapper);
45516
- // `editor-tray-footer` is deprecated - use red-ui-tray-footer instead
45517
- var footer = $('<div class="red-ui-tray-footer"></div>').appendTo(el);
45518
47137
  var resizer = $('<div class="red-ui-tray-resize-handle"></div>').appendTo(el);
47138
+ var wrapper = $('<div class="red-ui-tray-wrapper"></div>').appendTo(el);
47139
+
47140
+ var header = $('<div class="red-ui-tray-header"></div>').appendTo(wrapper);
47141
+ var bodyWrapper = $('<div class="red-ui-tray-body-wrapper"></div>').appendTo(wrapper);
47142
+ var body = $('<div class="red-ui-tray-body"></div>').appendTo(bodyWrapper);
47143
+ // `editor-tray-footer` is deprecated - use red-ui-tray-footer instead
47144
+ var footer = $('<div class="red-ui-tray-footer"></div>').appendTo(wrapper);
45519
47145
  // var growButton = $('<a class="red-ui-tray-resize-button" style="cursor: w-resize;"><i class="fa fa-angle-left"></i></a>').appendTo(resizer);
45520
47146
  // var shrinkButton = $('<a class="red-ui-tray-resize-button" style="cursor: e-resize;"><i style="margin-left: 1px;" class="fa fa-angle-right"></i></a>').appendTo(resizer);
45521
47147
  if (options.title) {
@@ -45677,6 +47303,8 @@ RED.eventLog = (function() {
45677
47303
  }
45678
47304
 
45679
47305
  function handleWindowResize() {
47306
+ let sidebarWidth = $("#red-ui-sidebar").is(":visible") ? $("#red-ui-sidebar").outerWidth() : 0;
47307
+ $("#red-ui-editor-stack").css('right', sidebarWidth + $("#red-ui-sidebar + .red-ui-sidebar-tab-bar").outerWidth() + 4);
45680
47308
  if (stack.length > 0) {
45681
47309
  var tray = stack[stack.length-1];
45682
47310
  if (tray.options.maximized || tray.width > $("#red-ui-editor-stack").position().left-8) {
@@ -45701,11 +47329,11 @@ RED.eventLog = (function() {
45701
47329
  function raiseTrayZ() {
45702
47330
  setTimeout(function(){
45703
47331
  $('#red-ui-editor-stack').css("zIndex","13");
45704
- },300);
47332
+ },100);
45705
47333
  }
45706
47334
  //lower tray z-index back to original place for correct slide animation (related to fix for editor context menu clipped by sidebar)
45707
47335
  function lowerTrayZ(){
45708
- $('#red-ui-editor-stack').css("zIndex","9");
47336
+ $('#red-ui-editor-stack').css("zIndex","11");
45709
47337
  }
45710
47338
 
45711
47339
  return {
@@ -48838,6 +50466,12 @@ RED.search = (function() {
48838
50466
  $('<div>',{class:"red-ui-search-result-node-type"}).text(node.type).appendTo(contentDiv);
48839
50467
  $('<div>',{class:"red-ui-search-result-node-id"}).text(node.id).appendTo(contentDiv);
48840
50468
 
50469
+ div.on("mouseover", function(evt) {
50470
+ if ( node.z == RED.workspaces.active() ) {
50471
+ RED.view.reveal(node.id)
50472
+ }
50473
+ });
50474
+
48841
50475
  div.on("click", function(evt) {
48842
50476
  evt.preventDefault();
48843
50477
  currentIndex = i;
@@ -48916,8 +50550,7 @@ RED.search = (function() {
48916
50550
  $("#red-ui-header-shade").show();
48917
50551
  $("#red-ui-editor-shade").show();
48918
50552
  $("#red-ui-palette-shade").show();
48919
- $("#red-ui-sidebar-shade").show();
48920
- $("#red-ui-sidebar-separator").hide();
50553
+ $(".red-ui-sidebar-shade").show();
48921
50554
 
48922
50555
  if (dialog === null) {
48923
50556
  createDialog();
@@ -48941,8 +50574,7 @@ RED.search = (function() {
48941
50574
  $("#red-ui-header-shade").hide();
48942
50575
  $("#red-ui-editor-shade").hide();
48943
50576
  $("#red-ui-palette-shade").hide();
48944
- $("#red-ui-sidebar-shade").hide();
48945
- $("#red-ui-sidebar-separator").show();
50577
+ $(".red-ui-sidebar-shade").hide();
48946
50578
  if (dialog !== null) {
48947
50579
  dialog.slideUp(200,function() {
48948
50580
  searchInput.searchBox('value','');
@@ -49042,7 +50674,7 @@ RED.search = (function() {
49042
50674
  $("#red-ui-header-shade").on('mousedown',hide);
49043
50675
  $("#red-ui-editor-shade").on('mousedown',hide);
49044
50676
  $("#red-ui-palette-shade").on('mousedown',hide);
49045
- $("#red-ui-sidebar-shade").on('mousedown',hide);
50677
+ $(".red-ui-sidebar-shade").on('mousedown',hide);
49046
50678
 
49047
50679
  $("#red-ui-view-searchtools-close").on("click", function close() {
49048
50680
  clearActiveSearch();
@@ -49533,8 +51165,7 @@ RED.actionList = (function() {
49533
51165
  $("#red-ui-header-shade").show();
49534
51166
  $("#red-ui-editor-shade").show();
49535
51167
  $("#red-ui-palette-shade").show();
49536
- $("#red-ui-sidebar-shade").show();
49537
- $("#red-ui-sidebar-separator").hide();
51168
+ $(".red-ui-sidebar-shade").show();
49538
51169
  if (dialog === null) {
49539
51170
  createDialog();
49540
51171
  }
@@ -49568,8 +51199,7 @@ RED.actionList = (function() {
49568
51199
  $("#red-ui-header-shade").hide();
49569
51200
  $("#red-ui-editor-shade").hide();
49570
51201
  $("#red-ui-palette-shade").hide();
49571
- $("#red-ui-sidebar-shade").hide();
49572
- $("#red-ui-sidebar-separator").show();
51202
+ $(".red-ui-sidebar-shade").hide();
49573
51203
  if (dialog !== null) {
49574
51204
  dialog.slideUp(200,function() {
49575
51205
  searchInput.searchBox('value','');
@@ -49601,7 +51231,7 @@ RED.actionList = (function() {
49601
51231
  $("#red-ui-header-shade").on('mousedown',hide);
49602
51232
  $("#red-ui-editor-shade").on('mousedown',hide);
49603
51233
  $("#red-ui-palette-shade").on('mousedown',hide);
49604
- $("#red-ui-sidebar-shade").on('mousedown',hide);
51234
+ $(".red-ui-sidebar-shade").on('mousedown',hide);
49605
51235
  }
49606
51236
 
49607
51237
  return {
@@ -52423,7 +54053,7 @@ RED.userSettings = (function() {
52423
54053
  });
52424
54054
  settingsContent.i18n();
52425
54055
  settingsTabs.activateTab("red-ui-settings-tab-"+(initialTab||'view'))
52426
- $("#red-ui-sidebar-shade").show();
54056
+ $(".red-ui-sidebar-shade").show();
52427
54057
  },
52428
54058
  close: function() {
52429
54059
  settingsVisible = false;
@@ -52432,7 +54062,7 @@ RED.userSettings = (function() {
52432
54062
  pane.close();
52433
54063
  }
52434
54064
  });
52435
- $("#red-ui-sidebar-shade").hide();
54065
+ $(".red-ui-sidebar-shade").hide();
52436
54066
 
52437
54067
  },
52438
54068
  show: function() {}
@@ -55225,7 +56855,7 @@ RED.projects.settings = (function() {
55225
56855
  });
55226
56856
  settingsContent.i18n();
55227
56857
  settingsTabs.activateTab("red-ui-project-settings-tab-"+(initialTab||'main'))
55228
- $("#red-ui-sidebar-shade").show();
56858
+ $(".red-ui-sidebar-shade").show();
55229
56859
  },
55230
56860
  close: function() {
55231
56861
  settingsVisible = false;
@@ -55234,7 +56864,7 @@ RED.projects.settings = (function() {
55234
56864
  pane.close();
55235
56865
  }
55236
56866
  });
55237
- $("#red-ui-sidebar-shade").hide();
56867
+ $(".red-ui-sidebar-shade").hide();
55238
56868
 
55239
56869
  },
55240
56870
  show: function() {}
@@ -59248,10 +60878,15 @@ RED.touch.radialMenu = (function() {
59248
60878
 
59249
60879
  function listTour() {
59250
60880
  return [
60881
+ {
60882
+ id: "5_0",
60883
+ label: "5.0",
60884
+ path: "./tours/welcome.js"
60885
+ },
59251
60886
  {
59252
60887
  id: "4_1",
59253
60888
  label: "4.1",
59254
- path: "./tours/welcome.js"
60889
+ path: "./tours/4.1/welcome.js"
59255
60890
  },
59256
60891
  {
59257
60892
  id: "4_0",