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

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,34 @@ 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.help.init();
914
+ RED.sidebar.config.init();
915
+ RED.sidebar.context.init();
916
+ // hide sidebar at start if screen rather narrow...
917
+ if ($("#red-ui-editor").width() < 600) { RED.menu.setSelected("menu-item-sidebar", false); }
912
918
 
913
- RED.nodes.init();
914
- RED.runtime.init()
919
+ RED.envVar.init();
915
920
 
916
- if (RED.settings.theme("multiplayer.enabled",false)) {
917
- RED.multiplayer.init()
918
- }
919
- RED.comms.connect();
921
+ RED.nodes.init();
922
+ RED.runtime.init()
920
923
 
921
- $("#red-ui-main-container").show();
924
+ if (RED.settings.theme("multiplayer.enabled",false)) {
925
+ RED.multiplayer.init()
926
+ }
927
+ RED.comms.connect();
928
+
929
+ $("#red-ui-main-container").show();
930
+ RED.events.emit("sidebar:resize")
922
931
 
923
- loadPluginList();
932
+ loadPluginList();
933
+ });
924
934
  }
925
935
 
926
936
 
@@ -929,13 +939,18 @@ var RED = (function() {
929
939
  var logo = $('<span class="red-ui-header-logo"></span>').appendTo(header);
930
940
  $('<ul class="red-ui-header-toolbar hide"></ul>').appendTo(header);
931
941
  $('<div id="red-ui-header-shade" class="hide"></div>').appendTo(header);
932
- $('<div id="red-ui-main-container" class="red-ui-sidebar-closed hide">'+
942
+ $('<div id="red-ui-main-container">'+
943
+ '<div id="red-ui-sidebar-left"></div>'+
933
944
  '<div id="red-ui-workspace"></div>'+
934
- '<div id="red-ui-editor-stack" tabindex="-1"></div>'+
935
- '<div id="red-ui-palette"></div>'+
936
945
  '<div id="red-ui-sidebar"></div>'+
937
- '<div id="red-ui-sidebar-separator"></div>'+
946
+ '<div id="red-ui-editor-stack" tabindex="-1"></div>'+
947
+ // '<div id="red-ui-palette"></div>'+
938
948
  '</div>').appendTo(options.target);
949
+
950
+ // Don't use the `hide` class on this container, as the show reverts it to block rather
951
+ // than the expected flex. So hide via jQuery as it'll track the show state internally.
952
+ options.target.find('#red-ui-main-container').hide()
953
+
939
954
  $('<div id="red-ui-editor-plugin-configs"></div>').appendTo(options.target);
940
955
  $('<div id="red-ui-editor-node-configs"></div>').appendTo(options.target);
941
956
  $('<div id="red-ui-full-shade" class="hide"></div>').appendTo(options.target);
@@ -13203,7 +13218,7 @@ RED.menu = (function() {
13203
13218
  } else {
13204
13219
  for (var i=0;i<groupItems.length;i++) {
13205
13220
  var groupItem = groupItems[i];
13206
- var label = $(groupItem).find(".red-ui-menu-label").html();
13221
+ var label = $(groupItem).find(".red-ui-menu-label span").text();
13207
13222
  if (opt.label < label) {
13208
13223
  $(groupItem).before(item);
13209
13224
  break;
@@ -14869,7 +14884,6 @@ RED.tabs = (function() {
14869
14884
  ul.find("li.red-ui-tab.active .red-ui-tab-label").css({paddingLeft:""})
14870
14885
  }
14871
14886
  }
14872
-
14873
14887
  }
14874
14888
 
14875
14889
  ul.find("li.red-ui-tab a")
@@ -15369,7 +15383,8 @@ RED.tabs = (function() {
15369
15383
  pinnedButtons["__menu__"].appendTo(collapsedButtonsRow);
15370
15384
  updateTabWidths();
15371
15385
  }
15372
- }
15386
+ },
15387
+ container: wrapper
15373
15388
  }
15374
15389
  return tabAPI;
15375
15390
  }
@@ -17932,13 +17947,13 @@ RED.deploy = (function() {
17932
17947
  $("#red-ui-header-shade").show();
17933
17948
  $("#red-ui-editor-shade").show();
17934
17949
  $("#red-ui-palette-shade").show();
17935
- $("#red-ui-sidebar-shade").show();
17950
+ $(".red-ui-sidebar-shade").show();
17936
17951
  }
17937
17952
  function shadeHide() {
17938
17953
  $("#red-ui-header-shade").hide();
17939
17954
  $("#red-ui-editor-shade").hide();
17940
17955
  $("#red-ui-palette-shade").hide();
17941
- $("#red-ui-sidebar-shade").hide();
17956
+ $(".red-ui-sidebar-shade").hide();
17942
17957
  }
17943
17958
  function deployButtonSetBusy(){
17944
17959
  $(".red-ui-deploy-button-content").css('opacity',0);
@@ -19752,11 +19767,11 @@ RED.diagnostics = (function () {
19752
19767
  diffTable.finish();
19753
19768
  diffTable.list.show();
19754
19769
  },300);
19755
- $("#red-ui-sidebar-shade").show();
19770
+ $(".red-ui-sidebar-shade").show();
19756
19771
  },
19757
19772
  close: function() {
19758
19773
  diffVisible = false;
19759
- $("#red-ui-sidebar-shade").hide();
19774
+ $(".red-ui-sidebar-shade").hide();
19760
19775
 
19761
19776
  },
19762
19777
  show: function() {
@@ -22683,11 +22698,29 @@ RED.view = (function() {
22683
22698
  node_height = 30,
22684
22699
  dblClickInterval = 650;
22685
22700
 
22701
+ var cancelInProgressAnimation = null; // For smooth zoom animation
22702
+
22686
22703
  var touchLongPressTimeout = 1000,
22687
22704
  startTouchDistance = 0,
22688
22705
  startTouchCenter = [],
22689
22706
  moveTouchCenter = [],
22690
- touchStartTime = 0;
22707
+ touchStartTime = 0,
22708
+ gesture = {};
22709
+
22710
+ var spacebarPressed = false;
22711
+
22712
+ // Momentum scrolling state
22713
+ var scrollVelocity = { x: 0, y: 0 };
22714
+ var lastScrollTime = 0;
22715
+ var lastScrollPos = { x: 0, y: 0 };
22716
+ var scrollAnimationId = null;
22717
+ var momentumActive = false;
22718
+
22719
+ // Bounce effect parameters
22720
+ var BOUNCE_DAMPING = 0.6;
22721
+ var BOUNCE_TENSION = 0.3;
22722
+ var MIN_VELOCITY = 0.5;
22723
+ var FRICTION = 0.95;
22691
22724
 
22692
22725
  var workspaceScrollPositions = {};
22693
22726
 
@@ -22964,6 +22997,24 @@ RED.view = (function() {
22964
22997
  function init() {
22965
22998
 
22966
22999
  chart = $("#red-ui-workspace-chart");
23000
+
23001
+ // Add invisible spacer div to ensure scrollable area matches canvas dimensions
23002
+ // At minimum zoom with "cover" behavior, SVG may be smaller than viewport in one dimension
23003
+ // This spacer forces the browser to calculate scrollWidth/Height based on full canvas size
23004
+ // Browser's maxScroll = scrollWidth - viewport will then correctly show canvas edges
23005
+ var scrollSpacer = $('<div>')
23006
+ .css({
23007
+ position: 'absolute',
23008
+ top: 0,
23009
+ left: 0,
23010
+ width: space_width + 'px',
23011
+ height: space_height + 'px',
23012
+ pointerEvents: 'none',
23013
+ visibility: 'hidden'
23014
+ })
23015
+ .attr('id', 'red-ui-workspace-scroll-spacer')
23016
+ .appendTo(chart);
23017
+
22967
23018
  chart.on('contextmenu', function(evt) {
22968
23019
  if (RED.view.DEBUG) {
22969
23020
  console.warn("contextmenu", { mouse_mode, event: d3.event });
@@ -23008,8 +23059,9 @@ RED.view = (function() {
23008
23059
  lasso.remove();
23009
23060
  lasso = null;
23010
23061
  }
23011
- } else if (mouse_mode === RED.state.PANNING && d3.event.buttons !== 4) {
23012
- resetMouseVars();
23062
+ } else if (mouse_mode === RED.state.PANNING) {
23063
+ // ensure the cursor is set to grab when re-entering the canvas while panning
23064
+ outer.style('cursor', 'grabbing');
23013
23065
  } else if (slicePath) {
23014
23066
  if (d3.event.buttons !== 2) {
23015
23067
  slicePath.remove();
@@ -23026,11 +23078,15 @@ RED.view = (function() {
23026
23078
  if (RED.touch.radialMenu.active()) {
23027
23079
  return;
23028
23080
  }
23081
+ // End gesture when touches end
23082
+ RED.view.zoomAnimator.endGesture();
23029
23083
  canvasMouseUp.call(this);
23030
23084
  })
23031
23085
  .on("touchcancel", function() {
23032
23086
  if (RED.view.DEBUG) { console.warn("eventLayer.touchcancel", mouse_mode); }
23033
23087
  d3.event.preventDefault();
23088
+ // End gesture when touches are cancelled
23089
+ RED.view.zoomAnimator.endGesture();
23034
23090
  canvasMouseUp.call(this);
23035
23091
  })
23036
23092
  .on("touchstart", function() {
@@ -23056,6 +23112,20 @@ RED.view = (function() {
23056
23112
  touch1["pageY"]+(a/2)
23057
23113
  ]
23058
23114
  startTouchDistance = Math.sqrt((a*a)+(b*b));
23115
+
23116
+ // Store initial scale for ratio-based zoom calculation
23117
+ gesture = {
23118
+ initialScale: scaleFactor,
23119
+ initialDistance: startTouchDistance,
23120
+ mode: null // Will be determined on first significant move
23121
+ };
23122
+
23123
+ // Start gesture with fixed focal point (store in workspace coordinates)
23124
+ var focalPoint = [
23125
+ (touch0["pageX"] + touch1["pageX"]) / 2 - offset.left,
23126
+ (touch0["pageY"] + touch1["pageY"]) / 2 - offset.top
23127
+ ];
23128
+ RED.view.zoomAnimator.startGesture(focalPoint, scaleFactor, scrollPos, scaleFactor);
23059
23129
  } else {
23060
23130
  var obj = d3.select(document.body);
23061
23131
  touch0 = d3.event.touches.item(0);
@@ -23105,33 +23175,93 @@ RED.view = (function() {
23105
23175
  var offset = chart.offset();
23106
23176
  var scrollPos = [chart.scrollLeft(),chart.scrollTop()];
23107
23177
  var moveTouchDistance = Math.sqrt((a*a)+(b*b));
23108
- var touchCenter = [
23109
- touch1["pageX"]+(b/2),
23110
- touch1["pageY"]+(a/2)
23178
+
23179
+ // Calculate center point of two fingers
23180
+ var currentTouchCenter = [
23181
+ (touch0["pageX"] + touch1["pageX"]) / 2,
23182
+ (touch0["pageY"] + touch1["pageY"]) / 2
23111
23183
  ];
23112
23184
 
23113
23185
  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;
23186
+ // Determine gesture mode on first significant movement
23187
+ if (!gesture.mode) {
23188
+ var distanceChange = Math.abs(moveTouchDistance - startTouchDistance);
23189
+ var centerChange = moveTouchCenter ?
23190
+ Math.sqrt(Math.pow(currentTouchCenter[0] - moveTouchCenter[0], 2) +
23191
+ Math.pow(currentTouchCenter[1] - moveTouchCenter[1], 2)) : 0;
23192
+
23193
+ // Lock into zoom mode if distance changes significantly (>10px)
23194
+ // Lock into pan mode if center moves significantly (>5px) without distance change
23195
+ if (distanceChange > 10) {
23196
+ gesture.mode = 'zoom';
23197
+ } else if (centerChange > 5) {
23198
+ gesture.mode = 'pan';
23199
+ }
23200
+ }
23201
+
23202
+ // Once mode is determined, stay in that mode for the entire gesture
23203
+ if (gesture.mode === 'zoom') {
23204
+ oldScaleFactor = scaleFactor;
23205
+ // Use smooth ratio-based scaling for natural pinch-to-zoom
23206
+ var zoomRatio = moveTouchDistance / startTouchDistance;
23207
+ var minZoom = calculateMinZoom();
23208
+ var newScaleFactor = Math.min(RED.view.zoomConstants.MAX_ZOOM,
23209
+ Math.max(minZoom, gesture.initialScale * zoomRatio));
23210
+
23211
+ // Use gesture state management to maintain fixed focal point
23212
+ var gestureState = RED.view.zoomAnimator.updateGesture(newScaleFactor);
23213
+
23214
+ // Only call zoomView if scale is actually changing (not at limits)
23215
+ if (Math.abs(scaleFactor - newScaleFactor) >= 0.001) {
23216
+ // Get focal point converted back to current screen coordinates
23217
+ var currentScrollPos = [chart.scrollLeft(), chart.scrollTop()];
23218
+ var focalPoint = RED.view.zoomAnimator.getGestureFocalPoint(currentScrollPos, scaleFactor);
23219
+
23220
+ if (focalPoint) {
23221
+ // Use the fixed focal point from gesture start (converted from workspace coords)
23222
+ zoomView(newScaleFactor, focalPoint);
23223
+ } else {
23224
+ // Fallback to current behavior if gesture not active
23225
+ var touchCenter = [
23226
+ touch1["pageX"]+(b/2),
23227
+ touch1["pageY"]+(a/2)
23228
+ ];
23229
+ var pinchCenter = [
23230
+ touchCenter[0] - offset.left,
23231
+ touchCenter[1] - offset.top
23232
+ ];
23233
+ zoomView(newScaleFactor, pinchCenter);
23234
+ }
23235
+ }
23236
+ } else if (gesture.mode === 'pan' || !gesture.mode) {
23237
+ // Two-finger pan: allow immediate panning even if mode not determined
23238
+ // Clear touchStartTime to prevent issues with next gesture
23239
+ if (touchStartTime) {
23240
+ clearTimeout(touchStartTime);
23241
+ touchStartTime = null;
23242
+ }
23243
+ if (moveTouchCenter) {
23244
+ var dx = currentTouchCenter[0] - moveTouchCenter[0];
23245
+ var dy = currentTouchCenter[1] - moveTouchCenter[1];
23246
+
23247
+ // Pan the canvas
23248
+ var currentScroll = [chart.scrollLeft(), chart.scrollTop()];
23249
+ chart.scrollLeft(currentScroll[0] - dx);
23250
+ chart.scrollTop(currentScroll[1] - dy);
23251
+ RED.events.emit("view:navigate");
23252
+ }
23253
+ // Update the center for next move
23254
+ moveTouchCenter = currentTouchCenter;
23255
+ }
23124
23256
 
23125
- chart.scrollLeft(scrollPos[0]+deltaTouchCenter[0]);
23126
- chart.scrollTop(scrollPos[1]+deltaTouchCenter[1]);
23127
- redraw();
23257
+ // Don't update startTouchDistance - keep initial distance for ratio calculation
23128
23258
  }
23129
23259
  }
23130
23260
  d3.event.preventDefault();
23131
23261
  });
23132
-
23133
-
23134
- const handleAltToggle = (event) => {
23262
+
23263
+ const handleChartKeyboardEvents = (event) => {
23264
+ // Handle Alt toggle for pulling nodes out of groups
23135
23265
  if (mouse_mode === RED.state.MOVING_ACTIVE && event.key === 'Alt' && groupAddParentGroup) {
23136
23266
  RED.nodes.group(groupAddParentGroup).dirty = true
23137
23267
  for (let n = 0; n<movingSet.length(); n++) {
@@ -23150,10 +23280,67 @@ RED.view = (function() {
23150
23280
  }
23151
23281
  }
23152
23282
  RED.view.redraw()
23283
+ } else if (event.keyCode === 32 || event.key === ' ') {
23284
+ if (mouse_mode === RED.state.PANNING) {
23285
+ // Already in panning mode - just prevent the event default handler
23286
+ event.preventDefault()
23287
+ event.stopPropagation()
23288
+ if (event.type === 'keyup' && spacebarPressed) {
23289
+ spacebarPressed = false
23290
+ }
23291
+ } else if (mouse_mode === RED.state.DEFAULT) {
23292
+ // Handle spacebar for panning
23293
+ event.preventDefault();
23294
+ event.stopPropagation();
23295
+ if (event.type === "keydown" && !spacebarPressed) {
23296
+ spacebarPressed = true;
23297
+ // Change cursor to grab hand when spacebar is pressed
23298
+ outer.style('cursor', 'grab');
23299
+ } else if (event.type === "keyup" && spacebarPressed) {
23300
+ spacebarPressed = false;
23301
+ // Revert cursor when spacebar is released
23302
+ outer.style('cursor', '');
23303
+ }
23304
+ }
23153
23305
  }
23154
23306
  }
23155
- document.addEventListener("keyup", handleAltToggle)
23156
- document.addEventListener("keydown", handleAltToggle)
23307
+ chart.on("keydown", handleChartKeyboardEvents)
23308
+ chart.on("keyup", handleChartKeyboardEvents)
23309
+
23310
+ // // // Window-level keyup listener to catch spacebar release when cursor is outside canvas
23311
+ // // function handleWindowSpacebarUp(e) {
23312
+ // // if ((e.keyCode === 32 || e.key === ' ') && spacebarPressed) {
23313
+ // // spacebarPressed = false;
23314
+ // // // Revert cursor when spacebar is released outside canvas
23315
+ // // outer.style('cursor', '');
23316
+ // // e.preventDefault();
23317
+ // // e.stopPropagation();
23318
+ // // }
23319
+ // // }
23320
+ // // chart.on("keyup", handleSpacebarToggle)
23321
+ // // chart.on("keydown", handleSpacebarToggle)
23322
+ // // Additional window-level keyup listener to ensure spacebar state is cleared
23323
+ // // when cursor leaves canvas area while spacebar is held
23324
+ // window.addEventListener("keyup", handleWindowSpacebarUp)
23325
+
23326
+ // // Reset spacebar state when window loses focus to prevent stuck state
23327
+ // window.addEventListener("blur", function() {
23328
+ // if (spacebarPressed) {
23329
+ // spacebarPressed = false;
23330
+ // // Revert cursor when window loses focus
23331
+ // outer.style('cursor', '');
23332
+ // }
23333
+ // })
23334
+
23335
+ // Recalculate minimum zoom when window resizes
23336
+ $(window).on("resize.red-ui-view", function() {
23337
+ // Recalculate minimum zoom to ensure canvas fits in viewport
23338
+ var newMinZoom = calculateMinZoom();
23339
+ // If current zoom is below new minimum, adjust it
23340
+ if (scaleFactor < newMinZoom) {
23341
+ zoomView(newMinZoom);
23342
+ }
23343
+ })
23157
23344
 
23158
23345
  // Workspace Background
23159
23346
  eventLayer.append("svg:rect")
@@ -23245,22 +23432,194 @@ RED.view = (function() {
23245
23432
  '<button class="red-ui-footer-button" id="red-ui-view-zoom-out"><i class="fa fa-minus"></i></button>'+
23246
23433
  '<button class="red-ui-footer-button" id="red-ui-view-zoom-zero"><i class="fa fa-circle-o"></i></button>'+
23247
23434
  '<button class="red-ui-footer-button" id="red-ui-view-zoom-in"><i class="fa fa-plus"></i></button>'+
23435
+ '<button class="red-ui-footer-button" id="red-ui-view-zoom-fit"><i class="fa fa-compress"></i></button>'+
23248
23436
  '</span>')
23249
23437
  })
23250
23438
 
23251
- $("#red-ui-view-zoom-out").on("click", zoomOut);
23439
+ $("#red-ui-view-zoom-out").on("click", function() { zoomOut(); });
23252
23440
  RED.popover.tooltip($("#red-ui-view-zoom-out"),RED._('actions.zoom-out'),'core:zoom-out');
23253
23441
  $("#red-ui-view-zoom-zero").on("click", zoomZero);
23254
23442
  RED.popover.tooltip($("#red-ui-view-zoom-zero"),RED._('actions.zoom-reset'),'core:zoom-reset');
23255
- $("#red-ui-view-zoom-in").on("click", zoomIn);
23443
+ $("#red-ui-view-zoom-in").on("click", function() { zoomIn(); });
23256
23444
  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 ) {
23445
+ $("#red-ui-view-zoom-fit").on("click", zoomToFitAll);
23446
+ RED.popover.tooltip($("#red-ui-view-zoom-fit"),RED._('actions.zoom-fit'),'core:zoom-fit');
23447
+ // Legacy mouse wheel handler - disabled in favor of modern wheel event
23448
+ // chart.on("DOMMouseScroll mousewheel", function (evt) {
23449
+ // if ( evt.altKey || spacebarPressed ) {
23450
+ // evt.preventDefault();
23451
+ // evt.stopPropagation();
23452
+ // // Get cursor position relative to the chart
23453
+ // var offset = chart.offset();
23454
+ // var cursorPos = [
23455
+ // evt.originalEvent.pageX - offset.left,
23456
+ // evt.originalEvent.pageY - offset.top
23457
+ // ];
23458
+ // var move = -(evt.originalEvent.detail) || evt.originalEvent.wheelDelta;
23459
+ // if (move <= 0) { zoomOut(cursorPos); }
23460
+ // else { zoomIn(cursorPos); }
23461
+ // }
23462
+ // });
23463
+
23464
+ // Modern wheel event handler for better trackpad support (pinch-to-zoom) and momentum
23465
+ var momentumTimer = null;
23466
+ var trackpadGestureTimer = null;
23467
+ var lastWheelEventTime = 0;
23468
+ var wheelEventContinuityThreshold = 100; // Events within 100ms are same gesture
23469
+ var gestureEndThreshold = 500; // 500ms+ gap means gesture ended
23470
+
23471
+ // Prevent browser zoom on non-canvas areas
23472
+ document.addEventListener("wheel", function(e) {
23473
+ if (e.ctrlKey && !e.target.closest('#red-ui-workspace-chart')) {
23474
+ e.preventDefault();
23475
+ }
23476
+ }, { passive: false });
23477
+
23478
+ chart.on("wheel", function(evt) {
23479
+ if (mouse_mode === RED.state.PANNING) {
23480
+ // Ignore wheel events while panning
23481
+ return;
23482
+ }
23483
+ // ctrlKey is set during pinch gestures on trackpads
23484
+ if (evt.ctrlKey || evt.altKey || spacebarPressed) {
23485
+ evt.preventDefault();
23486
+ evt.stopPropagation();
23487
+
23488
+ var currentTime = Date.now();
23489
+ var timeSinceLastEvent = currentTime - lastWheelEventTime;
23490
+
23491
+ // Get cursor position relative to the chart
23492
+ var offset = chart.offset();
23493
+ var cursorPos = [
23494
+ evt.originalEvent.pageX - offset.left,
23495
+ evt.originalEvent.pageY - offset.top
23496
+ ];
23497
+ var delta = evt.originalEvent.deltaY;
23498
+
23499
+ // For trackpad pinch (Ctrl+wheel), use smooth proportional zoom
23500
+ if (evt.ctrlKey && !evt.altKey && !spacebarPressed) {
23501
+ // Detect input device: trackpad has small deltas, mouse wheel has large deltas
23502
+ var isTrackpadInput = Math.abs(delta) < 50;
23503
+ // Invert delta: spreading fingers (negative deltaY) should zoom in
23504
+ var scaleDelta = RED.view.zoomAnimator.calculateZoomDelta(scaleFactor, -delta, isTrackpadInput);
23505
+ var minZoom = calculateMinZoom();
23506
+ var newScale = Math.min(RED.view.zoomConstants.MAX_ZOOM,
23507
+ Math.max(minZoom, scaleFactor + scaleDelta));
23508
+
23509
+ // Session-based gesture tracking:
23510
+ // - If no active gesture OR gap > gestureEndThreshold, start new gesture
23511
+ // - If gap < wheelEventContinuityThreshold, continue current gesture
23512
+ // - If gap between continuity and end threshold, keep current gesture but don't update focal point
23513
+
23514
+ if (!RED.view.zoomAnimator.isGestureActive() || timeSinceLastEvent > gestureEndThreshold) {
23515
+ // Start new gesture session - store focal point in workspace coordinates
23516
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
23517
+ RED.view.zoomAnimator.startGesture(cursorPos, scaleFactor, scrollPos, scaleFactor);
23518
+ } else if (timeSinceLastEvent <= wheelEventContinuityThreshold) {
23519
+ // Events are continuous - this is the same gesture, focal point remains locked
23520
+ // No need to update focal point
23521
+ }
23522
+ // For gaps between continuity and end threshold, keep existing gesture state
23523
+
23524
+ // Update gesture with new scale, maintaining locked focal point
23525
+ RED.view.zoomAnimator.updateGesture(newScale);
23526
+ // Only call zoomView if scale is actually changing (not at limits)
23527
+ if (Math.abs(scaleFactor - newScale) >= 0.001) {
23528
+ // Get focal point converted back to current screen coordinates
23529
+ var currentScrollPos = [chart.scrollLeft(), chart.scrollTop()];
23530
+ var focalPoint = RED.view.zoomAnimator.getGestureFocalPoint(currentScrollPos, scaleFactor);
23531
+ zoomView(newScale, focalPoint); // Direct call, no animation
23532
+ }
23533
+
23534
+ // Update last event time for continuity tracking
23535
+ lastWheelEventTime = currentTime;
23536
+
23537
+ // Reset gesture timeout - end gesture when no more events come in for gestureEndThreshold
23538
+ if (trackpadGestureTimer) {
23539
+ clearTimeout(trackpadGestureTimer);
23540
+ }
23541
+ trackpadGestureTimer = setTimeout(function() {
23542
+ RED.view.zoomAnimator.endGesture();
23543
+ trackpadGestureTimer = null;
23544
+ // Store zoom level when gesture completes
23545
+ if (RED.settings.get("editor.view.view-store-zoom")) {
23546
+ RED.settings.setLocal('zoom-level', scaleFactor.toFixed(1));
23547
+ }
23548
+ }, gestureEndThreshold); // Use 500ms timeout for gesture end detection
23549
+ } else {
23550
+ // Regular Alt+scroll or Space+scroll - use smooth zoom without animation
23551
+ // Detect input device: trackpad has small deltas, mouse wheel has large deltas
23552
+ var isTrackpadInput = Math.abs(delta) < 50;
23553
+ var scaleDelta = RED.view.zoomAnimator.calculateZoomDelta(scaleFactor, -delta, isTrackpadInput);
23554
+ var minZoom = calculateMinZoom();
23555
+ var newScale = Math.min(RED.view.zoomConstants.MAX_ZOOM,
23556
+ Math.max(minZoom, scaleFactor + scaleDelta));
23557
+
23558
+ // Use gesture tracking for stable focal point like trackpad pinch
23559
+ if (!RED.view.zoomAnimator.isGestureActive() || timeSinceLastEvent > gestureEndThreshold) {
23560
+ // Start new gesture session - store focal point in workspace coordinates
23561
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
23562
+ RED.view.zoomAnimator.startGesture(cursorPos, scaleFactor, scrollPos, scaleFactor);
23563
+ } else if (timeSinceLastEvent <= wheelEventContinuityThreshold) {
23564
+ // Events are continuous - same gesture, focal point remains locked
23565
+ }
23566
+
23567
+ // Update gesture with new scale, maintaining locked focal point
23568
+ RED.view.zoomAnimator.updateGesture(newScale);
23569
+
23570
+ // Only zoom if scale is actually changing
23571
+ if (Math.abs(scaleFactor - newScale) >= 0.001) {
23572
+ // Get focal point converted back to current screen coordinates
23573
+ var currentScrollPos = [chart.scrollLeft(), chart.scrollTop()];
23574
+ var focalPoint = RED.view.zoomAnimator.getGestureFocalPoint(currentScrollPos, scaleFactor);
23575
+ zoomView(newScale, focalPoint);
23576
+ }
23577
+
23578
+ // Update last event time for continuity tracking
23579
+ lastWheelEventTime = currentTime;
23580
+
23581
+ // Reset gesture timeout
23582
+ if (trackpadGestureTimer) {
23583
+ clearTimeout(trackpadGestureTimer);
23584
+ }
23585
+ trackpadGestureTimer = setTimeout(function() {
23586
+ RED.view.zoomAnimator.endGesture();
23587
+ trackpadGestureTimer = null;
23588
+ // Store zoom level when gesture completes
23589
+ if (RED.settings.get("editor.view.view-store-zoom")) {
23590
+ RED.settings.setLocal('zoom-level', scaleFactor.toFixed(1));
23591
+ }
23592
+ }, gestureEndThreshold);
23593
+ }
23594
+ } else {
23595
+ // Regular scroll - prevent default and manually handle both axes
23259
23596
  evt.preventDefault();
23260
23597
  evt.stopPropagation();
23261
- var move = -(evt.originalEvent.detail) || evt.originalEvent.wheelDelta;
23262
- if (move <= 0) { zoomOut(); }
23263
- else { zoomIn(); }
23598
+
23599
+ // Apply scroll deltas directly to both axes
23600
+ var deltaX = evt.originalEvent.deltaX;
23601
+ var deltaY = evt.originalEvent.deltaY;
23602
+
23603
+ chart.scrollLeft(chart.scrollLeft() + deltaX);
23604
+ chart.scrollTop(chart.scrollTop() + deltaY);
23605
+
23606
+ // Emit navigate event for minimap
23607
+ RED.events.emit("view:navigate");
23608
+
23609
+ // Track velocity and apply momentum
23610
+ handleScroll();
23611
+
23612
+ // Cancel previous momentum timer
23613
+ if (momentumTimer) {
23614
+ clearTimeout(momentumTimer);
23615
+ }
23616
+
23617
+ // Start momentum after scroll stops
23618
+ momentumTimer = setTimeout(function() {
23619
+ if (Math.abs(scrollVelocity.x) > MIN_VELOCITY || Math.abs(scrollVelocity.y) > MIN_VELOCITY) {
23620
+ startMomentumScroll();
23621
+ }
23622
+ }, 100);
23264
23623
  }
23265
23624
  });
23266
23625
 
@@ -23431,6 +23790,12 @@ RED.view = (function() {
23431
23790
  });
23432
23791
  chart.on("blur", function() {
23433
23792
  $("#red-ui-workspace-tabs").removeClass("red-ui-workspace-focussed");
23793
+ // Reset spacebar state when chart loses focus to prevent stuck state
23794
+ if (spacebarPressed) {
23795
+ spacebarPressed = false;
23796
+ // Revert cursor when chart loses focus
23797
+ outer.style('cursor', '');
23798
+ }
23434
23799
  });
23435
23800
 
23436
23801
  RED.actions.add("core:copy-selection-to-internal-clipboard",copySelection);
@@ -23495,6 +23860,7 @@ RED.view = (function() {
23495
23860
  RED.actions.add("core:zoom-in",zoomIn);
23496
23861
  RED.actions.add("core:zoom-out",zoomOut);
23497
23862
  RED.actions.add("core:zoom-reset",zoomZero);
23863
+ RED.actions.add("core:zoom-fit",zoomToFitAll);
23498
23864
  RED.actions.add("core:enable-selected-nodes", function() { setSelectedNodeState(false)});
23499
23865
  RED.actions.add("core:disable-selected-nodes", function() { setSelectedNodeState(true)});
23500
23866
 
@@ -23610,6 +23976,9 @@ RED.view = (function() {
23610
23976
  RED.settings.setLocal('scroll-positions', JSON.stringify(workspaceScrollPositions) )
23611
23977
  }
23612
23978
  chart.on("scroll", function() {
23979
+ // Track scroll velocity for momentum
23980
+ handleScroll();
23981
+
23613
23982
  if (RED.settings.get("editor.view.view-store-position")) {
23614
23983
  if (onScrollTimer) {
23615
23984
  clearTimeout(onScrollTimer)
@@ -23887,12 +24256,26 @@ RED.view = (function() {
23887
24256
  return;
23888
24257
  }
23889
24258
 
24259
+ // Spacebar + left click for panning
24260
+ if (spacebarPressed && d3.event.button === 0) {
24261
+ d3.event.preventDefault();
24262
+ d3.event.stopPropagation();
24263
+ mouse_mode = RED.state.PANNING;
24264
+ mouse_position = [d3.event.pageX,d3.event.pageY]
24265
+ scroll_position = [chart.scrollLeft(),chart.scrollTop()];
24266
+ // Change cursor to grabbing while actively panning
24267
+ outer.style('cursor', 'grabbing');
24268
+ return;
24269
+ }
24270
+
23890
24271
  if (d3.event.button === 1) {
23891
24272
  // Middle Click pan
23892
24273
  d3.event.preventDefault();
23893
24274
  mouse_mode = RED.state.PANNING;
23894
24275
  mouse_position = [d3.event.pageX,d3.event.pageY]
23895
24276
  scroll_position = [chart.scrollLeft(),chart.scrollTop()];
24277
+ // Change cursor to grabbing while actively panning
24278
+ outer.style('cursor', 'grabbing');
23896
24279
  return;
23897
24280
  }
23898
24281
  if (d3.event.button === 2) {
@@ -24424,6 +24807,30 @@ RED.view = (function() {
24424
24807
  redraw();
24425
24808
  }
24426
24809
 
24810
+ function startPanning () {
24811
+
24812
+ }
24813
+ window.addEventListener('mousemove', windowMouseMove)
24814
+ window.addEventListener('touchmove', windowMouseMove)
24815
+
24816
+ function windowMouseMove (event) {
24817
+ if (mouse_mode === RED.state.PANNING) {
24818
+ let pos = [event.pageX, event.pageY]
24819
+ if (event.touches) {
24820
+ let touch0 = event.touches.item(0)
24821
+ pos = [touch0.pageX, touch0.pageY]
24822
+ }
24823
+ const deltaPos = [
24824
+ mouse_position[0]-pos[0],
24825
+ mouse_position[1]-pos[1]
24826
+ ]
24827
+ chart.scrollLeft(scroll_position[0]+deltaPos[0])
24828
+ chart.scrollTop(scroll_position[1]+deltaPos[1])
24829
+ RED.events.emit("view:navigate");
24830
+ return
24831
+ }
24832
+ }
24833
+
24427
24834
  function canvasMouseMove() {
24428
24835
  var i;
24429
24836
  var node;
@@ -24438,18 +24845,8 @@ RED.view = (function() {
24438
24845
  //console.log(d3.mouse(this),container.offsetWidth,container.offsetHeight,container.scrollLeft,container.scrollTop);
24439
24846
 
24440
24847
  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])
24848
+ // A window-level handler is used for panning so the mouse can leave the confines of the chart
24849
+ // but continue panning
24453
24850
  return
24454
24851
  }
24455
24852
 
@@ -24793,6 +25190,12 @@ RED.view = (function() {
24793
25190
  }
24794
25191
  if (mouse_mode === RED.state.PANNING) {
24795
25192
  resetMouseVars();
25193
+ // Revert to grab cursor if spacebar still held, otherwise clear cursor
25194
+ if (spacebarPressed) {
25195
+ outer.style('cursor', 'grab');
25196
+ } else {
25197
+ outer.style('cursor', '');
25198
+ }
24796
25199
  return
24797
25200
  }
24798
25201
  if (mouse_mode === RED.state.SELECTING_NODE) {
@@ -25124,39 +25527,454 @@ RED.view = (function() {
25124
25527
 
25125
25528
  }
25126
25529
 
25127
- function zoomIn() {
25128
- if (scaleFactor < 2) {
25129
- zoomView(scaleFactor+0.1);
25530
+ function calculateMinZoom() {
25531
+ // Calculate the minimum zoom to ensure canvas always fills the viewport (no empty space)
25532
+ var viewportWidth = chart.width();
25533
+ var viewportHeight = chart.height();
25534
+
25535
+ // Canvas is 8000x8000, calculate zoom to cover viewport
25536
+ var zoomToFitWidth = viewportWidth / space_width;
25537
+ var zoomToFitHeight = viewportHeight / space_height;
25538
+
25539
+ // Use the LARGER zoom to ensure canvas covers entire viewport (no empty space visible)
25540
+ var calculatedMinZoom = Math.max(zoomToFitWidth, zoomToFitHeight);
25541
+
25542
+ // Return the larger of the calculated min or the configured min
25543
+ // This ensures canvas always fills the viewport
25544
+ return Math.max(calculatedMinZoom, RED.view.zoomConstants.MIN_ZOOM);
25545
+ }
25546
+
25547
+ // Track focal point for sequential button/hotkey zoom operations
25548
+ // Store in workspace coordinates so it remains valid after viewport shifts
25549
+ var buttonZoomWorkspaceCenter = null;
25550
+ var buttonZoomTimeout = null;
25551
+ var BUTTON_ZOOM_FOCAL_TIMEOUT = 1000; // ms - time to keep same focal point
25552
+
25553
+ function zoomIn(focalPoint) {
25554
+ if (scaleFactor < RED.view.zoomConstants.MAX_ZOOM) {
25555
+ var useFocalPoint = null;
25556
+
25557
+ // If focalPoint is explicitly provided (e.g., from wheel/pinch), use it directly
25558
+ if (focalPoint) {
25559
+ useFocalPoint = focalPoint;
25560
+ } else {
25561
+ // For button/hotkey zoom, maintain the same workspace center across sequential zooms
25562
+ if (!buttonZoomWorkspaceCenter) {
25563
+ // First button zoom - calculate and store workspace center
25564
+ var screenSize = [chart.width(), chart.height()];
25565
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
25566
+ // Convert viewport center to workspace coordinates
25567
+ buttonZoomWorkspaceCenter = [
25568
+ (scrollPos[0] + screenSize[0]/2) / scaleFactor,
25569
+ (scrollPos[1] + screenSize[1]/2) / scaleFactor
25570
+ ];
25571
+ }
25572
+
25573
+ // ALWAYS use viewport center as focal point (fixed screen position)
25574
+ // The stored workspace center will be kept at this screen position
25575
+ var screenSize = [chart.width(), chart.height()];
25576
+ useFocalPoint = [screenSize[0]/2, screenSize[1]/2];
25577
+
25578
+ // Reset timeout
25579
+ clearTimeout(buttonZoomTimeout);
25580
+ buttonZoomTimeout = setTimeout(function() {
25581
+ buttonZoomWorkspaceCenter = null;
25582
+ }, BUTTON_ZOOM_FOCAL_TIMEOUT);
25583
+ }
25584
+
25585
+ animatedZoomView(scaleFactor + RED.view.zoomConstants.ZOOM_STEP, useFocalPoint, buttonZoomWorkspaceCenter);
25586
+ }
25587
+ }
25588
+ function zoomOut(focalPoint) {
25589
+ var minZoom = calculateMinZoom();
25590
+ if (scaleFactor > minZoom) {
25591
+ var useFocalPoint = null;
25592
+
25593
+ if (focalPoint) {
25594
+ useFocalPoint = focalPoint;
25595
+ } else {
25596
+ if (!buttonZoomWorkspaceCenter) {
25597
+ var screenSize = [chart.width(), chart.height()];
25598
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
25599
+ buttonZoomWorkspaceCenter = [
25600
+ (scrollPos[0] + screenSize[0]/2) / scaleFactor,
25601
+ (scrollPos[1] + screenSize[1]/2) / scaleFactor
25602
+ ];
25603
+ }
25604
+
25605
+ // ALWAYS use viewport center as focal point (fixed screen position)
25606
+ var screenSize = [chart.width(), chart.height()];
25607
+ useFocalPoint = [screenSize[0]/2, screenSize[1]/2];
25608
+
25609
+ clearTimeout(buttonZoomTimeout);
25610
+ buttonZoomTimeout = setTimeout(function() {
25611
+ buttonZoomWorkspaceCenter = null;
25612
+ }, BUTTON_ZOOM_FOCAL_TIMEOUT);
25613
+ }
25614
+
25615
+ animatedZoomView(Math.max(scaleFactor - RED.view.zoomConstants.ZOOM_STEP, minZoom), useFocalPoint, buttonZoomWorkspaceCenter);
25130
25616
  }
25131
25617
  }
25132
- function zoomOut() {
25133
- if (scaleFactor > 0.3) {
25134
- zoomView(scaleFactor-0.1);
25618
+ function zoomZero() {
25619
+ // Reset button zoom focal point for zoom reset
25620
+ clearTimeout(buttonZoomTimeout);
25621
+ buttonZoomWorkspaceCenter = null;
25622
+ animatedZoomView(1);
25623
+ }
25624
+
25625
+ function zoomToFitAll() {
25626
+ // Refresh active nodes to ensure we have the latest
25627
+ updateActiveNodes();
25628
+
25629
+ // Get all nodes in active workspace
25630
+ if (!activeNodes || activeNodes.length === 0) {
25631
+ return; // No nodes to fit
25632
+ }
25633
+
25634
+ // Calculate bounding box of all nodes
25635
+ var minX = Infinity, minY = Infinity;
25636
+ var maxX = -Infinity, maxY = -Infinity;
25637
+
25638
+ activeNodes.forEach(function(node) {
25639
+ var nodeLeft = node.x - node.w / 2;
25640
+ var nodeRight = node.x + node.w / 2;
25641
+ var nodeTop = node.y - node.h / 2;
25642
+ var nodeBottom = node.y + node.h / 2;
25643
+
25644
+ minX = Math.min(minX, nodeLeft);
25645
+ maxX = Math.max(maxX, nodeRight);
25646
+ minY = Math.min(minY, nodeTop);
25647
+ maxY = Math.max(maxY, nodeBottom);
25648
+ });
25649
+
25650
+ // Add padding around nodes for visual breathing room
25651
+ var padding = 80;
25652
+ minX -= padding;
25653
+ minY -= padding;
25654
+ maxX += padding;
25655
+ maxY += padding;
25656
+
25657
+ // Calculate dimensions of bounding box
25658
+ var boundingWidth = maxX - minX;
25659
+ var boundingHeight = maxY - minY;
25660
+
25661
+ // Get viewport dimensions
25662
+ var viewportWidth = chart.width();
25663
+ var viewportHeight = chart.height();
25664
+
25665
+ // Calculate zoom level that fits bounding box in viewport
25666
+ var zoomX = viewportWidth / boundingWidth;
25667
+ var zoomY = viewportHeight / boundingHeight;
25668
+ var targetZoom = Math.min(zoomX, zoomY);
25669
+
25670
+ // Respect minimum and maximum zoom limits
25671
+ var minZoom = calculateMinZoom();
25672
+ targetZoom = Math.max(minZoom, Math.min(RED.view.zoomConstants.MAX_ZOOM, targetZoom));
25673
+
25674
+ // Calculate center point of bounding box in workspace coordinates
25675
+ var centerX = (minX + maxX) / 2;
25676
+ var centerY = (minY + maxY) / 2;
25677
+
25678
+ // Reset button zoom focal point for zoom-to-fit
25679
+ clearTimeout(buttonZoomTimeout);
25680
+ buttonZoomWorkspaceCenter = null;
25681
+
25682
+ // Pass the bounding box center as workspace center
25683
+ // This ensures the nodes are centered in viewport after zoom
25684
+ var focalPoint = [viewportWidth / 2, viewportHeight / 2];
25685
+
25686
+ // If zoom level won't change significantly, animate just the pan
25687
+ if (Math.abs(scaleFactor - targetZoom) < 0.01) {
25688
+ var targetScrollLeft = centerX * scaleFactor - viewportWidth / 2;
25689
+ var targetScrollTop = centerY * scaleFactor - viewportHeight / 2;
25690
+
25691
+ // Calculate pan distance to determine duration (match zoom animation logic)
25692
+ var startScrollLeft = chart.scrollLeft();
25693
+ var startScrollTop = chart.scrollTop();
25694
+ var panDistance = Math.sqrt(
25695
+ Math.pow(targetScrollLeft - startScrollLeft, 2) +
25696
+ Math.pow(targetScrollTop - startScrollTop, 2)
25697
+ );
25698
+
25699
+ // Use similar duration calculation as zoom: scale with distance
25700
+ // Normalize by viewport diagonal for consistent feel
25701
+ var viewportDiagonal = Math.sqrt(viewportWidth * viewportWidth + viewportHeight * viewportHeight);
25702
+ var relativeDistance = panDistance / viewportDiagonal;
25703
+ // Duration scales with distance, matching zoom animation feel
25704
+ var duration = Math.max(200, Math.min(350, relativeDistance * RED.view.zoomConstants.DEFAULT_ZOOM_DURATION * 4));
25705
+
25706
+ RED.view.zoomAnimator.easeToValuesRAF({
25707
+ fromValues: {
25708
+ scrollLeft: startScrollLeft,
25709
+ scrollTop: startScrollTop
25710
+ },
25711
+ toValues: {
25712
+ scrollLeft: targetScrollLeft,
25713
+ scrollTop: targetScrollTop
25714
+ },
25715
+ duration: duration,
25716
+ onStep: function(values) {
25717
+ chart.scrollLeft(values.scrollLeft);
25718
+ chart.scrollTop(values.scrollTop);
25719
+ },
25720
+ onStart: function() {
25721
+ RED.events.emit("view:navigate");
25722
+ }
25723
+ });
25724
+ } else {
25725
+ animatedZoomView(targetZoom, focalPoint, [centerX, centerY]);
25135
25726
  }
25136
25727
  }
25137
- function zoomZero() { zoomView(1); }
25728
+
25138
25729
  function searchFlows() { RED.actions.invoke("core:search", $(this).data("term")); }
25139
25730
  function searchPrev() { RED.actions.invoke("core:search-previous"); }
25140
25731
  function searchNext() { RED.actions.invoke("core:search-next"); }
25141
25732
 
25142
25733
 
25143
- function zoomView(factor) {
25734
+ function zoomView(factor, focalPoint) {
25735
+ // Early return if scale factor isn't actually changing
25736
+ // This prevents focal point shifts when at zoom limits
25737
+ if (Math.abs(scaleFactor - factor) < 0.001) {
25738
+ return;
25739
+ }
25740
+ // Make scale 1 'sticky'
25741
+ if (Math.abs(1.0 - factor) < 0.02) {
25742
+ factor = 1
25743
+ }
25744
+
25745
+ console.log(factor)
25144
25746
  var screenSize = [chart.width(),chart.height()];
25145
25747
  var scrollPos = [chart.scrollLeft(),chart.scrollTop()];
25146
- var center = [(scrollPos[0] + screenSize[0]/2)/scaleFactor,(scrollPos[1] + screenSize[1]/2)/scaleFactor];
25748
+ var oldScaleFactor = scaleFactor;
25749
+
25750
+ // Calculate workspace coordinates of the point that should remain fixed
25751
+ var center;
25752
+ if (focalPoint) {
25753
+ // focalPoint is in screen coordinates, convert to workspace coordinates
25754
+ center = [(scrollPos[0] + focalPoint[0])/oldScaleFactor, (scrollPos[1] + focalPoint[1])/oldScaleFactor];
25755
+ } else {
25756
+ // Default to viewport center in workspace coordinates
25757
+ center = [(scrollPos[0] + screenSize[0]/2)/oldScaleFactor, (scrollPos[1] + screenSize[1]/2)/oldScaleFactor];
25758
+ }
25759
+
25147
25760
  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]);
25761
+
25762
+ // Calculate new scroll position to keep the center point at the same screen position
25763
+ if (focalPoint) {
25764
+ // Keep the focal point at the same screen position
25765
+ chart.scrollLeft(center[0] * scaleFactor - focalPoint[0]);
25766
+ chart.scrollTop(center[1] * scaleFactor - focalPoint[1]);
25767
+ } else {
25768
+ // Keep viewport center on the same workspace coordinates
25769
+ var newScrollLeft = center[0] * scaleFactor - screenSize[0]/2;
25770
+ var newScrollTop = center[1] * scaleFactor - screenSize[1]/2;
25771
+ chart.scrollLeft(newScrollLeft);
25772
+ chart.scrollTop(newScrollTop);
25773
+ }
25152
25774
 
25153
25775
  RED.view.navigator.resize();
25154
25776
  redraw();
25777
+ RED.events.emit("view:navigate");
25155
25778
  if (RED.settings.get("editor.view.view-store-zoom")) {
25156
25779
  RED.settings.setLocal('zoom-level', factor.toFixed(1))
25157
25780
  }
25158
25781
  }
25159
25782
 
25783
+ function animatedZoomView(targetFactor, focalPoint, workspaceCenter) {
25784
+ // Cancel any in-progress animation
25785
+ if (cancelInProgressAnimation) {
25786
+ cancelInProgressAnimation();
25787
+ cancelInProgressAnimation = null;
25788
+ }
25789
+
25790
+ // Calculate the actual minimum zoom to fit canvas
25791
+ var minZoom = calculateMinZoom();
25792
+
25793
+ // Clamp target factor to valid range
25794
+ targetFactor = Math.max(minZoom,
25795
+ Math.min(RED.view.zoomConstants.MAX_ZOOM, targetFactor));
25796
+
25797
+ // If we're already at the target, no need to animate
25798
+ // Use a more tolerant threshold to account for floating-point precision
25799
+ if (Math.abs(scaleFactor - targetFactor) < 0.01) {
25800
+ return;
25801
+ }
25802
+ // Make scale 1 'sticky'
25803
+ if (Math.abs(1.0 - targetFactor) < 0.02) {
25804
+ targetFactor = 1
25805
+ }
25806
+
25807
+
25808
+ var startFactor = scaleFactor;
25809
+ var screenSize = [chart.width(), chart.height()];
25810
+ var scrollPos = [chart.scrollLeft(), chart.scrollTop()];
25811
+
25812
+ // Calculate the focal point in workspace coordinates (will remain constant)
25813
+ var center;
25814
+ if (workspaceCenter) {
25815
+ // Use the provided workspace center directly (for button zoom focal point locking)
25816
+ center = workspaceCenter;
25817
+ } else if (focalPoint) {
25818
+ // focalPoint is in screen coordinates, convert to workspace coordinates
25819
+ center = [(scrollPos[0] + focalPoint[0])/scaleFactor, (scrollPos[1] + focalPoint[1])/scaleFactor];
25820
+ } else {
25821
+ // Default to viewport center
25822
+ center = [(scrollPos[0] + screenSize[0]/2)/scaleFactor, (scrollPos[1] + screenSize[1]/2)/scaleFactor];
25823
+ }
25824
+
25825
+ // Calculate duration based on relative zoom change to maintain consistent velocity
25826
+ // Use logarithmic scaling since zoom feels exponential to the user
25827
+ var zoomRatio = targetFactor / startFactor;
25828
+ var logChange = Math.abs(Math.log(zoomRatio));
25829
+ // Scale duration more aggressively: multiply by 2 for stronger effect
25830
+ // At extreme zoom levels, animation will be noticeably longer
25831
+ var duration = Math.max(200, Math.min(350, logChange / 0.693 * RED.view.zoomConstants.DEFAULT_ZOOM_DURATION * 2));
25832
+
25833
+ // Start the animation
25834
+ cancelInProgressAnimation = RED.view.zoomAnimator.easeToValuesRAF({
25835
+ fromValues: {
25836
+ zoom: startFactor
25837
+ },
25838
+ toValues: {
25839
+ zoom: targetFactor
25840
+ },
25841
+ duration: duration,
25842
+ interpolateValue: true, // Use exponential interpolation for zoom
25843
+ onStep: function(values) {
25844
+ var currentFactor = values.zoom;
25845
+ scaleFactor = currentFactor;
25846
+
25847
+ // Calculate new scroll position to maintain focal point
25848
+ var currentScreenSize = [chart.width(), chart.height()];
25849
+ var newScrollPos;
25850
+
25851
+ if (focalPoint) {
25852
+ // Keep the focal point at the same screen position
25853
+ newScrollPos = [
25854
+ center[0] * scaleFactor - focalPoint[0],
25855
+ center[1] * scaleFactor - focalPoint[1]
25856
+ ];
25857
+ } else {
25858
+ // Keep viewport center steady
25859
+ newScrollPos = [
25860
+ center[0] * scaleFactor - currentScreenSize[0]/2,
25861
+ center[1] * scaleFactor - currentScreenSize[1]/2
25862
+ ];
25863
+ }
25864
+
25865
+ chart.scrollLeft(newScrollPos[0]);
25866
+ chart.scrollTop(newScrollPos[1]);
25867
+
25868
+ // During animation, only update the scale transform, not the full redraw
25869
+ // This is much more performant with many nodes
25870
+ eventLayer.attr("transform", "scale(" + scaleFactor + ")");
25871
+ outer.attr("width", space_width * scaleFactor).attr("height", space_height * scaleFactor);
25872
+ RED.view.navigator.resize();
25873
+ },
25874
+ onStart: function() {
25875
+ // Show minimap when zoom animation starts
25876
+ RED.events.emit("view:navigate");
25877
+ },
25878
+ onEnd: function() {
25879
+ cancelInProgressAnimation = null;
25880
+ // Ensure scaleFactor is exactly the target to prevent precision issues
25881
+ scaleFactor = targetFactor;
25882
+ // Full redraw at the end to ensure everything is correct
25883
+ redraw();
25884
+ if (RED.settings.get("editor.view.view-store-zoom")) {
25885
+ RED.settings.setLocal('zoom-level', targetFactor.toFixed(1));
25886
+ }
25887
+ },
25888
+ onCancel: function() {
25889
+ cancelInProgressAnimation = null;
25890
+ // Ensure scaleFactor is set to current target on cancel
25891
+ scaleFactor = targetFactor;
25892
+ }
25893
+ });
25894
+ }
25895
+
25896
+ // Momentum scrolling functions
25897
+ function startMomentumScroll() {
25898
+ if (scrollAnimationId) {
25899
+ cancelAnimationFrame(scrollAnimationId);
25900
+ }
25901
+ momentumActive = true;
25902
+ animateMomentumScroll();
25903
+ }
25904
+
25905
+ function animateMomentumScroll() {
25906
+ if (!momentumActive) {
25907
+ return;
25908
+ }
25909
+
25910
+ var scrollX = chart.scrollLeft();
25911
+ var scrollY = chart.scrollTop();
25912
+ var maxScrollX = chart[0].scrollWidth - chart.width();
25913
+ var maxScrollY = chart[0].scrollHeight - chart.height();
25914
+
25915
+ // Apply friction
25916
+ scrollVelocity.x *= FRICTION;
25917
+ scrollVelocity.y *= FRICTION;
25918
+
25919
+ // Check for edges and apply bounce
25920
+ var newScrollX = scrollX + scrollVelocity.x;
25921
+ var newScrollY = scrollY + scrollVelocity.y;
25922
+
25923
+ // Bounce effect at edges
25924
+ if (newScrollX < 0) {
25925
+ newScrollX = 0;
25926
+ scrollVelocity.x = -scrollVelocity.x * BOUNCE_DAMPING;
25927
+ } else if (newScrollX > maxScrollX) {
25928
+ newScrollX = maxScrollX;
25929
+ scrollVelocity.x = -scrollVelocity.x * BOUNCE_DAMPING;
25930
+ }
25931
+
25932
+ if (newScrollY < 0) {
25933
+ newScrollY = 0;
25934
+ scrollVelocity.y = -scrollVelocity.y * BOUNCE_DAMPING;
25935
+ } else if (newScrollY > maxScrollY) {
25936
+ newScrollY = maxScrollY;
25937
+ scrollVelocity.y = -scrollVelocity.y * BOUNCE_DAMPING;
25938
+ }
25939
+
25940
+ // Apply new scroll position
25941
+ chart.scrollLeft(newScrollX);
25942
+ chart.scrollTop(newScrollY);
25943
+
25944
+ // Stop if velocity is too small
25945
+ if (Math.abs(scrollVelocity.x) < MIN_VELOCITY && Math.abs(scrollVelocity.y) < MIN_VELOCITY) {
25946
+ momentumActive = false;
25947
+ scrollVelocity.x = 0;
25948
+ scrollVelocity.y = 0;
25949
+ } else {
25950
+ scrollAnimationId = requestAnimationFrame(animateMomentumScroll);
25951
+ }
25952
+ }
25953
+
25954
+ function handleScroll() {
25955
+ var now = Date.now();
25956
+ var scrollX = chart.scrollLeft();
25957
+ var scrollY = chart.scrollTop();
25958
+
25959
+ if (lastScrollTime) {
25960
+ var dt = now - lastScrollTime;
25961
+ if (dt > 0 && dt < 100) { // Only calculate velocity for recent scrolls
25962
+ scrollVelocity.x = (scrollX - lastScrollPos.x) / dt * 16; // Normalize to 60fps
25963
+ scrollVelocity.y = (scrollY - lastScrollPos.y) / dt * 16;
25964
+ }
25965
+ }
25966
+
25967
+ lastScrollTime = now;
25968
+ lastScrollPos.x = scrollX;
25969
+ lastScrollPos.y = scrollY;
25970
+
25971
+ // Cancel any ongoing momentum animation
25972
+ if (scrollAnimationId) {
25973
+ cancelAnimationFrame(scrollAnimationId);
25974
+ scrollAnimationId = null;
25975
+ }
25976
+ }
25977
+
25160
25978
  function selectNone() {
25161
25979
  if (mouse_mode === RED.state.MOVING || mouse_mode === RED.state.MOVING_ACTIVE) {
25162
25980
  return;
@@ -25861,6 +26679,9 @@ RED.view = (function() {
25861
26679
 
25862
26680
  function portMouseDown(d,portType,portIndex, evt) {
25863
26681
  if (RED.view.DEBUG) { console.warn("portMouseDown", mouse_mode,d,portType,portIndex); }
26682
+ if (spacebarPressed) {
26683
+ return
26684
+ }
25864
26685
  clearSuggestedFlow();
25865
26686
  RED.contextMenu.hide();
25866
26687
  evt = evt || d3.event;
@@ -26290,6 +27111,9 @@ RED.view = (function() {
26290
27111
  (d3.event || event).stopPropagation();
26291
27112
  return;
26292
27113
  }
27114
+ if (spacebarPressed) {
27115
+ return
27116
+ }
26293
27117
  clearTimeout(portLabelHoverTimeout);
26294
27118
  var active = (mouse_mode!=RED.state.JOINING && mouse_mode != RED.state.QUICK_JOINING) || // Not currently joining - all ports active
26295
27119
  (
@@ -26446,6 +27270,9 @@ RED.view = (function() {
26446
27270
  }
26447
27271
  function nodeMouseDown(d) {
26448
27272
  if (RED.view.DEBUG) { console.warn("nodeMouseDown", mouse_mode,d); }
27273
+ if (spacebarPressed) {
27274
+ return
27275
+ }
26449
27276
  clearSuggestedFlow()
26450
27277
  focusView();
26451
27278
  RED.contextMenu.hide();
@@ -26624,6 +27451,9 @@ RED.view = (function() {
26624
27451
 
26625
27452
  function nodeMouseOver(d) {
26626
27453
  if (RED.view.DEBUG) { console.warn("nodeMouseOver", mouse_mode,d); }
27454
+ if (spacebarPressed) {
27455
+ return
27456
+ }
26627
27457
  if (mouse_mode === 0 || mouse_mode === RED.state.SELECTING_NODE) {
26628
27458
  if (mouse_mode === RED.state.SELECTING_NODE && selectNodesOptions && selectNodesOptions.filter) {
26629
27459
  if (selectNodesOptions.filter(d)) {
@@ -26797,6 +27627,9 @@ RED.view = (function() {
26797
27627
  if (RED.view.DEBUG) {
26798
27628
  console.warn("groupMouseDown", { mouse_mode, point: mouse, event: d3.event });
26799
27629
  }
27630
+ if (spacebarPressed) {
27631
+ return
27632
+ }
26800
27633
  RED.contextMenu.hide();
26801
27634
  focusView();
26802
27635
  if (d3.event.button === 1) {
@@ -27256,6 +28089,15 @@ RED.view = (function() {
27256
28089
  eventLayer.attr("transform","scale("+scaleFactor+")");
27257
28090
  outer.attr("width", space_width*scaleFactor).attr("height", space_height*scaleFactor);
27258
28091
 
28092
+ // Update scroll spacer to match scaled canvas size
28093
+ // This ensures scrollable area = canvas area
28094
+ // Browser calculates maxScroll = scrollWidth - viewport, which correctly
28095
+ // allows scrolling to see the far edges of canvas without going beyond
28096
+ $('#red-ui-workspace-scroll-spacer').css({
28097
+ width: (space_width * scaleFactor) + 'px',
28098
+ height: (space_height * scaleFactor) + 'px'
28099
+ });
28100
+
27259
28101
  // Don't bother redrawing nodes if we're drawing links
27260
28102
  if (forceFullRedraw || showAllLinkPorts !== -1 || mouse_mode != RED.state.JOINING) {
27261
28103
  forceFullRedraw = false
@@ -29616,7 +30458,7 @@ RED.view = (function() {
29616
30458
  selectNodes: function(options) {
29617
30459
  $("#red-ui-workspace-tabs-shade").show();
29618
30460
  $("#red-ui-palette-shade").show();
29619
- $("#red-ui-sidebar-shade").show();
30461
+ $(".red-ui-sidebar-shade").show();
29620
30462
  $("#red-ui-header-shade").show();
29621
30463
  $("#red-ui-workspace").addClass("red-ui-workspace-select-mode");
29622
30464
 
@@ -29638,7 +30480,7 @@ RED.view = (function() {
29638
30480
  clearSelection();
29639
30481
  $("#red-ui-workspace-tabs-shade").hide();
29640
30482
  $("#red-ui-palette-shade").hide();
29641
- $("#red-ui-sidebar-shade").hide();
30483
+ $(".red-ui-sidebar-shade").hide();
29642
30484
  $("#red-ui-header-shade").hide();
29643
30485
  $("#red-ui-workspace").removeClass("red-ui-workspace-select-mode");
29644
30486
  resetMouseVars();
@@ -29707,7 +30549,283 @@ RED.view = (function() {
29707
30549
  applySuggestedFlow
29708
30550
  };
29709
30551
  })();
29710
- ;RED.view.annotations = (function() {
30552
+ ;/**
30553
+ * Zoom configuration constants
30554
+ */
30555
+ RED.view.zoomConstants = {
30556
+ // Zoom limits
30557
+ MIN_ZOOM: 0.05, // Default minimum, will be dynamically calculated to fit canvas
30558
+ MAX_ZOOM: 2.0,
30559
+
30560
+ // Zoom step for keyboard/button controls
30561
+ ZOOM_STEP: 0.2,
30562
+
30563
+ // Animation settings
30564
+ DEFAULT_ZOOM_DURATION: 125, // ms, faster animation
30565
+
30566
+ // Gesture thresholds
30567
+ PINCH_THRESHOLD: 10, // minimum pixel movement to trigger zoom
30568
+
30569
+ // Momentum and friction for smooth scrolling
30570
+ FRICTION: 0.92,
30571
+ BOUNCE_DAMPING: 0.6
30572
+ };;/**
30573
+ * Copyright JS Foundation and other contributors, http://js.foundation
30574
+ *
30575
+ * Licensed under the Apache License, Version 2.0 (the "License");
30576
+ * you may not use this file except in compliance with the License.
30577
+ * You may obtain a copy of the License at
30578
+ *
30579
+ * http://www.apache.org/licenses/LICENSE-2.0
30580
+ *
30581
+ * Unless required by applicable law or agreed to in writing, software
30582
+ * distributed under the License is distributed on an "AS IS" BASIS,
30583
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
30584
+ * See the License for the specific language governing permissions and
30585
+ * limitations under the License.
30586
+ **/
30587
+
30588
+ RED.view.zoomAnimator = (function() {
30589
+
30590
+ /**
30591
+ * Easing function for smooth deceleration
30592
+ * Creates natural-feeling animation curves
30593
+ * @param {number} t - Progress from 0 to 1
30594
+ * @returns {number} - Eased value from 0 to 1
30595
+ */
30596
+ function easeOut(t) {
30597
+ // Cubic ease-out for smooth deceleration
30598
+ return 1 - Math.pow(1 - t, 3);
30599
+ }
30600
+
30601
+ /**
30602
+ * Animate values using requestAnimationFrame with easing
30603
+ * Based on Excalidraw's implementation for smooth zoom transitions
30604
+ *
30605
+ * @param {Object} options - Animation options
30606
+ * @param {Object} options.fromValues - Starting values object
30607
+ * @param {Object} options.toValues - Target values object
30608
+ * @param {Function} options.onStep - Callback for each animation frame
30609
+ * @param {number} [options.duration=250] - Animation duration in ms
30610
+ * @param {Function} [options.interpolateValue] - Custom interpolation function
30611
+ * @param {Function} [options.onStart] - Animation start callback
30612
+ * @param {Function} [options.onEnd] - Animation end callback
30613
+ * @param {Function} [options.onCancel] - Animation cancel callback
30614
+ * @returns {Function} - Cancel function to stop animation
30615
+ */
30616
+ function easeToValuesRAF(options) {
30617
+ const {
30618
+ fromValues,
30619
+ toValues,
30620
+ onStep,
30621
+ duration = 250,
30622
+ interpolateValue,
30623
+ onStart,
30624
+ onEnd,
30625
+ onCancel
30626
+ } = options;
30627
+
30628
+ let startTime = null;
30629
+ let animationId = null;
30630
+ let cancelled = false;
30631
+
30632
+ function step(timestamp) {
30633
+ if (cancelled) {
30634
+ return;
30635
+ }
30636
+
30637
+ if (!startTime) {
30638
+ startTime = timestamp;
30639
+ if (onStart) {
30640
+ onStart();
30641
+ }
30642
+ }
30643
+
30644
+ const elapsed = timestamp - startTime;
30645
+ const progress = Math.min(elapsed / duration, 1);
30646
+ const easedProgress = easeOut(progress);
30647
+
30648
+ const interpolatedValues = {};
30649
+
30650
+ for (const key in fromValues) {
30651
+ if (fromValues.hasOwnProperty(key)) {
30652
+ const from = fromValues[key];
30653
+ const to = toValues[key];
30654
+
30655
+ if (interpolateValue && key === 'zoom') {
30656
+ // Special interpolation for zoom to feel more natural
30657
+ // Exponential interpolation preserves relative zoom feel
30658
+ interpolatedValues[key] = from * Math.pow(to / from, easedProgress);
30659
+ } else {
30660
+ // Linear interpolation for other values
30661
+ interpolatedValues[key] = from + (to - from) * easedProgress;
30662
+ }
30663
+ }
30664
+ }
30665
+
30666
+ onStep(interpolatedValues);
30667
+
30668
+ if (progress < 1) {
30669
+ animationId = requestAnimationFrame(step);
30670
+ } else {
30671
+ if (onEnd) {
30672
+ onEnd();
30673
+ }
30674
+ }
30675
+ }
30676
+
30677
+ animationId = requestAnimationFrame(step);
30678
+
30679
+ // Return cancel function
30680
+ return function cancel() {
30681
+ cancelled = true;
30682
+ if (animationId) {
30683
+ cancelAnimationFrame(animationId);
30684
+ }
30685
+ if (onCancel) {
30686
+ onCancel();
30687
+ }
30688
+ };
30689
+ }
30690
+
30691
+ /**
30692
+ * Calculate smooth zoom delta with acceleration
30693
+ * Provides consistent zoom speed regardless of input device
30694
+ *
30695
+ * @param {number} currentScale - Current zoom scale
30696
+ * @param {number} delta - Input delta (wheel, gesture, etc)
30697
+ * @param {boolean} isTrackpad - Whether input is from trackpad
30698
+ * @returns {number} - Calculated zoom delta
30699
+ */
30700
+ function calculateZoomDelta(currentScale, delta, isTrackpad) {
30701
+ // Normalize delta across different input devices
30702
+ let normalizedDelta = delta;
30703
+
30704
+ if (isTrackpad) {
30705
+ // Trackpad deltas are typically smaller and more frequent
30706
+ normalizedDelta = delta * 0.005; // Reduced from 0.01 for gentler zoom
30707
+ } else {
30708
+ // Mouse wheel deltas are larger and less frequent
30709
+ // Reduce zoom out speed more than zoom in
30710
+ normalizedDelta = delta > 0 ? 0.06 : -0.08; // Reduced from 0.1, asymmetric for gentler zoom out
30711
+ }
30712
+
30713
+ // Apply gentler acceleration based on current zoom level
30714
+ // Less aggressive acceleration to prevent rapid zoom out
30715
+ const acceleration = Math.max(0.7, Math.min(1.1, 1 / currentScale)); // Reduced from 0.5-1.2 to 0.7-1.1
30716
+
30717
+ return normalizedDelta * acceleration;
30718
+ }
30719
+
30720
+ /**
30721
+ * Gesture state management for consistent focal points
30722
+ */
30723
+ const gestureState = {
30724
+ active: false,
30725
+ initialFocalPoint: null, // Will store workspace coordinates
30726
+ initialScale: 1,
30727
+ currentScale: 1,
30728
+ lastDistance: 0,
30729
+ scrollPosAtStart: null, // Store initial scroll position
30730
+ scaleFatorAtStart: 1 // Store initial scale factor
30731
+ };
30732
+
30733
+ /**
30734
+ * Start a zoom gesture with fixed focal point
30735
+ * @param {Array} focalPoint - [x, y] coordinates of focal point in workspace
30736
+ * @param {number} scale - Initial scale value
30737
+ * @param {Array} scrollPos - Current scroll position [x, y]
30738
+ * @param {number} currentScaleFactor - Current scale factor for coordinate conversion
30739
+ */
30740
+ function startGesture(focalPoint, scale, scrollPos, currentScaleFactor) {
30741
+ gestureState.active = true;
30742
+ // Store the focal point in workspace coordinates for stability
30743
+ // This ensures the point remains fixed even if scroll changes due to canvas edge constraints
30744
+ if (focalPoint && scrollPos && currentScaleFactor) {
30745
+ gestureState.initialFocalPoint = [
30746
+ (scrollPos[0] + focalPoint[0]) / currentScaleFactor,
30747
+ (scrollPos[1] + focalPoint[1]) / currentScaleFactor
30748
+ ];
30749
+ gestureState.scrollPosAtStart = [...scrollPos];
30750
+ gestureState.scaleFatorAtStart = currentScaleFactor;
30751
+ } else {
30752
+ gestureState.initialFocalPoint = focalPoint ? [...focalPoint] : null;
30753
+ }
30754
+ gestureState.initialScale = scale;
30755
+ gestureState.currentScale = scale;
30756
+ return gestureState;
30757
+ }
30758
+
30759
+ /**
30760
+ * Update gesture maintaining fixed focal point
30761
+ * @param {number} newScale - New scale value
30762
+ * @returns {Object} - Gesture state with fixed focal point
30763
+ */
30764
+ function updateGesture(newScale) {
30765
+ if (!gestureState.active) {
30766
+ return null;
30767
+ }
30768
+
30769
+ gestureState.currentScale = newScale;
30770
+
30771
+ return {
30772
+ scale: newScale,
30773
+ focalPoint: gestureState.initialFocalPoint,
30774
+ active: gestureState.active
30775
+ };
30776
+ }
30777
+
30778
+ /**
30779
+ * End the current gesture
30780
+ */
30781
+ function endGesture() {
30782
+ gestureState.active = false;
30783
+ gestureState.initialFocalPoint = null;
30784
+ gestureState.lastDistance = 0;
30785
+ }
30786
+
30787
+ /**
30788
+ * Check if a gesture is currently active
30789
+ */
30790
+ function isGestureActive() {
30791
+ return gestureState.active;
30792
+ }
30793
+
30794
+ /**
30795
+ * Get the fixed focal point for the current gesture
30796
+ * @param {Array} currentScrollPos - Current scroll position [x, y]
30797
+ * @param {number} currentScaleFactor - Current scale factor
30798
+ * @returns {Array} - Focal point in screen coordinates or null
30799
+ */
30800
+ function getGestureFocalPoint(currentScrollPos, currentScaleFactor) {
30801
+ if (!gestureState.initialFocalPoint) {
30802
+ return null;
30803
+ }
30804
+
30805
+ // If we stored workspace coordinates, convert back to screen coordinates
30806
+ if (gestureState.scrollPosAtStart && currentScrollPos && currentScaleFactor) {
30807
+ // Convert workspace coordinates back to current screen coordinates
30808
+ return [
30809
+ gestureState.initialFocalPoint[0] * currentScaleFactor - currentScrollPos[0],
30810
+ gestureState.initialFocalPoint[1] * currentScaleFactor - currentScrollPos[1]
30811
+ ];
30812
+ }
30813
+
30814
+ return gestureState.initialFocalPoint;
30815
+ }
30816
+
30817
+ return {
30818
+ easeOut: easeOut,
30819
+ easeToValuesRAF: easeToValuesRAF,
30820
+ calculateZoomDelta: calculateZoomDelta,
30821
+ gestureState: gestureState,
30822
+ startGesture: startGesture,
30823
+ updateGesture: updateGesture,
30824
+ endGesture: endGesture,
30825
+ isGestureActive: isGestureActive,
30826
+ getGestureFocalPoint: getGestureFocalPoint
30827
+ };
30828
+ })();;RED.view.annotations = (function() {
29711
30829
 
29712
30830
  var annotations = {};
29713
30831
 
@@ -29907,139 +31025,202 @@ RED.view = (function() {
29907
31025
  **/
29908
31026
 
29909
31027
 
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
- })
31028
+ RED.view.navigator = (function() {
31029
+ var nav_scale = 50;
31030
+ var nav_width = 8000/nav_scale;
31031
+ var nav_height = 8000/nav_scale;
31032
+ var navContainer;
31033
+ var navBox;
31034
+ var navBorder;
31035
+ var navVis;
31036
+ var scrollPos;
31037
+ var scaleFactor;
31038
+ var chartSize;
31039
+ var dimensions;
31040
+ var isDragging;
31041
+ var isShowing = false;
31042
+ var toggleTimeout;
31043
+ var autoHideTimeout;
31044
+ var isManuallyToggled = false;
31045
+ var isTemporaryShow = false;
31046
+ function refreshNodes() {
31047
+ if (!isShowing) {
31048
+ return;
31049
+ }
31050
+ var navNode = navVis.selectAll(".red-ui-navigator-node").data(RED.view.getActiveNodes(),function(d){return d.id});
31051
+ navNode.exit().remove();
31052
+ navNode.enter().insert("rect")
31053
+ .attr('class','red-ui-navigator-node')
31054
+ .attr("pointer-events", "none");
31055
+ navNode.each(function(d) {
31056
+ d3.select(this).attr("x",function(d) { return (d.x-d.w/2)/nav_scale })
31057
+ .attr("y",function(d) { return (d.y-d.h/2)/nav_scale })
31058
+ .attr("width",function(d) { return Math.max(9,d.w/nav_scale) })
31059
+ .attr("height",function(d) { return Math.max(3,d.h/nav_scale) })
31060
+ .attr("fill",function(d) { return RED.utils.getNodeColor(d.type,d._def);})
31061
+ });
31062
+ }
31063
+ function onScroll() {
31064
+ if (!isDragging) {
31065
+ resizeNavBorder();
31066
+ }
31067
+ }
31068
+ function resizeNavBorder() {
31069
+ if (navBorder) {
31070
+ scaleFactor = RED.view.scale();
31071
+ chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
31072
+ scrollPos = [$("#red-ui-workspace-chart").scrollLeft(),$("#red-ui-workspace-chart").scrollTop()];
31073
+ // Convert scroll position (in scaled pixels) to workspace coordinates, then to minimap coordinates
31074
+ // scrollPos is in scaled canvas pixels, divide by scaleFactor to get workspace coords
31075
+ navBorder.attr('x',scrollPos[0]/scaleFactor/nav_scale)
31076
+ .attr('y',scrollPos[1]/scaleFactor/nav_scale)
31077
+ .attr('width',chartSize[0]/nav_scale/scaleFactor)
31078
+ .attr('height',chartSize[1]/nav_scale/scaleFactor)
31079
+ }
31080
+ }
31081
+ function show () {
31082
+ if (!isShowing) {
31083
+ isShowing = true;
31084
+ clearTimeout(autoHideTimeout);
31085
+ $("#red-ui-view-navigate").addClass("selected");
31086
+ resizeNavBorder();
31087
+ refreshNodes();
31088
+ $("#red-ui-workspace-chart").on("scroll",onScroll);
31089
+ navContainer.addClass('red-ui-navigator-container');
31090
+ navContainer.show();
31091
+ clearTimeout(toggleTimeout)
31092
+ toggleTimeout = setTimeout(function() {
31093
+ navContainer.addClass('red-ui-navigator-visible');
31094
+ }, 10);
31095
+ }
31096
+ }
31097
+ function hide () {
31098
+ if (isShowing) {
31099
+ isShowing = false;
31100
+ isTemporaryShow = false;
31101
+ isManuallyToggled = false;
31102
+ clearTimeout(autoHideTimeout);
31103
+ navContainer.removeClass('red-ui-navigator-visible');
31104
+ clearTimeout(toggleTimeout)
31105
+ toggleTimeout = setTimeout(function() {
31106
+ navContainer.hide();
31107
+ }, 300);
31108
+ $("#red-ui-workspace-chart").off("scroll",onScroll);
31109
+ $("#red-ui-view-navigate").removeClass("selected");
31110
+ }
31111
+ }
31112
+ function toggle() {
31113
+ if (!isShowing) {
31114
+ isManuallyToggled = true;
31115
+ show()
31116
+ } else {
31117
+ isManuallyToggled = false;
31118
+ hide()
31119
+ }
31120
+ }
31121
+ function setupAutoHide () {
31122
+ clearTimeout(autoHideTimeout);
31123
+ autoHideTimeout = setTimeout(function() {
31124
+ hide()
31125
+ }, 2000)
31126
+ }
31127
+ function showTemporary() {
31128
+ if (!isManuallyToggled) {
31129
+ isTemporaryShow = true
31130
+ clearTimeout(autoHideTimeout);
31131
+ show()
31132
+ setupAutoHide()
31133
+ }
31134
+ }
31135
+ return {
31136
+ init: function() {
31137
+ $(window).on("resize", resizeNavBorder);
31138
+ RED.events.on("sidebar:resize",resizeNavBorder);
31139
+ RED.actions.add("core:toggle-navigator",toggle);
31140
+ navContainer = $('<div>').css({
31141
+ "position":"absolute",
31142
+ "bottom":$("#red-ui-workspace-footer").height(),
31143
+ "right":0,
31144
+ zIndex: 1
31145
+ }).addClass('red-ui-navigator-container').appendTo("#red-ui-workspace").hide();
31146
+ navBox = d3.select(navContainer[0])
31147
+ .append("svg:svg")
31148
+ .attr("width", nav_width)
31149
+ .attr("height", nav_height)
31150
+ .attr("pointer-events", "all")
31151
+ .attr("id","red-ui-navigator-canvas")
31152
+ navBox.append("rect").attr("x",0).attr("y",0).attr("width",nav_width).attr("height",nav_height).style({
31153
+ fill:"none",
31154
+ stroke:"none",
31155
+ pointerEvents:"all"
31156
+ }).on("mousedown", function() {
31157
+ // Update these in case they have changed
31158
+ scaleFactor = RED.view.scale();
31159
+ chartSize = [ $("#red-ui-workspace-chart").width(), $("#red-ui-workspace-chart").height()];
31160
+ dimensions = [chartSize[0]/nav_scale/scaleFactor, chartSize[1]/nav_scale/scaleFactor];
31161
+ var newX = Math.max(0,Math.min(d3.event.offsetX+dimensions[0]/2,nav_width)-dimensions[0]);
31162
+ var newY = Math.max(0,Math.min(d3.event.offsetY+dimensions[1]/2,nav_height)-dimensions[1]);
31163
+ navBorder.attr('x',newX).attr('y',newY);
31164
+ isDragging = true;
31165
+ $("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
31166
+ $("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
31167
+ }).on("mousemove", function() {
31168
+ if (!isDragging) { return }
31169
+ if (d3.event.buttons === 0) {
31170
+ isDragging = false;
31171
+ return;
31172
+ }
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
+ $("#red-ui-workspace-chart").scrollLeft(newX*nav_scale*scaleFactor);
31177
+ $("#red-ui-workspace-chart").scrollTop(newY*nav_scale*scaleFactor);
31178
+ }).on("mouseup", function() {
31179
+ isDragging = false;
31180
+ }).on("mouseenter", function () {
31181
+ if (isTemporaryShow) {
31182
+ // If user hovers over the minimap while it's temporarily shown, keep it shown
31183
+ clearTimeout(autoHideTimeout);
31184
+ }
31185
+ }).on("mouseleave", function () {
31186
+ if (isTemporaryShow) {
31187
+ // Restart the auto-hide timer after mouse leaves the minimap
31188
+ setupAutoHide()
31189
+ }
31190
+ })
31191
+ navBorder = navBox.append("rect").attr("class","red-ui-navigator-border")
31192
+ navVis = navBox.append("svg:g")
31193
+ RED.statusBar.add({
31194
+ id: "view-navigator",
31195
+ align: "right",
31196
+ element: $('<button class="red-ui-footer-button-toggle single" id="red-ui-view-navigate"><i class="fa fa-map-o"></i></button>')
31197
+ })
30037
31198
 
30038
31199
  $("#red-ui-view-navigate").on("click", function(evt) {
30039
31200
  evt.preventDefault();
30040
31201
  toggle();
30041
31202
  })
30042
31203
  RED.popover.tooltip($("#red-ui-view-navigate"),RED._('actions.toggle-navigator'),'core:toggle-navigator');
31204
+
31205
+ // Listen for canvas interactions to show minimap temporarily
31206
+ // Only show on actual pan/zoom navigation, not selection changes
31207
+ // RED.events.on("view:navigate", function() {
31208
+ // showTemporary();
31209
+ // });
31210
+
31211
+ // Show minimap briefly when workspace changes (includes initial load)
31212
+ // RED.events.on("workspace:change", function(event) {
31213
+ // // Only show if there's an active workspace with nodes
31214
+ // if (event.workspace && RED.nodes.getWorkspaceOrder().length > 0) {
31215
+ // // Small delay to ensure nodes are rendered
31216
+ // setTimeout(function() {
31217
+ // var activeNodes = RED.nodes.filterNodes({z: event.workspace});
31218
+ // if (activeNodes.length > 0) {
31219
+ // showTemporary();
31220
+ // }
31221
+ // }, 100);
31222
+ // }
31223
+ // });
30043
31224
  },
30044
31225
  refresh: refreshNodes,
30045
31226
  resize: resizeNavBorder,
@@ -31507,19 +32688,56 @@ RED.view.tools = (function() {
31507
32688
  * limitations under the License.
31508
32689
  **/
31509
32690
  RED.sidebar = (function() {
32691
+ const sidebars = {
32692
+ primary: {
32693
+ id: 'primary',
32694
+ direction: 'right',
32695
+ menuToggle: 'menu-item-sidebar',
32696
+ minimumWidth: 180,
32697
+ maximumWidth: 800,
32698
+ defaultWidth: 300
32699
+ },
32700
+ secondary: {
32701
+ id: 'secondary',
32702
+ direction: 'left',
32703
+ menuToggle: 'menu-item-palette',
32704
+ minimumWidth: 180,
32705
+ maximumWidth: 800,
32706
+ // Make LH side slightly narrower by default as its the palette that doesn't require a lot of width
32707
+ defaultWidth: 210
32708
+ }
32709
+ }
32710
+ const defaultSidebarConfiguration = {
32711
+ primary: ['info','debug','help','config','context'],
32712
+ secondary: ['palette']
32713
+ }
32714
+
32715
+ const knownTabs = {};
32716
+
32717
+ function exportSidebarState () {
32718
+ const state = {
32719
+ primary: [],
32720
+ secondary: []
32721
+ }
32722
+ sidebars.primary.tabBar.children('button').each(function() {
32723
+ const tabId = $(this).attr('data-tab-id');
32724
+ state.primary.push(tabId);
32725
+ })
32726
+ sidebars.secondary.tabBar.children('button').each(function() {
32727
+ const tabId = $(this).attr('data-tab-id');
32728
+ state.secondary.push(tabId);
32729
+ })
32730
+ RED.settings.set('editor.sidebar.state', state)
32731
+ }
31510
32732
 
31511
- //$('#sidebar').tabs();
31512
- var sidebar_tabs;
31513
- var knownTabs = {};
31514
32733
 
31515
32734
  // We store the current sidebar tab id in localStorage as 'last-sidebar-tab'
31516
32735
  // This is restored when the editor is reloaded.
31517
- // We use sidebar_tabs.onchange to update localStorage. However that will
32736
+ // We use sidebars.primary.tabs.onchange to update localStorage. However that will
31518
32737
  // also get triggered when the first tab gets added to the tabs - typically
31519
32738
  // the 'info' tab. So we use the following variable to store the retrieved
31520
32739
  // value from localStorage before we start adding the actual tabs
31521
- var lastSessionSelectedTab = null;
31522
-
32740
+ let lastSessionSelectedTabs = {}
31523
32741
 
31524
32742
  function addTab(title,content,closeable,visible) {
31525
32743
  var options;
@@ -31536,10 +32754,34 @@ RED.sidebar = (function() {
31536
32754
  } else if (typeof title === "object") {
31537
32755
  options = title;
31538
32756
  }
32757
+ options.target = options.target || 'primary';
32758
+ let targetTabButtonIndex = -1 // Append to end by default
32759
+
32760
+ // Check the saved sidebar state to see if this tab should be added to the primary or secondary sidebar
32761
+ const savedState = RED.settings.get('editor.sidebar.state', defaultSidebarConfiguration)
32762
+ if (savedState) {
32763
+ let targetSidebar = null
32764
+ let sidebarState
32765
+ if (savedState.secondary.includes(options.id)) {
32766
+ options.target = 'secondary'
32767
+ sidebarState = savedState.secondary
32768
+ targetSidebar = sidebars.secondary
32769
+ } else if (savedState.primary.includes(options.id)) {
32770
+ options.target = 'primary'
32771
+ sidebarState = savedState.primary
32772
+ targetSidebar = sidebars.primary
32773
+ }
32774
+ if (targetSidebar) {
32775
+ // This tab was found in the saved sidebar state. Now find the target position for the tab button
32776
+ targetTabButtonIndex = sidebarState.indexOf(options.id)
32777
+ }
32778
+ }
31539
32779
 
32780
+
32781
+ const targetSidebar = options.target === 'secondary' ? sidebars.secondary : sidebars.primary;
31540
32782
  delete options.closeable;
31541
32783
 
31542
- options.wrapper = $('<div>',{style:"height:100%"}).appendTo("#red-ui-sidebar-content")
32784
+ options.wrapper = $('<div>',{style:"height:100%"}).appendTo(targetSidebar.content)
31543
32785
  options.wrapper.append(options.content);
31544
32786
  options.wrapper.hide();
31545
32787
 
@@ -31548,11 +32790,12 @@ RED.sidebar = (function() {
31548
32790
  }
31549
32791
 
31550
32792
  if (options.toolbar) {
31551
- $("#red-ui-sidebar-footer").append(options.toolbar);
32793
+ targetSidebar.footer.append(options.toolbar);
31552
32794
  $(options.toolbar).hide();
31553
32795
  }
31554
32796
  var id = options.id;
31555
32797
 
32798
+ // console.log('menu', options.id, options.name)
31556
32799
  RED.menu.addItem("menu-item-view-menu",{
31557
32800
  id:"menu-item-view-menu-"+options.id,
31558
32801
  label:options.name,
@@ -31565,208 +32808,315 @@ RED.sidebar = (function() {
31565
32808
  options.iconClass = options.iconClass || "fa fa-square-o"
31566
32809
 
31567
32810
  knownTabs[options.id] = options;
32811
+ options.tabButton = $('<button></button>')
32812
+ // Insert the tab button at the correct index
32813
+ if (targetTabButtonIndex === -1 || targetTabButtonIndex >= targetSidebar.tabBar.children().length) {
32814
+ // Append to end
32815
+ options.tabButton = $('<button></button>').appendTo(targetSidebar.tabBar);
32816
+ } else {
32817
+ // Insert before the item at targetTabButtonIndex
32818
+ options.tabButton = $('<button></button>').insertBefore(targetSidebar.tabBar.children().eq(targetTabButtonIndex));
32819
+ }
32820
+ options.tabButton.attr('data-tab-id', options.id)
31568
32821
 
31569
- if (options.visible !== false) {
31570
- sidebar_tabs.addTab(knownTabs[options.id]);
32822
+ options.tabButtonTooltip = RED.popover.tooltip(options.tabButton, options.name, options.action);
32823
+ if (options.icon) {
32824
+ $('<i>',{class: 'red-ui-sidebar-tab-icon', style:"mask-image: url("+options.icon+"); -webkit-mask-image: url("+options.icon+");"}).appendTo(options.tabButton);
32825
+ } else if (options.iconClass) {
32826
+ $('<i>',{class:options.iconClass}).appendTo(options.tabButton);
32827
+ }
32828
+ options.tabButton.on('mouseup', function(evt) {
32829
+ if (draggingTabButton) {
32830
+ draggingTabButton = false
32831
+ return
32832
+ }
32833
+ const targetSidebar = options.target === 'secondary' ? sidebars.secondary : sidebars.primary;
32834
+ if (targetSidebar.activeTab === options.id && RED.menu.isSelected(targetSidebar.menuToggle)) {
32835
+ RED.menu.setSelected(targetSidebar.menuToggle, false);
32836
+ } else {
32837
+ RED.sidebar.show(options.id)
32838
+ }
32839
+ })
32840
+ if (targetSidebar.content.children().length === 1) {
32841
+ RED.sidebar.show(options.id)
31571
32842
  }
31572
32843
  }
31573
32844
 
31574
32845
  function removeTab(id) {
31575
- sidebar_tabs.removeTab(id);
31576
- $(knownTabs[id].wrapper).remove();
31577
- if (knownTabs[id].footer) {
31578
- knownTabs[id].footer.remove();
32846
+ if (knownTabs[id]) {
32847
+ const targetSidebar = knownTabs[id].target === 'secondary' ? sidebars.secondary : sidebars.primary;
32848
+ $(knownTabs[id].wrapper).remove();
32849
+ if (knownTabs[id].footer) {
32850
+ knownTabs[id].footer.remove();
32851
+ }
32852
+ targetSidebar.tabBar.find('button[data-tab-id="'+id+'"]').remove()
32853
+ RED.menu.removeItem("menu-item-view-menu-"+id);
32854
+ if (knownTabs[id].onremove) {
32855
+ knownTabs[id].onremove.call(knownTabs[id]);
32856
+ }
32857
+ delete knownTabs[id];
32858
+ const firstTab = targetSidebar.tabBar.find('button').first().attr('data-tab-id');
32859
+ if (firstTab) {
32860
+ RED.sidebar.show(firstTab);
32861
+ }
31579
32862
  }
31580
- delete knownTabs[id];
31581
- RED.menu.removeItem("menu-item-view-menu-"+id);
31582
32863
  }
31583
32864
 
31584
- var sidebarSeparator = {};
31585
- sidebarSeparator.dragging = false;
31586
-
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
- }
31617
-
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
- }
31625
-
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;
31635
- }
31636
- } else if (newSidebarWidth > 150 && (sidebarSeparator.closing || sidebarSeparator.opening)) {
31637
- sidebarSeparator.closing = false;
31638
- $("#red-ui-sidebar").removeClass("closing");
32865
+ function moveTab(id, srcSidebar, targetSidebar) {
32866
+ const options = knownTabs[id];
32867
+ options.target = targetSidebar.id;
32868
+ $(options.wrapper).appendTo(targetSidebar.content);
32869
+ if (options.toolbar) {
32870
+ targetSidebar.footer.append(options.toolbar);
32871
+ }
32872
+ // Reset the tooltip so its left/right direction is recalculated
32873
+ options.tabButtonTooltip.delete()
32874
+ options.tabButtonTooltip = RED.popover.tooltip(options.tabButton, options.name, options.action);
32875
+
32876
+ if (targetSidebar.content.children().length === 1) {
32877
+ RED.sidebar.show(options.id)
32878
+ }
32879
+ if (srcSidebar.content.children().length === 0) {
32880
+ RED.menu.setSelected(srcSidebar.menuToggle, false);
32881
+ }
32882
+ }
32883
+
32884
+ let draggingTabButton = false
32885
+ function setupSidebarTabs(sidebar) {
32886
+ const tabBar = $('<div class="red-ui-sidebar-tab-bar"></div>').addClass('red-ui-sidebar-' + sidebar.direction);
32887
+ tabBar.attr('id', sidebar.container.attr('id') + '-tab-bar')
32888
+ tabBar.data('sidebar', sidebar.id)
32889
+ if (sidebar.direction === 'right') {
32890
+ tabBar.insertAfter(sidebar.container);
32891
+ } else if (sidebar.direction === 'left') {
32892
+ tabBar.insertBefore(sidebar.container);
32893
+ }
32894
+ tabBar.sortable({
32895
+ distance: 10,
32896
+ cancel: false,
32897
+ placeholder: "red-ui-sidebar-tab-bar-button-placeholder",
32898
+ connectWith: ".red-ui-sidebar-tab-bar",
32899
+ start: function(event, ui) {
32900
+ // Remove the tooltip so it doesn't display unexpectedly whilst dragging
32901
+ const tabId = ui.item.attr('data-tab-id');
32902
+ const options = knownTabs[tabId];
32903
+ options.tabButtonTooltip.delete()
32904
+ draggingTabButton = true
32905
+ tabBar.css('z-index','inherit')
32906
+ },
32907
+ stop: function(event, ui) {
32908
+ // Restore the tooltip
32909
+ const tabId = ui.item.attr('data-tab-id');
32910
+ const options = knownTabs[tabId];
32911
+ options.tabButtonTooltip.delete()
32912
+ options.tabButtonTooltip = RED.popover.tooltip(options.tabButton, options.name, options.action);
32913
+ // Save the sidebar state
32914
+ exportSidebarState()
32915
+ tabBar.css('z-index','')
32916
+ },
32917
+ receive: function(event, ui) {
32918
+ // Tab has been moved from one sidebar to another
32919
+ const src = sidebars[ui.sender.data('sidebar')]
32920
+ const dest = sidebars[$(this).data('sidebar')]
32921
+ const tabId = ui.item.attr('data-tab-id');
32922
+ moveTab(tabId, src, dest)
32923
+ if (ui.item.hasClass('selected')) {
32924
+ const firstTab = src.tabBar.find('button').first().attr('data-tab-id');
32925
+ if (firstTab) {
32926
+ RED.sidebar.show(firstTab);
31639
32927
  }
31640
-
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);
31645
-
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");
32928
+ }
32929
+ RED.sidebar.show(tabId)
32930
+ }
32931
+ })
32932
+ // $(window).on("resize", function () {
32933
+ // const lastChild = tabBar.children().last();
32934
+ // if (lastChild.length > 0) {
32935
+ // const tabBarHeight = tabBar.height();
32936
+ // const lastChildBottom = lastChild.position().top + lastChild.outerHeight();
32937
+ // if (lastChildBottom > tabBarHeight) {
32938
+ // console.log('overflow')
32939
+ // }
32940
+ // }
32941
+ // })
32942
+ return tabBar
32943
+ }
32944
+ function setupSidebarSeparator(sidebar) {
32945
+ const separator = $('<div class="red-ui-sidebar-separator"></div>');
32946
+ separator.attr('id', sidebar.container.attr('id') + '-separator')
32947
+ $('<div class="red-ui-sidebar-shade hide"></div>').appendTo(separator);
32948
+ $('<div class="red-ui-sidebar-separator-handle"></div>').appendTo(separator);
32949
+ let scaleFactor = 1;
32950
+ if (sidebar.direction === 'right') {
32951
+ separator.insertBefore(sidebar.container);
32952
+ } else if (sidebar.direction === 'left') {
32953
+ scaleFactor = -1;
32954
+ separator.insertAfter(sidebar.container);
32955
+ }
32956
+ // Track sidebar state whilst dragging
32957
+ const sidebarSeparator = {}
32958
+ separator.draggable({
32959
+ axis: "x",
32960
+ start:function(event,ui) {
32961
+ sidebarSeparator.closing = false;
32962
+ sidebarSeparator.opening = false;
32963
+ // var winWidth = $("#red-ui-editor").width();
32964
+ sidebarSeparator.start = ui.position.left;
32965
+ sidebarSeparator.width = sidebar.container.width();
32966
+ sidebarSeparator.chartWidth = $("#red-ui-workspace").width();
32967
+ sidebarSeparator.dragging = true;
32968
+
32969
+ if (!RED.menu.isSelected(sidebar.menuToggle)) {
32970
+ sidebarSeparator.opening = true;
32971
+ sidebar.container.width(0);
32972
+ RED.menu.setSelected(sidebar.menuToggle,true);
31662
32973
  RED.events.emit("sidebar:resize");
31663
32974
  }
31664
- });
32975
+ sidebarSeparator.width = sidebar.container.width();
32976
+ },
32977
+ drag: function(event,ui) {
32978
+ var d = scaleFactor * (ui.position.left-sidebarSeparator.start);
31665
32979
 
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");
32980
+ var newSidebarWidth = sidebarSeparator.width - d;
32981
+ if (newSidebarWidth > sidebar.maximumWidth) {
32982
+ newSidebarWidth = sidebar.maximumWidth;
32983
+ d = sidebarSeparator.width - sidebar.maximumWidth;
32984
+ ui.position.left = sidebarSeparator.start + scaleFactor * d;
32985
+ }
32986
+
32987
+ if (newSidebarWidth > sidebar.minimumWidth) {
32988
+ if (sidebarSeparator.chartWidth + d < 200) {
32989
+ // Chart is now too small, but we have room to resize the sidebar
32990
+ d += (200 - (sidebarSeparator.chartWidth + d));
32991
+ newSidebarWidth = sidebarSeparator.width - d;
32992
+ ui.position.left = sidebarSeparator.start + scaleFactor * d;
32993
+ }
32994
+ } else if (newSidebarWidth < sidebar.minimumWidth) {
32995
+ if (newSidebarWidth > 100) {
32996
+ newSidebarWidth = sidebar.minimumWidth
32997
+ sidebarSeparator.closing = false
32998
+ } else {
32999
+ newSidebarWidth = 0
33000
+ sidebarSeparator.closing = true
33001
+ }
31675
33002
  } else {
31676
- sidebarControls.find("i").removeClass("fa-chevron-right").addClass("fa-chevron-left");
33003
+ sidebarSeparator.closing = false
31677
33004
  }
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();
33005
+ sidebar.container.width(newSidebarWidth);
33006
+ ui.position.left -= scaleFactor * d
33007
+
33008
+ // sidebar.tabs.resize();
33009
+ RED.events.emit("sidebar:resize");
33010
+ },
33011
+ stop:function(event,ui) {
33012
+ sidebarSeparator.dragging = false;
33013
+ if (sidebarSeparator.closing) {
33014
+ sidebar.container.removeClass("closing");
33015
+ if (sidebar.menuToggle) {
33016
+ RED.menu.setSelected(sidebar.menuToggle,false);
33017
+ }
33018
+ sidebar.container.hide()
33019
+ sidebar.separator.hide()
33020
+ if (sidebar.container.width() < sidebar.minimumWidth) {
33021
+ sidebar.container.width(sidebar.defaultWidth);
33022
+ }
33023
+ }
33024
+ RED.events.emit("sidebar:resize");
31685
33025
  }
31686
33026
  });
33027
+ return separator
31687
33028
  }
31688
33029
 
31689
- function toggleSidebar(state) {
33030
+ function toggleSidebar(sidebar, state) {
31690
33031
  if (!state) {
31691
- $("#red-ui-main-container").addClass("red-ui-sidebar-closed");
33032
+ sidebar.container.hide()
33033
+ sidebar.separator.hide()
33034
+ sidebar.tabBar.find('button').removeClass('selected')
31692
33035
  } else {
31693
- $("#red-ui-main-container").removeClass("red-ui-sidebar-closed");
31694
- sidebar_tabs.resize();
33036
+ sidebar.container.show()
33037
+ sidebar.separator.show()
31695
33038
  }
31696
33039
  RED.events.emit("sidebar:resize");
31697
33040
  }
31698
33041
 
31699
33042
  function showSidebar(id, skipShowSidebar) {
31700
33043
  if (id === ":first") {
31701
- id = lastSessionSelectedTab || RED.settings.get("editor.sidebar.order",["info", "help", "version-control", "debug"])[0]
33044
+ // Show the last selected tab for each sidebar
33045
+ Object.keys(sidebars).forEach(function(sidebarKey) {
33046
+ const sidebar = sidebars[sidebarKey];
33047
+ let lastTabId = lastSessionSelectedTabs[sidebarKey];
33048
+ if (!lastTabId) {
33049
+ lastTabId = sidebar.tabBar.children('button').first().attr('data-tab-id');
33050
+ }
33051
+ showSidebar(lastTabId, true)
33052
+ })
33053
+ return
31702
33054
  }
31703
33055
  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);
33056
+ const tabOptions = knownTabs[id];
33057
+ if (tabOptions) {
33058
+ const targetSidebar = tabOptions.target === 'secondary' ? sidebars.secondary : sidebars.primary;
33059
+ targetSidebar.content.children().hide();
33060
+ targetSidebar.footer.children().hide();
33061
+ if (tabOptions.onchange) {
33062
+ tabOptions.onchange.call(tabOptions);
33063
+ }
33064
+ $(tabOptions.wrapper).show();
33065
+ if (tabOptions.toolbar) {
33066
+ $(tabOptions.toolbar).show();
33067
+ }
33068
+ RED.settings.setLocal("last-sidebar-tab-" + targetSidebar.id, tabOptions.id)
33069
+ targetSidebar.tabBar.find('button').removeClass('selected')
33070
+ targetSidebar.tabBar.find('button[data-tab-id="'+id+'"]').addClass('selected')
33071
+ targetSidebar.activeTab = id
33072
+
33073
+ if (!skipShowSidebar && !RED.menu.isSelected(targetSidebar.menuToggle)) {
33074
+ RED.menu.setSelected(targetSidebar.menuToggle,true);
33075
+ }
31710
33076
  }
31711
33077
  }
31712
33078
  }
31713
33079
 
31714
33080
  function containsTab(id) {
31715
- return sidebar_tabs.contains(id);
33081
+ return sidebars.primary.tabs.contains(id);
31716
33082
  }
31717
33083
 
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)
31733
- },
31734
- onremove: function(tab) {
31735
- $(tab.wrapper).hide();
31736
- if (tab.onremove) {
31737
- tab.onremove.call(tab);
31738
- }
31739
- },
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
31747
- });
33084
+ function setupSidebar(sidebar) {
33085
+ sidebar.container.addClass("red-ui-sidebar").addClass('red-ui-sidebar-' + sidebar.direction);
33086
+ sidebar.container.width(sidebar.defaultWidth);
33087
+ sidebar.separator = setupSidebarSeparator(sidebar);
33088
+ sidebar.tabBar = setupSidebarTabs(sidebar)
33089
+ sidebar.content = $('<div class="red-ui-sidebar-content"></div>').appendTo(sidebar.container);
33090
+ sidebar.footer = $('<div class="red-ui-sidebar-footer"></div>').appendTo(sidebar.container);
33091
+ sidebar.shade = $('<div class="red-ui-sidebar-shade hide"></div>').appendTo(sidebar.container);
31748
33092
 
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");
33093
+ }
33094
+ function init () {
33095
+ sidebars.primary.container = $("#red-ui-sidebar");
33096
+ setupSidebar(sidebars.primary)
33097
+ sidebars.secondary.container = $("#red-ui-sidebar-left");
33098
+ setupSidebar(sidebars.secondary)
31752
33099
 
31753
33100
  RED.actions.add("core:toggle-sidebar",function(state){
31754
33101
  if (state === undefined) {
31755
- RED.menu.toggleSelected("menu-item-sidebar");
33102
+ RED.menu.toggleSelected(sidebars.primary.menuToggle);
31756
33103
  } else {
31757
- toggleSidebar(state);
33104
+ toggleSidebar(sidebars.primary, state);
33105
+ }
33106
+ });
33107
+ RED.actions.add("core:toggle-palette", function(state) {
33108
+ if (state === undefined) {
33109
+ RED.menu.toggleSelected(sidebars.secondary.menuToggle);
33110
+ } else {
33111
+ toggleSidebar(sidebars.secondary, state);
31758
33112
  }
31759
33113
  });
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
33114
 
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); }
33115
+ // Remember the last selected tab for each sidebar before
33116
+ // the tabs are readded causing the state to get updated
33117
+ Object.keys(sidebars).forEach(function(sidebarKey) {
33118
+ lastSessionSelectedTabs[sidebarKey] = RED.settings.getLocal("last-sidebar-tab-" + sidebarKey)
33119
+ })
31770
33120
  }
31771
33121
 
31772
33122
  return {
@@ -31814,7 +33164,6 @@ RED.palette = (function() {
31814
33164
  ];
31815
33165
 
31816
33166
  var categoryContainers = {};
31817
- var sidebarControls;
31818
33167
 
31819
33168
  let paletteState = { filter: "", collapsed: [] };
31820
33169
 
@@ -32091,6 +33440,7 @@ RED.palette = (function() {
32091
33440
  width: "300px",
32092
33441
  content: "hi",
32093
33442
  delay: { show: 750, hide: 50 }
33443
+ // direction: "left"
32094
33444
  });
32095
33445
 
32096
33446
  d.data('popover',popover);
@@ -32113,7 +33463,8 @@ RED.palette = (function() {
32113
33463
  revert: 'invalid',
32114
33464
  revertDuration: 200,
32115
33465
  containment:'#red-ui-main-container',
32116
- start: function() {
33466
+ start: function(e, ui) {
33467
+ ui.helper.css('z-index', 1000);
32117
33468
  dropEnabled = !(RED.nodes.workspace(RED.workspaces.active())?.locked);
32118
33469
  paletteWidth = $("#red-ui-palette").width();
32119
33470
  paletteTop = $("#red-ui-palette").parent().position().top + $("#red-ui-palette-container").position().top;
@@ -32139,7 +33490,9 @@ RED.palette = (function() {
32139
33490
  },
32140
33491
  drag: function(e,ui) {
32141
33492
  var paletteNode = getPaletteNode(nt);
32142
- ui.originalPosition.left = paletteNode.offset().left;
33493
+ console.log(ui.originalPosition.left, paletteNode.offset().left)
33494
+ // ui.originalPosition.left = paletteNode.offset().left;
33495
+ // console.log(paletteNode.offset())
32143
33496
  if (dropEnabled) {
32144
33497
  mouseX = ui.position.left - paletteWidth + (ui.helper.width()/2) + chart.scrollLeft();
32145
33498
  mouseY = ui.position.top - paletteTop + (ui.helper.height()/2) + chart.scrollTop() + 10;
@@ -32388,11 +33741,24 @@ RED.palette = (function() {
32388
33741
 
32389
33742
  function init() {
32390
33743
 
33744
+ const content = $('<div id="red-ui-palette" class="red-ui-sidebar-tab-content">')
33745
+ const toolbar = $('<div></div>');
33746
+ RED.sidebar.addTab({
33747
+ target: 'secondary',
33748
+ id: "palette",
33749
+ label: "Palette",
33750
+ name: "Palette",
33751
+ icon: "red/images/subflow_tab.svg",
33752
+ content,
33753
+ toolbar,
33754
+ pinned: true,
33755
+ enableOnEdit: true
33756
+ });
33757
+
32391
33758
  $('<img src="red/images/spin.svg" class="red-ui-palette-spinner hide"/>').appendTo("#red-ui-palette");
32392
33759
  $('<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
33760
  $('<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");
33761
+ // $('<div id="red-ui-palette-shade" class="hide"></div>').appendTo("#red-ui-palette");
32396
33762
 
32397
33763
  $("#red-ui-palette > .red-ui-palette-spinner").show();
32398
33764
 
@@ -32451,19 +33817,6 @@ RED.palette = (function() {
32451
33817
  }
32452
33818
  });
32453
33819
 
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
33820
  var userCategories = [];
32468
33821
  if (RED.settings.paletteCategories) {
32469
33822
  userCategories = RED.settings.paletteCategories;
@@ -32485,7 +33838,7 @@ RED.palette = (function() {
32485
33838
  }
32486
33839
  });
32487
33840
 
32488
- var paletteFooterButtons = $('<span class="button-group"></span>').appendTo("#red-ui-palette .red-ui-component-footer");
33841
+ var paletteFooterButtons = $('<span class="button-group"></span>').appendTo(toolbar);
32489
33842
  var paletteCollapseAll = $('<button type="button" class="red-ui-footer-button"><i class="fa fa-angle-double-up"></i></button>').appendTo(paletteFooterButtons);
32490
33843
  paletteCollapseAll.on("click", function(e) {
32491
33844
  e.preventDefault();
@@ -32508,13 +33861,7 @@ RED.palette = (function() {
32508
33861
  });
32509
33862
  RED.popover.tooltip(paletteExpandAll,RED._('palette.actions.expand-all'));
32510
33863
 
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
- });
33864
+
32518
33865
 
32519
33866
  try {
32520
33867
  paletteState = JSON.parse(RED.settings.getLocal("palette-state") || '{"filter":"", "collapsed": []}');
@@ -32532,18 +33879,6 @@ RED.palette = (function() {
32532
33879
  }, 10000)
32533
33880
  }
32534
33881
 
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
33882
  function getCategories() {
32548
33883
  var categories = [];
32549
33884
  $("#red-ui-palette-container .red-ui-palette-category").each(function(i,d) {
@@ -32691,9 +34026,10 @@ RED.sidebar.info = (function() {
32691
34026
 
32692
34027
  RED.sidebar.addTab({
32693
34028
  id: "info",
34029
+ // target: "secondary",
32694
34030
  label: RED._("sidebar.info.label"),
32695
34031
  name: RED._("sidebar.info.name"),
32696
- iconClass: "fa fa-info",
34032
+ icon: "red/images/explorer.svg",
32697
34033
  action:"core:show-info-tab",
32698
34034
  content: content,
32699
34035
  pinned: true,
@@ -32729,6 +34065,8 @@ RED.sidebar.info = (function() {
32729
34065
  tips.stop();
32730
34066
  }
32731
34067
 
34068
+ resizeStack();
34069
+
32732
34070
  }
32733
34071
 
32734
34072
  function show() {
@@ -45677,6 +47015,8 @@ RED.eventLog = (function() {
45677
47015
  }
45678
47016
 
45679
47017
  function handleWindowResize() {
47018
+ let sidebarWidth = $("#red-ui-sidebar").is(":visible") ? $("#red-ui-sidebar").outerWidth() + $("#red-ui-sidebar-separator").outerWidth() : 0;
47019
+ $("#red-ui-editor-stack").css('right', sidebarWidth + $("#red-ui-sidebar-tab-bar").outerWidth() + 1);
45680
47020
  if (stack.length > 0) {
45681
47021
  var tray = stack[stack.length-1];
45682
47022
  if (tray.options.maximized || tray.width > $("#red-ui-editor-stack").position().left-8) {
@@ -48838,6 +50178,12 @@ RED.search = (function() {
48838
50178
  $('<div>',{class:"red-ui-search-result-node-type"}).text(node.type).appendTo(contentDiv);
48839
50179
  $('<div>',{class:"red-ui-search-result-node-id"}).text(node.id).appendTo(contentDiv);
48840
50180
 
50181
+ div.on("mouseover", function(evt) {
50182
+ if ( node.z == RED.workspaces.active() ) {
50183
+ RED.view.reveal(node.id)
50184
+ }
50185
+ });
50186
+
48841
50187
  div.on("click", function(evt) {
48842
50188
  evt.preventDefault();
48843
50189
  currentIndex = i;
@@ -48916,8 +50262,7 @@ RED.search = (function() {
48916
50262
  $("#red-ui-header-shade").show();
48917
50263
  $("#red-ui-editor-shade").show();
48918
50264
  $("#red-ui-palette-shade").show();
48919
- $("#red-ui-sidebar-shade").show();
48920
- $("#red-ui-sidebar-separator").hide();
50265
+ $(".red-ui-sidebar-shade").show();
48921
50266
 
48922
50267
  if (dialog === null) {
48923
50268
  createDialog();
@@ -48941,8 +50286,7 @@ RED.search = (function() {
48941
50286
  $("#red-ui-header-shade").hide();
48942
50287
  $("#red-ui-editor-shade").hide();
48943
50288
  $("#red-ui-palette-shade").hide();
48944
- $("#red-ui-sidebar-shade").hide();
48945
- $("#red-ui-sidebar-separator").show();
50289
+ $(".red-ui-sidebar-shade").hide();
48946
50290
  if (dialog !== null) {
48947
50291
  dialog.slideUp(200,function() {
48948
50292
  searchInput.searchBox('value','');
@@ -49042,7 +50386,7 @@ RED.search = (function() {
49042
50386
  $("#red-ui-header-shade").on('mousedown',hide);
49043
50387
  $("#red-ui-editor-shade").on('mousedown',hide);
49044
50388
  $("#red-ui-palette-shade").on('mousedown',hide);
49045
- $("#red-ui-sidebar-shade").on('mousedown',hide);
50389
+ $(".red-ui-sidebar-shade").on('mousedown',hide);
49046
50390
 
49047
50391
  $("#red-ui-view-searchtools-close").on("click", function close() {
49048
50392
  clearActiveSearch();
@@ -49533,8 +50877,7 @@ RED.actionList = (function() {
49533
50877
  $("#red-ui-header-shade").show();
49534
50878
  $("#red-ui-editor-shade").show();
49535
50879
  $("#red-ui-palette-shade").show();
49536
- $("#red-ui-sidebar-shade").show();
49537
- $("#red-ui-sidebar-separator").hide();
50880
+ $(".red-ui-sidebar-shade").show();
49538
50881
  if (dialog === null) {
49539
50882
  createDialog();
49540
50883
  }
@@ -49568,8 +50911,7 @@ RED.actionList = (function() {
49568
50911
  $("#red-ui-header-shade").hide();
49569
50912
  $("#red-ui-editor-shade").hide();
49570
50913
  $("#red-ui-palette-shade").hide();
49571
- $("#red-ui-sidebar-shade").hide();
49572
- $("#red-ui-sidebar-separator").show();
50914
+ $(".red-ui-sidebar-shade").hide();
49573
50915
  if (dialog !== null) {
49574
50916
  dialog.slideUp(200,function() {
49575
50917
  searchInput.searchBox('value','');
@@ -49601,7 +50943,7 @@ RED.actionList = (function() {
49601
50943
  $("#red-ui-header-shade").on('mousedown',hide);
49602
50944
  $("#red-ui-editor-shade").on('mousedown',hide);
49603
50945
  $("#red-ui-palette-shade").on('mousedown',hide);
49604
- $("#red-ui-sidebar-shade").on('mousedown',hide);
50946
+ $(".red-ui-sidebar-shade").on('mousedown',hide);
49605
50947
  }
49606
50948
 
49607
50949
  return {
@@ -52423,7 +53765,7 @@ RED.userSettings = (function() {
52423
53765
  });
52424
53766
  settingsContent.i18n();
52425
53767
  settingsTabs.activateTab("red-ui-settings-tab-"+(initialTab||'view'))
52426
- $("#red-ui-sidebar-shade").show();
53768
+ $(".red-ui-sidebar-shade").show();
52427
53769
  },
52428
53770
  close: function() {
52429
53771
  settingsVisible = false;
@@ -52432,7 +53774,7 @@ RED.userSettings = (function() {
52432
53774
  pane.close();
52433
53775
  }
52434
53776
  });
52435
- $("#red-ui-sidebar-shade").hide();
53777
+ $(".red-ui-sidebar-shade").hide();
52436
53778
 
52437
53779
  },
52438
53780
  show: function() {}
@@ -55225,7 +56567,7 @@ RED.projects.settings = (function() {
55225
56567
  });
55226
56568
  settingsContent.i18n();
55227
56569
  settingsTabs.activateTab("red-ui-project-settings-tab-"+(initialTab||'main'))
55228
- $("#red-ui-sidebar-shade").show();
56570
+ $(".red-ui-sidebar-shade").show();
55229
56571
  },
55230
56572
  close: function() {
55231
56573
  settingsVisible = false;
@@ -55234,7 +56576,7 @@ RED.projects.settings = (function() {
55234
56576
  pane.close();
55235
56577
  }
55236
56578
  });
55237
- $("#red-ui-sidebar-shade").hide();
56579
+ $(".red-ui-sidebar-shade").hide();
55238
56580
 
55239
56581
  },
55240
56582
  show: function() {}
@@ -59248,10 +60590,15 @@ RED.touch.radialMenu = (function() {
59248
60590
 
59249
60591
  function listTour() {
59250
60592
  return [
60593
+ {
60594
+ id: "5_0",
60595
+ label: "5.0",
60596
+ path: "./tours/welcome.js"
60597
+ },
59251
60598
  {
59252
60599
  id: "4_1",
59253
60600
  label: "4.1",
59254
- path: "./tours/welcome.js"
60601
+ path: "./tours/4.1/welcome.js"
59255
60602
  },
59256
60603
  {
59257
60604
  id: "4_0",