lexgui 0.2.0 → 0.4.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.
@@ -8,11 +8,12 @@
8
8
  */
9
9
 
10
10
  var LX = {
11
- version: "0.2.0",
11
+ version: "0.4.0",
12
12
  ready: false,
13
- components: [], // specific pre-build components
14
- signals: {}, // events and triggers
15
- extraCommandbarEntries: [] // user specific entries for command bar
13
+ components: [], // Specific pre-build components
14
+ signals: {}, // Events and triggers
15
+ extraCommandbarEntries: [], // User specific entries for command bar
16
+ activeDraggable: null // Watch for the current active draggable
16
17
  };
17
18
 
18
19
  LX.MOUSE_LEFT_CLICK = 0;
@@ -25,6 +26,8 @@ LX.MOUSE_TRIPLE_CLICK = 3;
25
26
  LX.CURVE_MOVEOUT_CLAMP = 0;
26
27
  LX.CURVE_MOVEOUT_DELETE = 1;
27
28
 
29
+ LX.DRAGGABLE_Z_INDEX = 101;
30
+
28
31
  function clamp( num, min, max ) { return Math.min( Math.max( num, min ), max ); }
29
32
  function round( number, precision ) { return precision == 0 ? Math.floor( number ) : +(( number ).toFixed( precision ?? 2 ).replace( /([0-9]+(\.[0-9]+[1-9])?)(\.?0+$)/, '$1' )); }
30
33
  function remapRange( oldValue, oldMin, oldMax, newMin, newMax ) { return ((( oldValue - oldMin ) * ( newMax - newMin )) / ( oldMax - oldMin )) + newMin; }
@@ -84,7 +87,7 @@ LX.doAsync = doAsync;
84
87
  */
85
88
  function getSupportedDOMName( text )
86
89
  {
87
- return text.replace(/\s/g, '').replaceAll('@', '_').replaceAll('+', '_plus_').replaceAll('.', '');
90
+ return text.replace( /\s/g, '' ).replaceAll('@', '_').replaceAll('+', '_plus_').replaceAll( '.', '' );
88
91
  }
89
92
 
90
93
  LX.getSupportedDOMName = getSupportedDOMName;
@@ -223,7 +226,8 @@ LX.hexToRgb = hexToRgb;
223
226
  function rgbToHex( rgb )
224
227
  {
225
228
  let hex = "#";
226
- for( let c of rgb ) {
229
+ for( let c of rgb )
230
+ {
227
231
  c = Math.floor( c * 255 );
228
232
  hex += c.toString( 16 );
229
233
  }
@@ -298,7 +302,7 @@ LX.buildTextPattern = buildTextPattern;
298
302
 
299
303
  /**
300
304
  * @method makeDraggable
301
- * @description Allow an element to be dragged
305
+ * @description Allows an element to be dragged
302
306
  * @param {Element} domEl
303
307
  * @param {Object} options
304
308
  * autoAdjust (Bool): Sets in a correct position at the beggining
@@ -323,6 +327,7 @@ function makeDraggable( domEl, options = { } )
323
327
  top = top ?? e.clientY - offsetY - parentRect.y;
324
328
  domEl.style.left = clamp( left, dragMargin + fixedOffset.x, fixedOffset.x + parentRect.width - domEl.offsetWidth - dragMargin ) + 'px';
325
329
  domEl.style.top = clamp( top, dragMargin + fixedOffset.y, fixedOffset.y + parentRect.height - domEl.offsetHeight - dragMargin ) + 'px';
330
+ domEl.style.translate = "none"; // Force remove translation
326
331
  };
327
332
 
328
333
  // Initial adjustment
@@ -367,26 +372,45 @@ function makeDraggable( domEl, options = { } )
367
372
  e.preventDefault();
368
373
  e.stopPropagation();
369
374
  e.stopImmediatePropagation();
370
- if( !currentTarget ) return;
375
+
376
+ if( !currentTarget )
377
+ {
378
+ return;
379
+ }
380
+
371
381
  // Remove image when dragging
372
382
  var img = new Image();
373
383
  img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
374
384
  e.dataTransfer.setDragImage( img, 0, 0 );
375
385
  e.dataTransfer.effectAllowed = "move";
386
+
376
387
  const rect = e.target.getBoundingClientRect();
377
388
  const parentRect = currentTarget.parentElement.getBoundingClientRect();
378
389
  const isFixed = ( currentTarget.style.position == "fixed" );
379
390
  const fixedOffset = isFixed ? new LX.vec2( parentRect.x, parentRect.y ) : new LX.vec2();
380
391
  offsetX = e.clientX - rect.x - fixedOffset.x;
381
392
  offsetY = e.clientY - rect.y - fixedOffset.y;
393
+
382
394
  document.addEventListener( "mousemove", onMove );
395
+
396
+ currentTarget._eventCatched = true;
397
+
398
+ // Force active dialog to show on top
399
+ if( LX.activeDraggable )
400
+ {
401
+ LX.activeDraggable.style.zIndex = LX.DRAGGABLE_Z_INDEX;
402
+ }
403
+
404
+ LX.activeDraggable = domEl;
405
+ LX.activeDraggable.style.zIndex = LX.DRAGGABLE_Z_INDEX + 1;
406
+
383
407
  if( onDragStart )
384
408
  {
385
409
  onDragStart( currentTarget, e );
386
410
  }
387
411
  }, false );
388
412
 
389
- document.addEventListener( 'mouseup', () => {
413
+ document.addEventListener( 'mouseup', (e) => {
390
414
  if( currentTarget )
391
415
  {
392
416
  currentTarget = null;
@@ -397,6 +421,48 @@ function makeDraggable( domEl, options = { } )
397
421
 
398
422
  LX.makeDraggable = makeDraggable;
399
423
 
424
+ /**
425
+ * @method makeCollapsible
426
+ * @description Allows an element to be collapsed/expanded
427
+ * @param {Element} domEl: Element to be treated as collapsible
428
+ * @param {Element} content: Content to display/hide on collapse/extend
429
+ * @param {Element} parent: Element where the content will be appended (default is domEl.parent)
430
+ * @param {Object} options
431
+ */
432
+ function makeCollapsible( domEl, content, parent, options = { } )
433
+ {
434
+ domEl.classList.add( "collapsible" );
435
+
436
+ const collapsed = ( options.collapsed ?? true );
437
+ const actionIcon = LX.makeIcon( "Right" );
438
+ actionIcon.classList.add( "collapser" );
439
+ actionIcon.dataset[ "collapsed" ] = collapsed;
440
+ actionIcon.style.marginLeft = "auto";
441
+
442
+ actionIcon.addEventListener( "click", function(e) {
443
+ e.preventDefault();
444
+ e.stopPropagation();
445
+ if( this.dataset[ "collapsed" ] )
446
+ {
447
+ delete this.dataset[ "collapsed" ];
448
+ content.style.display = "block";
449
+ }
450
+ else
451
+ {
452
+ this.dataset[ "collapsed" ] = true;
453
+ content.style.display = "none";
454
+ }
455
+ } );
456
+
457
+ domEl.appendChild( actionIcon );
458
+
459
+ parent = parent ?? domEl.parentElement;
460
+
461
+ parent.appendChild( content );
462
+ }
463
+
464
+ LX.makeCollapsible = makeCollapsible;
465
+
400
466
  /**
401
467
  * @method makeCodeSnippet
402
468
  * @description Create a code snippet in a specific language
@@ -499,6 +565,23 @@ function makeCodeSnippet( code, size, options = { } )
499
565
 
500
566
  LX.makeCodeSnippet = makeCodeSnippet;
501
567
 
568
+ /**
569
+ * @method makeIcon
570
+ * @description Gets an SVG element using one of LX.ICONS
571
+ * @param {String} iconName
572
+ * @param {String} iconTitle
573
+ */
574
+ function makeIcon( iconName, iconTitle )
575
+ {
576
+ const icon = document.createElement( "a" );
577
+ icon.title = iconTitle ?? "";
578
+ icon.className = "lexicon";
579
+ icon.innerHTML = LX.ICONS[ iconName ] ?? "";
580
+ return icon;
581
+ }
582
+
583
+ LX.makeIcon = makeIcon;
584
+
502
585
  /**
503
586
  * @method registerCommandbarEntry
504
587
  * @description Adds an extra command bar entry
@@ -768,12 +851,12 @@ function _createCommandbar( root )
768
851
  if( LX.has('CodeEditor') )
769
852
  {
770
853
  const instances = LX.CodeEditor.getInstances();
771
- if(!instances.length) return;
854
+ if( !instances.length ) return;
772
855
 
773
856
  const languages = instances[ 0 ].languages;
774
857
 
775
- for( let l of Object.keys( languages ) ) {
776
-
858
+ for( let l of Object.keys( languages ) )
859
+ {
777
860
  const key = "Language: " + l;
778
861
  const icon = instances[ 0 ]._getFileIcon( null, languages[ l ].ext );
779
862
 
@@ -781,9 +864,11 @@ function _createCommandbar( root )
781
864
  "<img src='" + ( "https://raw.githubusercontent.com/jxarco/lexgui.js/master/" + icon ) + "'>";
782
865
 
783
866
  value += key + " <span class='lang-ext'>(" + languages[ l ].ext + ")</span>";
784
- if( key.toLowerCase().includes( filter ) ) {
867
+ if( key.toLowerCase().includes( filter ) )
868
+ {
785
869
  _addElement( value, () => {
786
- for( let i of instances ) {
870
+ for( let i of instances )
871
+ {
787
872
  i._changeLanguage( l );
788
873
  }
789
874
  }, "", {} );
@@ -856,6 +941,25 @@ function init( options = { } )
856
941
  this.root = document.body;
857
942
  }
858
943
 
944
+ // Notifications
945
+ {
946
+ const notifSection = document.createElement( "section" );
947
+ notifSection.className = "notifications";
948
+ this.notifications = document.createElement( "ol" );
949
+ this.notifications.className = "";
950
+ this.notifications.iWidth = 0;
951
+ notifSection.appendChild( this.notifications );
952
+ this.container.appendChild( notifSection );
953
+
954
+ this.notifications.addEventListener( "mouseenter", () => {
955
+ this.notifications.classList.add( "list" );
956
+ } );
957
+
958
+ this.notifications.addEventListener( "mouseleave", () => {
959
+ this.notifications.classList.remove( "list" );
960
+ } );
961
+ }
962
+
859
963
  // Disable drag icon
860
964
  root.addEventListener( 'dragover', function( e ) {
861
965
  e.preventDefault();
@@ -1006,6 +1110,7 @@ LX.popup = popup;
1006
1110
  function prompt( text, title, callback, options = {} )
1007
1111
  {
1008
1112
  options.modal = true;
1113
+ options.className = "prompt";
1009
1114
 
1010
1115
  let value = "";
1011
1116
 
@@ -1020,7 +1125,9 @@ function prompt( text, title, callback, options = {} )
1020
1125
 
1021
1126
  p.sameLine( 2 );
1022
1127
 
1023
- p.addButton( null, options.accept || "OK", () => {
1128
+ p.addButton(null, "Cancel", () => {if(options.on_cancel) options.on_cancel(); dialog.close();} );
1129
+
1130
+ p.addButton( null, options.accept || "Continue", () => {
1024
1131
  if( options.required && value === '' )
1025
1132
  {
1026
1133
  text += text.includes("You must fill the input text.") ? "": "\nYou must fill the input text.";
@@ -1034,8 +1141,6 @@ function prompt( text, title, callback, options = {} )
1034
1141
  }
1035
1142
  }, { buttonClass: "primary" });
1036
1143
 
1037
- p.addButton(null, "Cancel", () => {if(options.on_cancel) options.on_cancel(); dialog.close();} );
1038
-
1039
1144
  }, options );
1040
1145
 
1041
1146
  // Focus text prompt
@@ -1049,6 +1154,101 @@ function prompt( text, title, callback, options = {} )
1049
1154
 
1050
1155
  LX.prompt = prompt;
1051
1156
 
1157
+ /**
1158
+ * @method toast
1159
+ * @param {String} title
1160
+ * @param {String} description (Optional)
1161
+ * @param {*} options
1162
+ * action: Data of the custom action { name, callback }
1163
+ * closable: Allow closing the toast
1164
+ * timeout: Time in which the toast closed automatically, in ms. -1 means persistent. [3000]
1165
+ */
1166
+
1167
+ function toast( title, description, options = {} )
1168
+ {
1169
+ if( !title )
1170
+ {
1171
+ throw( "The toast needs at least a title!" );
1172
+ }
1173
+
1174
+ console.assert( this.notifications );
1175
+
1176
+ const toast = document.createElement( "li" );
1177
+ toast.className = "lextoast";
1178
+ toast.style.translate = "0 calc(100% + 30px)";
1179
+ this.notifications.prepend( toast );
1180
+
1181
+ doAsync( () => {
1182
+
1183
+ if( this.notifications.offsetWidth > this.notifications.iWidth )
1184
+ {
1185
+ this.notifications.iWidth = Math.min( this.notifications.offsetWidth, 480 );
1186
+ this.notifications.style.width = this.notifications.iWidth + "px";
1187
+ }
1188
+
1189
+ toast.dataset[ "open" ] = true;
1190
+ }, 10 );
1191
+
1192
+ const content = document.createElement( "div" );
1193
+ content.className = "lextoastcontent";
1194
+ toast.appendChild( content );
1195
+
1196
+ const titleContent = document.createElement( "div" );
1197
+ titleContent.className = "title";
1198
+ titleContent.innerHTML = title;
1199
+ content.appendChild( titleContent );
1200
+
1201
+ if( description )
1202
+ {
1203
+ const desc = document.createElement( "div" );
1204
+ desc.className = "desc";
1205
+ desc.innerHTML = description;
1206
+ content.appendChild( desc );
1207
+ }
1208
+
1209
+ if( options.action )
1210
+ {
1211
+ const panel = new Panel();
1212
+ panel.addButton(null, options.action.name ?? "Accept", options.action.callback.bind( this, toast ), { width: "auto", maxWidth: "150px", className: "right", buttonClass: "outline" });
1213
+ toast.appendChild( panel.root.childNodes[ 0 ] );
1214
+ }
1215
+
1216
+ const that = this;
1217
+
1218
+ toast.close = function() {
1219
+ this.dataset[ "closed" ] = true;
1220
+ doAsync( () => {
1221
+ this.remove();
1222
+ if( !that.notifications.childElementCount )
1223
+ {
1224
+ that.notifications.style.width = "unset";
1225
+ that.notifications.iWidth = 0;
1226
+ }
1227
+ }, 500 );
1228
+ };
1229
+
1230
+ if( options.closable ?? true )
1231
+ {
1232
+ const closeButton = document.createElement( "a" );
1233
+ closeButton.className = "fa fa-xmark lexicon closer";
1234
+ closeButton.addEventListener( "click", () => {
1235
+ toast.close();
1236
+ } );
1237
+ toast.appendChild( closeButton );
1238
+ }
1239
+
1240
+ const timeout = options.timeout ?? 3000;
1241
+
1242
+ if( timeout != -1 )
1243
+ {
1244
+ doAsync( () => {
1245
+ toast.close();
1246
+ }, timeout );
1247
+ }
1248
+ }
1249
+
1250
+ LX.toast = toast;
1251
+
1052
1252
  /**
1053
1253
  * @method badge
1054
1254
  * @param {String} text
@@ -1350,7 +1550,8 @@ class Area {
1350
1550
 
1351
1551
  function inner_mousemove( e )
1352
1552
  {
1353
- switch( that.type ) {
1553
+ switch( that.type )
1554
+ {
1354
1555
  case "right":
1355
1556
  var dt = ( lastMousePosition[ 0 ] - e.x );
1356
1557
  var size = ( that.root.offsetWidth + dt );
@@ -1545,7 +1746,8 @@ class Area {
1545
1746
 
1546
1747
  // Listen resize event on first area
1547
1748
  const resizeObserver = new ResizeObserver( entries => {
1548
- for (const entry of entries) {
1749
+ for ( const entry of entries )
1750
+ {
1549
1751
  const bb = entry.contentRect;
1550
1752
  area2.root.style.height = "calc(100% - " + ( bb.height + 4) + "px )";
1551
1753
  }
@@ -1686,9 +1888,15 @@ class Area {
1686
1888
  this.root.style.height = height;
1687
1889
  }
1688
1890
 
1689
- this.size = [ this.root.clientWidth, this.root.clientHeight ];
1891
+ if( this.onresize )
1892
+ {
1893
+ this.onresize( this.root.getBoundingClientRect() );
1894
+ }
1690
1895
 
1691
- this.propagateEvent( "onresize" );
1896
+ doAsync( () => {
1897
+ this.size = [ this.root.clientWidth, this.root.clientHeight ];
1898
+ this.propagateEvent( "onresize" );
1899
+ }, 150 );
1692
1900
  }
1693
1901
 
1694
1902
  /**
@@ -1705,7 +1913,7 @@ class Area {
1705
1913
  let [area1, area2] = this.sections;
1706
1914
  this.splitExtended = true;
1707
1915
 
1708
- if(this.type == "vertical")
1916
+ if( this.type == "vertical")
1709
1917
  {
1710
1918
  this.offset = area2.root.offsetHeight;
1711
1919
  area2.root.classList.add("fadeout-vertical");
@@ -1719,7 +1927,6 @@ class Area {
1719
1927
  this._moveSplit(-Infinity, true, 8);
1720
1928
  }
1721
1929
 
1722
- // Async resize in some ms...
1723
1930
  doAsync( () => this.propagateEvent('onresize'), 150 );
1724
1931
  }
1725
1932
 
@@ -1735,7 +1942,7 @@ class Area {
1735
1942
  this.splitExtended = false;
1736
1943
  let [area1, area2] = this.sections;
1737
1944
 
1738
- if(this.type == "vertical")
1945
+ if( this.type == "vertical")
1739
1946
  {
1740
1947
  area2.root.classList.add("fadein-vertical");
1741
1948
  this._moveSplit(this.offset);
@@ -1746,7 +1953,6 @@ class Area {
1746
1953
  this._moveSplit(this.offset);
1747
1954
  }
1748
1955
 
1749
- // Async resize in some ms...
1750
1956
  doAsync( () => this.propagateEvent('onresize'), 150 );
1751
1957
  }
1752
1958
 
@@ -1780,11 +1986,15 @@ class Area {
1780
1986
 
1781
1987
  propagateEvent( eventName ) {
1782
1988
 
1783
- for(var i = 0; i < this.sections.length; i++)
1989
+ for( var i = 0; i < this.sections.length; i++ )
1784
1990
  {
1785
- const area = this.sections[i];
1786
- if(area[ eventName ])
1991
+ const area = this.sections[ i ];
1992
+
1993
+ if( area[ eventName ] )
1994
+ {
1787
1995
  area[ eventName ].call( this, area.root.getBoundingClientRect() );
1996
+ }
1997
+
1788
1998
  area.propagateEvent( eventName );
1789
1999
  }
1790
2000
  }
@@ -1807,42 +2017,63 @@ class Area {
1807
2017
  * @param {Function} callback Function to fill the menubar
1808
2018
  * @param {*} options:
1809
2019
  * float: Justify content (left, center, right) [left]
2020
+ * sticky: Fix menubar at the top [true]
1810
2021
  */
1811
2022
 
1812
2023
  addMenubar( callback, options = {} ) {
1813
2024
 
1814
- let menubar = new Menubar(options);
2025
+ let menubar = new Menubar( options );
1815
2026
 
1816
- if(callback) callback( menubar );
2027
+ if( callback )
2028
+ {
2029
+ callback( menubar );
2030
+ }
1817
2031
 
1818
2032
  LX.menubars.push( menubar );
1819
2033
 
1820
2034
  const height = 48; // pixels
2035
+ const [ bar, content ] = this.split({ type: 'vertical', sizes: [height, null], resize: false, menubar: true });
2036
+ menubar.siblingArea = content;
1821
2037
 
1822
- const [bar, content] = this.split({type: 'vertical', sizes: [height, null], resize: false, menubar: true});
1823
2038
  bar.attach( menubar );
1824
- bar.is_menubar = true;
2039
+ bar.isMenubar = true;
2040
+
2041
+ if( options.sticky ?? true )
2042
+ {
2043
+ bar.root.classList.add( "sticky" );
2044
+ }
2045
+
1825
2046
  return menubar;
1826
2047
  }
1827
2048
 
1828
2049
  /**
1829
2050
  * @method addSidebar
1830
2051
  * @param {Function} callback Function to fill the sidebar
2052
+ * @param {Object} options: Sidebar options
2053
+ * width: Width of the sidebar [16rem]
1831
2054
  */
1832
2055
 
1833
2056
  addSidebar( callback, options = {} ) {
1834
2057
 
1835
2058
  let sidebar = new SideBar( options );
1836
2059
 
1837
- if( callback ) callback( sidebar );
2060
+ if( callback )
2061
+ {
2062
+ callback( sidebar );
2063
+ }
2064
+
2065
+ // Generate DOM elements after adding all entries
2066
+ sidebar._build();
1838
2067
 
1839
2068
  LX.menubars.push( sidebar );
1840
2069
 
1841
- const width = 64; // pixels
2070
+ const width = options.width ?? "16rem";
2071
+ const [ bar, content ] = this.split( { type: 'horizontal', sizes: [ width, null ], resize: false, sidebar: true } );
2072
+ sidebar.siblingArea = content;
1842
2073
 
1843
- const [bar, content] = this.split( { type: 'horizontal', sizes: [ width, null ], resize: false, sidebar: true } );
1844
2074
  bar.attach( sidebar );
1845
- bar.is_sidebar = true;
2075
+ bar.isSidebar = true;
2076
+
1846
2077
  return sidebar;
1847
2078
  }
1848
2079
 
@@ -2098,7 +2329,8 @@ class Area {
2098
2329
 
2099
2330
  this.size = [ rect.width, rect.height ];
2100
2331
 
2101
- for(var i = 0; i < this.sections.length; i++) {
2332
+ for( var i = 0; i < this.sections.length; i++ )
2333
+ {
2102
2334
  this.sections[i]._update();
2103
2335
  }
2104
2336
  }
@@ -2122,7 +2354,7 @@ class Tabs {
2122
2354
  static TAB_SIZE = 28;
2123
2355
  static TAB_ID = 0;
2124
2356
 
2125
- constructor( area, options = {} ) {
2357
+ constructor( area, options = {} ) {
2126
2358
 
2127
2359
  this.onclose = options.onclose;
2128
2360
 
@@ -2204,24 +2436,28 @@ class Tabs {
2204
2436
  }
2205
2437
 
2206
2438
  // debug
2207
- if(folding)
2439
+ if( folding )
2208
2440
  {
2209
2441
  this.folded = true;
2210
2442
  this.folding = folding;
2211
2443
 
2212
- if(folding == "up") area.root.insertChildAtIndex(area.sections[1].root, 0);
2444
+ if( folding == "up" )
2445
+ {
2446
+ area.root.insertChildAtIndex(area.sections[1].root, 0);
2447
+ }
2213
2448
 
2214
2449
  // Listen resize event on parent area
2215
2450
  const resizeObserver = new ResizeObserver((entries) => {
2216
- for (const entry of entries) {
2451
+ for (const entry of entries)
2452
+ {
2217
2453
  const bb = entry.contentRect;
2218
- const sibling = area.parentArea.sections[0].root;
2219
- const add_offset = true; // hardcoded...
2220
- sibling.style.height = "calc(100% - " + ((add_offset ? 42 : 0) + bb.height) + "px )";
2454
+ const sibling = area.parentArea.sections[ 0 ].root;
2455
+ const addOffset = true; // hardcoded...
2456
+ sibling.style.height = "calc(100% - " + ((addOffset ? 42 : 0) + bb.height) + "px )";
2221
2457
  }
2222
2458
  });
2223
2459
 
2224
- resizeObserver.observe(this.area.root);
2460
+ resizeObserver.observe( this.area.root );
2225
2461
  this.area.root.classList.add('folded');
2226
2462
  }
2227
2463
  }
@@ -2358,7 +2594,8 @@ class Tabs {
2358
2594
 
2359
2595
  setTimeout( () => {
2360
2596
 
2361
- if( options.onCreate ) {
2597
+ if( options.onCreate )
2598
+ {
2362
2599
  options.onCreate.call(this, this.area.root.getBoundingClientRect());
2363
2600
  }
2364
2601
 
@@ -2420,34 +2657,41 @@ LX.Tabs = Tabs;
2420
2657
 
2421
2658
  class Menubar {
2422
2659
 
2423
- constructor( options = {} ) {
2660
+ constructor( options = {} ) {
2424
2661
 
2425
- this.root = document.createElement('div');
2662
+ this.root = document.createElement( "div" );
2426
2663
  this.root.className = "lexmenubar";
2427
- if(options.float)
2664
+
2665
+ if( options.float )
2666
+ {
2428
2667
  this.root.style.justifyContent = options.float;
2429
- this.items = [];
2668
+ }
2430
2669
 
2431
- this.icons = {};
2432
- this.shorts = {};
2433
- this.buttons = [];
2670
+ this.items = [ ];
2671
+ this.buttons = [ ];
2672
+ this.icons = { };
2673
+ this.shorts = { };
2434
2674
  }
2435
2675
 
2436
2676
  /**
2437
2677
  * @method add
2438
- * @param {*} options:
2678
+ * @param {Object} options:
2439
2679
  * callback: Function to call on each item
2680
+ * icon: Entry icon
2681
+ * short: Entry shortcut name
2440
2682
  */
2441
2683
 
2442
2684
  add( path, options = {} ) {
2443
2685
 
2444
- if(options.constructor == Function)
2686
+ if( options.constructor == Function )
2687
+ {
2445
2688
  options = { callback: options };
2689
+ }
2446
2690
 
2447
- // process path
2448
- const tokens = path.split("/");
2691
+ // Process path
2692
+ const tokens = path.split( "/" );
2449
2693
 
2450
- // assign icons and shortcuts to last token in path
2694
+ // Assign icons and shortcuts to last token in path
2451
2695
  const lastPath = tokens[tokens.length - 1];
2452
2696
  this.icons[ lastPath ] = options.icon;
2453
2697
  this.shorts[ lastPath ] = options.short;
@@ -2455,105 +2699,146 @@ class Menubar {
2455
2699
  let idx = 0;
2456
2700
  let that = this;
2457
2701
 
2458
- const insert = (token, list) => {
2459
- if(token == undefined) return;
2702
+ const _insertEntry = ( token, list ) => {
2703
+ if( token == undefined )
2704
+ {
2705
+ return;
2706
+ }
2460
2707
 
2461
2708
  let found = null;
2462
2709
  list.forEach( o => {
2463
- const keys = Object.keys(o);
2710
+ const keys = Object.keys( o );
2464
2711
  const key = keys.find( t => t == token );
2465
- if(key) found = o[ key ];
2712
+ if( key ) found = o[ key ];
2466
2713
  } );
2467
2714
 
2468
- if(found) {
2469
- insert( tokens[idx++], found );
2715
+ if( found )
2716
+ {
2717
+ _insertEntry( tokens[ idx++ ], found );
2470
2718
  }
2471
- else {
2719
+ else
2720
+ {
2472
2721
  let item = {};
2473
2722
  item[ token ] = [];
2474
- const next_token = tokens[idx++];
2723
+ const nextToken = tokens[ idx++ ];
2475
2724
  // Check if last token -> add callback
2476
- if(!next_token) {
2725
+ if( !nextToken )
2726
+ {
2477
2727
  item[ 'callback' ] = options.callback;
2728
+ item[ 'disabled' ] = options.disabled;
2478
2729
  item[ 'type' ] = options.type;
2479
2730
  item[ 'checked' ] = options.checked;
2480
2731
  }
2481
2732
  list.push( item );
2482
- insert( next_token, item[ token ] );
2733
+ _insertEntry( nextToken, item[ token ] );
2483
2734
  }
2484
2735
  };
2485
2736
 
2486
- insert( tokens[idx++], this.items );
2737
+ _insertEntry( tokens[idx++], this.items );
2487
2738
 
2488
2739
  // Create elements
2489
2740
 
2490
2741
  for( let item of this.items )
2491
2742
  {
2492
- let key = Object.keys(item)[0];
2493
- let pKey = key.replace(/\s/g, '').replaceAll('.', '');
2743
+ let key = Object.keys( item )[ 0 ];
2744
+ let pKey = key.replace( /\s/g, '' ).replaceAll( '.', '' );
2494
2745
 
2495
2746
  // Item already created
2496
- if( this.root.querySelector("#" + pKey) )
2747
+ if( this.root.querySelector( "#" + pKey ) )
2748
+ {
2497
2749
  continue;
2750
+ }
2498
2751
 
2499
2752
  let entry = document.createElement('div');
2500
2753
  entry.className = "lexmenuentry";
2501
2754
  entry.id = pKey;
2502
2755
  entry.innerHTML = "<span>" + key + "</span>";
2503
- if(options.position == "left") {
2756
+ entry.tabIndex = "1";
2757
+
2758
+ if( options.position == "left" )
2759
+ {
2504
2760
  this.root.prepend( entry );
2505
2761
  }
2506
- else {
2507
- if(options.position == "right")
2762
+ else
2763
+ {
2764
+ if( options.position == "right" )
2765
+ {
2508
2766
  entry.right = true;
2509
- if(this.root.lastChild && this.root.lastChild.right) {
2767
+ }
2768
+
2769
+ if( this.root.lastChild && this.root.lastChild.right )
2770
+ {
2510
2771
  this.root.lastChild.before( entry );
2511
2772
  }
2512
- else {
2773
+ else
2774
+ {
2513
2775
  this.root.appendChild( entry );
2514
2776
  }
2515
2777
  }
2516
2778
 
2779
+ const _resetMenubar = function() {
2780
+ // Menu entries are in the menubar..
2781
+ that.root.querySelectorAll(".lexmenuentry").forEach( _entry => {
2782
+ _entry.classList.remove( 'selected' );
2783
+ _entry.built = false;
2784
+ } );
2785
+ // Menuboxes are in the root area!
2786
+ LX.root.querySelectorAll(".lexmenubox").forEach(e => e.remove());
2787
+ // Next time we need to click again
2788
+ that.focused = false;
2789
+ };
2790
+
2517
2791
  const create_submenu = function( o, k, c, d ) {
2518
2792
 
2519
- let contextmenu = document.createElement('div');
2520
- contextmenu.className = "lexcontextmenu";
2521
- contextmenu.tabIndex = "0";
2522
- const isSubMenu = c.classList.contains('lexcontextmenuentry');
2793
+ let menuElement = document.createElement('div');
2794
+ menuElement.className = "lexmenubox";
2795
+ menuElement.tabIndex = "0";
2796
+ c.currentMenu = menuElement;
2797
+ const isSubMenu = c.classList.contains( "lexmenuboxentry" );
2798
+ if( isSubMenu ) menuElement.dataset[ "submenu" ] = true;
2523
2799
  var rect = c.getBoundingClientRect();
2524
- contextmenu.style.left = (isSubMenu ? rect.width : rect.left) + "px";
2525
- // Entries use css to set top relative to parent
2526
- contextmenu.style.top = (isSubMenu ? 0 : rect.bottom - 4) + "px";
2527
- c.appendChild( contextmenu );
2800
+ menuElement.style.left = ( isSubMenu ? ( rect.x + rect.width ) : rect.left ) + "px";
2801
+ menuElement.style.top = ( isSubMenu ? rect.y : rect.bottom - 4 ) + "px";
2802
+ rect = menuElement.getBoundingClientRect();
2528
2803
 
2529
- contextmenu.focus();
2804
+ doAsync( () => {
2805
+ menuElement.dataset[ "open" ] = true;
2806
+ }, 10 );
2530
2807
 
2531
- rect = contextmenu.getBoundingClientRect();
2808
+ LX.root.appendChild( menuElement );
2532
2809
 
2533
- for( var i = 0; i < o[k].length; ++i )
2810
+ for( var i = 0; i < o[ k ].length; ++i )
2534
2811
  {
2535
- const subitem = o[k][i];
2536
- const subkey = Object.keys(subitem)[0];
2812
+ const subitem = o[ k ][ i ];
2813
+ const subkey = Object.keys( subitem )[ 0 ];
2537
2814
  const hasSubmenu = subitem[ subkey ].length;
2538
2815
  const isCheckbox = subitem[ 'type' ] == 'checkbox';
2539
2816
  let subentry = document.createElement('div');
2540
- subentry.className = "lexcontextmenuentry";
2541
- subentry.className += (i == o[k].length - 1 ? " last" : "");
2542
- if(subkey == '')
2543
- subentry.className = " lexseparator";
2544
- else {
2817
+ subentry.className = "lexmenuboxentry";
2818
+ subentry.className += (i == o[k].length - 1 ? " last" : "") + ( subitem.disabled ? " disabled" : "" );
2545
2819
 
2820
+ if( subkey == '' )
2821
+ {
2822
+ subentry.className = " lexseparator";
2823
+ }
2824
+ else
2825
+ {
2546
2826
  subentry.id = subkey;
2547
2827
  let subentrycont = document.createElement('div');
2548
2828
  subentrycont.innerHTML = "";
2549
- subentrycont.classList = "lexcontextmenuentrycontainer";
2829
+ subentrycont.classList = "lexmenuboxentrycontainer";
2550
2830
  subentry.appendChild(subentrycont);
2551
2831
  const icon = that.icons[ subkey ];
2552
- if(isCheckbox){
2832
+ if( isCheckbox )
2833
+ {
2553
2834
  subentrycont.innerHTML += "<input type='checkbox' >";
2554
- }else if(icon) {
2835
+ }
2836
+ else if( icon )
2837
+ {
2555
2838
  subentrycont.innerHTML += "<a class='" + icon + " fa-sm'></a>";
2556
- }else {
2839
+ }
2840
+ else
2841
+ {
2557
2842
  subentrycont.innerHTML += "<a class='fa-solid fa-sm noicon'></a>";
2558
2843
  subentrycont.classList.add( "noicon" );
2559
2844
 
@@ -2561,46 +2846,54 @@ class Menubar {
2561
2846
  subentrycont.innerHTML += "<div class='lexentryname'>" + subkey + "</div>";
2562
2847
  }
2563
2848
 
2564
- let checkbox_input = subentry.querySelector('input');
2565
- if(checkbox_input) {
2566
- checkbox_input.checked = subitem.checked ?? false;
2567
- checkbox_input.addEventListener('change', (e) => {
2568
- subitem.checked = checkbox_input.checked;
2849
+ let checkboxInput = subentry.querySelector('input');
2850
+ if( checkboxInput )
2851
+ {
2852
+ checkboxInput.checked = subitem.checked ?? false;
2853
+ checkboxInput.addEventListener('change', e => {
2854
+ subitem.checked = checkboxInput.checked;
2569
2855
  const f = subitem[ 'callback' ];
2570
- if(f) {
2856
+ if( f )
2857
+ {
2571
2858
  f.call( this, subitem.checked, subkey, subentry );
2572
- that.root.querySelectorAll(".lexcontextmenu").forEach(e => e.remove());
2859
+ _resetMenubar();
2573
2860
  }
2574
2861
  e.stopPropagation();
2575
2862
  e.stopImmediatePropagation();
2576
2863
  })
2577
2864
  }
2578
2865
 
2579
- contextmenu.appendChild( subentry );
2866
+ menuElement.appendChild( subentry );
2580
2867
 
2581
2868
  // Nothing more for separators
2582
- if(subkey == '') continue;
2869
+ if( subkey == '' )
2870
+ {
2871
+ continue;
2872
+ }
2583
2873
 
2584
- contextmenu.addEventListener('keydown', function(e) {
2874
+ menuElement.addEventListener('keydown', function(e) {
2585
2875
  e.preventDefault();
2586
2876
  let short = that.shorts[ subkey ];
2587
2877
  if(!short) return;
2588
2878
  // check if it's a letter or other key
2589
2879
  short = short.length == 1 ? short.toLowerCase() : short;
2590
- if(short == e.key) {
2880
+ if( short == e.key )
2881
+ {
2591
2882
  subentry.click()
2592
2883
  }
2593
2884
  });
2594
2885
 
2595
2886
  // Add callback
2596
2887
  subentry.addEventListener("click", e => {
2597
- if(checkbox_input) {
2888
+ if( checkboxInput )
2889
+ {
2598
2890
  subitem.checked = !subitem.checked;
2599
2891
  }
2600
2892
  const f = subitem[ 'callback' ];
2601
- if(f) {
2602
- f.call( this, checkbox_input ? subitem.checked : subkey, checkbox_input ? subkey : subentry );
2603
- that.root.querySelectorAll(".lexcontextmenu").forEach(e => e.remove());
2893
+ if( f )
2894
+ {
2895
+ f.call( this, checkboxInput ? subitem.checked : subkey, checkboxInput ? subkey : subentry );
2896
+ _resetMenubar();
2604
2897
  }
2605
2898
  e.stopPropagation();
2606
2899
  e.stopImmediatePropagation();
@@ -2609,7 +2902,8 @@ class Menubar {
2609
2902
  // Add icon if has submenu, else check for shortcut
2610
2903
  if( !hasSubmenu)
2611
2904
  {
2612
- if(that.shorts[ subkey ]) {
2905
+ if( that.shorts[ subkey ] )
2906
+ {
2613
2907
  let shortEl = document.createElement('div');
2614
2908
  shortEl.className = "lexentryshort";
2615
2909
  shortEl.innerText = that.shorts[ subkey ];
@@ -2623,43 +2917,66 @@ class Menubar {
2623
2917
  subentry.appendChild( submenuIcon );
2624
2918
 
2625
2919
  subentry.addEventListener("mouseover", e => {
2626
- if(subentry.built)
2627
- return;
2920
+ if( subentry.built )
2921
+ {
2922
+ return;
2923
+ }
2628
2924
  subentry.built = true;
2629
2925
  create_submenu( subitem, subkey, subentry, ++d );
2630
2926
  e.stopPropagation();
2631
2927
  });
2632
2928
 
2633
- subentry.addEventListener("mouseleave", () => {
2634
- d = -1; // Reset depth
2635
- delete subentry.built;
2636
- contextmenu.querySelectorAll(".lexcontextmenu").forEach(e => e.remove());
2929
+ subentry.addEventListener("mouseleave", (e) => {
2930
+ if( subentry.currentMenu && ( subentry.currentMenu != e.toElement ) )
2931
+ {
2932
+ d = -1; // Reset depth
2933
+ delete subentry.built;
2934
+ subentry.currentMenu.remove();
2935
+ delete subentry.currentMenu;
2936
+ }
2637
2937
  });
2638
2938
  }
2639
2939
 
2640
2940
  // Set final width
2641
- contextmenu.style.width = contextmenu.offsetWidth + "px";
2941
+ menuElement.style.width = menuElement.offsetWidth + "px";
2642
2942
  };
2643
2943
 
2644
- entry.addEventListener("click", () => {
2944
+ const _showEntry = () => {
2945
+ _resetMenubar();
2946
+ entry.classList.add( "selected" );
2947
+ entry.built = true;
2948
+ create_submenu( item, key, entry, -1 );
2949
+ };
2645
2950
 
2951
+ entry.addEventListener("click", () => {
2646
2952
  const f = item[ 'callback' ];
2647
- if(f) {
2953
+ if( f )
2954
+ {
2648
2955
  f.call( this, key, entry );
2649
2956
  return;
2650
2957
  }
2651
2958
 
2652
- // Manage selected
2653
- this.root.querySelectorAll(".lexmenuentry").forEach( e => e.classList.remove( 'selected' ) );
2654
- entry.classList.add( "selected" );
2959
+ _showEntry();
2655
2960
 
2656
- this.root.querySelectorAll(".lexcontextmenu").forEach( e => e.remove() );
2657
- create_submenu( item, key, entry, -1 );
2961
+ this.focused = true;
2962
+ });
2963
+
2964
+ entry.addEventListener( "mouseover", (e) => {
2965
+
2966
+ if( this.focused && !entry.built )
2967
+ {
2968
+ _showEntry();
2969
+ }
2658
2970
  });
2659
2971
 
2660
- entry.addEventListener("mouseleave", () => {
2661
- this.root.querySelectorAll(".lexmenuentry").forEach( e => e.classList.remove( 'selected' ) );
2662
- this.root.querySelectorAll(".lexcontextmenu").forEach(e => e.remove());
2972
+ entry.addEventListener("blur", (e) => {
2973
+
2974
+ if( e.relatedTarget && e.relatedTarget.classList.contains( "lexmenubox" ) )
2975
+ {
2976
+ return;
2977
+ }
2978
+
2979
+ _resetMenubar();
2663
2980
  });
2664
2981
  }
2665
2982
  }
@@ -2678,20 +2995,24 @@ class Menubar {
2678
2995
  * @param {Object} item: parent item
2679
2996
  * @param {Array} tokens: split path strings
2680
2997
  */
2681
- getSubitem(item, tokens) {
2998
+ getSubitem( item, tokens ) {
2682
2999
 
2683
3000
  let subitem = null;
2684
- let path = tokens[0];
2685
- for(let i = 0; i < item.length; i++) {
2686
- if(item[i][path]) {
3001
+ let path = tokens[ 0 ];
2687
3002
 
2688
- if(tokens.length == 1) {
2689
- subitem = item[i];
3003
+ for( let i = 0; i < item.length; i++ )
3004
+ {
3005
+ if( item[ i ][ path ] )
3006
+ {
3007
+ if( tokens.length == 1 )
3008
+ {
3009
+ subitem = item[ i ];
2690
3010
  return subitem;
2691
3011
  }
2692
- else {
2693
- tokens.splice(0,1);
2694
- return this.getSubitem(item[i][path], tokens);
3012
+ else
3013
+ {
3014
+ tokens.splice( 0, 1 );
3015
+ return this.getSubitem( item[ i ][ path ], tokens );
2695
3016
  }
2696
3017
 
2697
3018
  }
@@ -2718,11 +3039,12 @@ class Menubar {
2718
3039
  setButtonIcon( title, icon, callback, options = {} ) {
2719
3040
 
2720
3041
  const button = this.buttons[ title ];
2721
- if(button) {
2722
-
3042
+ if( button )
3043
+ {
2723
3044
  button.querySelector('a').className = "fa-solid" + " " + icon + " lexicon";
2724
3045
  }
2725
- else {
3046
+ else
3047
+ {
2726
3048
  let button = document.createElement('div');
2727
3049
  const disabled = options.disabled ?? false;
2728
3050
  button.className = "lexmenubutton" + (disabled ? " disabled" : "");
@@ -2732,15 +3054,21 @@ class Menubar {
2732
3054
  button.style.maxHeight = "calc(100% - 10px)";
2733
3055
  button.style.alignItems = "center";
2734
3056
 
2735
- if(options.float == "right")
3057
+ if( options.float == "right" )
3058
+ {
2736
3059
  button.right = true;
2737
- if(this.root.lastChild && this.root.lastChild.right) {
3060
+ }
3061
+
3062
+ if( this.root.lastChild && this.root.lastChild.right )
3063
+ {
2738
3064
  this.root.lastChild.before( button );
2739
3065
  }
2740
- else if(options.float == "left") {
2741
- this.root.prepend(button);
3066
+ else if( options.float == "left" )
3067
+ {
3068
+ this.root.prepend( button );
2742
3069
  }
2743
- else {
3070
+ else
3071
+ {
2744
3072
  this.root.appendChild( button );
2745
3073
  }
2746
3074
 
@@ -2760,11 +3088,12 @@ class Menubar {
2760
3088
 
2761
3089
  setButtonImage( title, src, callback, options = {} ) {
2762
3090
  const button = this.buttons[ title ];
2763
- if(button) {
2764
-
3091
+ if( button )
3092
+ {
2765
3093
  button.querySelector('a').className = "fa-solid" + " " + icon + " lexicon";
2766
3094
  }
2767
- else {
3095
+ else
3096
+ {
2768
3097
  let button = document.createElement('div');
2769
3098
  const disabled = options.disabled ?? false;
2770
3099
  button.className = "lexmenubutton" + (disabled ? " disabled" : "");
@@ -2773,15 +3102,21 @@ class Menubar {
2773
3102
  button.style.padding = "5px";
2774
3103
  button.style.alignItems = "center";
2775
3104
 
2776
- if(options.float == "right")
3105
+ if( options.float == "right" )
3106
+ {
2777
3107
  button.right = true;
2778
- if(this.root.lastChild && this.root.lastChild.right) {
3108
+ }
3109
+
3110
+ if( this.root.lastChild && this.root.lastChild.right )
3111
+ {
2779
3112
  this.root.lastChild.before( button );
2780
3113
  }
2781
- else if(options.float == "left") {
2782
- this.root.prepend(button);
3114
+ else if( options.float == "left" )
3115
+ {
3116
+ this.root.prepend( button );
2783
3117
  }
2784
- else {
3118
+ else
3119
+ {
2785
3120
  this.root.appendChild( button );
2786
3121
  }
2787
3122
 
@@ -2894,90 +3229,306 @@ LX.Menubar = Menubar;
2894
3229
 
2895
3230
  class SideBar {
2896
3231
 
2897
- constructor( options = {} ) {
3232
+ /**
3233
+ * @param {Object} options
3234
+ * inset: TODO
3235
+ * filter: TODO
3236
+ * skipHeader: Do not use sidebar header [false]
3237
+ * headerImg: Image to be shown as avatar
3238
+ * headerIcon: Icon to be shown as avatar (from LX.ICONS)
3239
+ * headerTitle
3240
+ * headerSubtitle
3241
+ * skipFooter: Do not use sidebar footer [false]
3242
+ * footerImg: Image to be shown as avatar
3243
+ * footerIcon: Icon to be shown as avatar (from LX.ICONS)
3244
+ * footerTitle
3245
+ * footerSubtitle
3246
+ * collapsable: Sidebar can toggle between collapsed/expanded [true]
3247
+ * collapseToIcons: When Sidebar collapses, icons remains visible [true]
3248
+ * onHeaderPressed: Function to call when header is pressed
3249
+ * onFooterPressed: Function to call when footer is pressed
3250
+ */
3251
+
3252
+ constructor( options = {} ) {
2898
3253
 
2899
3254
  this.root = document.createElement( 'div' );
2900
3255
  this.root.className = "lexsidebar";
2901
3256
 
2902
- this.footer = document.createElement( 'div' );
2903
- this.footer.className = "lexsidebarfooter";
2904
- this.root.appendChild( this.footer );
3257
+ window.sidebar = this;
2905
3258
 
2906
- this.items = [ ];
2907
- }
3259
+ this.collapsable = options.collapsable ?? true;
3260
+ this.collapseWidth = ( options.collapseToIcons ?? true ) ? "58px" : "0px";
3261
+ this.collapsed = false;
2908
3262
 
2909
- /**
2910
- * @method add
2911
- * @param {*} options:
2912
- * callback: Function to call on each item
2913
- * bottom: Bool to set item at the bottom as helper button (not selectable)
2914
- * className: Add class to the entry DOM element
2915
- */
3263
+ doAsync( () => {
2916
3264
 
2917
- add( key, options = {} ) {
3265
+ this.root.parentElement.ogWidth = this.root.parentElement.style.width;
3266
+ this.root.parentElement.style.transition = "width 0.25s ease-out";
2918
3267
 
2919
- if( options.constructor == Function )
2920
- options = { callback: options };
3268
+ this.resizeObserver = new ResizeObserver( entries => {
3269
+ for ( const entry of entries )
3270
+ {
3271
+ this.siblingArea.setSize( [ "calc(100% - " + ( entry.contentRect.width ) + "px )", null ] );
3272
+ }
3273
+ });
3274
+
3275
+ }, 100 );
3276
+
3277
+ // This account for header, footer and all inner paddings
3278
+ let contentOffset = 32;
3279
+
3280
+ // Header
3281
+ if( !( options.skipHeader ?? false ) )
3282
+ {
3283
+ this.header = document.createElement( 'div' );
3284
+ this.header.className = "lexsidebarheader";
3285
+ this.root.appendChild( this.header );
3286
+
3287
+ this.header.addEventListener( "click", e => {
3288
+ if( this.collapsed )
3289
+ {
3290
+ e.preventDefault();
3291
+ e.stopPropagation();
3292
+ this.toggleCollapsed();
3293
+ }
3294
+ else if( options.onHeaderPressed )
3295
+ {
3296
+ options.onHeaderPressed( e );
3297
+ }
3298
+ } );
3299
+
3300
+ const avatar = document.createElement( 'span' );
3301
+ avatar.className = "lexavatar";
3302
+ this.header.appendChild( avatar );
3303
+
3304
+ if( options.headerImage )
3305
+ {
3306
+ const avatarImg = document.createElement( 'img' );
3307
+ avatarImg.src = options.headerImage;
3308
+ avatar.appendChild( avatarImg );
3309
+ }
3310
+ else if( options.headerIcon )
3311
+ {
3312
+ const avatarIcon = LX.makeIcon( options.headerIcon );
3313
+ avatar.appendChild( avatarIcon );
3314
+ }
3315
+
3316
+ // Info
3317
+ {
3318
+ const info = document.createElement( 'div' );
3319
+ this.header.appendChild( info );
3320
+
3321
+ const infoText = document.createElement( 'span' );
3322
+ infoText.innerHTML = options.headerTitle ?? "";
3323
+ info.appendChild( infoText );
3324
+
3325
+ const infoSubtext = document.createElement( 'span' );
3326
+ infoSubtext.innerHTML = options.headerSubtitle ?? "";
3327
+ info.appendChild( infoSubtext );
3328
+ }
3329
+
3330
+ if( this.collapsable )
3331
+ {
3332
+ const icon = LX.makeIcon( "Sidebar", "Toggle Sidebar" );
3333
+ this.header.appendChild( icon );
3334
+
3335
+ icon.addEventListener( "click", (e) => {
3336
+ e.preventDefault();
3337
+ e.stopPropagation();
3338
+ this.toggleCollapsed();
3339
+ } );
3340
+ }
3341
+
3342
+ contentOffset += 52;
3343
+ }
3344
+
3345
+ // Content
3346
+ {
3347
+ this.content = document.createElement( 'div' );
3348
+ this.content.className = "lexsidebarcontent";
3349
+ this.root.appendChild( this.content );
3350
+ }
3351
+
3352
+ // Footer
3353
+ if( !( options.skipFooter ?? false ) )
3354
+ {
3355
+ this.footer = document.createElement( 'div' );
3356
+ this.footer.className = "lexsidebarfooter";
3357
+ this.root.appendChild( this.footer );
3358
+
3359
+ this.footer.addEventListener( "click", e => {
3360
+ if( options.onFooterPressed )
3361
+ {
3362
+ options.onFooterPressed( e );
3363
+ }
3364
+ } );
2921
3365
 
2922
- let pKey = key.replace( /\s/g, '' ).replaceAll( '.', '' );
3366
+ const avatar = document.createElement( 'span' );
3367
+ avatar.className = "lexavatar";
3368
+ this.footer.appendChild( avatar );
2923
3369
 
2924
- if( this.items.findIndex( (v, i) => v.key == pKey ) > -1 )
3370
+ if( options.footerImage )
3371
+ {
3372
+ const avatarImg = document.createElement( 'img' );
3373
+ avatarImg.src = options.footerImage;
3374
+ avatar.appendChild( avatarImg );
3375
+ }
3376
+ else if( options.footerIcon )
3377
+ {
3378
+ const avatarIcon = LX.makeIcon( options.footerIcon );
3379
+ avatar.appendChild( avatarIcon );
3380
+ }
3381
+
3382
+ // Info
3383
+ {
3384
+ const info = document.createElement( 'div' );
3385
+ this.footer.appendChild( info );
3386
+
3387
+ const infoText = document.createElement( 'span' );
3388
+ infoText.innerHTML = options.footerTitle ?? "";
3389
+ info.appendChild( infoText );
3390
+
3391
+ const infoSubtext = document.createElement( 'span' );
3392
+ infoSubtext.innerHTML = options.footerSubtitle ?? "";
3393
+ info.appendChild( infoSubtext );
3394
+ }
3395
+
3396
+ const icon = LX.makeIcon( "MenuArrows" );
3397
+ this.footer.appendChild( icon );
3398
+
3399
+ contentOffset += 52;
3400
+ }
3401
+
3402
+ // Set width depending on header/footer
3403
+ this.content.style.height = `calc(100% - ${ contentOffset }px)`;
3404
+
3405
+ this.items = [ ];
3406
+ this.icons = { };
3407
+ this.groups = { };
3408
+ }
3409
+
3410
+ /**
3411
+ * @method toggleCollapsed
3412
+ */
3413
+
3414
+ toggleCollapsed() {
3415
+
3416
+ if( !this.collapsable )
2925
3417
  {
2926
- console.warn( `'${key}' already created in Sidebar` );
2927
3418
  return;
2928
3419
  }
2929
3420
 
2930
- let entry = document.createElement( 'div' );
2931
- entry.className = "lexsidebarentry " + ( options.className ?? "" );
2932
- entry.id = pKey;
3421
+ this.collapsed = !this.collapsed;
2933
3422
 
2934
- if( options.bottom )
3423
+ if( this.collapsed )
2935
3424
  {
2936
- this.footer.appendChild( entry );
3425
+ this.root.classList.add( "collapsing" );
3426
+ this.root.parentElement.style.width = this.collapseWidth;
2937
3427
  }
2938
3428
  else
2939
3429
  {
2940
- this.root.appendChild( entry );
3430
+ this.root.classList.remove( "collapsing" );
3431
+ this.root.classList.remove( "collapsed" );
3432
+ this.root.parentElement.style.width = this.root.parentElement.ogWidth;
2941
3433
  }
2942
3434
 
2943
- // Reappend footer in root
2944
- this.root.appendChild( this.footer );
3435
+ this.resizeObserver.observe( this.root.parentElement );
2945
3436
 
2946
- let button = document.createElement( 'button' );
2947
- button.innerHTML = "<i class='"+ (options.icon ?? "") + "'></i>";
2948
- entry.appendChild( button );
3437
+ doAsync( () => {
2949
3438
 
2950
- let desc = document.createElement( 'span' );
2951
- desc.className = 'lexsidebarentrydesc';
2952
- desc.innerHTML = key;
2953
- entry.appendChild( desc );
3439
+ this.root.classList.toggle( "collapsed", this.collapsed );
3440
+ this.resizeObserver.unobserve( this.root.parentElement );
2954
3441
 
2955
- button.addEventListener("mouseenter", () => {
2956
- setTimeout( () => {
2957
- desc.style.display = "unset";
2958
- }, 100 );
2959
- });
3442
+ }, 250 );
3443
+ }
2960
3444
 
2961
- button.addEventListener("mouseleave", () => {
2962
- setTimeout( () => {
2963
- desc.style.display = "none";
2964
- }, 100 );
2965
- });
3445
+ /**
3446
+ * @method separator
3447
+ */
3448
+
3449
+ separator() {
3450
+
3451
+ this.currentGroup = null;
3452
+
3453
+ this.add( "" );
3454
+ }
2966
3455
 
2967
- entry.addEventListener("click", () => {
3456
+ /**
3457
+ * @method group
3458
+ * @param {String} groupName
3459
+ * @param {Object} action: { icon, callback }
3460
+ */
3461
+
3462
+ group( groupName, action ) {
2968
3463
 
2969
- const f = options.callback;
2970
- if( f ) f.call( this, key, entry );
3464
+ this.currentGroup = groupName;
3465
+
3466
+ this.groups[ groupName ] = action;
3467
+ }
2971
3468
 
2972
- // Manage selected
2973
- if( !options.bottom )
3469
+ /**
3470
+ * @method add
3471
+ * @param {String} path
3472
+ * @param {Object} options:
3473
+ * callback: Function to call on each item
3474
+ * icon: Entry icon
3475
+ * collapsable: Add entry as a collapsable section
3476
+ * className: Add class to the entry DOM element
3477
+ */
3478
+
3479
+ add( path, options = {} ) {
3480
+
3481
+ if( options.constructor == Function )
3482
+ {
3483
+ options = { callback: options };
3484
+ }
3485
+
3486
+ // Process path
3487
+ const tokens = path.split( "/" );
3488
+
3489
+ // Assign icons and shortcuts to last token in path
3490
+ const lastPath = tokens[tokens.length - 1];
3491
+ this.icons[ lastPath ] = options.icon;
3492
+
3493
+
3494
+ let idx = 0;
3495
+
3496
+ const _insertEntry = ( token, list ) => {
3497
+
3498
+ if( token == undefined )
2974
3499
  {
2975
- this.root.querySelectorAll(".lexsidebarentry").forEach( e => e.classList.remove( 'selected' ) );
2976
- entry.classList.add( "selected" );
3500
+ return;
2977
3501
  }
2978
- });
2979
3502
 
2980
- this.items.push( { name: pKey, domEl: entry, callback: options.callback } );
3503
+ let found = null;
3504
+ list.forEach( o => {
3505
+ const keys = Object.keys( o );
3506
+ const key = keys.find( t => t == token );
3507
+ if( key ) found = o[ key ];
3508
+ } );
3509
+
3510
+ if( found )
3511
+ {
3512
+ _insertEntry( tokens[ idx++ ], found );
3513
+ }
3514
+ else
3515
+ {
3516
+ let item = {};
3517
+ item[ token ] = [];
3518
+ const nextToken = tokens[ idx++ ];
3519
+ // Check if last token -> add callback
3520
+ if( !nextToken )
3521
+ {
3522
+ item[ 'callback' ] = options.callback;
3523
+ item[ 'group' ] = this.currentGroup;
3524
+ item[ 'options' ] = options;
3525
+ }
3526
+ list.push( item );
3527
+ _insertEntry( nextToken, item[ token ] );
3528
+ }
3529
+ };
3530
+
3531
+ _insertEntry( tokens[idx++], this.items );
2981
3532
  }
2982
3533
 
2983
3534
  /**
@@ -2996,6 +3547,246 @@ class SideBar {
2996
3547
 
2997
3548
  entry.domEl.click();
2998
3549
  }
3550
+
3551
+ _build() {
3552
+
3553
+ for( let item of this.items )
3554
+ {
3555
+ const options = item.options ?? { };
3556
+
3557
+ // Item already created
3558
+ if( item.dom )
3559
+ {
3560
+ continue;
3561
+ }
3562
+
3563
+ let key = Object.keys( item )[ 0 ];
3564
+ let pKey = key.replace( /\s/g, '' ).replaceAll( '.', '' );
3565
+ let currentGroup = null;
3566
+
3567
+ let entry = document.createElement( 'div' );
3568
+ entry.className = "lexsidebarentry " + ( options.className ?? "" );
3569
+ entry.id = pKey;
3570
+
3571
+ if( item.group )
3572
+ {
3573
+ const pGroupKey = item.group.replace( /\s/g, '' ).replaceAll( '.', '' );
3574
+ currentGroup = this.content.querySelector( "#" + pGroupKey );
3575
+
3576
+ if( !currentGroup )
3577
+ {
3578
+ currentGroup = document.createElement( 'div' );
3579
+ currentGroup.id = pGroupKey;
3580
+ currentGroup.className = "lexsidebargroup";
3581
+ this.content.appendChild( currentGroup );
3582
+
3583
+ let groupEntry = document.createElement( 'div' );
3584
+ groupEntry.className = "lexsidebargrouptitle";
3585
+ currentGroup.appendChild( groupEntry );
3586
+
3587
+ let groupLabel = document.createElement( 'div' );
3588
+ groupLabel.innerHTML = item.group;
3589
+ groupEntry.appendChild( groupLabel );
3590
+
3591
+ if( this.groups[ item.group ] != null )
3592
+ {
3593
+ let groupAction = document.createElement( 'a' );
3594
+ groupAction.className = ( this.groups[ item.group ].icon ?? "" ) + " lexicon";
3595
+ groupEntry.appendChild( groupAction );
3596
+ groupAction.addEventListener( "click", (e) => {
3597
+ if( this.groups[ item.group ].callback )
3598
+ {
3599
+ this.groups[ item.group ].callback( item.group, e );
3600
+ }
3601
+ } );
3602
+ }
3603
+
3604
+ }
3605
+ else if( !currentGroup.classList.contains( "lexsidebargroup" ) )
3606
+ {
3607
+ throw( "Bad id: " + item.group );
3608
+ }
3609
+ }
3610
+
3611
+ if( pKey == "" )
3612
+ {
3613
+ let separatorDom = document.createElement( 'div' );
3614
+ separatorDom.className = "lexsidebarseparator";
3615
+ this.content.appendChild( separatorDom );
3616
+ continue;
3617
+ }
3618
+
3619
+ if( this.collapseContainer )
3620
+ {
3621
+ this.collapseContainer.appendChild( entry );
3622
+
3623
+ this.collapseQueue--;
3624
+ if( !this.collapseQueue )
3625
+ {
3626
+ delete this.collapseContainer;
3627
+ }
3628
+ }
3629
+ else if( currentGroup )
3630
+ {
3631
+ currentGroup.appendChild( entry );
3632
+ }
3633
+ else
3634
+ {
3635
+ this.content.appendChild( entry );
3636
+ }
3637
+
3638
+ let itemDom = document.createElement( 'div' );
3639
+ entry.appendChild( itemDom );
3640
+ item.dom = itemDom;
3641
+
3642
+ if( options.type == "checkbox" )
3643
+ {
3644
+ item.value = options.value ?? false;
3645
+ const panel = new Panel();
3646
+ item.checkbox = panel.addCheckbox(null, item.value, (value, event) => {
3647
+ event.preventDefault();
3648
+ event.stopPropagation();
3649
+ const f = options.callback;
3650
+ item.value = value;
3651
+ if( f ) f.call( this, key, value, event );
3652
+ }, { label: key, signal: ( "@checkbox_" + key ) });
3653
+ itemDom.appendChild( panel.root.childNodes[ 0 ] );
3654
+ }
3655
+ else
3656
+ {
3657
+ if( options.icon )
3658
+ {
3659
+ let itemIcon = document.createElement( 'i' );
3660
+ itemIcon.className = options.icon;
3661
+ itemDom.appendChild( itemIcon );
3662
+ }
3663
+
3664
+ let itemName = document.createElement( 'a' );
3665
+ itemName.innerHTML = key;
3666
+ itemDom.appendChild( itemName );
3667
+ }
3668
+
3669
+ entry.addEventListener("click", ( e ) => {
3670
+ if( e.target && e.target.classList.contains( "lexcheckbox" ) )
3671
+ {
3672
+ return;
3673
+ }
3674
+
3675
+ if( options.collapsable )
3676
+ {
3677
+ itemDom.querySelector( ".collapser" ).click();
3678
+ }
3679
+ else
3680
+ {
3681
+ const f = options.callback;
3682
+ if( f ) f.call( this, key, item.value, e );
3683
+
3684
+ if( item.checkbox )
3685
+ {
3686
+ item.value = !item.value;
3687
+ item.checkbox.set( item.value, true );
3688
+ }
3689
+ }
3690
+
3691
+ // Manage selected
3692
+ this.root.querySelectorAll(".lexsidebarentry").forEach( e => e.classList.remove( 'selected' ) );
3693
+ entry.classList.add( "selected" );
3694
+ });
3695
+
3696
+ if( options.action )
3697
+ {
3698
+ const actionIcon = LX.makeIcon( options.action.icon ?? "MoreHorizontal", options.action.name );
3699
+ itemDom.appendChild( actionIcon );
3700
+
3701
+ actionIcon.addEventListener( "click", (e) => {
3702
+ e.preventDefault();
3703
+ e.stopImmediatePropagation();
3704
+ const f = options.action.callback;
3705
+ if( f ) f.call( this, key, e );
3706
+ } );
3707
+ }
3708
+ else if( options.collapsable )
3709
+ {
3710
+ const collapsableContent = document.createElement( 'div' );
3711
+ Object.assign( collapsableContent.style, { width: "100%", display: "none" } );
3712
+ LX.makeCollapsible( itemDom, collapsableContent, currentGroup ?? this.content );
3713
+ this.collapseQueue = options.collapsable;
3714
+ this.collapseContainer = collapsableContent;
3715
+ }
3716
+
3717
+ let desc = document.createElement( 'span' );
3718
+ desc.className = 'lexsidebarentrydesc';
3719
+ desc.innerHTML = key;
3720
+ entry.appendChild( desc );
3721
+
3722
+ itemDom.addEventListener("mouseenter", () => {
3723
+ setTimeout( () => {
3724
+ desc.style.display = "unset";
3725
+ }, 150 );
3726
+ });
3727
+
3728
+ itemDom.addEventListener("mouseleave", () => {
3729
+ setTimeout( () => {
3730
+ desc.style.display = "none";
3731
+ }, 150 );
3732
+ });
3733
+
3734
+ // Subentries
3735
+ if( !item[ key ].length )
3736
+ {
3737
+ continue;
3738
+ }
3739
+
3740
+ let subentryContainer = document.createElement( 'div' );
3741
+ subentryContainer.className = "lexsidebarsubentrycontainer";
3742
+
3743
+ if( currentGroup )
3744
+ {
3745
+ currentGroup.appendChild( subentryContainer );
3746
+ }
3747
+ else
3748
+ {
3749
+ this.content.appendChild( subentryContainer );
3750
+ }
3751
+
3752
+ for( var i = 0; i < item[ key ].length; ++i )
3753
+ {
3754
+ const subitem = item[ key ][ i ];
3755
+ const suboptions = subitem.options ?? {};
3756
+ const subkey = Object.keys( subitem )[ 0 ];
3757
+
3758
+ let subentry = document.createElement( 'div' );
3759
+ subentry.innerHTML = `<span>${ subkey }</span>`;
3760
+
3761
+ if( suboptions.action )
3762
+ {
3763
+ const actionIcon = LX.makeIcon( suboptions.action.icon ?? "MoreHorizontal", suboptions.action.name );
3764
+ subentry.appendChild( actionIcon );
3765
+
3766
+ actionIcon.addEventListener( "click", (e) => {
3767
+ e.preventDefault();
3768
+ e.stopImmediatePropagation();
3769
+ const f = suboptions.action.callback;
3770
+ if( f ) f.call( this, subkey, e );
3771
+ } );
3772
+ }
3773
+
3774
+ subentry.className = "lexsidebarentry";
3775
+ subentry.id = subkey;
3776
+ subentryContainer.appendChild( subentry );
3777
+
3778
+ subentry.addEventListener("click", (e) => {
3779
+
3780
+ const f = suboptions.callback;
3781
+ if( f ) f.call( this, subkey, subentry, e );
3782
+
3783
+ // Manage selected
3784
+ this.root.querySelectorAll(".lexsidebarentry").forEach( e => e.classList.remove( 'selected' ) );
3785
+ entry.classList.add( "selected" );
3786
+ });
3787
+ }
3788
+ }
3789
+ }
2999
3790
  };
3000
3791
 
3001
3792
  LX.SideBar = SideBar;
@@ -3013,30 +3804,32 @@ class Widget {
3013
3804
  static DROPDOWN = 4;
3014
3805
  static CHECKBOX = 5;
3015
3806
  static TOGGLE = 6;
3016
- static COLOR = 7;
3017
- static NUMBER = 8;
3018
- static TITLE = 9;
3019
- static VECTOR = 10;
3020
- static TREE = 11;
3021
- static PROGRESS = 12;
3022
- static FILE = 13;
3023
- static LAYERS = 14;
3024
- static ARRAY = 15;
3025
- static LIST = 16;
3026
- static TAGS = 17;
3027
- static CURVE = 18;
3028
- static CARD = 19;
3029
- static IMAGE = 20;
3030
- static CONTENT = 21;
3031
- static CUSTOM = 22;
3032
- static SEPARATOR = 23;
3033
- static KNOB = 24;
3034
- static SIZE = 25;
3035
- static PAD = 26;
3036
- static FORM = 27;
3037
- static DIAL = 28;
3038
- static COUNTER = 29;
3039
- static TABLE = 30;
3807
+ static RADIO = 7;
3808
+ static COLOR = 8;
3809
+ static RANGE = 9;
3810
+ static NUMBER = 10;
3811
+ static TITLE = 11;
3812
+ static VECTOR = 12;
3813
+ static TREE = 13;
3814
+ static PROGRESS = 14;
3815
+ static FILE = 15;
3816
+ static LAYERS = 16;
3817
+ static ARRAY = 17;
3818
+ static LIST = 18;
3819
+ static TAGS = 19;
3820
+ static CURVE = 20;
3821
+ static CARD = 21;
3822
+ static IMAGE = 22;
3823
+ static CONTENT = 23;
3824
+ static CUSTOM = 24;
3825
+ static SEPARATOR = 25;
3826
+ static KNOB = 26;
3827
+ static SIZE = 27;
3828
+ static PAD = 28;
3829
+ static FORM = 29;
3830
+ static DIAL = 30;
3831
+ static COUNTER = 31;
3832
+ static TABLE = 32;
3040
3833
 
3041
3834
  static NO_CONTEXT_TYPES = [
3042
3835
  Widget.BUTTON,
@@ -3064,7 +3857,9 @@ class Widget {
3064
3857
  set( value, skipCallback = false, signalName = "" ) {
3065
3858
 
3066
3859
  if( this.onSetValue )
3860
+ {
3067
3861
  return this.onSetValue( value, skipCallback );
3862
+ }
3068
3863
 
3069
3864
  console.warn("Can't set value of " + this.typeName());
3070
3865
  }
@@ -3103,14 +3898,17 @@ class Widget {
3103
3898
 
3104
3899
  typeName() {
3105
3900
 
3106
- switch( this.type ) {
3901
+ switch( this.type )
3902
+ {
3107
3903
  case Widget.TEXT: return "Text";
3108
3904
  case Widget.TEXTAREA: return "TextArea";
3109
3905
  case Widget.BUTTON: return "Button";
3110
3906
  case Widget.DROPDOWN: return "Dropdown";
3111
3907
  case Widget.CHECKBOX: return "Checkbox";
3112
3908
  case Widget.TOGGLE: return "Toggle";
3909
+ case Widget.RADIO: return "Radio";
3113
3910
  case Widget.COLOR: return "Color";
3911
+ case Widget.RANGE: return "Range";
3114
3912
  case Widget.NUMBER: return "Number";
3115
3913
  case Widget.VECTOR: return "Vector";
3116
3914
  case Widget.TREE: return "Tree";
@@ -3188,11 +3986,12 @@ function ADD_CUSTOM_WIDGET( custom_widget_name, options = {} )
3188
3986
  buttonName += "<a class='fa-solid " + (instance ? "fa-bars-staggered" : " ") + " menu' style='float:right; width:5%;'></a>";
3189
3987
 
3190
3988
  let buttonEl = this.addButton(null, buttonName, (value, event) => {
3191
-
3192
- if( instance ) {
3989
+ if( instance )
3990
+ {
3193
3991
  element.querySelector(".lexcustomitems").toggleAttribute('hidden');
3194
3992
  }
3195
- else {
3993
+ else
3994
+ {
3196
3995
  addContextMenu(null, event, c => {
3197
3996
  c.add("New " + custom_widget_name, () => {
3198
3997
  instance = {};
@@ -3379,16 +4178,20 @@ class NodeTree {
3379
4178
 
3380
4179
  // Add or remove
3381
4180
  const idx = this.selected.indexOf( node );
3382
- if( idx > -1 ) {
4181
+ if( idx > -1 )
4182
+ {
3383
4183
  item.classList.remove( 'selected' );
3384
4184
  this.selected.splice( idx, 1 );
3385
- }else {
4185
+ }
4186
+ else
4187
+ {
3386
4188
  item.classList.add( 'selected' );
3387
4189
  this.selected.push( node );
3388
4190
  }
3389
4191
 
3390
4192
  // Only Show children...
3391
- if( isParent && node.id.length > 1 /* Strange case... */) {
4193
+ if( isParent && node.id.length > 1 /* Strange case... */)
4194
+ {
3392
4195
  node.closed = false;
3393
4196
  if( that.onevent )
3394
4197
  {
@@ -3426,7 +4229,7 @@ class NodeTree {
3426
4229
 
3427
4230
  e.preventDefault();
3428
4231
 
3429
- if( that.onevent )
4232
+ if( !that.onevent )
3430
4233
  {
3431
4234
  return;
3432
4235
  }
@@ -3482,7 +4285,8 @@ class NodeTree {
3482
4285
  return;
3483
4286
  }
3484
4287
 
3485
- if( that.onevent ) {
4288
+ if( that.onevent )
4289
+ {
3486
4290
  const event = new TreeEvent( TreeEvent.NODE_DELETED, node, e );
3487
4291
  that.onevent( event );
3488
4292
  }
@@ -3509,7 +4313,8 @@ class NodeTree {
3509
4313
  if( e.key == "Delete" )
3510
4314
  {
3511
4315
  // Send event now so we have the info in selected array..
3512
- if( that.onevent ) {
4316
+ if( that.onevent )
4317
+ {
3513
4318
  const event = new TreeEvent( TreeEvent.NODE_DELETED, this.selected.length > 1 ? this.selected : node, e );
3514
4319
  event.multiple = this.selected.length > 1;
3515
4320
  that.onevent( event );
@@ -3540,22 +4345,24 @@ class NodeTree {
3540
4345
 
3541
4346
  // Node rename
3542
4347
 
3543
- let name_input = document.createElement('input');
3544
- name_input.toggleAttribute('hidden', !node.rename);
3545
- name_input.value = node.id;
3546
- item.appendChild(name_input);
4348
+ const nameInput = document.createElement( "input" );
4349
+ nameInput.toggleAttribute( "hidden", !node.rename );
4350
+ nameInput.value = node.id;
4351
+ item.appendChild(nameInput);
3547
4352
 
3548
- if(node.rename) {
4353
+ if( node.rename )
4354
+ {
3549
4355
  item.classList.add('selected');
3550
- name_input.focus();
4356
+ nameInput.focus();
3551
4357
  }
3552
4358
 
3553
- name_input.addEventListener("keyup", function(e){
3554
- if(e.key == 'Enter') {
3555
-
4359
+ nameInput.addEventListener("keyup", function(e){
4360
+ if( e.key == "Enter" )
4361
+ {
3556
4362
  this.value = this.value.replace(/\s/g, '_');
3557
4363
 
3558
- if(that.onevent) {
4364
+ if( that.onevent )
4365
+ {
3559
4366
  const event = new TreeEvent(TreeEvent.NODE_RENAMED, node, this.value);
3560
4367
  that.onevent( event );
3561
4368
  }
@@ -3565,18 +4372,20 @@ class NodeTree {
3565
4372
  that.frefresh( node.id );
3566
4373
  list.querySelector("#" + node.id).classList.add('selected');
3567
4374
  }
3568
- if(e.key == 'Escape') {
4375
+ else if(e.key == "Escape")
4376
+ {
3569
4377
  delete node.rename;
3570
4378
  that.frefresh( node.id );
3571
4379
  }
3572
4380
  });
3573
4381
 
3574
- name_input.addEventListener("blur", function(e){
4382
+ nameInput.addEventListener("blur", function(e){
3575
4383
  delete node.rename;
3576
4384
  that.refresh();
3577
4385
  });
3578
4386
 
3579
- if(this.options.draggable ?? true) {
4387
+ if( this.options.draggable ?? true )
4388
+ {
3580
4389
  // Drag nodes
3581
4390
  if(parent) // Root doesn't move!
3582
4391
  {
@@ -3602,29 +4411,32 @@ class NodeTree {
3602
4411
  return;
3603
4412
  let target = node;
3604
4413
  // Can't drop to same node
3605
- if(dragged.id == target.id) {
4414
+ if( dragged.id == target.id )
4415
+ {
3606
4416
  console.warn("Cannot parent node to itself!");
3607
4417
  return;
3608
4418
  }
3609
4419
 
3610
4420
  // Can't drop to child node
3611
- const isChild = function(new_parent, node) {
4421
+ const isChild = function( newParent, node ) {
3612
4422
  var result = false;
3613
- for( var c of node.children ) {
3614
- if( c.id == new_parent.id )
3615
- return true;
3616
- result |= isChild(new_parent, c);
4423
+ for( var c of node.children )
4424
+ {
4425
+ if( c.id == newParent.id ) return true;
4426
+ result |= isChild( newParent, c );
3617
4427
  }
3618
4428
  return result;
3619
4429
  };
3620
4430
 
3621
- if(isChild(target, dragged)) {
4431
+ if( isChild( target, dragged ))
4432
+ {
3622
4433
  console.warn("Cannot parent node to a current child!");
3623
4434
  return;
3624
4435
  }
3625
4436
 
3626
4437
  // Trigger node dragger event
3627
- if(that.onevent) {
4438
+ if( that.onevent )
4439
+ {
3628
4440
  const event = new TreeEvent(TreeEvent.NODE_DRAGGED, dragged, target);
3629
4441
  that.onevent( event );
3630
4442
  }
@@ -3640,7 +4452,8 @@ class NodeTree {
3640
4452
  let handled = false;
3641
4453
 
3642
4454
  // Show/hide children
3643
- if(isParent) {
4455
+ if( isParent )
4456
+ {
3644
4457
  item.querySelector('a.hierarchy').addEventListener("click", function(e) {
3645
4458
 
3646
4459
  handled = true;
@@ -3648,7 +4461,8 @@ class NodeTree {
3648
4461
  e.stopPropagation();
3649
4462
 
3650
4463
  node.closed = !node.closed;
3651
- if(that.onevent) {
4464
+ if( that.onevent )
4465
+ {
3652
4466
  const event = new TreeEvent(TreeEvent.NODE_CARETCHANGED, node, node.closed);
3653
4467
  that.onevent( event );
3654
4468
  }
@@ -3668,7 +4482,8 @@ class NodeTree {
3668
4482
  node.visible = node.visible === undefined ? false : !node.visible;
3669
4483
  this.className = "itemicon fa-solid fa-eye" + (!node.visible ? "-slash" : "");
3670
4484
  // Trigger visibility event
3671
- if(that.onevent) {
4485
+ if( that.onevent )
4486
+ {
3672
4487
  const event = new TreeEvent(TreeEvent.NODE_VISIBILITY, node, node.visible);
3673
4488
  that.onevent( event );
3674
4489
  }
@@ -3677,9 +4492,10 @@ class NodeTree {
3677
4492
  item.appendChild(visibility);
3678
4493
  }
3679
4494
 
3680
- if(node.actions)
4495
+ if( node.actions )
3681
4496
  {
3682
- for(var i = 0; i < node.actions.length; ++i) {
4497
+ for( var i = 0; i < node.actions.length; ++i )
4498
+ {
3683
4499
  let a = node.actions[i];
3684
4500
  var actionEl = document.createElement('a');
3685
4501
  actionEl.className = "itemicon " + a.icon;
@@ -3692,20 +4508,25 @@ class NodeTree {
3692
4508
  }
3693
4509
  }
3694
4510
 
3695
- if(selectedId != undefined && node.id == selectedId) {
3696
- this.selected = [node];
4511
+ if( selectedId != undefined && node.id == selectedId )
4512
+ {
4513
+ this.selected = [ node ];
3697
4514
  item.click();
3698
4515
  }
3699
4516
 
3700
- if(node.closed )
4517
+ if( node.closed )
4518
+ {
3701
4519
  return;
4520
+ }
3702
4521
 
3703
4522
  for( var i = 0; i < node.children.length; ++i )
3704
4523
  {
3705
- let child = node.children[i];
4524
+ let child = node.children[ i ];
3706
4525
 
3707
- if( this.options.onlyFolders && child.type != 'folder')
4526
+ if( this.options.onlyFolders && child.type != 'folder' )
4527
+ {
3708
4528
  continue;
4529
+ }
3709
4530
 
3710
4531
  this._create_item( node, child, level + 1 );
3711
4532
  }
@@ -3744,12 +4565,12 @@ class Panel {
3744
4565
  * style: CSS Style object to be applied to the panel
3745
4566
  */
3746
4567
 
3747
- constructor( options = {} ) {
4568
+ constructor( options = {} ) {
3748
4569
  var root = document.createElement('div');
3749
4570
  root.className = "lexpanel";
3750
- if(options.id)
4571
+ if( options.id )
3751
4572
  root.id = options.id;
3752
- if(options.className)
4573
+ if( options.className )
3753
4574
  root.className += " " + options.className;
3754
4575
 
3755
4576
  root.style.width = options.width || "calc( 100% - 6px )";
@@ -3826,23 +4647,31 @@ class Panel {
3826
4647
  this.branches = [];
3827
4648
  this.current_branch = null;
3828
4649
 
3829
- for(let w in this.widgets) {
3830
- if(this.widgets[w].options && this.widgets[w].options.signal) {
4650
+ for( let w in this.widgets )
4651
+ {
4652
+ if( this.widgets[w].options && this.widgets[w].options.signal )
4653
+ {
3831
4654
  const signal = this.widgets[w].options.signal;
3832
- for(let i = 0; i < LX.signals[signal].length; i++) {
3833
- if(LX.signals[signal][i] == this.widgets[w]) {
4655
+ for( let i = 0; i < LX.signals[signal].length; i++ )
4656
+ {
4657
+ if( LX.signals[signal][i] == this.widgets[w] )
4658
+ {
3834
4659
  LX.signals[signal] = [...LX.signals[signal].slice(0, i), ...LX.signals[signal].slice(i+1)];
3835
4660
  }
3836
4661
  }
3837
4662
  }
3838
4663
  }
3839
4664
 
3840
- if(this.signals) {
3841
- for(let w = 0; w < this.signals.length; w++) {
4665
+ if( this.signals )
4666
+ {
4667
+ for( let w = 0; w < this.signals.length; w++ )
4668
+ {
3842
4669
  let widget = Object.values(this.signals[w])[0];
3843
4670
  let signal = widget.options.signal;
3844
- for(let i = 0; i < LX.signals[signal].length; i++) {
3845
- if(LX.signals[signal][i] == widget) {
4671
+ for( let i = 0; i < LX.signals[signal].length; i++ )
4672
+ {
4673
+ if( LX.signals[signal][i] == widget )
4674
+ {
3846
4675
  LX.signals[signal] = [...LX.signals[signal].slice(0, i), ...LX.signals[signal].slice(i+1)];
3847
4676
  }
3848
4677
  }
@@ -3880,10 +4709,12 @@ class Panel {
3880
4709
 
3881
4710
  this._inline_widgets_left = -1;
3882
4711
 
3883
- if(!this._inlineContainer) {
4712
+ if( !this._inlineContainer )
4713
+ {
3884
4714
  this._inlineContainer = document.createElement('div');
3885
4715
  this._inlineContainer.className = "lexinlinewidgets";
3886
- if(justifyContent)
4716
+
4717
+ if( justifyContent )
3887
4718
  {
3888
4719
  this._inlineContainer.style.justifyContent = justifyContent;
3889
4720
  }
@@ -3897,7 +4728,7 @@ class Panel {
3897
4728
  if(is_pair)
3898
4729
  {
3899
4730
  // eg. an array, inline items appended later to
3900
- if(this._inline_queued_container)
4731
+ if( this._inline_queued_container)
3901
4732
  this._inlineContainer.appendChild( item[0] );
3902
4733
  // eg. a dropdown, item is appended to parent, not to inline cont.
3903
4734
  else
@@ -3909,7 +4740,7 @@ class Panel {
3909
4740
 
3910
4741
  if(!this._inline_queued_container)
3911
4742
  {
3912
- if(this.current_branch)
4743
+ if( this.current_branch)
3913
4744
  this.current_branch.content.appendChild( this._inlineContainer );
3914
4745
  else
3915
4746
  this.root.appendChild( this._inlineContainer );
@@ -3948,7 +4779,7 @@ class Panel {
3948
4779
  this.current_branch = branch;
3949
4780
 
3950
4781
  // Append to panel
3951
- if(this.branches.length == 0)
4782
+ if( this.branches.length == 0)
3952
4783
  branch.root.classList.add('first');
3953
4784
 
3954
4785
  // This is the last!
@@ -3959,7 +4790,8 @@ class Panel {
3959
4790
  this.root.appendChild( branch.root );
3960
4791
 
3961
4792
  // Add widget filter
3962
- if(options.filter) {
4793
+ if( options.filter )
4794
+ {
3963
4795
  this._addFilter( options.filter, {callback: this._searchWidgets.bind(this, branch.name)} );
3964
4796
  }
3965
4797
 
@@ -4076,14 +4908,18 @@ class Panel {
4076
4908
  element.jsInstance = widget;
4077
4909
 
4078
4910
  const insert_widget = el => {
4079
- if(options.container)
4080
- options.container.appendChild(el);
4081
- else if(!this.queuedContainer) {
4082
-
4083
- if(this.current_branch)
4911
+ if( options.container )
4912
+ {
4913
+ options.container.appendChild( el );
4914
+ }
4915
+ else if( !this.queuedContainer )
4916
+ {
4917
+ if( this.current_branch )
4084
4918
  {
4085
- if(!options.skipWidget)
4919
+ if( !options.skipWidget )
4920
+ {
4086
4921
  this.current_branch.widgets.push( widget );
4922
+ }
4087
4923
  this.current_branch.content.appendChild( el );
4088
4924
  }
4089
4925
  else
@@ -4093,40 +4929,47 @@ class Panel {
4093
4929
  }
4094
4930
  }
4095
4931
  // Append content to queued tab container
4096
- else {
4932
+ else
4933
+ {
4097
4934
  this.queuedContainer.appendChild( el );
4098
4935
  }
4099
4936
  };
4100
4937
 
4101
4938
  const store_widget = el => {
4102
4939
 
4103
- if(!this.queuedContainer) {
4940
+ if( !this.queuedContainer )
4941
+ {
4104
4942
  this._inlineWidgets.push( el );
4105
4943
  }
4106
4944
  // Append content to queued tab container
4107
- else {
4945
+ else
4946
+ {
4108
4947
  this._inlineWidgets.push( [el, this.queuedContainer] );
4109
4948
  }
4110
4949
  };
4111
4950
 
4112
4951
  // Process inline widgets
4113
- if(this._inline_widgets_left > 0 && !options.skipInlineCount)
4952
+ if( this._inline_widgets_left > 0 && !options.skipInlineCount )
4114
4953
  {
4115
- if(!this._inlineWidgets) {
4954
+ if( !this._inlineWidgets )
4955
+ {
4116
4956
  this._inlineWidgets = [];
4117
4957
  }
4118
4958
 
4119
4959
  // Store widget and its container
4120
- store_widget(element);
4960
+ store_widget( element );
4121
4961
 
4122
4962
  this._inline_widgets_left--;
4123
4963
 
4124
4964
  // Last widget
4125
- if(!this._inline_widgets_left) {
4965
+ if( !this._inline_widgets_left )
4966
+ {
4126
4967
  this.endLine();
4127
4968
  }
4128
- }else {
4129
- insert_widget(element);
4969
+ }
4970
+ else
4971
+ {
4972
+ insert_widget( element );
4130
4973
  }
4131
4974
 
4132
4975
  return widget;
@@ -4163,15 +5006,20 @@ class Panel {
4163
5006
 
4164
5007
  _searchWidgets(branchName, value) {
4165
5008
 
4166
- for( let b of this.branches ) {
4167
-
4168
- if(b.name !== branchName)
5009
+ for( let b of this.branches )
5010
+ {
5011
+ if( b.name !== branchName )
5012
+ {
4169
5013
  continue;
5014
+ }
4170
5015
 
4171
5016
  // remove all widgets
4172
- for( let w of b.widgets ) {
4173
- if(w.domEl.classList.contains('lexfilter'))
5017
+ for( let w of b.widgets )
5018
+ {
5019
+ if( w.domEl.classList.contains('lexfilter') )
5020
+ {
4174
5021
  continue;
5022
+ }
4175
5023
  w.domEl.remove();
4176
5024
  }
4177
5025
 
@@ -4181,9 +5029,9 @@ class Panel {
4181
5029
  const emptyFilter = !value.length;
4182
5030
 
4183
5031
  // add widgets
4184
- for( let w of b.widgets ) {
4185
-
4186
- if(!emptyFilter)
5032
+ for( let w of b.widgets )
5033
+ {
5034
+ if( !emptyFilter )
4187
5035
  {
4188
5036
  if(!w.name) continue;
4189
5037
  const filterWord = value.toLowerCase();
@@ -4277,7 +5125,7 @@ class Panel {
4277
5125
 
4278
5126
  clearQueue() {
4279
5127
 
4280
- if(this._queue && this._queue.length)
5128
+ if( this._queue && this._queue.length)
4281
5129
  {
4282
5130
  this.queuedContainer = this._queue.pop();
4283
5131
  return;
@@ -4503,7 +5351,8 @@ class Panel {
4503
5351
  element.appendChild( container );
4504
5352
 
4505
5353
  // Remove branch padding and margins
4506
- if( !widget.name ) {
5354
+ if( !widget.name )
5355
+ {
4507
5356
  element.className += " noname";
4508
5357
  container.style.width = "100%";
4509
5358
  }
@@ -4603,7 +5452,8 @@ class Panel {
4603
5452
  element.appendChild(container);
4604
5453
 
4605
5454
  // Remove branch padding and margins
4606
- if(!widget.name) {
5455
+ if( !widget.name )
5456
+ {
4607
5457
  element.className += " noname";
4608
5458
  container.style.width = "100%";
4609
5459
  }
@@ -4708,11 +5558,12 @@ class Panel {
4708
5558
  /**
4709
5559
  * @method addComboButtons
4710
5560
  * @param {String} name Widget name
4711
- * @param {Array} values Each of the {value, callback} items
5561
+ * @param {Array} values Each of the {value, callback, selected, disabled} items
4712
5562
  * @param {*} options:
4713
5563
  * float: Justify content (left, center, right) [center]
4714
- * selected: Selected item by default by value
5564
+ * @legacy selected: Selected item by default by value
4715
5565
  * noSelection: Buttons can be clicked, but they are not selectable
5566
+ * toggle: Buttons can be toggled insted of selecting only one
4716
5567
  */
4717
5568
 
4718
5569
  addComboButtons( name, values, options = {} ) {
@@ -4734,7 +5585,8 @@ class Panel {
4734
5585
  let buttonsBox = document.createElement('div');
4735
5586
  buttonsBox.className = "lexcombobuttonsbox ";
4736
5587
 
4737
- let shouldSelect = !( options.noSelection ?? false );
5588
+ const shouldSelect = !( options.noSelection ?? false );
5589
+ const shouldToggle = shouldSelect && ( options.toggle ?? false );
4738
5590
 
4739
5591
  for( let b of values )
4740
5592
  {
@@ -4753,14 +5605,14 @@ class Panel {
4753
5605
  buttonEl.classList.add( options.buttonClass );
4754
5606
  }
4755
5607
 
4756
- if( shouldSelect && options.selected == b.value )
5608
+ if( shouldSelect && ( b.selected || options.selected == b.value ) )
4757
5609
  {
4758
5610
  buttonEl.classList.add("selected");
4759
5611
  }
4760
5612
 
4761
5613
  buttonEl.innerHTML = ( b.icon ? "<a class='" + b.icon +"'></a>" : "" ) + "<span>" + ( b.icon ? "" : b.value ) + "</span>";
4762
5614
 
4763
- if( options.disabled )
5615
+ if( b.disabled )
4764
5616
  {
4765
5617
  buttonEl.setAttribute( "disabled", true );
4766
5618
  }
@@ -4768,8 +5620,15 @@ class Panel {
4768
5620
  buttonEl.addEventListener("click", function( e ) {
4769
5621
  if( shouldSelect )
4770
5622
  {
4771
- container.querySelectorAll('button').forEach( s => s.classList.remove('selected'));
4772
- this.classList.add('selected');
5623
+ if( shouldToggle )
5624
+ {
5625
+ this.classList.toggle('selected');
5626
+ }
5627
+ else
5628
+ {
5629
+ container.querySelectorAll('button').forEach( s => s.classList.remove('selected'));
5630
+ this.classList.add('selected');
5631
+ }
4773
5632
  }
4774
5633
 
4775
5634
  that._trigger( new IEvent( name, b.value, e ), b.callback );
@@ -4779,7 +5638,7 @@ class Panel {
4779
5638
  }
4780
5639
 
4781
5640
  // Remove branch padding and margins
4782
- if( !widget.name)
5641
+ if( !widget.name )
4783
5642
  {
4784
5643
  element.className += " noname";
4785
5644
  container.style.width = "100%";
@@ -4953,7 +5812,8 @@ class Panel {
4953
5812
 
4954
5813
  element.appendChild( container );
4955
5814
 
4956
- if( !widget.name || options.hideName ) {
5815
+ if( !widget.name || options.hideName )
5816
+ {
4957
5817
  element.className += " noname";
4958
5818
  container.style.width = "100%";
4959
5819
  }
@@ -5089,8 +5949,6 @@ class Panel {
5089
5949
 
5090
5950
  const _placeOptions = ( parent ) => {
5091
5951
 
5092
- console.log("Replacing container");
5093
-
5094
5952
  const overflowContainer = parent.getParentArea();
5095
5953
  const rect = selectedOption.getBoundingClientRect();
5096
5954
  const nestedDialog = parent.parentElement.closest( "dialog" );
@@ -5374,15 +6232,16 @@ class Panel {
5374
6232
 
5375
6233
  addCurve( name, values, callback, options = {} ) {
5376
6234
 
5377
- if(!name) {
5378
- throw("Set Widget Name!");
6235
+ if( !name )
6236
+ {
6237
+ throw( "Set Widget Name!" );
5379
6238
  }
5380
6239
 
5381
6240
  let that = this;
5382
- let widget = this.create_widget(name, Widget.CURVE, options);
6241
+ let widget = this.create_widget( name, Widget.CURVE, options );
5383
6242
 
5384
6243
  widget.onGetValue = () => {
5385
- return JSON.parse(JSON.stringify(curveInstance.element.value));
6244
+ return JSON.parse(JSON.stringify( curveInstance.element.value ));
5386
6245
  };
5387
6246
 
5388
6247
  widget.onSetValue = ( newValue, skipCallback ) => {
@@ -5527,7 +6386,8 @@ class Panel {
5527
6386
 
5528
6387
  addLayers( name, value, callback, options = {} ) {
5529
6388
 
5530
- if(!name) {
6389
+ if( !name )
6390
+ {
5531
6391
  throw("Set Widget Name!");
5532
6392
  }
5533
6393
 
@@ -5569,7 +6429,8 @@ class Panel {
5569
6429
  let binary = value.toString( 2 );
5570
6430
  let nbits = binary.length;
5571
6431
  // fill zeros
5572
- for(var i = 0; i < (16 - nbits); ++i) {
6432
+ for( var i = 0; i < (16 - nbits); ++i )
6433
+ {
5573
6434
  binary = '0' + binary;
5574
6435
  }
5575
6436
 
@@ -5622,8 +6483,9 @@ class Panel {
5622
6483
 
5623
6484
  addArray( name, values = [], callback, options = {} ) {
5624
6485
 
5625
- if(!name) {
5626
- throw("Set Widget Name!");
6486
+ if( !name )
6487
+ {
6488
+ throw( "Set Widget Name!" );
5627
6489
  }
5628
6490
 
5629
6491
  let widget = this.create_widget(name, Widget.ARRAY, options);
@@ -5804,7 +6666,8 @@ class Panel {
5804
6666
  widget.updateValues( values );
5805
6667
 
5806
6668
  // Remove branch padding and margins
5807
- if( !widget.name ) {
6669
+ if( !widget.name )
6670
+ {
5808
6671
  element.className += " noname";
5809
6672
  listContainer.style.width = "100%";
5810
6673
  }
@@ -5891,7 +6754,7 @@ class Panel {
5891
6754
  tagsContainer.appendChild( tagInput );
5892
6755
 
5893
6756
  tagInput.onkeydown = function( e ) {
5894
- const val = this.value.replace(/\s/g, '');
6757
+ const val = this.value.replace( /\s/g, '' );
5895
6758
  if( e.key == ' ' || e.key == 'Enter' )
5896
6759
  {
5897
6760
  e.preventDefault();
@@ -5929,15 +6792,16 @@ class Panel {
5929
6792
  * @param {Function} callback Callback function on change
5930
6793
  * @param {*} options:
5931
6794
  * disabled: Make the widget disabled [false]
6795
+ * label: Checkbox label
5932
6796
  * suboptions: Callback to add widgets in case of TRUE value
5933
- * className: Customize colors
6797
+ * className: Extra classes to customize style
5934
6798
  */
5935
6799
 
5936
6800
  addCheckbox( name, value, callback, options = {} ) {
5937
6801
 
5938
- if( !name )
6802
+ if( !name && !options.label )
5939
6803
  {
5940
- throw( "Set Widget Name!" );
6804
+ throw( "Set Widget Name or at least a label!" );
5941
6805
  }
5942
6806
 
5943
6807
  let widget = this.create_widget( name, Widget.CHECKBOX, options );
@@ -5957,10 +6821,13 @@ class Panel {
5957
6821
  let element = widget.domEl;
5958
6822
 
5959
6823
  // Add reset functionality
5960
- Panel._add_reset_property( element.domName, function() {
5961
- checkbox.checked = !checkbox.checked;
5962
- Panel._dispatch_event( checkbox, "change" );
5963
- });
6824
+ if( name )
6825
+ {
6826
+ Panel._add_reset_property( element.domName, function() {
6827
+ checkbox.checked = !checkbox.checked;
6828
+ Panel._dispatch_event( checkbox, "change" );
6829
+ });
6830
+ }
5964
6831
 
5965
6832
  // Add widget value
5966
6833
 
@@ -5976,7 +6843,7 @@ class Panel {
5976
6843
 
5977
6844
  let valueName = document.createElement( 'span' );
5978
6845
  valueName.className = "checkboxtext";
5979
- valueName.innerHTML = "On";
6846
+ valueName.innerHTML = options.label ?? "On";
5980
6847
 
5981
6848
  container.appendChild( checkbox );
5982
6849
  container.appendChild( valueName );
@@ -6114,6 +6981,96 @@ class Panel {
6114
6981
  return widget;
6115
6982
  }
6116
6983
 
6984
+ /**
6985
+ * @method addRadioGroup
6986
+ * @param {String} label Radio label
6987
+ * @param {Array} values Radio options
6988
+ * @param {Function} callback Callback function on change
6989
+ * @param {*} options:
6990
+ * disabled: Make the widget disabled [false]
6991
+ * className: Customize colors
6992
+ */
6993
+
6994
+ addRadioGroup( label, values, callback, options = {} ) {
6995
+
6996
+ let widget = this.create_widget( null, Widget.RADIO, options );
6997
+
6998
+ widget.onGetValue = () => {
6999
+ const items = container.querySelectorAll( 'button' );
7000
+ for( let i = 0; i < items.length; ++i )
7001
+ {
7002
+ const optionItem = items[ i ];
7003
+ if( optionItem.checked )
7004
+ {
7005
+ return [ i, values[ i ] ];
7006
+ }
7007
+ }
7008
+ };
7009
+
7010
+ widget.onSetValue = ( newValue, skipCallback ) => {
7011
+ const items = container.querySelectorAll( 'button' );
7012
+ for( let i = 0; i < items.length; ++i )
7013
+ {
7014
+ const optionItem = items[ i ];
7015
+ if( newValue == i )
7016
+ {
7017
+ Panel._dispatch_event( optionItem, "click", skipCallback );
7018
+ }
7019
+ }
7020
+ };
7021
+
7022
+ let element = widget.domEl;
7023
+
7024
+ // Add widget value
7025
+ var container = document.createElement( 'div' );
7026
+ container.className = "lexradiogroup " + ( options.className ?? "" );
7027
+
7028
+ let labelSpan = document.createElement( 'span' );
7029
+ labelSpan.innerHTML = label;
7030
+ container.appendChild( labelSpan );
7031
+
7032
+ const that = this;
7033
+
7034
+ for( let i = 0; i < values.length; ++i )
7035
+ {
7036
+ const optionItem = document.createElement( 'div' );
7037
+ optionItem.className = "lexradiogroupitem";
7038
+ container.appendChild( optionItem );
7039
+
7040
+ const optionButton = document.createElement( 'button' );
7041
+ optionButton.className = "lexbutton";
7042
+ optionButton.disabled = options.disabled ?? false;
7043
+ optionItem.appendChild( optionButton );
7044
+
7045
+ optionButton.addEventListener( "click", function( e ) {
7046
+ const skipCallback = ( e.detail?.constructor == Number ? null : e.detail );
7047
+ container.querySelectorAll( 'button' ).forEach( e => { e.checked = false; e.classList.remove( "checked" ) } );
7048
+ this.checked = !this.checked;
7049
+ this.classList.toggle( "checked" );
7050
+ if( !skipCallback ) that._trigger( new IEvent( null, [ i, values[ i ] ], e ), callback );
7051
+ } );
7052
+
7053
+ {
7054
+ const checkedSpan = document.createElement( 'span' );
7055
+ optionButton.appendChild( checkedSpan );
7056
+ }
7057
+
7058
+ const optionLabel = document.createElement( 'span' );
7059
+ optionLabel.innerHTML = values[ i ];
7060
+ optionItem.appendChild( optionLabel );
7061
+ }
7062
+
7063
+ if( options.selected )
7064
+ {
7065
+ console.assert( options.selected.constructor == Number );
7066
+ widget.set( options.selected, true );
7067
+ }
7068
+
7069
+ element.appendChild( container );
7070
+
7071
+ return widget;
7072
+ }
7073
+
6117
7074
  /**
6118
7075
  * @method addColor
6119
7076
  * @param {String} name Widget name
@@ -6126,7 +7083,8 @@ class Panel {
6126
7083
 
6127
7084
  addColor( name, value, callback, options = {} ) {
6128
7085
 
6129
- if( !name ) {
7086
+ if( !name )
7087
+ {
6130
7088
  throw( "Set Widget Name!" );
6131
7089
  }
6132
7090
 
@@ -6164,7 +7122,8 @@ class Panel {
6164
7122
  color.useRGB = options.useRGB ?? false;
6165
7123
  color.value = color.iValue = value.constructor === Array ? rgbToHex( value ) : value;
6166
7124
 
6167
- if( options.disabled ) {
7125
+ if( options.disabled )
7126
+ {
6168
7127
  color.disabled = true;
6169
7128
  }
6170
7129
 
@@ -6209,6 +7168,137 @@ class Panel {
6209
7168
  return widget;
6210
7169
  }
6211
7170
 
7171
+ /**
7172
+ * @method addRange
7173
+ * @param {String} name Widget name
7174
+ * @param {Number} value Default number value
7175
+ * @param {Function} callback Callback function on change
7176
+ * @param {*} options:
7177
+ * className: Extra classes to customize style
7178
+ * disabled: Make the widget disabled [false]
7179
+ * left: The slider goes to the left instead of the right
7180
+ * fill: Fill slider progress [true]
7181
+ * step: Step of the input
7182
+ * min, max: Min and Max values for the input
7183
+ */
7184
+
7185
+ addRange( name, value, callback, options = {} ) {
7186
+
7187
+ let widget = this.create_widget( name, Widget.RANGE, options );
7188
+
7189
+ widget.onGetValue = () => {
7190
+ return +slider.value;
7191
+ };
7192
+
7193
+ widget.onSetValue = ( newValue, skipCallback ) => {
7194
+ slider.value = newValue;
7195
+ Panel._dispatch_event( slider, "input", skipCallback );
7196
+ };
7197
+
7198
+ let element = widget.domEl;
7199
+
7200
+ // add reset functionality
7201
+ if( widget.name )
7202
+ {
7203
+ Panel._add_reset_property( element.domName, function() {
7204
+ this.style.display = "none";
7205
+ slider.value = slider.iValue;
7206
+ Panel._dispatch_event( slider, "input" );
7207
+ });
7208
+ }
7209
+
7210
+ // add widget value
7211
+
7212
+ var container = document.createElement( 'div' );
7213
+ container.className = "lexrange";
7214
+ container.style.width = options.inputWidth || "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
7215
+
7216
+ let slider = document.createElement( 'input' );
7217
+ slider.className = "lexrangeslider " + ( options.className ?? "" );
7218
+ slider.value = slider.iValue = value;
7219
+ slider.min = options.min;
7220
+ slider.max = options.max;
7221
+ slider.step = options.step ?? 1;
7222
+ slider.type = "range";
7223
+ slider.disabled = options.disabled ?? false;
7224
+
7225
+ if( options.left ?? false )
7226
+ {
7227
+ slider.classList.add( "left" );
7228
+ }
7229
+
7230
+ if( !( options.fill ?? true ) )
7231
+ {
7232
+ slider.classList.add( "no-fill" );
7233
+ }
7234
+
7235
+ slider.addEventListener( "input", e => {
7236
+
7237
+ if( isNaN( e.target.valueAsNumber ) )
7238
+ {
7239
+ return;
7240
+ }
7241
+
7242
+ const skipCallback = e.detail;
7243
+
7244
+ let val = e.target.value = clamp( +e.target.valueAsNumber, +slider.min, +slider.max );
7245
+ slider.value = val;
7246
+
7247
+ // Reset button (default value)
7248
+ if( !skipCallback )
7249
+ {
7250
+ let btn = element.querySelector( ".lexwidgetname .lexicon" );
7251
+ if( btn ) btn.style.display = val != slider.iValue ? "block": "none";
7252
+ }
7253
+
7254
+ if( options.left )
7255
+ {
7256
+ val = ( +slider.max ) - val + ( +slider.min );
7257
+ }
7258
+
7259
+ if( !skipCallback ) this._trigger( new IEvent( name, val, e ), callback );
7260
+ }, { passive: false });
7261
+
7262
+ slider.addEventListener( "mousedown", function( e ) {
7263
+ if( options.onPress )
7264
+ {
7265
+ options.onPress.bind( slider )( e, slider );
7266
+ }
7267
+ }, false );
7268
+
7269
+ slider.addEventListener( "mouseup", function( e ) {
7270
+ if( options.onRelease )
7271
+ {
7272
+ options.onRelease.bind( slider )( e, slider );
7273
+ }
7274
+ }, false );
7275
+
7276
+ // Method to change min, max, step parameters
7277
+ widget.setLimits = ( newMin, newMax, newStep ) => {
7278
+ slider.min = newMin ?? slider.min;
7279
+ slider.max = newMax ?? slider.max;
7280
+ slider.step = newStep ?? slider.step;
7281
+ Panel._dispatch_event( slider, "input", true );
7282
+ };
7283
+
7284
+ if( value.constructor == Number )
7285
+ {
7286
+ value = clamp( value, +slider.min, +slider.max );
7287
+ }
7288
+
7289
+ container.appendChild( slider );
7290
+ element.appendChild( container );
7291
+
7292
+ // Remove branch padding and margins
7293
+ if( !widget.name )
7294
+ {
7295
+ element.className += " noname";
7296
+ container.style.width = "100%";
7297
+ }
7298
+
7299
+ return widget;
7300
+ }
7301
+
6212
7302
  /**
6213
7303
  * @method addNumber
6214
7304
  * @param {String} name Widget name
@@ -6241,7 +7331,8 @@ class Panel {
6241
7331
  let element = widget.domEl;
6242
7332
 
6243
7333
  // add reset functionality
6244
- if( widget.name ) {
7334
+ if( widget.name )
7335
+ {
6245
7336
  Panel._add_reset_property( element.domName, function() {
6246
7337
  this.style.display = "none";
6247
7338
  vecinput.value = vecinput.iValue;
@@ -6509,7 +7600,8 @@ class Panel {
6509
7600
  return;
6510
7601
  }
6511
7602
 
6512
- for( let i = 0; i < inputs.length; ++i ) {
7603
+ for( let i = 0; i < inputs.length; ++i )
7604
+ {
6513
7605
  let value = newValue[ i ];
6514
7606
  inputs[ i ].value = round( value, options.precision ) ?? 0;
6515
7607
  Panel._dispatch_event( inputs[ i ], "change", skipCallback );
@@ -6521,7 +7613,8 @@ class Panel {
6521
7613
  // Add reset functionality
6522
7614
  Panel._add_reset_property( element.domName, function() {
6523
7615
  this.style.display = "none";
6524
- for( let v of element.querySelectorAll( ".vecinput" ) ) {
7616
+ for( let v of element.querySelectorAll( ".vecinput" ) )
7617
+ {
6525
7618
  v.value = v.iValue;
6526
7619
  Panel._dispatch_event( v, "change" );
6527
7620
  }
@@ -6533,8 +7626,8 @@ class Panel {
6533
7626
  container.className = "lexvector";
6534
7627
  container.style.width = "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
6535
7628
 
6536
- for( let i = 0; i < num_components; ++i ) {
6537
-
7629
+ for( let i = 0; i < num_components; ++i )
7630
+ {
6538
7631
  let box = document.createElement( 'div' );
6539
7632
  box.className = "vecbox";
6540
7633
  box.innerHTML = "<span class='" + Panel.VECTOR_COMPONENTS[ i ] + "'></span>";
@@ -6610,7 +7703,8 @@ class Panel {
6610
7703
 
6611
7704
  if( locker.locked )
6612
7705
  {
6613
- for( let v of element.querySelectorAll( ".vecinput" ) ) {
7706
+ for( let v of element.querySelectorAll( ".vecinput" ) )
7707
+ {
6614
7708
  v.value = val;
6615
7709
  value[ v.idx ] = val;
6616
7710
  }
@@ -6668,7 +7762,8 @@ class Panel {
6668
7762
 
6669
7763
  if( locker.locked )
6670
7764
  {
6671
- for( let v of element.querySelectorAll( ".vecinput" ) ) {
7765
+ for( let v of element.querySelectorAll( ".vecinput" ) )
7766
+ {
6672
7767
  v.value = round( +v.valueAsNumber + mult * dt, options.precision );
6673
7768
  Panel._dispatch_event( v, "change" );
6674
7769
  }
@@ -6794,7 +7889,7 @@ class Panel {
6794
7889
  const value = [];
6795
7890
  for( let i = 0; i < element.dimensions.length; ++i )
6796
7891
  {
6797
- value.push( element.dimensions[ i ].onGetValue() );
7892
+ value.push( element.dimensions[ i ].value() );
6798
7893
  }
6799
7894
  return value;
6800
7895
  };
@@ -6802,7 +7897,7 @@ class Panel {
6802
7897
  widget.onSetValue = ( newValue, skipCallback ) => {
6803
7898
  for( let i = 0; i < element.dimensions.length; ++i )
6804
7899
  {
6805
- element.dimensions[ i ].onSetValue( newValue[ i ], skipCallback );
7900
+ element.dimensions[ i ].set( newValue[ i ], skipCallback );
6806
7901
  }
6807
7902
  };
6808
7903
 
@@ -6817,14 +7912,14 @@ class Panel {
6817
7912
  {
6818
7913
  element.dimensions[ i ] = this.addNumber( null, value[ i ], ( v ) => {
6819
7914
 
6820
- const value = widget.onGetValue();
7915
+ const value = widget.value();
6821
7916
 
6822
7917
  if( element.locked )
6823
7918
  {
6824
7919
  const ar = ( i == 0 ? 1.0 / element.aspectRatio : element.aspectRatio );
6825
7920
  const index = ( 1 + i ) % 2;
6826
7921
  value[ index ] = v * ar;
6827
- element.dimensions[ index ].onSetValue( value[ index ], true );
7922
+ element.dimensions[ index ].set( value[ index ], true );
6828
7923
  }
6829
7924
 
6830
7925
  if( callback )
@@ -6867,7 +7962,7 @@ class Panel {
6867
7962
  this.classList.remove( "fa-lock-open" );
6868
7963
 
6869
7964
  // Recompute ratio
6870
- const value = widget.onGetValue();
7965
+ const value = widget.value();
6871
7966
  element.aspectRatio = value[ 0 ] / value[ 1 ];
6872
7967
  }
6873
7968
  else
@@ -7264,7 +8359,8 @@ class Panel {
7264
8359
  let container = document.createElement('div');
7265
8360
  container.className = "lextree";
7266
8361
 
7267
- if(name) {
8362
+ if( name )
8363
+ {
7268
8364
  let title = document.createElement('span');
7269
8365
  title.innerHTML = name;
7270
8366
  container.appendChild(title);
@@ -7276,8 +8372,8 @@ class Panel {
7276
8372
  toolsDiv.className += " notitle";
7277
8373
 
7278
8374
  // Tree icons
7279
- if(options.icons) {
7280
-
8375
+ if( options.icons )
8376
+ {
7281
8377
  for( let data of options.icons )
7282
8378
  {
7283
8379
  let iconEl = document.createElement('a');
@@ -7337,11 +8433,15 @@ class Panel {
7337
8433
  let widget = new Widget( null, Widget.SEPARATOR );
7338
8434
  widget.domEl = element;
7339
8435
 
7340
- if(this.current_branch) {
8436
+ if( this.current_branch )
8437
+ {
7341
8438
  this.current_branch.content.appendChild( element );
7342
8439
  this.current_branch.widgets.push( widget );
7343
- } else
7344
- this.root.appendChild(element);
8440
+ }
8441
+ else
8442
+ {
8443
+ this.root.appendChild( element );
8444
+ }
7345
8445
  }
7346
8446
 
7347
8447
  /**
@@ -7666,6 +8766,12 @@ class Panel {
7666
8766
 
7667
8767
  input.addEventListener( 'change', function() {
7668
8768
  data.checkMap[ rowId ] = this.checked;
8769
+
8770
+ if( !this.checked )
8771
+ {
8772
+ const input = table.querySelector( "thead input[type='checkbox']" );
8773
+ input.checked = data.checkMap[ ":root" ] = false;
8774
+ }
7669
8775
  });
7670
8776
 
7671
8777
  row.appendChild( td );
@@ -7791,7 +8897,7 @@ class Branch {
7791
8897
  root.appendChild( title );
7792
8898
 
7793
8899
  var branchContent = document.createElement( 'div' );
7794
- branchContent.id = name.replace(/\s/g, '');
8900
+ branchContent.id = name.replace( /\s/g, '' );
7795
8901
  branchContent.className = "lexbranchcontent";
7796
8902
  root.appendChild(branchContent);
7797
8903
  this.content = branchContent;
@@ -7847,7 +8953,8 @@ class Branch {
7847
8953
 
7848
8954
  const dialog = new Dialog(this.name, p => {
7849
8955
  // add widgets
7850
- for( let w of this.widgets ) {
8956
+ for( let w of this.widgets )
8957
+ {
7851
8958
  p.root.appendChild( w.domEl );
7852
8959
  }
7853
8960
  });
@@ -7940,8 +9047,8 @@ class Branch {
7940
9047
  var size = this.grabber.style.marginLeft;
7941
9048
 
7942
9049
  // Update sizes of widgets inside
7943
- for(var i = 0; i < this.widgets.length; i++) {
7944
-
9050
+ for( var i = 0; i < this.widgets.length; i++ )
9051
+ {
7945
9052
  let widget = this.widgets[ i ];
7946
9053
  let element = widget.domEl;
7947
9054
 
@@ -7960,9 +9067,6 @@ class Branch {
7960
9067
  case Widget.FILE:
7961
9068
  padding = "10%";
7962
9069
  break;
7963
- case Widget.TEXT:
7964
- padding = "8px";
7965
- break;
7966
9070
  };
7967
9071
 
7968
9072
  value.style.width = "-moz-calc( 100% - " + size + " - " + padding + " )";
@@ -8101,7 +9205,7 @@ class Dialog {
8101
9205
  modal = options.modal ?? false;
8102
9206
 
8103
9207
  var root = document.createElement('dialog');
8104
- root.className = "lexdialog " + (options.class ?? "");
9208
+ root.className = "lexdialog " + (options.className ?? "");
8105
9209
  root.id = options.id ?? "dialog" + Dialog._last_id++;
8106
9210
  LX.root.appendChild( root );
8107
9211
 
@@ -8259,15 +9363,15 @@ class Dialog {
8259
9363
 
8260
9364
  root.style.width = size[ 0 ] ? (size[ 0 ]) : "25%";
8261
9365
  root.style.height = size[ 1 ] ? (size[ 1 ]) : "auto";
9366
+ root.style.translate = options.position ? "unset" : "-50% -50%";
8262
9367
 
8263
9368
  if( options.size )
8264
9369
  {
8265
9370
  this.size = size;
8266
9371
  }
8267
9372
 
8268
- let rect = root.getBoundingClientRect();
8269
- root.style.left = position[ 0 ] ? (position[ 0 ]) : "calc( 50% - " + ( rect.width * 0.5 ) + "px )";
8270
- root.style.top = position[ 1 ] ? (position[ 1 ]) : "calc( 50% - " + ( rect.height * 0.5 ) + "px )";
9373
+ root.style.left = position[ 0 ] ?? "50%";
9374
+ root.style.top = position[ 1 ] ?? "50%";
8271
9375
 
8272
9376
  panel.root.style.width = "calc( 100% - 30px )";
8273
9377
  panel.root.style.height = title ? "calc( 100% - " + ( titleDiv.offsetHeight + 30 ) + "px )" : "calc( 100% - 51px )";
@@ -8284,7 +9388,7 @@ class Dialog {
8284
9388
  this._oncreate.call(this, this.panel);
8285
9389
  }
8286
9390
 
8287
- setPosition(x, y) {
9391
+ setPosition( x, y ) {
8288
9392
 
8289
9393
  this.root.style.left = x + "px";
8290
9394
  this.root.style.top = y + "px";
@@ -8330,6 +9434,9 @@ class PocketDialog extends Dialog {
8330
9434
 
8331
9435
  // Custom
8332
9436
  this.root.classList.add( "pocket" );
9437
+
9438
+ this.root.style.translate = "none";
9439
+ this.root.style.top = "0";
8333
9440
  this.root.style.left = "unset";
8334
9441
 
8335
9442
  if( !options.position )
@@ -8345,6 +9452,11 @@ class PocketDialog extends Dialog {
8345
9452
  this.minimized = false;
8346
9453
  this.title.tabIndex = -1;
8347
9454
  this.title.addEventListener("click", e => {
9455
+ if( this.title._eventCatched )
9456
+ {
9457
+ this.title._eventCatched = false;
9458
+ return;
9459
+ }
8348
9460
 
8349
9461
  // Sized dialogs have to keep their size
8350
9462
  if( this.size )
@@ -8427,12 +9539,12 @@ class ContextMenu {
8427
9539
  constructor( event, title, options = {} ) {
8428
9540
 
8429
9541
  // remove all context menus
8430
- document.body.querySelectorAll(".lexcontextmenubox").forEach(e => e.remove());
9542
+ document.body.querySelectorAll( ".lexcontextmenu" ).forEach( e => e.remove() );
8431
9543
 
8432
- this.root = document.createElement('div');
8433
- this.root.className = "lexcontextmenubox";
8434
- this.root.style.left = (event.x - 48 + document.scrollingElement.scrollLeft) + "px";
8435
- this.root.style.top = (event.y - 8 + document.scrollingElement.scrollTop) + "px";
9544
+ this.root = document.createElement( "div" );
9545
+ this.root.className = "lexcontextmenu";
9546
+ this.root.style.left = ( event.x - 48 + document.scrollingElement.scrollLeft ) + "px";
9547
+ this.root.style.top = ( event.y - 8 + document.scrollingElement.scrollTop ) + "px";
8436
9548
 
8437
9549
  this.root.addEventListener("mouseleave", function() {
8438
9550
  this.remove();
@@ -8445,8 +9557,8 @@ class ContextMenu {
8445
9557
  {
8446
9558
  const item = {};
8447
9559
  item[ title ] = [];
8448
- item[ 'className' ] = "cmtitle";
8449
- item[ 'icon' ] = options.icon;
9560
+ item[ "className" ] = "cmtitle";
9561
+ item[ "icon" ] = options.icon;
8450
9562
  this.items.push( item );
8451
9563
  }
8452
9564
  }
@@ -8494,16 +9606,16 @@ class ContextMenu {
8494
9606
 
8495
9607
  _create_submenu( o, k, c, d ) {
8496
9608
 
8497
- this.root.querySelectorAll(".lexcontextmenubox").forEach( cm => cm.remove() );
9609
+ this.root.querySelectorAll( ".lexcontextmenu" ).forEach( cm => cm.remove() );
8498
9610
 
8499
9611
  let contextmenu = document.createElement('div');
8500
- contextmenu.className = "lexcontextmenubox";
9612
+ contextmenu.className = "lexcontextmenu";
8501
9613
  c.appendChild( contextmenu );
8502
9614
 
8503
9615
  for( var i = 0; i < o[k].length; ++i )
8504
9616
  {
8505
- const subitem = o[k][i];
8506
- const subkey = Object.keys(subitem)[0];
9617
+ const subitem = o[ k ][ i ];
9618
+ const subkey = Object.keys( subitem )[ 0 ];
8507
9619
  this._create_entry(subitem, subkey, contextmenu, d);
8508
9620
  }
8509
9621
 
@@ -8519,22 +9631,25 @@ class ContextMenu {
8519
9631
 
8520
9632
  const hasSubmenu = o[ k ].length;
8521
9633
  let entry = document.createElement('div');
8522
- entry.className = "lexcontextmenuentry" + (o[ 'className' ] ? " " + o[ 'className' ] : "" );
9634
+ entry.className = "lexmenuboxentry" + (o[ 'className' ] ? " " + o[ 'className' ] : "" );
8523
9635
  entry.id = o.id ?? ("eId" + getSupportedDOMName( k ));
8524
9636
  entry.innerHTML = "";
8525
9637
  const icon = o[ 'icon' ];
8526
- if(icon) {
9638
+ if( icon )
9639
+ {
8527
9640
  entry.innerHTML += "<a class='" + icon + " fa-sm'></a>";
8528
9641
  }
8529
9642
  const disabled = o['disabled'];
8530
9643
  entry.innerHTML += "<div class='lexentryname" + (disabled ? " disabled" : "") + "'>" + k + "</div>";
8531
9644
  c.appendChild( entry );
8532
9645
 
8533
- if( this.colors[ k ] ) {
9646
+ if( this.colors[ k ] )
9647
+ {
8534
9648
  entry.style.borderColor = this.colors[ k ];
8535
9649
  }
8536
9650
 
8537
- if( k == "" ) {
9651
+ if( k == "" )
9652
+ {
8538
9653
  entry.className += " cmseparator";
8539
9654
  return;
8540
9655
  }
@@ -8547,7 +9662,8 @@ class ContextMenu {
8547
9662
  if(disabled) return;
8548
9663
 
8549
9664
  const f = o[ 'callback' ];
8550
- if(f) {
9665
+ if( f )
9666
+ {
8551
9667
  f.call( this, k, entry );
8552
9668
  this.root.remove();
8553
9669
  }
@@ -8579,8 +9695,7 @@ class ContextMenu {
8579
9695
 
8580
9696
  entry.addEventListener("mouseleave", () => {
8581
9697
  d = -1; // Reset depth
8582
- // delete entry.built;
8583
- c.querySelectorAll(".lexcontextmenubox").forEach(e => e.remove());
9698
+ c.querySelectorAll(".lexcontextmenu").forEach(e => e.remove());
8584
9699
  });
8585
9700
  }
8586
9701
 
@@ -8613,22 +9728,25 @@ class ContextMenu {
8613
9728
  if(key) found = o[ key ];
8614
9729
  } );
8615
9730
 
8616
- if(found) {
9731
+ if( found )
9732
+ {
8617
9733
  insert( tokens[idx++], found );
8618
9734
  }
8619
- else {
9735
+ else
9736
+ {
8620
9737
  let item = {};
8621
9738
  item[ token ] = [];
8622
- const next_token = tokens[idx++];
9739
+ const nextToken = tokens[idx++];
8623
9740
  // Check if last token -> add callback
8624
- if(!next_token) {
9741
+ if( !nextToken )
9742
+ {
8625
9743
  item[ 'id' ] = options.id;
8626
9744
  item[ 'callback' ] = options.callback;
8627
9745
  item[ 'disabled' ] = options.disabled ?? false;
8628
9746
  }
8629
9747
 
8630
9748
  list.push( item );
8631
- insert( next_token, item[ token ] );
9749
+ insert( nextToken, item[ token ] );
8632
9750
  }
8633
9751
  };
8634
9752
 
@@ -8652,7 +9770,8 @@ class ContextMenu {
8652
9770
  _item[ key ].unshift( parent );
8653
9771
  }
8654
9772
 
8655
- for( var child of _item[ key ] ) {
9773
+ for( var child of _item[ key ] )
9774
+ {
8656
9775
  let k = Object.keys(child)[0];
8657
9776
  for( var i = 0; i < child[k].length; ++i )
8658
9777
  setParent(child);
@@ -8666,7 +9785,7 @@ class ContextMenu {
8666
9785
 
8667
9786
  for( let item of this.items )
8668
9787
  {
8669
- let key = Object.keys(item)[0];
9788
+ let key = Object.keys( item )[ 0 ];
8670
9789
  let pKey = "eId" + getSupportedDOMName( key );
8671
9790
 
8672
9791
  // Item already created
@@ -8783,7 +9902,7 @@ class Curve {
8783
9902
 
8784
9903
  var r = [];
8785
9904
  var dx = (element.xrange[1] - element.xrange[ 0 ]) / samples;
8786
- for(var i = element.xrange[0]; i <= element.xrange[1]; i += dx)
9905
+ for( var i = element.xrange[0]; i <= element.xrange[1]; i += dx )
8787
9906
  {
8788
9907
  r.push( element.getValueAt(i) );
8789
9908
  }
@@ -8792,7 +9911,8 @@ class Curve {
8792
9911
 
8793
9912
  element.addValue = function(v) {
8794
9913
 
8795
- for(var i = 0; i < element.value; i++) {
9914
+ for( var i = 0; i < element.value; i++ )
9915
+ {
8796
9916
  var value = element.value[i];
8797
9917
  if(value[0] < v[0]) continue;
8798
9918
  element.value.splice(i,0,v);
@@ -8818,7 +9938,7 @@ class Curve {
8818
9938
 
8819
9939
  var selected = -1;
8820
9940
 
8821
- element.redraw = function( o = {} ) {
9941
+ element.redraw = function( o = {} ) {
8822
9942
 
8823
9943
  if( o.value ) element.value = o.value;
8824
9944
  if( o.xrange ) element.xrange = o.xrange;
@@ -8847,13 +9967,16 @@ class Curve {
8847
9967
  ctx.moveTo( pos[ 0 ], pos[ 1 ] );
8848
9968
  let values = [pos[ 0 ], pos[ 1 ]];
8849
9969
 
8850
- for(var i in element.value) {
8851
- var value = element.value[i];
8852
- pos = convert(value);
8853
- values.push(pos[ 0 ]);
8854
- values.push(pos[ 1 ]);
8855
- if(!element.smooth)
9970
+ for( var i in element.value )
9971
+ {
9972
+ var value = element.value[ i ];
9973
+ pos = convert( value );
9974
+ values.push( pos[ 0 ] );
9975
+ values.push( pos[ 1 ] );
9976
+ if( !element.smooth )
9977
+ {
8856
9978
  ctx.lineTo( pos[ 0 ], pos[ 1 ] );
9979
+ }
8857
9980
  }
8858
9981
 
8859
9982
  pos = convert([ element.xrange[ 1 ], element.defaulty ]);
@@ -8870,7 +9993,8 @@ class Curve {
8870
9993
  }
8871
9994
 
8872
9995
  // Draw points
8873
- for( var i = 0; i < element.value.length; i += 1 ) {
9996
+ for( var i = 0; i < element.value.length; i += 1 )
9997
+ {
8874
9998
  var value = element.value[ i ];
8875
9999
  pos = convert( value );
8876
10000
  if( selected == i )
@@ -8882,10 +10006,11 @@ class Curve {
8882
10006
  ctx.fill();
8883
10007
  }
8884
10008
 
8885
- if(element.show_samples) {
10009
+ if( element.show_samples )
10010
+ {
8886
10011
  var samples = element.resample(element.show_samples);
8887
10012
  ctx.fillStyle = "#888";
8888
- for(var i = 0; i < samples.length; i += 1)
10013
+ for( var i = 0; i < samples.length; i += 1)
8889
10014
  {
8890
10015
  var value = [ i * ((element.xrange[ 1 ] - element.xrange[ 0 ]) / element.show_samples) + element.xrange[ 0 ], samples[ i ] ];
8891
10016
  pos = convert(value);
@@ -8908,7 +10033,8 @@ class Curve {
8908
10033
 
8909
10034
  selected = computeSelected( mousex, canvas.height - mousey );
8910
10035
 
8911
- if( e.button == LX.MOUSE_LEFT_CLICK && selected == -1 && element.allow_add_values ) {
10036
+ if( e.button == LX.MOUSE_LEFT_CLICK && selected == -1 && element.allow_add_values )
10037
+ {
8912
10038
  var v = unconvert([ mousex, canvas.height - mousey ]);
8913
10039
  element.value.push( v );
8914
10040
  sortValues();
@@ -8956,7 +10082,8 @@ class Curve {
8956
10082
  var dy = element.draggable_y ? last_mouse[ 1 ] - mousey : 0;
8957
10083
  var delta = unconvert([ -dx, dy ]);
8958
10084
 
8959
- if( selected != -1 ) {
10085
+ if( selected != -1 )
10086
+ {
8960
10087
  var minx = element.xrange[ 0 ];
8961
10088
  var maxx = element.xrange[ 1 ];
8962
10089
 
@@ -9113,7 +10240,7 @@ class Dial {
9113
10240
 
9114
10241
  var r = [];
9115
10242
  var dx = (element.xrange[1] - element.xrange[ 0 ]) / samples;
9116
- for(var i = element.xrange[0]; i <= element.xrange[1]; i += dx)
10243
+ for( var i = element.xrange[0]; i <= element.xrange[1]; i += dx)
9117
10244
  {
9118
10245
  r.push( element.getValueAt(i) );
9119
10246
  }
@@ -9122,15 +10249,16 @@ class Dial {
9122
10249
 
9123
10250
  element.addValue = function(v) {
9124
10251
 
9125
- for(var i = 0; i < element.value; i++) {
9126
- var value = element.value[i];
9127
- if(value[0] < v[0]) continue;
9128
- element.value.splice(i,0,v);
10252
+ for( var i = 0; i < element.value; i++ )
10253
+ {
10254
+ var value = element.value[ i ];
10255
+ if(value[ 0 ] < v[ 0 ]) continue;
10256
+ element.value.splice( i, 0, v );
9129
10257
  redraw();
9130
10258
  return;
9131
10259
  }
9132
10260
 
9133
- element.value.push(v);
10261
+ element.value.push( v );
9134
10262
  redraw();
9135
10263
  }
9136
10264
 
@@ -9150,7 +10278,7 @@ class Dial {
9150
10278
 
9151
10279
  var selected = -1;
9152
10280
 
9153
- element.redraw = function( o = {} ) {
10281
+ element.redraw = function( o = {} ) {
9154
10282
 
9155
10283
  if( o.value ) element.value = o.value;
9156
10284
  if( o.xrange ) element.xrange = o.xrange;
@@ -9179,17 +10307,17 @@ class Dial {
9179
10307
  ctx.moveTo( pos[ 0 ], pos[ 1 ] );
9180
10308
  let values = [pos[ 0 ], pos[ 1 ]];
9181
10309
 
9182
- for(var i in element.value) {
9183
- var value = element.value[i];
9184
- pos = convert(value);
9185
- values.push(pos[ 0 ]);
9186
- values.push(pos[ 1 ]);
9187
-
10310
+ for( var i in element.value)
10311
+ {
10312
+ var value = element.value[ i ];
10313
+ pos = convert( value );
10314
+ values.push( pos[ 0 ] );
10315
+ values.push( pos[ 1 ] );
9188
10316
  }
9189
10317
 
9190
10318
  pos = convert([ element.xrange[ 1 ], element.defaulty ]);
9191
- values.push(pos[ 0 ]);
9192
- values.push(pos[ 1 ]);
10319
+ values.push( pos[ 0 ] );
10320
+ values.push( pos[ 1 ] );
9193
10321
 
9194
10322
  // Draw points
9195
10323
  const center = [0,0];
@@ -9199,7 +10327,8 @@ class Dial {
9199
10327
  ctx.arc( pos[ 0 ], pos[ 1 ], 3, 0, Math.PI * 2);
9200
10328
  ctx.fill();
9201
10329
 
9202
- for( var i = 0; i < element.value.length; i += 1 ) {
10330
+ for( var i = 0; i < element.value.length; i += 1 )
10331
+ {
9203
10332
  var value = element.value[ i ];
9204
10333
  pos = convert( value );
9205
10334
  if( selected == i )
@@ -9211,10 +10340,11 @@ class Dial {
9211
10340
  ctx.fill();
9212
10341
  }
9213
10342
 
9214
- if(element.show_samples) {
10343
+ if( element.show_samples )
10344
+ {
9215
10345
  var samples = element.resample(element.show_samples);
9216
10346
  ctx.fillStyle = "#888";
9217
- for(var i = 0; i < samples.length; i += 1)
10347
+ for( var i = 0; i < samples.length; i += 1)
9218
10348
  {
9219
10349
  var value = [ i * ((element.xrange[ 1 ] - element.xrange[ 0 ]) / element.show_samples) + element.xrange[ 0 ], samples[ i ] ];
9220
10350
  pos = convert(value);
@@ -9237,7 +10367,8 @@ class Dial {
9237
10367
 
9238
10368
  selected = computeSelected( mousex, canvas.height - mousey );
9239
10369
 
9240
- if( e.button == LX.MOUSE_LEFT_CLICK && selected == -1 && element.allow_add_values ) {
10370
+ if( e.button == LX.MOUSE_LEFT_CLICK && selected == -1 && element.allow_add_values )
10371
+ {
9241
10372
  var v = unconvert([ mousex, canvas.height - mousey ]);
9242
10373
  element.value.push( v );
9243
10374
  sortValues();
@@ -9285,7 +10416,8 @@ class Dial {
9285
10416
  var dy = element.draggable_y ? last_mouse[ 1 ] - mousey : 0;
9286
10417
  var delta = unconvert([ -dx, dy ]);
9287
10418
 
9288
- if( selected != -1 ) {
10419
+ if( selected != -1 )
10420
+ {
9289
10421
  var minx = element.xrange[ 0 ];
9290
10422
  var maxx = element.xrange[ 1 ];
9291
10423
 
@@ -9393,7 +10525,8 @@ class AssetViewEvent {
9393
10525
  }
9394
10526
 
9395
10527
  string() {
9396
- switch(this.type) {
10528
+ switch(this.type)
10529
+ {
9397
10530
  case AssetViewEvent.NONE: return "assetview_event_none";
9398
10531
  case AssetViewEvent.ASSET_SELECTED: return "assetview_event_selected";
9399
10532
  case AssetViewEvent.ASSET_DELETED: return "assetview_event_deleted";
@@ -9747,8 +10880,8 @@ class AssetView {
9747
10880
  icon: "fa-solid fa-arrows-rotate",
9748
10881
  callback: domEl => { this._refreshContent(); }
9749
10882
  }
9750
- ], { width: "auto", noSelection: true } );
9751
- this.rightPanel.addText(null, this.path.join('/'), null, { disabled: true, signal: "@on_folder_change", style: { fontWeight: "bolder", fontSize: "16px", color: "#aaa" } });
10883
+ ], { width: "20%", minWidth: "164px", noSelection: true } );
10884
+ this.rightPanel.addText(null, this.path.join('/'), null, { width: "70%", maxWidth: "calc(70% - 64px)", minWidth: "164px", disabled: true, signal: "@on_folder_change", style: { fontWeight: "bolder", fontSize: "16px", color: "#aaa" } });
9752
10885
  this.rightPanel.addText(null, "Page " + this.contentPage + " / " + ((((this.currentData.length - 1) / AssetView.MAX_PAGE_ELEMENTS )|0) + 1), null, {disabled: true, signal: "@on_page_change", width: "fit-content"})
9753
10886
  this.rightPanel.endLine();
9754
10887
  }
@@ -9874,8 +11007,8 @@ class AssetView {
9874
11007
  title.innerText = item.id;
9875
11008
  itemEl.appendChild( title );
9876
11009
 
9877
- if( !that.skipPreview ) {
9878
-
11010
+ if( !that.skipPreview )
11011
+ {
9879
11012
  let preview = null;
9880
11013
  const hasImage = item.src && (['png', 'jpg'].indexOf( getExtension( item.src ) ) > -1 || item.src.includes("data:image/") ); // Support b64 image as src
9881
11014
 
@@ -9902,7 +11035,8 @@ class AssetView {
9902
11035
  var newEmSize = charsPerLine / newLength;
9903
11036
  var textBaseSize = 64;
9904
11037
 
9905
- if(newEmSize < 1) {
11038
+ if( newEmSize < 1 )
11039
+ {
9906
11040
  var newFontSize = newEmSize * textBaseSize;
9907
11041
  textEl.style.fontSize = newFontSize + "px";
9908
11042
  preview.style.paddingTop = "calc(50% - " + (textEl.offsetHeight * 0.5 + 10) + "px)"
@@ -10131,7 +11265,8 @@ class AssetView {
10131
11265
 
10132
11266
  this.currentData.push( item );
10133
11267
 
10134
- if(i == (num_files - 1)) {
11268
+ if( i == (num_files - 1) )
11269
+ {
10135
11270
  this._refreshContent();
10136
11271
  if( !this.skipBrowser )
10137
11272
  this.tree.refresh();
@@ -10183,7 +11318,7 @@ class AssetView {
10183
11318
  this.currentData.splice( idx, 1 );
10184
11319
  this._refreshContent( this.searchValue, this.filter );
10185
11320
 
10186
- if(this.onevent)
11321
+ if( this.onevent)
10187
11322
  {
10188
11323
  const event = new AssetViewEvent( AssetViewEvent.ASSET_DELETED, item );
10189
11324
  this.onevent( event );
@@ -10259,7 +11394,7 @@ Object.assign(LX, {
10259
11394
  xhr.onload = function(load)
10260
11395
  {
10261
11396
  var response = this.response;
10262
- if(this.status != 200)
11397
+ if( this.status != 200)
10263
11398
  {
10264
11399
  var err = "Error " + this.status;
10265
11400
  if(request.error)
@@ -10307,7 +11442,7 @@ Object.assign(LX, {
10307
11442
  var data = new FormData();
10308
11443
  if( request.data )
10309
11444
  {
10310
- for(var i in request.data)
11445
+ for( var i in request.data)
10311
11446
  data.append(i,request.data[i]);
10312
11447
  }
10313
11448
 
@@ -10368,7 +11503,7 @@ Object.assign(LX, {
10368
11503
  var size = total;
10369
11504
  var loaded_scripts = [];
10370
11505
 
10371
- for(var i in url)
11506
+ for( var i in url)
10372
11507
  {
10373
11508
  var script = document.createElement('script');
10374
11509
  script.num = i;
@@ -10493,6 +11628,18 @@ Element.prototype.getParentArea = function() {
10493
11628
  }
10494
11629
  }
10495
11630
 
11631
+ LX.ICONS = {
11632
+ "Sidebar": `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_iconCarrier"> <g id="Complete"> <g id="sidebar-left"> <g> <rect id="Square-2" data-name="Square" x="3" y="3" width="18" height="18" rx="2" ry="2" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></rect> <line x1="9" y1="21" x2="9" y2="3" fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="2"></line> </g> </g> </g> </g></svg>`,
11633
+ "More": `<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_iconCarrier"> <g id="Complete"> <g id="F-More"> <path id="Vertical" d="M12,16a2,2,0,1,1-2,2A2,2,0,0,1,12,16ZM10,6a2,2,0,1,0,2-2A2,2,0,0,0,10,6Zm0,6a2,2,0,1,0,2-2A2,2,0,0,0,10,12Z"></path> </g> </g> </g></svg>`,
11634
+ "MoreHorizontal": `<svg fill="#000000" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><g id="SVGRepo_iconCarrier"> <g id="Complete"> <g id="F-More"> <path id="Horizontal" d="M8,12a2,2,0,1,1-2-2A2,2,0,0,1,8,12Zm10-2a2,2,0,1,0,2,2A2,2,0,0,0,18,10Zm-6,0a2,2,0,1,0,2,2A2,2,0,0,0,12,10Z"></path> </g> </g> </g></svg>`,
11635
+ "MenuArrows": `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000" transform="rotate(90)"><g id="SVGRepo_iconCarrier"> <g id="Complete"> <g id="Code"> <g> <polyline id="Right-2" data-name="Right" points="15.5 7 20.5 12 15.5 17" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></polyline> <polyline id="Left-2" data-name="Left" points="8.5 7 3.5 12 8.5 17" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></polyline> </g> </g> </g> </g></svg>`,
11636
+ "Plus": `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000"<g id="SVGRepo_iconCarrier"> <g id="Complete"> <g id="add-2" data-name="add"> <g> <line x1="12" y1="19" x2="12" y2="5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line> <line x1="5" y1="12" x2="19" y2="12" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></line> </g> </g> </g> </g></svg>`,
11637
+ "Down": `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_iconCarrier"> <title>i</title> <g id="Complete"> <g id="F-Chevron"> <polyline id="Down" points="5 8.5 12 15.5 19 8.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></polyline> </g> </g> </g></svg>`,
11638
+ "Up": `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_iconCarrier"> <title>i</title> <g id="Complete"> <g id="F-Chevron"> <polyline id="Up" points="5 15.5 12 8.5 19 15.5" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></polyline> </g> </g> </g></svg>`,
11639
+ "Right": `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_iconCarrier"> <title>i</title> <g id="Complete"> <g id="F-Chevron"> <polyline id="Right" points="8.5 5 15.5 12 8.5 19" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></polyline> </g> </g> </g></svg>`,
11640
+ "Left": `<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" fill="#000000"><g id="SVGRepo_iconCarrier"> <title>i</title> <g id="Complete"> <g id="F-Chevron"> <polyline id="Left" points="15.5 5 8.5 12 15.5 19" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></polyline> </g> </g> </g></svg>`,
11641
+ }
11642
+
10496
11643
  LX.UTILS = {
10497
11644
  getTime() { return new Date().getTime() },
10498
11645
  compareThreshold( v, p, n, t ) { return Math.abs(v - p) >= t || Math.abs(v - n) >= t },
@@ -10532,17 +11679,19 @@ LX.UTILS = {
10532
11679
  drawSpline( ctx, pts, t ) {
10533
11680
 
10534
11681
  ctx.save();
10535
- var cp=[]; // array of control points, as x0,y0,x1,y1,...
10536
- var n=pts.length;
11682
+ var cp = []; // array of control points, as x0,y0,x1,y1,...
11683
+ var n = pts.length;
10537
11684
 
10538
11685
  // Draw an open curve, not connected at the ends
10539
- for(var i=0;i<n-4;i+=2) {
10540
- cp=cp.concat(LX.UTILS.getControlPoints(pts[i],pts[i+1],pts[i+2],pts[i+3],pts[i+4],pts[i+5],t));
11686
+ for( var i = 0; i < (n - 4); i += 2 )
11687
+ {
11688
+ cp = cp.concat(LX.UTILS.getControlPoints(pts[i],pts[i+1],pts[i+2],pts[i+3],pts[i+4],pts[i+5],t));
10541
11689
  }
10542
11690
 
10543
- for(var i=2;i<pts.length-5;i+=2) {
11691
+ for( var i = 2; i < ( pts.length - 5 ); i += 2 )
11692
+ {
10544
11693
  ctx.beginPath();
10545
- ctx.moveTo(pts[i],pts[i+1]);
11694
+ ctx.moveTo(pts[i], pts[i+1]);
10546
11695
  ctx.bezierCurveTo(cp[2*i-2],cp[2*i-1],cp[2*i],cp[2*i+1],pts[i+2],pts[i+3]);
10547
11696
  ctx.stroke();
10548
11697
  ctx.closePath();
@@ -10550,14 +11699,14 @@ LX.UTILS = {
10550
11699
 
10551
11700
  // For open curves the first and last arcs are simple quadratics.
10552
11701
  ctx.beginPath();
10553
- ctx.moveTo(pts[0],pts[1]);
10554
- ctx.quadraticCurveTo(cp[0],cp[1],pts[2],pts[3]);
11702
+ ctx.moveTo( pts[ 0 ], pts[ 1 ] );
11703
+ ctx.quadraticCurveTo( cp[ 0 ], cp[ 1 ], pts[ 2 ], pts[ 3 ]);
10555
11704
  ctx.stroke();
10556
11705
  ctx.closePath();
10557
11706
 
10558
11707
  ctx.beginPath();
10559
- ctx.moveTo(pts[n-2],pts[n-1]);
10560
- ctx.quadraticCurveTo(cp[2*n-10],cp[2*n-9],pts[n-4],pts[n-3]);
11708
+ ctx.moveTo( pts[ n-2 ], pts[ n-1 ] );
11709
+ ctx.quadraticCurveTo( cp[ 2*n-10 ], cp[ 2*n-9 ], pts[ n-4 ], pts[ n-3 ]);
10561
11710
  ctx.stroke();
10562
11711
  ctx.closePath();
10563
11712