@node-red/editor-client 5.0.0-beta.3 → 5.0.0-beta.4

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
@@ -706,7 +706,7 @@ var RED = (function() {
706
706
 
707
707
  loader.end();
708
708
 
709
- $(".red-ui-header-toolbar").removeClass('hide');
709
+ $("#red-ui-header-toolbar").removeClass('hide');
710
710
  RED.sidebar.show(":first", true);
711
711
 
712
712
  setTimeout(function() {
@@ -941,8 +941,7 @@ var RED = (function() {
941
941
  const logoContainer = $('<div id="red-ui-header-logo"></div>').appendTo(header);
942
942
  let logo = $('<span class="red-ui-header-logo"></span>').appendTo(logoContainer);
943
943
  $('<div id="red-ui-header-tabs" class="hide"></div>').appendTo(header);
944
- $('<ul class="red-ui-header-toolbar hide"></ul>').appendTo(header);
945
- $('<div id="red-ui-header-shade" class="hide"></div>').appendTo(header);
944
+ $('<div id="red-ui-header-toolbar" class="hide"><div id="red-ui-header-shade" class="hide"></div><ul class="red-ui-header-toolbar"></ul></div>').appendTo(header);
946
945
  $('<div id="red-ui-main-container">'+
947
946
  '<div id="red-ui-sidebar-left"></div>'+
948
947
  '<div id="red-ui-workspace"></div>'+
@@ -1384,6 +1383,7 @@ RED.i18n = (function() {
1384
1383
  apiRootUrl = options.apiRootUrl||"";
1385
1384
  var preferredLanguage = localStorage.getItem("editor-language") || detectLanguage();
1386
1385
  var opts = {
1386
+ showSupportNotice: false,
1387
1387
  backend: {
1388
1388
  loadPath: apiRootUrl+'locales/__ns__?lng=__lng__',
1389
1389
  },
@@ -10039,9 +10039,14 @@ RED.utils = (function() {
10039
10039
  window._marked.use({extensions: [descriptionList, description] } );
10040
10040
 
10041
10041
  function renderMarkdown(txt) {
10042
- var rendered = _marked.parse(txt);
10043
- const cleaned = DOMPurify.sanitize(rendered);
10044
- return cleaned;
10042
+ try {
10043
+ var rendered = _marked.parse(txt);
10044
+ const cleaned = DOMPurify.sanitize(rendered);
10045
+ return cleaned;
10046
+ } catch (err) {
10047
+ console.warn(err);
10048
+ return txt
10049
+ }
10045
10050
  }
10046
10051
 
10047
10052
  function formatString(str) {
@@ -11227,6 +11232,9 @@ RED.utils = (function() {
11227
11232
 
11228
11233
  function getNodeColor(type, def) {
11229
11234
  def = def || {};
11235
+ if (type === 'subflow') {
11236
+ return def.color
11237
+ }
11230
11238
  if (!nodeColorCache.hasOwnProperty(type)) {
11231
11239
  const paletteTheme = RED.settings.theme('palette.theme') || [];
11232
11240
  if (paletteTheme.length > 0) {
@@ -12593,12 +12601,12 @@ RED.utils = (function() {
12593
12601
  });
12594
12602
  // $('<span class="red-ui-treeList-icon"><i class="fa fa-folder-o" /></span>').appendTo(label);
12595
12603
  label.on("click.red-ui-treeList-expand", function(e) {
12596
- if (that.options.expandOnLabel !== false || item.expandOnLabel === true) {
12597
- if (container.hasClass("expanded")) {
12598
- if (item.hasOwnProperty('selected') || label.hasClass("selected")) {
12599
- item.treeList.collapse();
12600
- }
12601
- } else {
12604
+ if (container.hasClass("expanded")) {
12605
+ if (item.expandOnLabel === true || label.hasClass("selected")) {
12606
+ item.treeList.collapse();
12607
+ }
12608
+ } else {
12609
+ if (item.expandOnLabel === true || label.hasClass("selected")) {
12602
12610
  item.treeList.expand();
12603
12611
  }
12604
12612
  }
@@ -13078,9 +13086,75 @@ RED.utils = (function() {
13078
13086
  **/
13079
13087
  RED.menu = (function() {
13080
13088
 
13089
+ var currentPortaledSubmenu = null;
13081
13090
  var menuItems = {};
13082
13091
  let menuItemCount = 0
13083
13092
 
13093
+ /**
13094
+ * Position a submenu relative to its parent item, handling viewport collisions.
13095
+ * @param {HTMLElement} parentMenuEl - The parent menu item
13096
+ * @param {HTMLElement} submenuEl - The submenu to position
13097
+ * @param {string} preferredSide - 'left' or 'right'
13098
+ * @returns {{x: number, y: number, placement: string}}
13099
+ */
13100
+ function getSubmenuPosition(parentMenuEl, submenuEl, preferredSide) {
13101
+ var parentMenuRect = parentMenuEl.getBoundingClientRect();
13102
+ var submenuWidth = submenuEl.offsetWidth || 230;
13103
+ var submenuHeight = submenuEl.offsetHeight || 200;
13104
+ var viewportWidth = window.innerWidth;
13105
+ var viewportHeight = window.innerHeight;
13106
+ var padding = 10;
13107
+
13108
+ // Initial position variables
13109
+ var x;
13110
+ var y = parentMenuRect.top;
13111
+
13112
+ // Shift y if it would overflow bottom of viewport
13113
+ if (y + submenuHeight > viewportHeight - padding) {
13114
+ y = viewportHeight - submenuHeight - padding;
13115
+ }
13116
+
13117
+ // Shift y if it would overflow top of viewport
13118
+ if (y < padding) {
13119
+ y = padding;
13120
+ }
13121
+
13122
+ // Calculate x position based on preferred side
13123
+ if (preferredSide === 'left') {
13124
+ x = parentMenuRect.left - submenuWidth;
13125
+ // Flip to right if not enough space on left
13126
+ if (x < padding) {
13127
+ x = parentMenuRect.right;
13128
+ // If still not enough space, shift to fit
13129
+ if (x + submenuWidth > viewportWidth - padding) {
13130
+ x = viewportWidth - submenuWidth - padding;
13131
+ }
13132
+ }
13133
+ } else {
13134
+ x = parentMenuRect.right;
13135
+ // Flip to left if not enough space on right
13136
+ if (x + submenuWidth > viewportWidth - padding) {
13137
+ x = parentMenuRect.left - submenuWidth;
13138
+ // If still not enough space, shift to fit
13139
+ if (x < padding) {
13140
+ x = padding;
13141
+ }
13142
+ }
13143
+ }
13144
+
13145
+ return { x: x, y: y };
13146
+ }
13147
+
13148
+ /**
13149
+ * Clean up the currently portaled submenu (return it to original position)
13150
+ */
13151
+ function cleanupPortaledSubmenus() {
13152
+ if (currentPortaledSubmenu && currentPortaledSubmenu.isPortaled) {
13153
+ currentPortaledSubmenu.cleanUpSubmenu();
13154
+ }
13155
+ currentPortaledSubmenu = null;
13156
+ }
13157
+
13084
13158
  function createMenuItem(opt) {
13085
13159
  var item;
13086
13160
 
@@ -13092,7 +13166,10 @@ RED.menu = (function() {
13092
13166
  }
13093
13167
 
13094
13168
  function setInitialState() {
13095
- var savedStateActive = RED.settings.get("menu-" + opt.id);
13169
+ let savedStateActive;
13170
+ if (!opt.doNotPersist) {
13171
+ savedStateActive = RED.settings.get("menu-" + opt.id);
13172
+ }
13096
13173
  if (opt.setting) {
13097
13174
  // May need to migrate pre-0.17 setting
13098
13175
 
@@ -13125,6 +13202,7 @@ RED.menu = (function() {
13125
13202
  item = $('<li></li>');
13126
13203
  if (!opt.id) {
13127
13204
  opt.id = 'red-ui-menu-item-'+(menuItemCount++)
13205
+ opt.doNotPersist = true;
13128
13206
  }
13129
13207
  if (opt.group) {
13130
13208
  item.addClass("red-ui-menu-group-"+opt.group);
@@ -13192,6 +13270,14 @@ RED.menu = (function() {
13192
13270
  link.on("click", function(event) {
13193
13271
  event.preventDefault();
13194
13272
  });
13273
+ } else {
13274
+ // This is an item with a submenu. Clicking on it should not trigger the
13275
+ // document level click handler that closes the submenu.
13276
+ // This allows touch screen users to reliably open the submenu
13277
+ link.on("click", function (event) {
13278
+ event.preventDefault();
13279
+ event.stopPropagation();
13280
+ })
13195
13281
  }
13196
13282
  if (opt.options) {
13197
13283
  item.addClass("red-ui-menu-dropdown-submenu"+(opt.direction!=='right'?" pull-left":""));
@@ -13209,7 +13295,7 @@ RED.menu = (function() {
13209
13295
  opt.options[i].onpostselect = opt.onpostselect
13210
13296
  }
13211
13297
  opt.options[i].direction = opt.direction
13212
- hasIcons = hasIcons || (opt.options[i].icon);
13298
+ hasIcons = hasIcons || opt.options[i].icon || opt.options[i].toggle;
13213
13299
  hasSubmenus = hasSubmenus || (opt.options[i].options);
13214
13300
  }
13215
13301
 
@@ -13225,6 +13311,138 @@ RED.menu = (function() {
13225
13311
  submenu.addClass("red-ui-menu-dropdown-submenus")
13226
13312
  }
13227
13313
 
13314
+ // Setup submenu portaling for scrollable parent menus
13315
+ (function(item, submenu, direction) {
13316
+ var isPortaled = false;
13317
+ var originalParentItem = item;
13318
+
13319
+ function doesSubmenuNeedPortaling() {
13320
+ var parentMenu = item.closest(".red-ui-menu-dropdown");
13321
+ var overflowY = parentMenu.css("overflow-y");
13322
+ // If scroll, portal it
13323
+ if (overflowY === "auto" || overflowY === "scroll") {
13324
+ return true;
13325
+ }
13326
+
13327
+ var itemRect = item[0].getBoundingClientRect();
13328
+ var submenuWidth = submenu.outerWidth() || 230;
13329
+ var viewportWidth = window.innerWidth;
13330
+ var padding = 10;
13331
+
13332
+ // If right overflow, portal it
13333
+ if (itemRect.right + submenuWidth > viewportWidth - padding) {
13334
+ return true;
13335
+ }
13336
+ return false;
13337
+ }
13338
+
13339
+ function portalSubmenu() {
13340
+ if (!doesSubmenuNeedPortaling()) {
13341
+ return;
13342
+ }
13343
+ if (isPortaled) {
13344
+ updatePositionOfSubmenu();
13345
+ return;
13346
+ }
13347
+
13348
+ isPortaled = true;
13349
+ submenu.appendTo('body');
13350
+ submenu.addClass('red-ui-menu-dropdown-portaled');
13351
+ updatePositionOfSubmenu();
13352
+ }
13353
+
13354
+ function updatePositionOfSubmenu() {
13355
+ if (!isPortaled) {
13356
+ return;
13357
+ }
13358
+ var {x, y} = getSubmenuPosition(item[0], submenu[0], direction);
13359
+ submenu.css({
13360
+ 'top': y + 'px',
13361
+ 'left': x + 'px',
13362
+ maxHeight: ""
13363
+ });
13364
+ const windowHeight = $(window).height();
13365
+ const menuHeight = submenu.outerHeight();
13366
+ const spaceAbove = y;
13367
+ const bottomOverlap = menuHeight - (windowHeight - y);
13368
+ if (bottomOverlap > 0) {
13369
+ if (spaceAbove > bottomOverlap + 5) {
13370
+ // There is room to move the menu up and still show it all
13371
+ submenu.css({
13372
+ top: (menuHeight - bottomOverlap - 5) + "px"
13373
+ })
13374
+ } else {
13375
+ // There is not room for the whole menu.
13376
+ // 1. move it to the top
13377
+ // 2. enable overflow/scrolling
13378
+ submenu.css({
13379
+ top: "5px",
13380
+ maxHeight: (windowHeight - 25) + "px",
13381
+ overflowY: "auto"
13382
+ });
13383
+ }
13384
+ }
13385
+ }
13386
+
13387
+ function cleanUpSubmenu() {
13388
+ if (!isPortaled) {
13389
+ return;
13390
+ }
13391
+ isPortaled = false;
13392
+ submenu.removeClass('red-ui-menu-dropdown-portaled');
13393
+ submenu.appendTo(originalParentItem);
13394
+
13395
+ if (currentPortaledSubmenu && currentPortaledSubmenu.submenu === submenu) {
13396
+ currentPortaledSubmenu = null;
13397
+ }
13398
+ }
13399
+
13400
+ var submenuInfo = {
13401
+ submenu,
13402
+ cleanUpSubmenu
13403
+ };
13404
+
13405
+ Object.defineProperty(submenuInfo, 'isPortaled', {
13406
+ get: function() { return isPortaled; }
13407
+ });
13408
+
13409
+ item.on("mouseenter", function() {
13410
+ if (currentPortaledSubmenu && currentPortaledSubmenu.isPortaled && currentPortaledSubmenu.submenu !== submenu) {
13411
+ currentPortaledSubmenu.cleanUpSubmenu();
13412
+ }
13413
+
13414
+ portalSubmenu();
13415
+
13416
+ if (isPortaled) {
13417
+ currentPortaledSubmenu = submenuInfo;
13418
+ }
13419
+ });
13420
+
13421
+ item.on("mouseleave", function(e) {
13422
+ if (!isPortaled) {
13423
+ return;
13424
+ }
13425
+ // Check if mouse moved to the submenu
13426
+ var related = e.relatedTarget;
13427
+ if (related && (submenu[0].contains(related) || submenu[0] === related)) {
13428
+ return;
13429
+ }
13430
+ cleanUpSubmenu();
13431
+ });
13432
+
13433
+ submenu.on("mouseleave", function(e) {
13434
+ if (!isPortaled) {
13435
+ return;
13436
+ }
13437
+ // Check if mouse moved back to parent item
13438
+ var related = e.relatedTarget;
13439
+ if (related && (item[0].contains(related) || item[0] === related)) {
13440
+ return;
13441
+ }
13442
+ cleanUpSubmenu();
13443
+ });
13444
+
13445
+ })(item, submenu, opt.direction);
13228
13446
 
13229
13447
  }
13230
13448
  if (opt.disabled) {
@@ -13240,10 +13458,11 @@ RED.menu = (function() {
13240
13458
 
13241
13459
  }
13242
13460
  function createMenu(options) {
13243
- var topMenu = $("<ul/>",{class:"red-ui-menu red-ui-menu-dropdown pull-right"});
13461
+ var topMenu = $("<ul/>",{class: "red-ui-menu red-ui-menu-dropdown pull-right"});
13244
13462
  if (options.direction) {
13245
13463
  topMenu.addClass("red-ui-menu-dropdown-direction-"+options.direction)
13246
13464
  }
13465
+
13247
13466
  if (options.id) {
13248
13467
  topMenu.attr({id:options.id+"-submenu"});
13249
13468
  var menuParent = $("#"+options.id);
@@ -13254,15 +13473,31 @@ RED.menu = (function() {
13254
13473
  evt.preventDefault();
13255
13474
  if (topMenu.is(":visible")) {
13256
13475
  $(document).off("click.red-ui-menu");
13476
+ cleanupPortaledSubmenus();
13257
13477
  topMenu.hide();
13478
+ topMenu.css({ maxHeight: "", overflowY: "" });
13258
13479
  } else {
13259
13480
  $(document).on("click.red-ui-menu", function(evt) {
13260
13481
  $(document).off("click.red-ui-menu");
13261
13482
  activeMenu = null;
13483
+ cleanupPortaledSubmenus();
13262
13484
  topMenu.hide();
13485
+ topMenu.css({ maxHeight: "", overflowY: "" });
13263
13486
  });
13264
13487
  $(".red-ui-menu.red-ui-menu-dropdown").hide();
13265
13488
  topMenu.show();
13489
+
13490
+ // Enable scrolling if menu exceeds viewport
13491
+ var menuOffset = topMenu.offset();
13492
+ var menuHeight = topMenu.outerHeight();
13493
+ var windowHeight = $(window).height();
13494
+ var spaceBelow = windowHeight - menuOffset.top;
13495
+ if (menuHeight > spaceBelow - 10) {
13496
+ topMenu.css({
13497
+ maxHeight: (spaceBelow - 10) + "px",
13498
+ overflowY: "auto"
13499
+ });
13500
+ }
13266
13501
  }
13267
13502
  })
13268
13503
  }
@@ -13283,7 +13518,7 @@ RED.menu = (function() {
13283
13518
  opt.direction = options.direction || 'left'
13284
13519
  }
13285
13520
  if (opt !== null || !lastAddedSeparator) {
13286
- hasIcons = hasIcons || (opt && opt.icon);
13521
+ hasIcons = hasIcons || (opt && (opt.icon || opt.toggle));
13287
13522
  hasSubmenus = hasSubmenus || (opt && opt.options);
13288
13523
  var li = createMenuItem(opt);
13289
13524
  if (li) {
@@ -14670,8 +14905,9 @@ RED.tabs = (function() {
14670
14905
  })
14671
14906
  menu.appendTo("body");
14672
14907
  var elementPos = menuButton.offset();
14908
+ var top = elementPos.top + menuButton.height() - 2;
14673
14909
  menu.css({
14674
- top: (elementPos.top+menuButton.height()-2)+"px",
14910
+ top: top + "px",
14675
14911
  left: (elementPos.left - menu.width() + menuButton.width())+"px"
14676
14912
  })
14677
14913
  $(".red-ui-menu.red-ui-menu-dropdown").hide();
@@ -14681,6 +14917,17 @@ RED.tabs = (function() {
14681
14917
  menu.remove();
14682
14918
  });
14683
14919
  menu.show();
14920
+
14921
+ // Enable scrolling if menu exceeds viewport
14922
+ var menuHeight = menu.outerHeight();
14923
+ var windowHeight = $(window).height();
14924
+ var spaceBelow = windowHeight - top;
14925
+ if (menuHeight > spaceBelow - 10) {
14926
+ menu.css({
14927
+ maxHeight: (spaceBelow - 10) + "px",
14928
+ overflowY: "auto"
14929
+ });
14930
+ }
14684
14931
  })
14685
14932
  }
14686
14933
 
@@ -14748,20 +14995,36 @@ RED.tabs = (function() {
14748
14995
  collapsibleMenu.appendTo("body");
14749
14996
  }
14750
14997
  var elementPos = selectButton.offset();
14998
+ var top = elementPos.top + selectButton.height() - 2;
14751
14999
  collapsibleMenu.css({
14752
- top: (elementPos.top+selectButton.height()-2)+"px",
15000
+ top: top + "px",
14753
15001
  left: (elementPos.left - collapsibleMenu.width() + selectButton.width())+"px"
14754
15002
  })
14755
15003
  if (collapsibleMenu.is(":visible")) {
14756
15004
  $(document).off("click.red-ui-tabmenu");
15005
+ collapsibleMenu.css({ maxHeight: "", overflowY: "" });
14757
15006
  } else {
14758
15007
  $(".red-ui-menu.red-ui-menu-dropdown").hide();
14759
15008
  $(document).on("click.red-ui-tabmenu", function(evt) {
14760
15009
  $(document).off("click.red-ui-tabmenu");
14761
15010
  collapsibleMenu.hide();
15011
+ collapsibleMenu.css({ maxHeight: "", overflowY: "" });
14762
15012
  });
14763
15013
  }
14764
15014
  collapsibleMenu.toggle();
15015
+
15016
+ // Enable scrolling if menu exceeds viewport
15017
+ if (collapsibleMenu.is(":visible")) {
15018
+ var menuHeight = collapsibleMenu.outerHeight();
15019
+ var windowHeight = $(window).height();
15020
+ var spaceBelow = windowHeight - top;
15021
+ if (menuHeight > spaceBelow - 10) {
15022
+ collapsibleMenu.css({
15023
+ maxHeight: (spaceBelow - 10) + "px",
15024
+ overflowY: "auto"
15025
+ });
15026
+ }
15027
+ }
14765
15028
  })
14766
15029
  }
14767
15030
 
@@ -22322,6 +22585,9 @@ RED.workspaces = (function() {
22322
22585
  const chartSize = [ $("#red-ui-workspace-scroll-spacer").width(), $("#red-ui-workspace-scroll-spacer").height()];
22323
22586
  const scrollPos = [$("#red-ui-workspace-chart").scrollLeft(), $("#red-ui-workspace-chart").scrollTop()];
22324
22587
  const scrollRatio = [scrollPos[0]/(chartSize[0] - chartWindowSize[0]), scrollPos[1]/(chartSize[1] - chartWindowSize[1]) ];
22588
+ const toolbar = $("#red-ui-workspace-toolbar");
22589
+ const toolbarOffset = toolbar.is(":visible") ? toolbar.outerHeight() : 0;
22590
+ scrollbars.v.bar.css({ top: toolbarOffset + 2 });
22325
22591
  const scrollbarSize = [scrollbars.h.bar.width(), scrollbars.v.bar.height()]
22326
22592
  // Set the height of the handles to be the same ratio of chartWindowSize to chartSize, with a minimum size to ensure they are always draggable
22327
22593
 
@@ -22416,7 +22682,7 @@ RED.workspaces = (function() {
22416
22682
  $("#red-ui-workspace-scroll-h").css({ width: workspaceTargetWidth - 15 })
22417
22683
 
22418
22684
  const paletteWidth = $("#red-ui-sidebar-left").width()
22419
- $("#red-ui-header-logo").width(paletteWidth - 5)
22685
+ $("#red-ui-header-logo").width(paletteWidth - 3)
22420
22686
 
22421
22687
  // const workspacePosition = $("#red-ui-workspace").position()
22422
22688
  // $("#red-ui-header-tabs").css({ left: workspacePosition.left, width: workspaceTargetWidth })
@@ -23853,11 +24119,20 @@ RED.view = (function() {
23853
24119
  // Regular scroll - prevent default and manually handle both axes
23854
24120
  evt.preventDefault();
23855
24121
  evt.stopPropagation();
23856
-
23857
24122
  // Apply scroll deltas directly to both axes
23858
24123
  var deltaX = evt.originalEvent.deltaX;
23859
24124
  var deltaY = evt.originalEvent.deltaY;
23860
24125
 
24126
+ if (evt.shiftKey) {
24127
+ // Shift key pressed - this should cause a scroll on the X-axis
24128
+ // But - some OS/browser combinations don't flip them, so we'll do it ourselves.
24129
+ if (deltaX === 0 && deltaY !== 0) {
24130
+ // swap the deltas
24131
+ deltaX = deltaY
24132
+ deltaY = 0
24133
+ }
24134
+ }
24135
+
23861
24136
  chart.scrollLeft(chart.scrollLeft() + deltaX);
23862
24137
  chart.scrollTop(chart.scrollTop() + deltaY);
23863
24138
 
@@ -24240,10 +24515,11 @@ RED.view = (function() {
24240
24515
  show: function(n) { return !n.valid }
24241
24516
  })
24242
24517
 
24518
+ setScaleFactor(1)
24243
24519
  if (RED.settings.get("editor.view.view-store-zoom")) {
24244
24520
  var userZoomLevel = parseFloat(RED.settings.getLocal('zoom-level'))
24245
24521
  if (!isNaN(userZoomLevel)) {
24246
- scaleFactor = userZoomLevel
24522
+ setScaleFactor(userZoomLevel)
24247
24523
  }
24248
24524
  }
24249
24525
 
@@ -25947,8 +26223,8 @@ RED.view = (function() {
25947
26223
  var boundingWidth = maxX - minX;
25948
26224
  var boundingHeight = maxY - minY;
25949
26225
 
25950
- // Get viewport dimensions
25951
- var viewportWidth = chart.width();
26226
+ // Get viewport dimensions - accounting for the sidebar if visible
26227
+ var viewportWidth = chart.width() - $("#red-ui-sidebar").width();
25952
26228
  var viewportHeight = chart.height();
25953
26229
 
25954
26230
  // Calculate zoom level that fits bounding box in viewport
@@ -26046,7 +26322,7 @@ RED.view = (function() {
26046
26322
  center = [(scrollPos[0] + screenSize[0]/2)/oldScaleFactor, (scrollPos[1] + screenSize[1]/2)/oldScaleFactor];
26047
26323
  }
26048
26324
 
26049
- scaleFactor = factor;
26325
+ setScaleFactor(factor)
26050
26326
 
26051
26327
  // Calculate new scroll position to keep the center point at the same screen position
26052
26328
  if (focalPoint) {
@@ -26132,7 +26408,7 @@ RED.view = (function() {
26132
26408
  interpolateValue: true, // Use exponential interpolation for zoom
26133
26409
  onStep: function(values) {
26134
26410
  var currentFactor = values.zoom;
26135
- scaleFactor = currentFactor;
26411
+ setScaleFactor(currentFactor);
26136
26412
 
26137
26413
  // Calculate new scroll position to maintain focal point
26138
26414
  var currentScreenSize = [chart.width(), chart.height()];
@@ -26170,7 +26446,7 @@ RED.view = (function() {
26170
26446
  onEnd: function() {
26171
26447
  cancelInProgressAnimation = null;
26172
26448
  // Ensure scaleFactor is exactly the target to prevent precision issues
26173
- scaleFactor = targetFactor;
26449
+ setScaleFactor(targetFactor);
26174
26450
  // Full redraw at the end to ensure everything is correct
26175
26451
  _redraw();
26176
26452
  if (RED.settings.get("editor.view.view-store-zoom")) {
@@ -26181,7 +26457,7 @@ RED.view = (function() {
26181
26457
  onCancel: function() {
26182
26458
  cancelInProgressAnimation = null;
26183
26459
  // Ensure scaleFactor is set to current target on cancel
26184
- scaleFactor = targetFactor;
26460
+ setScaleFactor(targetFactor)
26185
26461
  _redraw();
26186
26462
  RED.events.emit("view:navigate");
26187
26463
  }
@@ -26383,6 +26659,9 @@ RED.view = (function() {
26383
26659
  movingSet.clear();
26384
26660
  selectedLinks.clear();
26385
26661
  selectedGroups.clear();
26662
+ $(".red-ui-flow-node-highlighted").removeClass("red-ui-flow-node-highlighted");
26663
+ $(".red-ui-flow-group-highlighted").removeClass("red-ui-flow-group-highlighted");
26664
+ cancelFlash();
26386
26665
  }
26387
26666
 
26388
26667
  var lastSelection = null;
@@ -26981,7 +27260,8 @@ RED.view = (function() {
26981
27260
  clearSuggestedFlow();
26982
27261
  RED.contextMenu.hide();
26983
27262
  evt = evt || d3.event;
26984
- if (evt.button !== 0) {
27263
+
27264
+ if (!evt.touches && evt.button !== 0) {
26985
27265
  return;
26986
27266
  }
26987
27267
  if (mouse_mode === RED.state.SELECTING_NODE) {
@@ -27846,7 +28126,7 @@ RED.view = (function() {
27846
28126
  d3.event.stopPropagation();
27847
28127
  return;
27848
28128
  }
27849
- if (d3.event.button !== 0) {
28129
+ if (!d3.event.touches && d3.event.button !== 0) {
27850
28130
  return
27851
28131
  }
27852
28132
  mousedown_link = d;
@@ -28115,17 +28395,17 @@ RED.view = (function() {
28115
28395
  img.src = iconUrl;
28116
28396
  img.onload = function() {
28117
28397
  if (!iconUrl.match(/\.svg$/)) {
28118
- var largestEdge = Math.max(img.width,img.height);
28119
- var scaleFactor = 1;
28398
+ const largestEdge = Math.max(img.width,img.height);
28399
+ let imgScaleFactor = 1;
28120
28400
  if (largestEdge > 30) {
28121
- scaleFactor = 30/largestEdge;
28401
+ imgScaleFactor = 30/largestEdge;
28122
28402
  }
28123
- var width = img.width * scaleFactor;
28403
+ var width = img.width * imgScaleFactor;
28124
28404
  if (width > 20) {
28125
- scaleFactor *= 20/width;
28405
+ imgScaleFactor *= 20/width;
28126
28406
  width = 20;
28127
28407
  }
28128
- var height = img.height * scaleFactor;
28408
+ var height = img.height * imgScaleFactor;
28129
28409
  icon.attr("width",width);
28130
28410
  icon.attr("height",height);
28131
28411
  icon.attr("x",15-width/2);
@@ -28156,6 +28436,7 @@ RED.view = (function() {
28156
28436
  // - node is disabled
28157
28437
  if (!showStatus || !d.status || d.d === true) {
28158
28438
  nodeEl.__statusGroup__.style.display = "none";
28439
+ d.statusHeight = 0;
28159
28440
  } else {
28160
28441
  nodeEl.__statusGroup__.style.display = "inline";
28161
28442
  let backgroundWidth = 15
@@ -28182,6 +28463,7 @@ RED.view = (function() {
28182
28463
  if (backgroundWidth > 0 && textSize.width > 0) {
28183
28464
  backgroundWidth += 6
28184
28465
  }
28466
+ d.statusHeight = nodeEl.__statusGroup__.getBBox().height
28185
28467
  nodeEl.__statusBackground__.setAttribute('width', backgroundWidth)
28186
28468
  }
28187
28469
  delete d.dirtyStatus;
@@ -28226,7 +28508,7 @@ RED.view = (function() {
28226
28508
  .on("touchstart",nodeTouchStart)
28227
28509
  .on("touchend",nodeTouchEnd)
28228
28510
  nodeContents.appendChild(mainRect);
28229
-
28511
+
28230
28512
  const port_label_group = document.createElementNS("http://www.w3.org/2000/svg","g");
28231
28513
  port_label_group.setAttribute("x",0);
28232
28514
  port_label_group.setAttribute("y",0);
@@ -28640,6 +28922,15 @@ RED.view = (function() {
28640
28922
 
28641
28923
  nodeContents.appendChild(statusEl);
28642
28924
 
28925
+ const nodeHalo = document.createElementNS("http://www.w3.org/2000/svg","rect");
28926
+ nodeHalo.setAttribute("class", "red-ui-flow-node-highlight");
28927
+ nodeHalo.setAttribute("rx", 5);
28928
+ nodeHalo.setAttribute("ry", 5);
28929
+ nodeHalo.setAttribute("x", -10)
28930
+ nodeHalo.setAttribute("y", -10)
28931
+ node[0][0].__halo__ = nodeHalo;
28932
+ nodeContents.appendChild(nodeHalo);
28933
+
28643
28934
  node[0][0].appendChild(nodeContents);
28644
28935
 
28645
28936
  if (!d.__ghost) {
@@ -28707,11 +28998,15 @@ RED.view = (function() {
28707
28998
  // This might be the first redraw after a node has been click-dragged to start a move.
28708
28999
  // So its selected state might have changed since the last redraw.
28709
29000
  this.classList.toggle("red-ui-flow-node-selected", !!d.selected )
29001
+ this.classList.toggle("red-ui-flow-node-highlighted",!!d.highlighted );
28710
29002
  if (mouse_mode != RED.state.MOVING_ACTIVE) {
28711
29003
  this.classList.toggle("red-ui-flow-node-disabled", d.d === true);
28712
29004
  this.__mainRect__.setAttribute("width", d.w)
28713
29005
  this.__mainRect__.setAttribute("height", d.h)
28714
- this.__mainRect__.classList.toggle("red-ui-flow-node-highlighted",!!d.highlighted );
29006
+
29007
+ this.__halo__.setAttribute("width", d.w + 20)
29008
+ this.__halo__.setAttribute("height", d.h + 20 + (d.statusHeight || 0))
29009
+
28715
29010
 
28716
29011
  if (labelParts) {
28717
29012
  // The label has changed
@@ -28932,6 +29227,7 @@ RED.view = (function() {
28932
29227
  });
28933
29228
 
28934
29229
  if (d._def.button) {
29230
+ let buttonVisible = true
28935
29231
  var buttonEnabled = isButtonEnabled(d);
28936
29232
  this.__buttonGroup__.classList.toggle("red-ui-flow-node-button-disabled", !buttonEnabled);
28937
29233
  if (RED.runtime && RED.runtime.started !== undefined) {
@@ -28952,11 +29248,18 @@ RED.view = (function() {
28952
29248
  if (typeof d._def.button.visible === "function") { // is defined and a function...
28953
29249
  if (d._def.button.visible.call(d) === false) {
28954
29250
  this.__buttonGroup__.style.display = "none";
28955
- }
28956
- else {
29251
+ buttonVisible = false
29252
+ } else {
28957
29253
  this.__buttonGroup__.style.display = "inherit";
28958
29254
  }
28959
29255
  }
29256
+ // Need to adjust the halo to encompass the button
29257
+ if (buttonVisible) {
29258
+ this.__halo__.setAttribute("width", d.w + 40)
29259
+ if (d._def.align !== 'right') {
29260
+ this.__halo__.setAttribute("x", -30)
29261
+ }
29262
+ }
28960
29263
  }
28961
29264
  // thisNode.selectAll(".node_badge_group").attr("transform",function(d){return "translate("+(d.w-40)+","+(d.h+3)+")";});
28962
29265
  // thisNode.selectAll("text.node_badge_label").text(function(d,i) {
@@ -29507,6 +29810,11 @@ RED.view = (function() {
29507
29810
  } else {
29508
29811
  selectGroup.classList.remove("red-ui-flow-group-selected")
29509
29812
  }
29813
+ if (d.highlighted) {
29814
+ selectGroup.classList.add("red-ui-flow-group-highlighted")
29815
+ } else {
29816
+ selectGroup.classList.remove("red-ui-flow-group-highlighted")
29817
+ }
29510
29818
  var selectGroupRect = selectGroup.children[0];
29511
29819
  // Background
29512
29820
  selectGroupRect.setAttribute("width",d.w+6)
@@ -29515,19 +29823,12 @@ RED.view = (function() {
29515
29823
  selectGroupRect = selectGroup.children[1];
29516
29824
  selectGroupRect.setAttribute("width",d.w+6)
29517
29825
  selectGroupRect.setAttribute("height",d.h+6)
29518
- selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0;
29826
+ // selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0;
29519
29827
  // Line
29520
29828
  selectGroupRect = selectGroup.children[2];
29521
29829
  selectGroupRect.setAttribute("width",d.w+6)
29522
29830
  selectGroupRect.setAttribute("height",d.h+6)
29523
- selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0;
29524
-
29525
- if (d.highlighted) {
29526
- selectGroup.classList.add("red-ui-flow-node-highlighted");
29527
- } else {
29528
- selectGroup.classList.remove("red-ui-flow-node-highlighted");
29529
- }
29530
-
29831
+ // selectGroupRect.style.strokeOpacity = (d.selected || d.highlighted)?0.8:0;
29531
29832
 
29532
29833
  g.selectAll(".red-ui-flow-group-body")
29533
29834
  .attr("width",d.w)
@@ -30240,33 +30541,28 @@ RED.view = (function() {
30240
30541
  return result;
30241
30542
  }
30242
30543
 
30243
-
30244
- function flashNode(n) {
30245
- let node = n;
30246
- if(typeof node === "string") { node = RED.nodes.node(n); }
30247
- if(!node) { return; }
30248
-
30249
- const flashingNode = flashingNodeId && RED.nodes.node(flashingNodeId);
30250
- if(flashingNode) {
30251
- //cancel current flashing node before flashing new node
30252
- clearInterval(flashingNode.__flashTimer);
30544
+ function cancelFlash() {
30545
+ const flashingNode = flashingNodeId && (RED.nodes.node(flashingNodeId) || RED.nodes.group(flashingNodeId));
30546
+ if (flashingNode) {
30547
+ clearTimeout(flashingNode.__flashTimer);
30253
30548
  delete flashingNode.__flashTimer;
30254
30549
  flashingNode.dirty = true;
30255
30550
  flashingNode.highlighted = false;
30551
+ _redraw()
30256
30552
  }
30257
- node.__flashTimer = setInterval(function(flashEndTime, n) {
30553
+ }
30554
+ function flashNode(n) {
30555
+ let node = n;
30556
+ if (typeof node === "string") { node = RED.nodes.node(n); }
30557
+ if (!node) { return; }
30558
+ cancelFlash()
30559
+ node.__flashTimer = setTimeout(function(n) {
30258
30560
  n.dirty = true;
30259
- if (flashEndTime >= Date.now()) {
30260
- n.highlighted = !n.highlighted;
30261
- } else {
30262
- clearInterval(n.__flashTimer);
30263
- delete n.__flashTimer;
30264
- flashingNodeId = null;
30265
- n.highlighted = false;
30266
- }
30561
+ n.highlighted = false;
30267
30562
  RED.view.redraw();
30268
- }, 100, Date.now() + 2200, node)
30563
+ }, 8100, node)
30269
30564
  flashingNodeId = node.id;
30565
+ node.dirty = true;
30270
30566
  node.highlighted = true;
30271
30567
  RED.view.redraw();
30272
30568
  }
@@ -30583,6 +30879,20 @@ RED.view = (function() {
30583
30879
  }
30584
30880
  }
30585
30881
 
30882
+ function setScaleFactor(sf) {
30883
+ scaleFactor = sf;
30884
+ if (sf <= 0.5) {
30885
+ chart.removeClass('red-ui-workspace-zoom-level-1')
30886
+ chart.addClass('red-ui-workspace-zoom-level-2')
30887
+ } else if (sf <= 0.75) {
30888
+ chart.addClass('red-ui-workspace-zoom-level-1')
30889
+ chart.removeClass('red-ui-workspace-zoom-level-2')
30890
+ } else {
30891
+ chart.removeClass('red-ui-workspace-zoom-level-1')
30892
+ chart.removeClass('red-ui-workspace-zoom-level-2')
30893
+ }
30894
+ }
30895
+
30586
30896
  return {
30587
30897
  init: init,
30588
30898
  state:function(state) {
@@ -30706,7 +31016,7 @@ RED.view = (function() {
30706
31016
  if (node.type === "group" && !node.w && !node.h) {
30707
31017
  _redraw();
30708
31018
  }
30709
- var screenSize = [chart[0].clientWidth/scaleFactor,chart[0].clientHeight/scaleFactor];
31019
+ var screenSize = [(chart[0].clientWidth - $("#red-ui-sidebar").width())/scaleFactor,chart[0].clientHeight/scaleFactor];
30710
31020
  var scrollPos = [chart.scrollLeft()/scaleFactor,chart.scrollTop()/scaleFactor];
30711
31021
  var cx = node.x;
30712
31022
  var cy = node.y;
@@ -33148,6 +33458,9 @@ RED.sidebar = (function() {
33148
33458
 
33149
33459
  if (options.toolbar) {
33150
33460
  targetSidebar.sections[targetSection].footer.append(options.toolbar);
33461
+ if (!options.enableOnEdit) {
33462
+ $('<div>',{class:"red-ui-sidebar-shade hide"}).appendTo(options.toolbar);
33463
+ }
33151
33464
  $(options.toolbar).hide();
33152
33465
  }
33153
33466
  var id = options.id;
@@ -33263,6 +33576,54 @@ RED.sidebar = (function() {
33263
33576
 
33264
33577
  let draggingTabButton = false
33265
33578
 
33579
+
33580
+ function getSidebarOptions(sidebar, targetSection) {
33581
+ const menuOptions = []
33582
+ const active = sidebar.tabBars[targetSection].active
33583
+ sidebar.tabBars[targetSection].container.find('button:not(.red-ui-sidebar-tab-bar-overflow-button)').each(function () {
33584
+ const tabId = $(this).attr('data-tab-id')
33585
+ const tabOptions = knownTabs[tabId]
33586
+ let initialStateSet = false
33587
+ menuOptions.push({
33588
+ label: tabOptions.name,
33589
+ selected: tabId === active,
33590
+ toggle: true,
33591
+ onselect: function(state) {
33592
+ if (!initialStateSet) {
33593
+ // This is called when showing the menu to initialise the state properly. This made
33594
+ // sense for other menus, but is inconvenient here. So we swallow the first call to onselect
33595
+ // without taking any action.
33596
+ initialStateSet = true
33597
+ return
33598
+ }
33599
+ if (state) {
33600
+ RED.sidebar.show(tabId)
33601
+ } else {
33602
+ // DRY: overlapping logic with the mouseup handler on the inidividual tab buttons
33603
+ if (!sidebar.sections[targetSection].hidden) {
33604
+ const otherSectionHidden = sidebar.sections[targetSection === 'top' ? 'bottom' : 'top'].hidden
33605
+ if (otherSectionHidden) {
33606
+ // Both sections are going to be hidden, so hide the sidebar first.
33607
+ // We do this *before* hiding the last section so that we remember which the 'last' section was and it can be
33608
+ // restored when the sidebar is shown again.
33609
+ RED.menu.setSelected(sidebar.menuToggle, false);
33610
+ } else {
33611
+ // Hiding just one section, clear its active setting
33612
+ sidebar.tabBars[targetSection].active = null
33613
+ }
33614
+ sidebar.hideSection(targetSection)
33615
+ } else {
33616
+ sidebar.showSection(targetSection)
33617
+ }
33618
+ exportSidebarState()
33619
+ }
33620
+
33621
+ }
33622
+ })
33623
+ })
33624
+ return menuOptions
33625
+ }
33626
+
33266
33627
  function setupSidebarTabs(sidebar) {
33267
33628
  const tabBar = $('<div class="red-ui-sidebar-tab-bar"></div>').addClass('red-ui-sidebar-' + sidebar.direction);
33268
33629
  if (sidebar.direction === 'right') {
@@ -33286,6 +33647,48 @@ RED.sidebar = (function() {
33286
33647
  // toggleSidebarButton.prependTo(tabBar);
33287
33648
  // }
33288
33649
 
33650
+ sidebar.tabOverflowButton = $('<button class="red-ui-sidebar-tab-bar-overflow-button"><i class="fa fa-ellipsis-h"></i></button>');
33651
+ sidebar.tabOverflowButton.hide()
33652
+ sidebar.tabOverflowButton.on('click', function(evt) {
33653
+ try {
33654
+ const menuOptions = getSidebarOptions(sidebar, 'top')
33655
+ const bottomMenuOptions = getSidebarOptions(sidebar, 'bottom')
33656
+ if (menuOptions.length > 0 && bottomMenuOptions.length > 0) {
33657
+ menuOptions.push(null) // separator
33658
+ }
33659
+ menuOptions.push(...bottomMenuOptions)
33660
+ if (menuOptions.length === 0) {
33661
+ return
33662
+ }
33663
+ const menu = RED.menu.init({ options: menuOptions });
33664
+ menu.attr("id",sidebar.container.attr('id')+"-menu");
33665
+ menu.css({
33666
+ position: "absolute"
33667
+ })
33668
+ menu.appendTo("body");
33669
+ var elementPos = sidebar.tabOverflowButton.offset();
33670
+ menu.css({
33671
+ top: (elementPos.top+sidebar.tabOverflowButton.height()- menu.height() - 10)+"px",
33672
+ left: sidebar.direction === 'left' ? ((elementPos.left + sidebar.tabOverflowButton.width() + 3)+"px") : ((elementPos.left - menu.width() - 3)+"px")
33673
+ })
33674
+ $(".red-ui-menu.red-ui-menu-dropdown").hide();
33675
+ setTimeout(() => {
33676
+ $(document).on("click.red-ui-sidebar-tabmenu", function(evt) {
33677
+ $(document).off("click.red-ui-sidebar-tabmenu");
33678
+ menu.remove();
33679
+ });
33680
+ }, 0)
33681
+ menu.show();
33682
+ } catch (err) {
33683
+ console.log(err)
33684
+ }
33685
+ })
33686
+
33687
+ if (sidebar.direction === 'right') {
33688
+ sidebar.tabOverflowButton.appendTo(tabBar);
33689
+ } else if (sidebar.direction === 'left') {
33690
+ sidebar.tabOverflowButton.prependTo(tabBar);
33691
+ }
33289
33692
 
33290
33693
 
33291
33694
  sidebar.tabBar = tabBar // sidebar.tabBars.top.container;
@@ -33324,51 +33727,7 @@ RED.sidebar = (function() {
33324
33727
  }
33325
33728
 
33326
33729
  function setupTabSection(sidebar, tabBar, position) {
33327
- const tabBarButtonsContainer = $('<div class="red-ui-sidebar-tab-bar-buttons"></div>').appendTo(tabBar);
33328
- const tabOverflowButton = $('<button class="red-ui-sidebar-tab-bar-overflow-button"><i class="fa fa-ellipsis-h"></i></button>').appendTo(tabBarButtonsContainer);
33329
- tabOverflowButton.hide()
33330
- tabOverflowButton.on('click', function(evt) {
33331
- try {
33332
- const menuOptions = []
33333
- tabBarButtonsContainer.find('button:not(.red-ui-sidebar-tab-bar-overflow-button)').each(function () {
33334
- if ($(this).is(':visible')) {
33335
- return
33336
- }
33337
- const tabId = $(this).attr('data-tab-id')
33338
- const tabOptions = knownTabs[tabId]
33339
- menuOptions.push({
33340
- label: tabOptions.name,
33341
- onselect: function() {
33342
- RED.sidebar.show(tabId)
33343
- }
33344
- })
33345
- })
33346
- if (menuOptions.length === 0) {
33347
- return
33348
- }
33349
- const menu = RED.menu.init({ options: menuOptions });
33350
- menu.attr("id",sidebar.container.attr('id')+"-menu");
33351
- menu.css({
33352
- position: "absolute"
33353
- })
33354
- menu.appendTo("body");
33355
- var elementPos = tabOverflowButton.offset();
33356
- menu.css({
33357
- top: (elementPos.top+tabOverflowButton.height()- menu.height() - 10)+"px",
33358
- left: sidebar.direction === 'left' ? ((elementPos.left + tabOverflowButton.width() + 3)+"px") : ((elementPos.left - menu.width() - 3)+"px")
33359
- })
33360
- $(".red-ui-menu.red-ui-menu-dropdown").hide();
33361
- setTimeout(() => {
33362
- $(document).on("click.red-ui-sidebar-tabmenu", function(evt) {
33363
- $(document).off("click.red-ui-sidebar-tabmenu");
33364
- menu.remove();
33365
- });
33366
- }, 0)
33367
- menu.show();
33368
- } catch (err) {
33369
- console.log(err)
33370
- }
33371
- })
33730
+ const tabBarButtonsContainer = $('<div class="red-ui-sidebar-tab-bar-buttons red-ui-sidebar-tab-bar-empty"></div>').appendTo(tabBar);
33372
33731
  tabBarButtonsContainer.data('sidebar', sidebar.id)
33373
33732
  tabBarButtonsContainer.data('sidebar-position', position)
33374
33733
  tabBarButtonsContainer.sortable({
@@ -33418,8 +33777,9 @@ RED.sidebar = (function() {
33418
33777
  return {
33419
33778
  container: tabBarButtonsContainer,
33420
33779
  addButton: function(button, position) {
33780
+ tabBarButtonsContainer.removeClass('red-ui-sidebar-tab-bar-empty');
33421
33781
  if (position === undefined || position >= tabBarButtonsContainer.children().length) {
33422
- button.insertBefore(tabOverflowButton);
33782
+ button.appendTo(tabBarButtonsContainer);
33423
33783
  } else {
33424
33784
  button.insertBefore(tabBarButtonsContainer.children().eq(position));
33425
33785
  }
@@ -33492,7 +33852,9 @@ RED.sidebar = (function() {
33492
33852
  }
33493
33853
  sidebar.container.width(newSidebarWidth);
33494
33854
  ui.position.left -= scaleFactor * d
33495
- sidebar.tabBar.css('min-width', sidebar.container.width() - 5)
33855
+ if (sidebar.direction === 'left') {
33856
+ sidebar.tabBar.css('min-width', sidebar.container.width() - 5)
33857
+ }
33496
33858
  RED.events.emit("sidebar:resize");
33497
33859
  },
33498
33860
  stop:function(event,ui) {
@@ -33508,7 +33870,9 @@ RED.sidebar = (function() {
33508
33870
  } else {
33509
33871
  sidebar.width = sidebar.container.width();
33510
33872
  }
33511
- sidebar.tabBar.css('min-width', sidebar.container.width() - 5)
33873
+ if (sidebar.direction === 'left') {
33874
+ sidebar.tabBar.css('min-width', sidebar.container.width() - 5)
33875
+ }
33512
33876
  RED.events.emit("sidebar:resize");
33513
33877
  }
33514
33878
  });
@@ -33552,7 +33916,9 @@ RED.sidebar = (function() {
33552
33916
  }
33553
33917
  }
33554
33918
  sidebar.container.width(sidebar.width || sidebar.defaultWidth)
33555
- sidebar.tabBar.css('min-width', sidebar.container.width() - 5)
33919
+ if (sidebar.direction === 'left') {
33920
+ sidebar.tabBar.css('min-width', sidebar.container.width() - 5)
33921
+ }
33556
33922
  sidebar.separator.show()
33557
33923
  if (sidebar.tabBars.top.active && !sidebar.sections.top.hidden) {
33558
33924
  sidebar.tabBars.top.container.find('button[data-tab-id="'+sidebar.tabBars.top.active+'"]').addClass('selected')
@@ -33624,8 +33990,8 @@ RED.sidebar = (function() {
33624
33990
  const sidebarHeight = $("#red-ui-main-container").height();
33625
33991
  sidebar.container.addClass("red-ui-sidebar").addClass('red-ui-sidebar-' + sidebar.direction);
33626
33992
  sidebar.container.width(sidebar.defaultWidth);
33627
- if (sidebar.direction === 'right') {
33628
- $('<div>',{class:"red-ui-sidebar-shade hide"}).css("z-index", 0).appendTo(sidebar.container);
33993
+ if (sidebar.direction === 'left') {
33994
+ // $('<div>',{class:"red-ui-sidebar-shade hide"}).css("z-index", 0).appendTo(sidebar.container);
33629
33995
  }
33630
33996
  sidebar.sections = {};
33631
33997
  sidebar.sections.top = {}
@@ -33689,9 +34055,37 @@ RED.sidebar = (function() {
33689
34055
  }
33690
34056
  }
33691
34057
  // Trigger a resize of the tab bars to handle overflow
33692
- sidebar.tabBar.css('min-width', sidebar.container.width() - 5)
34058
+ if (sidebar.direction === 'left') {
34059
+ sidebar.tabBar.css('min-width', sidebar.container.width() - 5)
34060
+ }
33693
34061
  RED.events.emit("sidebar:resize");
33694
34062
 
34063
+ if (sidebar.direction === 'right') {
34064
+ const windowWidth = $("#red-ui-editor").width();
34065
+ if (sidebar.tabOverflowButtonShown) {
34066
+ // Check if there's space to show the tab bar again
34067
+ if ($(window).width() - (sidebars.secondary.tabBar.width() + sidebar.tabOverflowButtonStatusBarWidth + sidebar.tabOverflowButtonSpace) > 0) {
34068
+ sidebar.tabOverflowButtonShown = false
34069
+ sidebar.tabOverflowButton.hide()
34070
+ sidebar.tabBars.top.container.show()
34071
+ sidebar.tabBars.bottom.container.show()
34072
+
34073
+ }
34074
+ }
34075
+ if (!sidebar.tabOverflowButtonShown) {
34076
+ const tabBarWidth = sidebar.tabBar.width()
34077
+ const tabBarPosition = sidebar.tabBar.position()
34078
+ if (tabBarWidth + tabBarPosition.left > windowWidth) {
34079
+ sidebar.tabOverflowButtonStatusBarWidth = $("#red-ui-statusbar").width()
34080
+ sidebar.tabOverflowButtonSpace = tabBarWidth
34081
+ sidebar.tabOverflowButtonShown = true
34082
+ sidebar.tabOverflowButton.show()
34083
+ sidebar.tabBars.top.container.hide()
34084
+ sidebar.tabBars.bottom.container.hide()
34085
+ }
34086
+ }
34087
+ }
34088
+
33695
34089
  }
33696
34090
  $(window).on("resize", sidebar.resizeSidebar)
33697
34091
 
@@ -35381,10 +35775,10 @@ RED.sidebar.info = (function() {
35381
35775
 
35382
35776
  function build() {
35383
35777
  var container = $("<div>", {class:"red-ui-info-outline"}).css({'height': '100%'});
35384
- var toolbar = $("<div>", {class:"red-ui-sidebar-header red-ui-info-toolbar"}).appendTo(container);
35778
+ var toolbar = $("<div>", {class:"red-ui-info-toolbar red-ui-palette-search"}).appendTo(container);
35385
35779
 
35386
35780
  searchInput = $('<input type="text" data-i18n="[placeholder]menu.label.search">').appendTo(toolbar).searchBox({
35387
- style: "compact",
35781
+ // style: "compact",
35388
35782
  delay: 500,
35389
35783
  change: function() {
35390
35784
  var val = $(this).val();
@@ -35439,8 +35833,6 @@ RED.sidebar.info = (function() {
35439
35833
  // } else {
35440
35834
  // // RED.view.select({nodes:[]})
35441
35835
  // }
35442
- } else {
35443
- RED.sidebar.info.refresh(null);
35444
35836
  }
35445
35837
  })
35446
35838
  treeList.on('treelistconfirm', function(e,item) {
@@ -35738,6 +36130,7 @@ RED.sidebar.info = (function() {
35738
36130
  config: true,
35739
36131
  flow: parent,
35740
36132
  types: {},
36133
+ expandOnLabel: true,
35741
36134
  label: RED._("menu.label.displayConfig"),
35742
36135
  children: []
35743
36136
  }
@@ -35748,6 +36141,7 @@ RED.sidebar.info = (function() {
35748
36141
  configNodeTypes[parent].types[type] = {
35749
36142
  config: true,
35750
36143
  label: type,
36144
+ expandOnLabel: true,
35751
36145
  children: []
35752
36146
  }
35753
36147
  configNodeTypes[parent].treeList.addChild(configNodeTypes[parent].types[type]);
@@ -36788,24 +37182,16 @@ RED.sidebar.config = (function() {
36788
37182
  //cancel current flashing node before flashing new node
36789
37183
  clearInterval(flashingConfigNodeTimer);
36790
37184
  flashingConfigNodeTimer = null;
36791
- flashingConfigNode.children("div").removeClass('highlighted');
37185
+ flashingConfigNode.children("div").removeClass('red-ui-flow-node-highlighted');
36792
37186
  flashingConfigNode = null;
36793
37187
  }
36794
37188
  if(!el || !el.children("div").length) { return; }
36795
37189
 
36796
- flashingConfigNodeTimer = setInterval(function(flashEndTime) {
36797
- if (flashEndTime >= Date.now()) {
36798
- const highlighted = el.children("div").hasClass("highlighted");
36799
- el.children("div").toggleClass('highlighted', !highlighted)
36800
- } else {
36801
- clearInterval(flashingConfigNodeTimer);
36802
- flashingConfigNodeTimer = null;
36803
- flashingConfigNode = null;
36804
- el.children("div").removeClass('highlighted');
36805
- }
36806
- }, 100, Date.now() + 2200);
37190
+ flashingConfigNodeTimer = setTimeout(function() {
37191
+ flashingConfigNode.children("div").removeClass('red-ui-flow-node-highlighted');
37192
+ }, 8100)
36807
37193
  flashingConfigNode = el;
36808
- el.children("div").addClass('highlighted');
37194
+ flashingConfigNode.children("div").addClass('red-ui-flow-node-highlighted');
36809
37195
  }
36810
37196
 
36811
37197
  function show(id) {
@@ -36880,8 +37266,6 @@ RED.sidebar.context = (function() {
36880
37266
  content = $("<div>").css({"position":"relative","height":"100%"});
36881
37267
  content.className = "red-ui-sidebar-context"
36882
37268
 
36883
- var footerToolbar = $('<div></div>');
36884
-
36885
37269
  var stackContainer = $("<div>",{class:"red-ui-sidebar-context-stack"}).appendTo(content);
36886
37270
  sections = RED.stack.create({
36887
37271
  container: stackContainer
@@ -36978,7 +37362,6 @@ RED.sidebar.context = (function() {
36978
37362
  name: RED._("sidebar.context.name"),
36979
37363
  iconClass: "fa fa-database",
36980
37364
  content: content,
36981
- toolbar: footerToolbar,
36982
37365
  // pinned: true,
36983
37366
  enableOnEdit: true,
36984
37367
  action: "core:show-context-tab"
@@ -41445,7 +41828,6 @@ RED.editor = (function() {
41445
41828
  }
41446
41829
  return {
41447
41830
  init: function() {
41448
- if(window.ace) { window.ace.config.set('basePath', 'vendor/ace'); }
41449
41831
  RED.tray.init();
41450
41832
  RED.actions.add("core:confirm-edit-tray", function() {
41451
41833
  $(document.activeElement).blur();
@@ -41457,7 +41839,6 @@ RED.editor = (function() {
41457
41839
  $("#node-dialog-cancel").trigger("click");
41458
41840
  $("#node-config-dialog-cancel").trigger("click");
41459
41841
  });
41460
- RED.editor.codeEditor.init();
41461
41842
  },
41462
41843
  generateViewStateId: generateViewStateId,
41463
41844
  edit: showEditDialog,
@@ -42921,7 +43302,7 @@ RED.editor = (function() {
42921
43302
  * @namespace RED.editor.codeEditor
42922
43303
  */
42923
43304
  RED.editor.codeEditor = (function() {
42924
-
43305
+ const BASIC = "basic";
42925
43306
  const MONACO = "monaco";
42926
43307
  const ACE = "ace";
42927
43308
  const defaultEditor = MONACO;
@@ -42931,22 +43312,30 @@ RED.editor = (function() {
42931
43312
 
42932
43313
  function init() {
42933
43314
  var codeEditorSettings = RED.editor.codeEditor.settings;
42934
- var editorChoice = codeEditorSettings.lib === MONACO ? MONACO : ACE;
43315
+ var editorChoice = codeEditorSettings.lib || defaultEditor;
43316
+ if (![MONACO, ACE, BASIC].includes(editorChoice)) {
43317
+ console.warn("Invalid code editor specified in settings - '" + editorChoice + "'. Falling back to basic editor.")
43318
+ editorChoice = BASIC;
43319
+ }
42935
43320
  try {
42936
- var browser = RED.utils.getBrowserInfo();
42937
43321
  selectedCodeEditor = RED.editor.codeEditor[editorChoice];
42938
- //fall back to default code editor if there are any issues
42939
- if (!selectedCodeEditor || (editorChoice === MONACO && (browser.ie || !window.monaco))) {
42940
- selectedCodeEditor = RED.editor.codeEditor[defaultEditor];
43322
+ // On first uncached load, editor may not yet be downloaded and in the DOM, so we check for the presence in `window` and defer
43323
+ // initialization until the editor is actually loaded. Not relevant for basic editor as it is always present.
43324
+ if (editorChoice === MONACO && !window.monaco) {
43325
+ console.debug("Monaco editor is not yet loaded, delaying initialization");
43326
+ return
43327
+ } else if (editorChoice === ACE && !window.ace) {
43328
+ console.debug("ACE editor is not yet loaded, delaying initialization");
43329
+ return
42941
43330
  }
42942
- initialised = selectedCodeEditor.init();
43331
+ initialised = selectedCodeEditor && selectedCodeEditor.init();
42943
43332
  } catch (error) {
42944
43333
  selectedCodeEditor = null;
42945
43334
  console.warn("Problem initialising '" + editorChoice + "' code editor", error);
42946
43335
  }
42947
43336
  if(!initialised) {
42948
43337
  selectedCodeEditor = RED.editor.codeEditor[defaultEditor];
42949
- initialised = selectedCodeEditor.init();
43338
+ initialised = selectedCodeEditor && selectedCodeEditor.init();
42950
43339
  }
42951
43340
 
42952
43341
  $('<div id="red-ui-drop-target-markdown-editor"><div><i class="fa fa-download"></i><br></div></div>').appendTo('#red-ui-editor');
@@ -42954,16 +43343,13 @@ RED.editor = (function() {
42954
43343
  }
42955
43344
 
42956
43345
  function create(options) {
42957
- //TODO: (quandry - for consideration)
42958
- // Below, I had to create a hidden element if options.id || options.element is not in the DOM
42959
- // I have seen 1 node calling `this.editor = RED.editor.createEditor()` with an
42960
- // invalid (non existing html element selector) (e.g. node-red-contrib-components does this)
42961
- // This causes monaco to throw an error when attempting to hook up its events to the dom & the rest of the 'oneditperapre'
42962
- // code is thus skipped.
42963
- // In ACE mode, creating an ACE editor (with an invalid ID) allows the editor to be created (but obviously there is no UI)
42964
- // Because one (or more) contrib nodes have left this bad code in place, how would we handle this?
42965
- // For compatibility, I have decided to create a hidden element so that at least an editor is created & errors do not occur.
42966
- // IMO, we should warn and exit as it is a coding error by the contrib author.
43346
+ if(!initialised) {
43347
+ RED.editor.codeEditor.init(); // try to initialize again in case ace/monaco have loaded since the first attempt
43348
+ if(!initialised) {
43349
+ console.debug("Preferred code editor is not initialised, falling back to basic code editor until it is loaded");
43350
+ return RED.editor.codeEditor.basic.create(options); // fallback to basic editor
43351
+ }
43352
+ }
42967
43353
 
42968
43354
  if (!options) {
42969
43355
  console.warn("createEditor() options are missing");
@@ -45663,6 +46049,7 @@ RED.editor.codeEditor.ace = (function() {
45663
46049
  var initOptions = {};
45664
46050
 
45665
46051
  function init(options) {
46052
+ window.ace.config.set('basePath', 'vendor/ace');
45666
46053
  initOptions = options || {};
45667
46054
  initialised = true;
45668
46055
  return initialised;
@@ -45843,7 +46230,404 @@ RED.editor.codeEditor.ace = (function() {
45843
46230
  */
45844
46231
  create: create
45845
46232
  }
45846
- })();;/*
46233
+ })();;
46234
+ /**
46235
+ * @namespace RED.editor.codeEditor.basic
46236
+ */
46237
+
46238
+ /*
46239
+ The code editor currenlty supports 2 functions init and create.
46240
+ * Init() - setup the editor / must return true
46241
+ * Create() - create an editor instance / returns an editor as generated by the editor lib
46242
+ * To be compatable with the original ace lib (for contrib nodes using it), the object returned by create() must (at minimum) support the following...
46243
+ property .selection = {};
46244
+ function .selection.getRange();
46245
+ property .session //the editor object
46246
+ function .session.insert = function(position, text)
46247
+ function .setReadOnly(readOnly)
46248
+ property .renderer = {};
46249
+ function .renderer.updateFull()
46250
+ function setMode(mode, cb)
46251
+ function getRange();
46252
+ function replace(range, text)
46253
+ function selectAll
46254
+ function clearSelection
46255
+ function getSelectedText()
46256
+ function destroy()
46257
+ function resize()
46258
+ function getSession()
46259
+ function getLength()
46260
+ function scrollToLine(lineNumber, scrollType)
46261
+ function moveCursorTo(lineNumber, colNumber)
46262
+ function getAnnotations()
46263
+ function gotoLine(row, col)
46264
+ function getCursorPosition()
46265
+ function setTheme(name)
46266
+ function setFontSize(size:Number) //Set a new font size (in pixels) for the editor text.
46267
+ function on(name, cb)
46268
+ function getUndoManager()
46269
+
46270
+ */
46271
+
46272
+ RED.editor.codeEditor.basic = (function() {
46273
+ var initialised = false;
46274
+ const type = "basic";
46275
+
46276
+
46277
+ function init(options) {
46278
+ options = options || {};
46279
+ initialised = true;
46280
+ return initialised;
46281
+ }
46282
+
46283
+ function create(options) {
46284
+ const editorSettings = RED.editor.codeEditor.settings || {};
46285
+ if(!options.stateId && options.stateId !== false) {
46286
+ options.stateId = RED.editor.generateViewStateId("basic", options, (options.mode || options.title || "").split("/").pop());
46287
+ }
46288
+ let el = options.element || $("#"+options.id)[0];
46289
+ const toolbarRow = $("<div>").appendTo(el);
46290
+ el = $("<div>").appendTo(el).addClass("red-ui-editor-text-container")[0];
46291
+ const textarea = $("<textarea>").appendTo(el)
46292
+ textarea.attr("id", options.id + "_editor");
46293
+ textarea.css({ width: "100%", height: "100%" });
46294
+ textarea.addClass("red-ui-editor-text-basic");
46295
+
46296
+ /** @type {HTMLTextAreaElement} */
46297
+ const textareaEl = textarea[0];
46298
+ const editorOptions = $.extend({}, editorSettings.options, options);
46299
+ editorOptions.language = options.mode;
46300
+ editorOptions.value = options.value || "";
46301
+ textarea.val(editorOptions.value);
46302
+
46303
+ /*********** Create the basic editor ***************/
46304
+ const ed = {
46305
+ selection: {},
46306
+ session: {},
46307
+ renderer: {},
46308
+ }
46309
+
46310
+ // #region "ACE compatability"
46311
+ ed.setMode = function(mode, cb) {
46312
+ editorOptions.language = mode;
46313
+ if (cb && typeof cb == "function") {
46314
+ cb();
46315
+ }
46316
+ }
46317
+
46318
+ ed.getRange = function getRange(){
46319
+ const value = textareaEl.value;
46320
+ const range = { start: { row: 0, column: 0 }, end: { row: 0, column: 0 } };
46321
+ range.start.row = value.substring(0, textareaEl.selectionStart).split("\n").length - 1;
46322
+ range.start.column = textareaEl.selectionStart - value.lastIndexOf("\n", textareaEl.selectionStart - 1) - 1;
46323
+ range.end.row = value.substring(0, textareaEl.selectionEnd).split("\n").length - 1;
46324
+ range.end.column = textareaEl.selectionEnd - value.lastIndexOf("\n", textareaEl.selectionEnd - 1) - 1;
46325
+ return range;
46326
+ }
46327
+
46328
+ ed.selection.getRange = ed.getRange;
46329
+
46330
+ ed.session.insert = function insert(position, text) {
46331
+ //position is zero based
46332
+ const col = position.column || 0;
46333
+ const row = position.row || 0;
46334
+ // calculate selectionStart and selectionEnd based on row and column
46335
+ const value = textareaEl.value;
46336
+ const lines = value.split("\n");
46337
+ let selectionStart = 0;
46338
+ for (let i = 0; i < row; i++) {
46339
+ selectionStart += lines[i].length + 1; // +1 for the newline character
46340
+ }
46341
+ selectionStart += col;
46342
+ const selectionEnd = selectionStart;
46343
+ textareaEl.focus();
46344
+ textareaEl.setSelectionRange(selectionStart, selectionEnd);
46345
+ textareaEl.setRangeText(text)
46346
+ }
46347
+
46348
+ ed.setReadOnly = function setReadOnly(readOnly) {
46349
+ textareaEl.readOnly = readOnly;
46350
+ }
46351
+
46352
+ ed.session.replace = function replace(range, text) {
46353
+ const value = textareaEl.value;
46354
+ const lines = value.split("\n");
46355
+ let selectionStart = 0;
46356
+ for (let i = 0; i < (range.start.row || 0); i++) {
46357
+ selectionStart += lines[i].length + 1; // +1 for the newline character
46358
+ }
46359
+ selectionStart += (range.start.column || 0);
46360
+ let selectionEnd = 0;
46361
+ for (let i = 0; i < (range.end.row || 0); i++) {
46362
+ selectionEnd += lines[i].length + 1; // +1 for the newline character
46363
+ }
46364
+ selectionEnd += (range.end.column || 0);
46365
+ textareaEl.focus();
46366
+ textareaEl.setSelectionRange(selectionStart, selectionEnd);
46367
+ textareaEl.setRangeText(text)
46368
+ }
46369
+
46370
+ ed.getValue = function getValue() {
46371
+ return textareaEl.value;
46372
+ }
46373
+
46374
+ ed.setValue = function setValue(value, cursorPos) {
46375
+ textareaEl.value = value;
46376
+ if (cursorPos === -1) {
46377
+ ed.clearSelection();
46378
+ } else if (cursorPos) {
46379
+ textareaEl.setSelectionRange(cursorPos, cursorPos);
46380
+ }
46381
+ }
46382
+
46383
+ ed.renderer.updateFull = function updateFull() {
46384
+ // no need to do anything as the textarea will handle this
46385
+ }
46386
+
46387
+ ed.selectAll = function selectAll() {
46388
+ textareaEl.focus();
46389
+ textareaEl.select();
46390
+ }
46391
+
46392
+ ed.clearSelection = function clearSelection() {
46393
+ textareaEl.focus();
46394
+ const selectionStart = textareaEl.selectionDirection == "backward" ? textareaEl.selectionEnd : textareaEl.selectionStart;
46395
+ textareaEl.setSelectionRange(selectionStart, selectionStart);
46396
+ }
46397
+
46398
+ ed.getSelectedText = function getSelectedText() {
46399
+ return textareaEl.value.substring(textareaEl.selectionStart, textareaEl.selectionEnd);
46400
+ }
46401
+
46402
+ ed.on = function (name, cb) {
46403
+ textareaEl.addEventListener(name, cb)
46404
+ }
46405
+
46406
+ ed.off = function (name, cb) {
46407
+ textarea.on(name, cb)
46408
+ }
46409
+
46410
+ ed.destroy = function destroy() {
46411
+ try {
46412
+ ed.saveView();
46413
+ ed._initState = null;
46414
+ textarea.off()
46415
+ } catch (e) { }
46416
+ $(textareaEl).remove();
46417
+ $(toolbarRow).remove();
46418
+ }
46419
+ ed.on("blur", function () {
46420
+ ed.focusMemory = false;
46421
+ ed.saveView();
46422
+ })
46423
+ ed.on("focus", function () {
46424
+ if (ed._initState) {
46425
+ ed.restoreView(ed._initState);
46426
+ ed._initState = null;
46427
+ }
46428
+ })
46429
+
46430
+ ed.focus = function focus() {
46431
+ textareaEl.focus();
46432
+ }
46433
+
46434
+ ed.resize = function resize() {
46435
+ // not needed as the textarea will handle this
46436
+ }
46437
+
46438
+ ed.getSession = function getSession() {
46439
+ return ed;
46440
+ }
46441
+
46442
+ ed.getLength = function getLength() {
46443
+ return textareaEl.value.length;
46444
+ }
46445
+
46446
+ /**
46447
+ * Scroll vertically as necessary and reveal a line.
46448
+ * @param {Number} lineNumber
46449
+ * @param {ScrollType|Number} [scrollType] Immediate: = 1, Smooth: = 0
46450
+ */
46451
+ ed.scrollToLine = function scrollToLine(lineNumber, scrollType) {
46452
+ const computedStyle = getComputedStyle(textareaEl);
46453
+ let lineHeight = parseFloat(computedStyle.lineHeight);
46454
+ if (isNaN(lineHeight)) { lineHeight = parseFloat(computedStyle.fontSize) }
46455
+ if (isNaN(lineHeight)) { lineHeight = 12 }
46456
+ const scrollTop = (lineNumber - 1) * lineHeight;
46457
+ if (scrollType === 0) {
46458
+ textareaEl.scrollTo({
46459
+ top: scrollTop,
46460
+ behavior: "smooth"
46461
+ });
46462
+ } else {
46463
+ textareaEl.scrollTop = scrollTop;
46464
+ }
46465
+ }
46466
+
46467
+ ed.moveCursorTo = function moveCursorTo(lineNumber, colNumber) {
46468
+ const value = textareaEl.value;
46469
+ const lines = value.split("\n");
46470
+ let selectionStart = 0;
46471
+ for (let i = 0; i < lineNumber - 1; i++) {
46472
+ selectionStart += lines[i].length + 1; // +1 for the newline character
46473
+ }
46474
+ selectionStart += colNumber - 1;
46475
+ textareaEl.focus();
46476
+ textareaEl.setSelectionRange(selectionStart, selectionStart);
46477
+ }
46478
+
46479
+ ed.getAnnotations = function getAnnotations() {
46480
+ return []
46481
+ }
46482
+
46483
+ //ACE row and col are zero based
46484
+ ed.gotoLine = function gotoLine(row, col) {
46485
+ ed.moveCursorTo(row, col);
46486
+ ed.scrollToLine(row);
46487
+ }
46488
+ //ACE row and col are zero based
46489
+ ed.getCursorPosition = function getCursorPosition() {
46490
+ const value = textareaEl.value;
46491
+ const lineNumber = value.substring(0, textareaEl.selectionStart).split("\n").length;
46492
+ const column = textareaEl.selectionStart - value.lastIndexOf("\n", textareaEl.selectionStart - 1);
46493
+ return { row: lineNumber-1, column: column-1 };
46494
+ }
46495
+
46496
+ ed.setTheme = function(theme) {
46497
+ const oldTheme = userSelectedTheme || editorSettings.options.theme || "light";
46498
+ userSelectedTheme = theme;//remember users choice for this session
46499
+ if (oldTheme) { el.classList.remove(oldTheme) }
46500
+ el.classList.add(theme)
46501
+ }
46502
+
46503
+ ed.on = function (name, cb) {
46504
+ textareaEl.addEventListener(name, cb)
46505
+ }
46506
+ ed.getUndoManager = function getUndoManager() {
46507
+ console.warn("Node-RED basic code editor does not support getUndoManager. Consider switching to Monaco or ACE editor.");
46508
+ return null
46509
+ }
46510
+ ed.setFontSize = function setFontSize(size) {
46511
+ textareaEl.style.fontSize = size + "px"
46512
+ }
46513
+ //#endregion "ACE compatability"
46514
+
46515
+ //final setup
46516
+ ed.focusMemory = options.focus
46517
+ ed.mode = editorOptions.language
46518
+
46519
+ if (editorOptions.language === 'markdown' || options.mode === "ace/mode/markdown") {
46520
+ $(el).addClass("red-ui-editor-text-container-toolbar");
46521
+ ed.toolbar = RED.editor.customEditTypes['_markdown'].buildToolbar(toolbarRow, ed);
46522
+ if (options.expandable !== false) {
46523
+ var expandButton = $('<button type="button" class="red-ui-button" style="float: right;"><i class="fa fa-expand"></i></button>').appendTo(ed.toolbar);
46524
+ RED.popover.tooltip(expandButton, RED._("markdownEditor.expand"));
46525
+ expandButton.on("click", function (e) {
46526
+ e.preventDefault();
46527
+ var value = textareaEl.value;
46528
+ RED.editor.editMarkdown({
46529
+ value: value,
46530
+ width: "Infinity",
46531
+ stateId: options.stateId,
46532
+ cancel: function () {
46533
+ ed.focus();
46534
+ },
46535
+ complete: function (v, cursor) {
46536
+ textareaEl.value = v;
46537
+ setTimeout(function () {
46538
+ ed.focus();
46539
+ // ed.restoreView();
46540
+ }, 300);
46541
+ }
46542
+ })
46543
+ });
46544
+ }
46545
+ var helpButton = $('<button type="button" class="red-ui-editor-text-help red-ui-button red-ui-button-small"><i class="fa fa-question"></i></button>').appendTo($(el).parent());
46546
+ RED.popover.create({
46547
+ target: helpButton,
46548
+ trigger: 'click',
46549
+ size: "small",
46550
+ direction: "left",
46551
+ content: RED._("markdownEditor.format"),
46552
+ autoClose: 50
46553
+ });
46554
+ }
46555
+ ed.getView = function () {
46556
+ const selection = {
46557
+ selectionStart: textareaEl.selectionStart,
46558
+ selectionEnd: textareaEl.selectionEnd,
46559
+ selectionDirection: textareaEl.selectionDirection
46560
+ }
46561
+ const scroll = {
46562
+ scrollTop: textareaEl.scrollTop,
46563
+ scrollLeft: textareaEl.scrollLeft
46564
+ }
46565
+ return {
46566
+ selection: selection,
46567
+ scroll: scroll
46568
+ }
46569
+ }
46570
+ ed.saveView = function () {
46571
+ if (!options.stateId) { return; } // only possible if created with a unique stateId
46572
+ window._editorStateBasic = window._editorStateBasic || {};
46573
+ const state = ed.getView();
46574
+ window._editorStateBasic[options.stateId] = state;
46575
+ return state;
46576
+ }
46577
+ ed.restoreView = function (state) {
46578
+ if (!window._editorStateBasic) { return; }
46579
+ var _state = state || window._editorStateBasic[options.stateId];
46580
+ if (_state) {
46581
+ try {
46582
+ const selection = _state.selection || {};
46583
+ const scroll = _state.scroll || {};
46584
+ textareaEl.setSelectionRange(selection.selectionStart, selection.selectionEnd, selection.selectionDirection);
46585
+ textareaEl.scrollTop = scroll.scrollTop;
46586
+ textareaEl.scrollLeft = scroll.scrollLeft;
46587
+ ed._initState = _state;
46588
+ } catch (error) {
46589
+ delete window._editorStateBasic[options.stateId];
46590
+ }
46591
+ }
46592
+ };
46593
+ ed.restoreView();
46594
+
46595
+ if (options.cursor && !ed._initState) {
46596
+ var row = options.cursor.row || options.cursor.lineNumber;
46597
+ var col = options.cursor.column || options.cursor.col;
46598
+ ed.gotoLine(row, col);
46599
+ }
46600
+ ed.type = type;
46601
+
46602
+ return ed;
46603
+ }
46604
+
46605
+ return {
46606
+ /**
46607
+ * Editor type
46608
+ * @memberof RED.editor.codeEditor.basic
46609
+ */
46610
+ get type() { return type; },
46611
+ /**
46612
+ * Editor initialised
46613
+ * @memberof RED.editor.codeEditor.basic
46614
+ */
46615
+ get initialised() { return initialised; },
46616
+ /**
46617
+ * Initialise code editor
46618
+ * @param {object} options - initialisation options
46619
+ * @memberof RED.editor.codeEditor.basic
46620
+ */
46621
+ init: init,
46622
+ /**
46623
+ * Create a code editor
46624
+ * @param {object} options - the editor options
46625
+ * @memberof RED.editor.codeEditor.basic
46626
+ */
46627
+ create: create
46628
+ }
46629
+ })();
46630
+ ;/*
45847
46631
  * Licensed under the Apache License, Version 2.0 (the "License");
45848
46632
  * you may not use this file except in compliance with the License.
45849
46633
  * You may obtain a copy of the License at
@@ -47711,6 +48495,14 @@ RED.eventLog = (function() {
47711
48495
  editorStack = $("#red-ui-editor-stack");
47712
48496
  $(window).on("resize", handleWindowResize);
47713
48497
  RED.events.on("sidebar:resize",handleWindowResize);
48498
+ $("#red-ui-editor-shade").on("click", function() {
48499
+ if (!openingTray) {
48500
+ var tray = stack[stack.length-1];
48501
+ if (tray && tray.primaryButton) {
48502
+ tray.primaryButton.click();
48503
+ }
48504
+ }
48505
+ });
47714
48506
  },
47715
48507
  show: function show(options) {
47716
48508
  lowerTrayZ();
@@ -51343,17 +52135,11 @@ if (typeof module !== "undefined" && module.exports) {
51343
52135
  })
51344
52136
  menu.appendTo("body");
51345
52137
 
51346
- // TODO: prevent the menu from overflowing the window.
51347
-
51348
52138
  var top = options.y
51349
52139
  var left = options.x
52140
+ var windowHeight = $(window).height();
52141
+ var windowWidth = $(window).width();
51350
52142
 
51351
- if (top + menu.height() - $(document).scrollTop() > $(window).height()) {
51352
- top -= (top + menu.height()) - $(window).height() + 22;
51353
- }
51354
- if (left + menu.width() - $(document).scrollLeft() > $(window).width()) {
51355
- left -= (left + menu.width()) - $(window).width() + 18;
51356
- }
51357
52143
  menu.css({
51358
52144
  top: top + "px",
51359
52145
  left: left + "px"
@@ -51366,6 +52152,36 @@ if (typeof module !== "undefined" && module.exports) {
51366
52152
  disposeMenu()
51367
52153
  });
51368
52154
  menu.show();
52155
+
52156
+ const menuHeight = menu.outerHeight();
52157
+ const menuWidth = menu.outerWidth();
52158
+ const spaceAbove = top;
52159
+ const bottomOverlap = menuHeight - (windowHeight - top);
52160
+
52161
+ // Adjust horizontal position if menu would overflow right edge
52162
+ if (left + menuWidth > windowWidth - 10) {
52163
+ left = windowWidth - menuWidth - 10;
52164
+ menu.css({ left: left + "px" });
52165
+ }
52166
+
52167
+ // Check if menu overflows below viewport
52168
+ if (bottomOverlap > 0) {
52169
+ if (spaceAbove > bottomOverlap + 5) {
52170
+ // There is room to move the menu up and still show it all
52171
+ menu.css({
52172
+ top: (top - bottomOverlap - 5) + "px"
52173
+ })
52174
+ } else {
52175
+ // There is not room for the whole menu.
52176
+ // 1. move it to the top
52177
+ // 2. enable overflow/scrolling
52178
+ menu.css({
52179
+ top: "5px",
52180
+ maxHeight: (windowHeight - 25) + "px",
52181
+ overflowY: "auto"
52182
+ });
52183
+ }
52184
+ }
51369
52185
  // set focus to first item so that pressing escape key closes the menu
51370
52186
  $("#red-ui-workspace-context-menu :first(ul) > a").trigger("focus")
51371
52187