lexgui 0.1.46 → 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,10 +8,12 @@
8
8
  */
9
9
 
10
10
  var LX = {
11
- version: "0.1.46",
11
+ version: "0.4.0",
12
12
  ready: false,
13
- components: [], // specific pre-build components
14
- signals: {} // events and triggers
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
15
17
  };
16
18
 
17
19
  LX.MOUSE_LEFT_CLICK = 0;
@@ -24,6 +26,8 @@ LX.MOUSE_TRIPLE_CLICK = 3;
24
26
  LX.CURVE_MOVEOUT_CLAMP = 0;
25
27
  LX.CURVE_MOVEOUT_DELETE = 1;
26
28
 
29
+ LX.DRAGGABLE_Z_INDEX = 101;
30
+
27
31
  function clamp( num, min, max ) { return Math.min( Math.max( num, min ), max ); }
28
32
  function round( number, precision ) { return precision == 0 ? Math.floor( number ) : +(( number ).toFixed( precision ?? 2 ).replace( /([0-9]+(\.[0-9]+[1-9])?)(\.?0+$)/, '$1' )); }
29
33
  function remapRange( oldValue, oldMin, oldMax, newMin, newMax ) { return ((( oldValue - oldMin ) * ( newMax - newMin )) / ( oldMax - oldMin )) + newMin; }
@@ -83,7 +87,7 @@ LX.doAsync = doAsync;
83
87
  */
84
88
  function getSupportedDOMName( text )
85
89
  {
86
- return text.replace(/\s/g, '').replaceAll('@', '_').replaceAll('+', '_plus_').replaceAll('.', '');
90
+ return text.replace( /\s/g, '' ).replaceAll('@', '_').replaceAll('+', '_plus_').replaceAll( '.', '' );
87
91
  }
88
92
 
89
93
  LX.getSupportedDOMName = getSupportedDOMName;
@@ -161,12 +165,13 @@ function getThemeColor( colorName )
161
165
  {
162
166
  const r = getComputedStyle( document.querySelector( ':root' ) );
163
167
  const value = r.getPropertyValue( '--' + colorName );
168
+ const theme = document.documentElement.getAttribute( "data-theme" );
164
169
 
165
- if( value.includes( "light-dark" ) && window.matchMedia )
170
+ if( value.includes( "light-dark" ) )
166
171
  {
167
172
  const currentScheme = r.getPropertyValue( "color-scheme" );
168
173
 
169
- if( ( window.matchMedia( "(prefers-color-scheme: light)" ).matches ) || ( currentScheme == "light" ) )
174
+ if( currentScheme == "light" )
170
175
  {
171
176
  return value.substring( value.indexOf( '(' ) + 1, value.indexOf( ',' ) ).replace( /\s/g, '' );
172
177
  }
@@ -221,7 +226,8 @@ LX.hexToRgb = hexToRgb;
221
226
  function rgbToHex( rgb )
222
227
  {
223
228
  let hex = "#";
224
- for( let c of rgb ) {
229
+ for( let c of rgb )
230
+ {
225
231
  c = Math.floor( c * 255 );
226
232
  hex += c.toString( 16 );
227
233
  }
@@ -296,7 +302,7 @@ LX.buildTextPattern = buildTextPattern;
296
302
 
297
303
  /**
298
304
  * @method makeDraggable
299
- * @description Allow an element to be dragged
305
+ * @description Allows an element to be dragged
300
306
  * @param {Element} domEl
301
307
  * @param {Object} options
302
308
  * autoAdjust (Bool): Sets in a correct position at the beggining
@@ -321,6 +327,7 @@ function makeDraggable( domEl, options = { } )
321
327
  top = top ?? e.clientY - offsetY - parentRect.y;
322
328
  domEl.style.left = clamp( left, dragMargin + fixedOffset.x, fixedOffset.x + parentRect.width - domEl.offsetWidth - dragMargin ) + 'px';
323
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
324
331
  };
325
332
 
326
333
  // Initial adjustment
@@ -365,26 +372,45 @@ function makeDraggable( domEl, options = { } )
365
372
  e.preventDefault();
366
373
  e.stopPropagation();
367
374
  e.stopImmediatePropagation();
368
- if( !currentTarget ) return;
375
+
376
+ if( !currentTarget )
377
+ {
378
+ return;
379
+ }
380
+
369
381
  // Remove image when dragging
370
382
  var img = new Image();
371
383
  img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs=';
372
384
  e.dataTransfer.setDragImage( img, 0, 0 );
373
385
  e.dataTransfer.effectAllowed = "move";
386
+
374
387
  const rect = e.target.getBoundingClientRect();
375
388
  const parentRect = currentTarget.parentElement.getBoundingClientRect();
376
389
  const isFixed = ( currentTarget.style.position == "fixed" );
377
390
  const fixedOffset = isFixed ? new LX.vec2( parentRect.x, parentRect.y ) : new LX.vec2();
378
391
  offsetX = e.clientX - rect.x - fixedOffset.x;
379
392
  offsetY = e.clientY - rect.y - fixedOffset.y;
393
+
380
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
+
381
407
  if( onDragStart )
382
408
  {
383
409
  onDragStart( currentTarget, e );
384
410
  }
385
411
  }, false );
386
412
 
387
- document.addEventListener( 'mouseup', () => {
413
+ document.addEventListener( 'mouseup', (e) => {
388
414
  if( currentTarget )
389
415
  {
390
416
  currentTarget = null;
@@ -395,6 +421,48 @@ function makeDraggable( domEl, options = { } )
395
421
 
396
422
  LX.makeDraggable = makeDraggable;
397
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
+
398
466
  /**
399
467
  * @method makeCodeSnippet
400
468
  * @description Create a code snippet in a specific language
@@ -404,6 +472,9 @@ LX.makeDraggable = makeDraggable;
404
472
  * language (String):
405
473
  * windowMode (Boolean):
406
474
  * lineNumbers (Boolean):
475
+ * firstLine (Number): TODO
476
+ * linesAdded (Array):
477
+ * linesRemoved (Array):
407
478
  * tabName (String):
408
479
  */
409
480
  function makeCodeSnippet( code, size, options = { } )
@@ -416,16 +487,15 @@ function makeCodeSnippet( code, size, options = { } )
416
487
 
417
488
  const snippet = document.createElement( "div" );
418
489
  snippet.className = "lexcodesnippet";
419
- snippet.style.width = size[ 0 ];
420
- snippet.style.height = size[ 1 ];
490
+ snippet.style.width = size ? size[ 0 ] : "auto";
491
+ snippet.style.height = size ? size[ 1 ] : "auto";
421
492
  const area = new Area( { noAppend: true } );
422
493
  let editor = new LX.CodeEditor( area, {
423
494
  skipInfo: true,
424
495
  disableEdition: true,
425
496
  allowAddScripts: false,
426
497
  name: options.tabName,
427
- // showTab: options.showTab ?? true,
428
- // lineNumbers: options.lineNumbers ?? true
498
+ // showTab: options.showTab ?? true
429
499
  } );
430
500
  editor.setText( code, options.language ?? "Plain Text" );
431
501
 
@@ -484,12 +554,47 @@ function makeCodeSnippet( code, size, options = { } )
484
554
  tabs.prepend( windowActionButtons );
485
555
  }
486
556
 
557
+ if( !( options.lineNumbers ?? true ) )
558
+ {
559
+ editor.root.classList.add( "no-gutter" );
560
+ }
561
+
487
562
  snippet.appendChild( area.root );
488
563
  return snippet;
489
564
  }
490
565
 
491
566
  LX.makeCodeSnippet = makeCodeSnippet;
492
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
+
585
+ /**
586
+ * @method registerCommandbarEntry
587
+ * @description Adds an extra command bar entry
588
+ * @param {String} name
589
+ * @param {Function} callback
590
+ */
591
+ function registerCommandbarEntry( name, callback )
592
+ {
593
+ LX.extraCommandbarEntries.push( { name, callback } );
594
+ }
595
+
596
+ LX.registerCommandbarEntry = registerCommandbarEntry;
597
+
493
598
  // Math classes
494
599
 
495
600
  class vec2 {
@@ -518,24 +623,23 @@ class vec2 {
518
623
 
519
624
  LX.vec2 = vec2;
520
625
 
521
- function create_global_searchbar( root )
626
+ function _createCommandbar( root )
522
627
  {
523
- let globalSearch = document.createElement("div");
524
- globalSearch.id = "global-search";
525
- globalSearch.className = "hidden";
526
- globalSearch.tabIndex = -1;
527
- root.appendChild( globalSearch );
628
+ let commandbar = document.createElement( "dialog" );
629
+ commandbar.className = "commandbar";
630
+ commandbar.tabIndex = -1;
631
+ root.appendChild( commandbar );
528
632
 
529
633
  let allItems = [];
530
634
  let hoverElId = null;
531
635
 
532
- globalSearch.addEventListener('keydown', function( e ) {
636
+ commandbar.addEventListener('keydown', function( e ) {
533
637
  e.stopPropagation();
534
638
  e.stopImmediatePropagation();
535
639
  hoverElId = hoverElId ?? -1;
536
640
  if( e.key == 'Escape' )
537
641
  {
538
- this.classList.add("hidden");
642
+ this.close();
539
643
  _resetBar( true );
540
644
  }
541
645
  else if( e.key == 'Enter' )
@@ -544,7 +648,7 @@ function create_global_searchbar( root )
544
648
  if( el )
545
649
  {
546
650
  const isCheckbox = (el.item.type && el.item.type === 'checkbox');
547
- this.classList.toggle('hidden');
651
+ this.close();
548
652
  if( isCheckbox )
549
653
  {
550
654
  el.item.checked = !el.item.checked;
@@ -559,7 +663,7 @@ function create_global_searchbar( root )
559
663
  else if ( e.key == 'ArrowDown' && hoverElId < (allItems.length - 1) )
560
664
  {
561
665
  hoverElId++;
562
- globalSearch.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
666
+ commandbar.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
563
667
  allItems[ hoverElId ].classList.add('hovered');
564
668
 
565
669
  let dt = allItems[ hoverElId ].offsetHeight * (hoverElId + 1) - itemContainer.offsetHeight;
@@ -574,19 +678,19 @@ function create_global_searchbar( root )
574
678
  } else if ( e.key == 'ArrowUp' && hoverElId > 0 )
575
679
  {
576
680
  hoverElId--;
577
- globalSearch.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
681
+ commandbar.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
578
682
  allItems[ hoverElId ].classList.add('hovered');
579
683
  }
580
684
  });
581
685
 
582
- globalSearch.addEventListener('focusout', function( e ) {
686
+ commandbar.addEventListener('focusout', function( e ) {
583
687
  if( e.relatedTarget == e.currentTarget )
584
688
  {
585
689
  return;
586
690
  }
587
691
  e.stopPropagation();
588
692
  e.stopImmediatePropagation();
589
- this.classList.add( "hidden" );
693
+ this.close();
590
694
  _resetBar( true );
591
695
  });
592
696
 
@@ -595,9 +699,7 @@ function create_global_searchbar( root )
595
699
  {
596
700
  e.stopImmediatePropagation();
597
701
  e.stopPropagation();
598
- globalSearch.classList.toggle('hidden');
599
- globalSearch.querySelector('input').focus();
600
- _addElements( undefined );
702
+ LX.setCommandbarState( true );
601
703
  }
602
704
  else
603
705
  {
@@ -675,22 +777,22 @@ function create_global_searchbar( root )
675
777
  const isCheckbox = (i && i.type && i.type === 'checkbox');
676
778
  if( isCheckbox )
677
779
  {
678
- searchItem.innerHTML = "<a class='fa fa-check'></a><span>" + p + t + "</span>"
780
+ searchItem.innerHTML = "<a class='fa fa-check'></a><span>" + ( p + t ) + "</span>"
679
781
  }
680
782
  else
681
783
  {
682
- searchItem.innerHTML = p + t;
784
+ searchItem.innerHTML = ( p + t );
683
785
  }
684
786
  searchItem.entry_name = t;
685
787
  searchItem.callback = c;
686
788
  searchItem.item = i;
687
789
  searchItem.addEventListener('click', function(e) {
688
- this.callback.call(window, this.entry_name);
689
- globalSearch.classList.toggle('hidden');
790
+ this.callback.call( window, this.entry_name );
791
+ LX.setCommandbarState( false );
690
792
  _resetBar( true );
691
793
  });
692
794
  searchItem.addEventListener('mouseenter', function(e) {
693
- globalSearch.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
795
+ commandbar.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
694
796
  this.classList.add('hovered');
695
797
  hoverElId = allItems.indexOf( this );
696
798
  });
@@ -724,7 +826,7 @@ function create_global_searchbar( root )
724
826
  _propagateAdd( c, filter, path );
725
827
  };
726
828
 
727
- const _addElements = filter => {
829
+ commandbar._addElements = filter => {
728
830
 
729
831
  _resetBar();
730
832
 
@@ -736,15 +838,25 @@ function create_global_searchbar( root )
736
838
  }
737
839
  }
738
840
 
841
+ for( let entry of LX.extraCommandbarEntries )
842
+ {
843
+ const name = entry.name;
844
+ if( !name.toLowerCase().includes( filter ) )
845
+ {
846
+ continue;
847
+ }
848
+ _addElement( name, entry.callback, "", {} );
849
+ }
850
+
739
851
  if( LX.has('CodeEditor') )
740
852
  {
741
853
  const instances = LX.CodeEditor.getInstances();
742
- if(!instances.length) return;
854
+ if( !instances.length ) return;
743
855
 
744
856
  const languages = instances[ 0 ].languages;
745
857
 
746
- for( let l of Object.keys( languages ) ) {
747
-
858
+ for( let l of Object.keys( languages ) )
859
+ {
748
860
  const key = "Language: " + l;
749
861
  const icon = instances[ 0 ]._getFileIcon( null, languages[ l ].ext );
750
862
 
@@ -752,9 +864,11 @@ function create_global_searchbar( root )
752
864
  "<img src='" + ( "https://raw.githubusercontent.com/jxarco/lexgui.js/master/" + icon ) + "'>";
753
865
 
754
866
  value += key + " <span class='lang-ext'>(" + languages[ l ].ext + ")</span>";
755
- if( key.toLowerCase().includes( filter ) ) {
756
- add_element( value, () => {
757
- for( let i of instances ) {
867
+ if( key.toLowerCase().includes( filter ) )
868
+ {
869
+ _addElement( value, () => {
870
+ for( let i of instances )
871
+ {
758
872
  i._changeLanguage( l );
759
873
  }
760
874
  }, "", {} );
@@ -764,14 +878,14 @@ function create_global_searchbar( root )
764
878
  }
765
879
 
766
880
  input.addEventListener('input', function(e) {
767
- _addElements( this.value.toLowerCase() );
881
+ commandbar._addElements( this.value.toLowerCase() );
768
882
  });
769
883
 
770
- globalSearch.appendChild( header );
771
- globalSearch.appendChild( tabArea.root );
772
- globalSearch.appendChild( itemContainer );
884
+ commandbar.appendChild( header );
885
+ commandbar.appendChild( tabArea.root );
886
+ commandbar.appendChild( itemContainer );
773
887
 
774
- return globalSearch;
888
+ return commandbar;
775
889
  }
776
890
 
777
891
  /**
@@ -781,6 +895,7 @@ function create_global_searchbar( root )
781
895
  * id: Id of the main area
782
896
  * skipRoot: Skip adding LX root container
783
897
  * skipDefaultArea: Skip creation of main area
898
+ * strictViewport: Use only window area
784
899
  */
785
900
 
786
901
  function init( options = { } )
@@ -803,16 +918,17 @@ function init( options = { } )
803
918
  this.root = root;
804
919
  this.container = document.body;
805
920
 
806
- // this.modal.toggleAttribute( 'hidden', true );
807
- // this.modal.toggle = function( force ) { this.toggleAttribute( 'hidden', force ); };
808
-
809
921
  this.modal.classList.add( 'hiddenOpacity' );
810
922
  this.modal.toggle = function( force ) { this.classList.toggle( 'hiddenOpacity', force ); };
811
923
 
812
924
  if( options.container )
925
+ {
813
926
  this.container = document.getElementById( options.container );
927
+ }
928
+
929
+ document.documentElement.setAttribute( "data-strictVP", ( options.strictViewport ?? true ) ? "true" : "false" );
814
930
 
815
- this.globalSearch = create_global_searchbar( this.container );
931
+ this.commandbar = _createCommandbar( this.container );
816
932
 
817
933
  this.container.appendChild( modal );
818
934
 
@@ -825,6 +941,25 @@ function init( options = { } )
825
941
  this.root = document.body;
826
942
  }
827
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
+
828
963
  // Disable drag icon
829
964
  root.addEventListener( 'dragover', function( e ) {
830
965
  e.preventDefault();
@@ -856,16 +991,48 @@ function init( options = { } )
856
991
  this.main_area = new Area( { id: options.id ?? 'mainarea' } );
857
992
  }
858
993
 
859
- window.matchMedia( "(prefers-color-scheme: dark)" ).addEventListener( "change", event => {
860
- const newColorScheme = event.matches ? "dark" : "light";
861
- LX.emit( "@on_new_color_scheme", newColorScheme );
862
- });
994
+ if( ( options.autoTheme ?? true ) && window.matchMedia && window.matchMedia( "(prefers-color-scheme: light)" ).matches )
995
+ {
996
+ LX.setTheme( "light" );
997
+
998
+ window.matchMedia( "(prefers-color-scheme: dark)" ).addEventListener( "change", event => {
999
+ LX.setTheme( event.matches ? "dark" : "light" );
1000
+ });
1001
+ }
863
1002
 
864
1003
  return this.main_area;
865
1004
  }
866
1005
 
867
1006
  LX.init = init;
868
1007
 
1008
+ /**
1009
+ * @method setCommandbarState
1010
+ * @param {Boolean} value
1011
+ * @param {Boolean} resetEntries
1012
+ */
1013
+
1014
+ function setCommandbarState( value, resetEntries = true )
1015
+ {
1016
+ const cb = this.commandbar;
1017
+
1018
+ if( value )
1019
+ {
1020
+ cb.show();
1021
+ cb.querySelector('input').focus();
1022
+
1023
+ if( resetEntries )
1024
+ {
1025
+ cb._addElements( undefined );
1026
+ }
1027
+ }
1028
+ else
1029
+ {
1030
+ cb.close();
1031
+ }
1032
+ }
1033
+
1034
+ LX.setCommandbarState = setCommandbarState;
1035
+
869
1036
  /**
870
1037
  * @method message
871
1038
  * @param {String} text
@@ -898,9 +1065,9 @@ LX.message = message;
898
1065
  * @param {String} title (Optional)
899
1066
  * @param {*} options
900
1067
  * id: Id of the message dialog
901
- * time: (Number) Delay time before close automatically (ms). Defalut: [3000]
902
- * position: (Array) [x,y] Dialog position in screen. Default: [screen centered]
903
- * size: (Array) [width, height]
1068
+ * timeout (Number): Delay time before it closes automatically (ms). Default: [3000]
1069
+ * position (Array): [x,y] Dialog position in screen. Default: [screen centered]
1070
+ * size (Array): [width, height]
904
1071
  */
905
1072
 
906
1073
  function popup( text, title, options = {} )
@@ -910,7 +1077,7 @@ function popup( text, title, options = {} )
910
1077
  throw("No message to show");
911
1078
  }
912
1079
 
913
- options.size = options.size ?? [ "auto", "auto" ];
1080
+ options.size = options.size ?? [ "max-content", "auto" ];
914
1081
  options.class = "lexpopup";
915
1082
 
916
1083
  const time = options.timeout || 3000;
@@ -918,13 +1085,9 @@ function popup( text, title, options = {} )
918
1085
  p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
919
1086
  }, options );
920
1087
 
921
- dialog.root.classList.add( 'fadein' );
922
- setTimeout(() => {
923
- dialog.root.classList.remove( 'fadein' );
924
- dialog.root.classList.add( 'fadeout' );
925
- }, time - 1000 );
926
-
927
- setTimeout( dialog.close, time );
1088
+ setTimeout( () => {
1089
+ dialog.close();
1090
+ }, Math.max( time, 150 ) );
928
1091
 
929
1092
  return dialog;
930
1093
  }
@@ -947,6 +1110,7 @@ LX.popup = popup;
947
1110
  function prompt( text, title, callback, options = {} )
948
1111
  {
949
1112
  options.modal = true;
1113
+ options.className = "prompt";
950
1114
 
951
1115
  let value = "";
952
1116
 
@@ -961,7 +1125,9 @@ function prompt( text, title, callback, options = {} )
961
1125
 
962
1126
  p.sameLine( 2 );
963
1127
 
964
- 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", () => {
965
1131
  if( options.required && value === '' )
966
1132
  {
967
1133
  text += text.includes("You must fill the input text.") ? "": "\nYou must fill the input text.";
@@ -975,8 +1141,6 @@ function prompt( text, title, callback, options = {} )
975
1141
  }
976
1142
  }, { buttonClass: "primary" });
977
1143
 
978
- p.addButton(null, "Cancel", () => {if(options.on_cancel) options.on_cancel(); dialog.close();} );
979
-
980
1144
  }, options );
981
1145
 
982
1146
  // Focus text prompt
@@ -990,6 +1154,101 @@ function prompt( text, title, callback, options = {} )
990
1154
 
991
1155
  LX.prompt = prompt;
992
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
+
993
1252
  /**
994
1253
  * @method badge
995
1254
  * @param {String} text
@@ -1009,6 +1268,25 @@ function badge( text, className, options = {} )
1009
1268
 
1010
1269
  LX.badge = badge;
1011
1270
 
1271
+ /**
1272
+ * @method makeContainer
1273
+ * @param {Array} size
1274
+ * @param {String} className
1275
+ * @param {Object} overrideStyle
1276
+ */
1277
+
1278
+ function makeContainer( size, className, overrideStyle = {} )
1279
+ {
1280
+ const container = document.createElement( "div" );
1281
+ container.className = "lexcontainer " + ( className ?? "" );
1282
+ container.style.width = size && size[ 0 ] ? size[ 0 ] : "100%";
1283
+ container.style.height = size && size[ 1 ] ? size[ 1 ] : "100%";
1284
+ Object.assign( container.style, overrideStyle );
1285
+ return container;
1286
+ }
1287
+
1288
+ LX.makeContainer = makeContainer;
1289
+
1012
1290
  /*
1013
1291
  * Events and Signals
1014
1292
  */
@@ -1272,7 +1550,8 @@ class Area {
1272
1550
 
1273
1551
  function inner_mousemove( e )
1274
1552
  {
1275
- switch( that.type ) {
1553
+ switch( that.type )
1554
+ {
1276
1555
  case "right":
1277
1556
  var dt = ( lastMousePosition[ 0 ] - e.x );
1278
1557
  var size = ( that.root.offsetWidth + dt );
@@ -1467,7 +1746,8 @@ class Area {
1467
1746
 
1468
1747
  // Listen resize event on first area
1469
1748
  const resizeObserver = new ResizeObserver( entries => {
1470
- for (const entry of entries) {
1749
+ for ( const entry of entries )
1750
+ {
1471
1751
  const bb = entry.contentRect;
1472
1752
  area2.root.style.height = "calc(100% - " + ( bb.height + 4) + "px )";
1473
1753
  }
@@ -1491,8 +1771,8 @@ class Area {
1491
1771
  }
1492
1772
 
1493
1773
  area1.root.style.width = "100%";
1494
- area1.root.style.height = "calc( " + height1 + " - " + data + " )";
1495
- area2.root.style.height = "calc( " + height2 + " - " + data + " )";
1774
+ area1.root.style.height = ( height1 == "auto" ? height1 : "calc( " + height1 + " - " + data + " )");
1775
+ area2.root.style.height = ( height2 == "auto" ? height2 : "calc( " + height2 + " - " + data + " )");
1496
1776
  }
1497
1777
  }
1498
1778
 
@@ -1608,9 +1888,15 @@ class Area {
1608
1888
  this.root.style.height = height;
1609
1889
  }
1610
1890
 
1611
- this.size = [ this.root.clientWidth, this.root.clientHeight ];
1891
+ if( this.onresize )
1892
+ {
1893
+ this.onresize( this.root.getBoundingClientRect() );
1894
+ }
1612
1895
 
1613
- this.propagateEvent( "onresize" );
1896
+ doAsync( () => {
1897
+ this.size = [ this.root.clientWidth, this.root.clientHeight ];
1898
+ this.propagateEvent( "onresize" );
1899
+ }, 150 );
1614
1900
  }
1615
1901
 
1616
1902
  /**
@@ -1627,7 +1913,7 @@ class Area {
1627
1913
  let [area1, area2] = this.sections;
1628
1914
  this.splitExtended = true;
1629
1915
 
1630
- if(this.type == "vertical")
1916
+ if( this.type == "vertical")
1631
1917
  {
1632
1918
  this.offset = area2.root.offsetHeight;
1633
1919
  area2.root.classList.add("fadeout-vertical");
@@ -1641,7 +1927,6 @@ class Area {
1641
1927
  this._moveSplit(-Infinity, true, 8);
1642
1928
  }
1643
1929
 
1644
- // Async resize in some ms...
1645
1930
  doAsync( () => this.propagateEvent('onresize'), 150 );
1646
1931
  }
1647
1932
 
@@ -1657,7 +1942,7 @@ class Area {
1657
1942
  this.splitExtended = false;
1658
1943
  let [area1, area2] = this.sections;
1659
1944
 
1660
- if(this.type == "vertical")
1945
+ if( this.type == "vertical")
1661
1946
  {
1662
1947
  area2.root.classList.add("fadein-vertical");
1663
1948
  this._moveSplit(this.offset);
@@ -1668,7 +1953,6 @@ class Area {
1668
1953
  this._moveSplit(this.offset);
1669
1954
  }
1670
1955
 
1671
- // Async resize in some ms...
1672
1956
  doAsync( () => this.propagateEvent('onresize'), 150 );
1673
1957
  }
1674
1958
 
@@ -1702,11 +1986,15 @@ class Area {
1702
1986
 
1703
1987
  propagateEvent( eventName ) {
1704
1988
 
1705
- for(var i = 0; i < this.sections.length; i++)
1989
+ for( var i = 0; i < this.sections.length; i++ )
1706
1990
  {
1707
- const area = this.sections[i];
1708
- if(area[ eventName ])
1991
+ const area = this.sections[ i ];
1992
+
1993
+ if( area[ eventName ] )
1994
+ {
1709
1995
  area[ eventName ].call( this, area.root.getBoundingClientRect() );
1996
+ }
1997
+
1710
1998
  area.propagateEvent( eventName );
1711
1999
  }
1712
2000
  }
@@ -1729,42 +2017,63 @@ class Area {
1729
2017
  * @param {Function} callback Function to fill the menubar
1730
2018
  * @param {*} options:
1731
2019
  * float: Justify content (left, center, right) [left]
2020
+ * sticky: Fix menubar at the top [true]
1732
2021
  */
1733
2022
 
1734
2023
  addMenubar( callback, options = {} ) {
1735
2024
 
1736
- let menubar = new Menubar(options);
2025
+ let menubar = new Menubar( options );
1737
2026
 
1738
- if(callback) callback( menubar );
2027
+ if( callback )
2028
+ {
2029
+ callback( menubar );
2030
+ }
1739
2031
 
1740
2032
  LX.menubars.push( menubar );
1741
2033
 
1742
2034
  const height = 48; // pixels
2035
+ const [ bar, content ] = this.split({ type: 'vertical', sizes: [height, null], resize: false, menubar: true });
2036
+ menubar.siblingArea = content;
1743
2037
 
1744
- const [bar, content] = this.split({type: 'vertical', sizes: [height, null], resize: false, menubar: true});
1745
2038
  bar.attach( menubar );
1746
- bar.is_menubar = true;
2039
+ bar.isMenubar = true;
2040
+
2041
+ if( options.sticky ?? true )
2042
+ {
2043
+ bar.root.classList.add( "sticky" );
2044
+ }
2045
+
1747
2046
  return menubar;
1748
2047
  }
1749
2048
 
1750
2049
  /**
1751
2050
  * @method addSidebar
1752
2051
  * @param {Function} callback Function to fill the sidebar
2052
+ * @param {Object} options: Sidebar options
2053
+ * width: Width of the sidebar [16rem]
1753
2054
  */
1754
2055
 
1755
2056
  addSidebar( callback, options = {} ) {
1756
2057
 
1757
2058
  let sidebar = new SideBar( options );
1758
2059
 
1759
- if( callback ) callback( sidebar );
2060
+ if( callback )
2061
+ {
2062
+ callback( sidebar );
2063
+ }
2064
+
2065
+ // Generate DOM elements after adding all entries
2066
+ sidebar._build();
1760
2067
 
1761
2068
  LX.menubars.push( sidebar );
1762
2069
 
1763
- 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;
1764
2073
 
1765
- const [bar, content] = this.split( { type: 'horizontal', sizes: [ width, null ], resize: false, sidebar: true } );
1766
2074
  bar.attach( sidebar );
1767
- bar.is_sidebar = true;
2075
+ bar.isSidebar = true;
2076
+
1768
2077
  return sidebar;
1769
2078
  }
1770
2079
 
@@ -2020,7 +2329,8 @@ class Area {
2020
2329
 
2021
2330
  this.size = [ rect.width, rect.height ];
2022
2331
 
2023
- for(var i = 0; i < this.sections.length; i++) {
2332
+ for( var i = 0; i < this.sections.length; i++ )
2333
+ {
2024
2334
  this.sections[i]._update();
2025
2335
  }
2026
2336
  }
@@ -2044,7 +2354,7 @@ class Tabs {
2044
2354
  static TAB_SIZE = 28;
2045
2355
  static TAB_ID = 0;
2046
2356
 
2047
- constructor( area, options = {} ) {
2357
+ constructor( area, options = {} ) {
2048
2358
 
2049
2359
  this.onclose = options.onclose;
2050
2360
 
@@ -2092,7 +2402,7 @@ class Tabs {
2092
2402
 
2093
2403
  area.root.classList.add( "lexareatabscontainer" );
2094
2404
 
2095
- area.split({type: 'vertical', sizes: "auto", resize: false, top: 6});
2405
+ area.split({type: 'vertical', sizes: options.sizes ?? "auto", resize: false, top: 6});
2096
2406
  area.sections[0].attach( container );
2097
2407
 
2098
2408
  this.area = area.sections[1];
@@ -2126,24 +2436,28 @@ class Tabs {
2126
2436
  }
2127
2437
 
2128
2438
  // debug
2129
- if(folding)
2439
+ if( folding )
2130
2440
  {
2131
2441
  this.folded = true;
2132
2442
  this.folding = folding;
2133
2443
 
2134
- 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
+ }
2135
2448
 
2136
2449
  // Listen resize event on parent area
2137
2450
  const resizeObserver = new ResizeObserver((entries) => {
2138
- for (const entry of entries) {
2451
+ for (const entry of entries)
2452
+ {
2139
2453
  const bb = entry.contentRect;
2140
- const sibling = area.parentArea.sections[0].root;
2141
- const add_offset = true; // hardcoded...
2142
- 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 )";
2143
2457
  }
2144
2458
  });
2145
2459
 
2146
- resizeObserver.observe(this.area.root);
2460
+ resizeObserver.observe( this.area.root );
2147
2461
  this.area.root.classList.add('folded');
2148
2462
  }
2149
2463
  }
@@ -2280,7 +2594,8 @@ class Tabs {
2280
2594
 
2281
2595
  setTimeout( () => {
2282
2596
 
2283
- if( options.onCreate ) {
2597
+ if( options.onCreate )
2598
+ {
2284
2599
  options.onCreate.call(this, this.area.root.getBoundingClientRect());
2285
2600
  }
2286
2601
 
@@ -2342,34 +2657,41 @@ LX.Tabs = Tabs;
2342
2657
 
2343
2658
  class Menubar {
2344
2659
 
2345
- constructor( options = {} ) {
2660
+ constructor( options = {} ) {
2346
2661
 
2347
- this.root = document.createElement('div');
2662
+ this.root = document.createElement( "div" );
2348
2663
  this.root.className = "lexmenubar";
2349
- if(options.float)
2664
+
2665
+ if( options.float )
2666
+ {
2350
2667
  this.root.style.justifyContent = options.float;
2351
- this.items = [];
2668
+ }
2352
2669
 
2353
- this.icons = {};
2354
- this.shorts = {};
2355
- this.buttons = [];
2670
+ this.items = [ ];
2671
+ this.buttons = [ ];
2672
+ this.icons = { };
2673
+ this.shorts = { };
2356
2674
  }
2357
2675
 
2358
2676
  /**
2359
2677
  * @method add
2360
- * @param {*} options:
2678
+ * @param {Object} options:
2361
2679
  * callback: Function to call on each item
2680
+ * icon: Entry icon
2681
+ * short: Entry shortcut name
2362
2682
  */
2363
2683
 
2364
2684
  add( path, options = {} ) {
2365
2685
 
2366
- if(options.constructor == Function)
2686
+ if( options.constructor == Function )
2687
+ {
2367
2688
  options = { callback: options };
2689
+ }
2368
2690
 
2369
- // process path
2370
- const tokens = path.split("/");
2691
+ // Process path
2692
+ const tokens = path.split( "/" );
2371
2693
 
2372
- // assign icons and shortcuts to last token in path
2694
+ // Assign icons and shortcuts to last token in path
2373
2695
  const lastPath = tokens[tokens.length - 1];
2374
2696
  this.icons[ lastPath ] = options.icon;
2375
2697
  this.shorts[ lastPath ] = options.short;
@@ -2377,105 +2699,146 @@ class Menubar {
2377
2699
  let idx = 0;
2378
2700
  let that = this;
2379
2701
 
2380
- const insert = (token, list) => {
2381
- if(token == undefined) return;
2702
+ const _insertEntry = ( token, list ) => {
2703
+ if( token == undefined )
2704
+ {
2705
+ return;
2706
+ }
2382
2707
 
2383
2708
  let found = null;
2384
2709
  list.forEach( o => {
2385
- const keys = Object.keys(o);
2710
+ const keys = Object.keys( o );
2386
2711
  const key = keys.find( t => t == token );
2387
- if(key) found = o[ key ];
2712
+ if( key ) found = o[ key ];
2388
2713
  } );
2389
2714
 
2390
- if(found) {
2391
- insert( tokens[idx++], found );
2715
+ if( found )
2716
+ {
2717
+ _insertEntry( tokens[ idx++ ], found );
2392
2718
  }
2393
- else {
2719
+ else
2720
+ {
2394
2721
  let item = {};
2395
2722
  item[ token ] = [];
2396
- const next_token = tokens[idx++];
2723
+ const nextToken = tokens[ idx++ ];
2397
2724
  // Check if last token -> add callback
2398
- if(!next_token) {
2725
+ if( !nextToken )
2726
+ {
2399
2727
  item[ 'callback' ] = options.callback;
2728
+ item[ 'disabled' ] = options.disabled;
2400
2729
  item[ 'type' ] = options.type;
2401
2730
  item[ 'checked' ] = options.checked;
2402
2731
  }
2403
2732
  list.push( item );
2404
- insert( next_token, item[ token ] );
2733
+ _insertEntry( nextToken, item[ token ] );
2405
2734
  }
2406
2735
  };
2407
2736
 
2408
- insert( tokens[idx++], this.items );
2737
+ _insertEntry( tokens[idx++], this.items );
2409
2738
 
2410
2739
  // Create elements
2411
2740
 
2412
2741
  for( let item of this.items )
2413
2742
  {
2414
- let key = Object.keys(item)[0];
2415
- let pKey = key.replace(/\s/g, '').replaceAll('.', '');
2743
+ let key = Object.keys( item )[ 0 ];
2744
+ let pKey = key.replace( /\s/g, '' ).replaceAll( '.', '' );
2416
2745
 
2417
2746
  // Item already created
2418
- if( this.root.querySelector("#" + pKey) )
2747
+ if( this.root.querySelector( "#" + pKey ) )
2748
+ {
2419
2749
  continue;
2750
+ }
2420
2751
 
2421
2752
  let entry = document.createElement('div');
2422
2753
  entry.className = "lexmenuentry";
2423
2754
  entry.id = pKey;
2424
2755
  entry.innerHTML = "<span>" + key + "</span>";
2425
- if(options.position == "left") {
2756
+ entry.tabIndex = "1";
2757
+
2758
+ if( options.position == "left" )
2759
+ {
2426
2760
  this.root.prepend( entry );
2427
2761
  }
2428
- else {
2429
- if(options.position == "right")
2762
+ else
2763
+ {
2764
+ if( options.position == "right" )
2765
+ {
2430
2766
  entry.right = true;
2431
- if(this.root.lastChild && this.root.lastChild.right) {
2767
+ }
2768
+
2769
+ if( this.root.lastChild && this.root.lastChild.right )
2770
+ {
2432
2771
  this.root.lastChild.before( entry );
2433
2772
  }
2434
- else {
2773
+ else
2774
+ {
2435
2775
  this.root.appendChild( entry );
2436
2776
  }
2437
2777
  }
2438
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
+
2439
2791
  const create_submenu = function( o, k, c, d ) {
2440
2792
 
2441
- let contextmenu = document.createElement('div');
2442
- contextmenu.className = "lexcontextmenu";
2443
- contextmenu.tabIndex = "0";
2444
- 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;
2445
2799
  var rect = c.getBoundingClientRect();
2446
- contextmenu.style.left = (isSubMenu ? rect.width : rect.left) + "px";
2447
- // Entries use css to set top relative to parent
2448
- contextmenu.style.top = (isSubMenu ? 0 : rect.bottom - 4) + "px";
2449
- 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();
2450
2803
 
2451
- contextmenu.focus();
2804
+ doAsync( () => {
2805
+ menuElement.dataset[ "open" ] = true;
2806
+ }, 10 );
2452
2807
 
2453
- rect = contextmenu.getBoundingClientRect();
2808
+ LX.root.appendChild( menuElement );
2454
2809
 
2455
- for( var i = 0; i < o[k].length; ++i )
2810
+ for( var i = 0; i < o[ k ].length; ++i )
2456
2811
  {
2457
- const subitem = o[k][i];
2458
- const subkey = Object.keys(subitem)[0];
2812
+ const subitem = o[ k ][ i ];
2813
+ const subkey = Object.keys( subitem )[ 0 ];
2459
2814
  const hasSubmenu = subitem[ subkey ].length;
2460
2815
  const isCheckbox = subitem[ 'type' ] == 'checkbox';
2461
2816
  let subentry = document.createElement('div');
2462
- subentry.className = "lexcontextmenuentry";
2463
- subentry.className += (i == o[k].length - 1 ? " last" : "");
2464
- if(subkey == '')
2465
- subentry.className = " lexseparator";
2466
- else {
2817
+ subentry.className = "lexmenuboxentry";
2818
+ subentry.className += (i == o[k].length - 1 ? " last" : "") + ( subitem.disabled ? " disabled" : "" );
2467
2819
 
2820
+ if( subkey == '' )
2821
+ {
2822
+ subentry.className = " lexseparator";
2823
+ }
2824
+ else
2825
+ {
2468
2826
  subentry.id = subkey;
2469
2827
  let subentrycont = document.createElement('div');
2470
2828
  subentrycont.innerHTML = "";
2471
- subentrycont.classList = "lexcontextmenuentrycontainer";
2829
+ subentrycont.classList = "lexmenuboxentrycontainer";
2472
2830
  subentry.appendChild(subentrycont);
2473
2831
  const icon = that.icons[ subkey ];
2474
- if(isCheckbox){
2832
+ if( isCheckbox )
2833
+ {
2475
2834
  subentrycont.innerHTML += "<input type='checkbox' >";
2476
- }else if(icon) {
2835
+ }
2836
+ else if( icon )
2837
+ {
2477
2838
  subentrycont.innerHTML += "<a class='" + icon + " fa-sm'></a>";
2478
- }else {
2839
+ }
2840
+ else
2841
+ {
2479
2842
  subentrycont.innerHTML += "<a class='fa-solid fa-sm noicon'></a>";
2480
2843
  subentrycont.classList.add( "noicon" );
2481
2844
 
@@ -2483,46 +2846,54 @@ class Menubar {
2483
2846
  subentrycont.innerHTML += "<div class='lexentryname'>" + subkey + "</div>";
2484
2847
  }
2485
2848
 
2486
- let checkbox_input = subentry.querySelector('input');
2487
- if(checkbox_input) {
2488
- checkbox_input.checked = subitem.checked ?? false;
2489
- checkbox_input.addEventListener('change', (e) => {
2490
- 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;
2491
2855
  const f = subitem[ 'callback' ];
2492
- if(f) {
2856
+ if( f )
2857
+ {
2493
2858
  f.call( this, subitem.checked, subkey, subentry );
2494
- that.root.querySelectorAll(".lexcontextmenu").forEach(e => e.remove());
2859
+ _resetMenubar();
2495
2860
  }
2496
2861
  e.stopPropagation();
2497
2862
  e.stopImmediatePropagation();
2498
2863
  })
2499
2864
  }
2500
2865
 
2501
- contextmenu.appendChild( subentry );
2866
+ menuElement.appendChild( subentry );
2502
2867
 
2503
2868
  // Nothing more for separators
2504
- if(subkey == '') continue;
2869
+ if( subkey == '' )
2870
+ {
2871
+ continue;
2872
+ }
2505
2873
 
2506
- contextmenu.addEventListener('keydown', function(e) {
2874
+ menuElement.addEventListener('keydown', function(e) {
2507
2875
  e.preventDefault();
2508
2876
  let short = that.shorts[ subkey ];
2509
2877
  if(!short) return;
2510
2878
  // check if it's a letter or other key
2511
2879
  short = short.length == 1 ? short.toLowerCase() : short;
2512
- if(short == e.key) {
2880
+ if( short == e.key )
2881
+ {
2513
2882
  subentry.click()
2514
2883
  }
2515
2884
  });
2516
2885
 
2517
2886
  // Add callback
2518
2887
  subentry.addEventListener("click", e => {
2519
- if(checkbox_input) {
2888
+ if( checkboxInput )
2889
+ {
2520
2890
  subitem.checked = !subitem.checked;
2521
2891
  }
2522
2892
  const f = subitem[ 'callback' ];
2523
- if(f) {
2524
- f.call( this, checkbox_input ? subitem.checked : subkey, checkbox_input ? subkey : subentry );
2525
- 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();
2526
2897
  }
2527
2898
  e.stopPropagation();
2528
2899
  e.stopImmediatePropagation();
@@ -2531,7 +2902,8 @@ class Menubar {
2531
2902
  // Add icon if has submenu, else check for shortcut
2532
2903
  if( !hasSubmenu)
2533
2904
  {
2534
- if(that.shorts[ subkey ]) {
2905
+ if( that.shorts[ subkey ] )
2906
+ {
2535
2907
  let shortEl = document.createElement('div');
2536
2908
  shortEl.className = "lexentryshort";
2537
2909
  shortEl.innerText = that.shorts[ subkey ];
@@ -2545,44 +2917,67 @@ class Menubar {
2545
2917
  subentry.appendChild( submenuIcon );
2546
2918
 
2547
2919
  subentry.addEventListener("mouseover", e => {
2548
- if(subentry.built)
2549
- return;
2920
+ if( subentry.built )
2921
+ {
2922
+ return;
2923
+ }
2550
2924
  subentry.built = true;
2551
2925
  create_submenu( subitem, subkey, subentry, ++d );
2552
2926
  e.stopPropagation();
2553
2927
  });
2554
2928
 
2555
- subentry.addEventListener("mouseleave", () => {
2556
- d = -1; // Reset depth
2557
- delete subentry.built;
2558
- 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
+ }
2559
2937
  });
2560
2938
  }
2561
2939
 
2562
2940
  // Set final width
2563
- contextmenu.style.width = contextmenu.offsetWidth + "px";
2941
+ menuElement.style.width = menuElement.offsetWidth + "px";
2564
2942
  };
2565
2943
 
2566
- 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
+ };
2567
2950
 
2951
+ entry.addEventListener("click", () => {
2568
2952
  const f = item[ 'callback' ];
2569
- if(f) {
2953
+ if( f )
2954
+ {
2570
2955
  f.call( this, key, entry );
2571
2956
  return;
2572
2957
  }
2573
2958
 
2574
- // Manage selected
2575
- this.root.querySelectorAll(".lexmenuentry").forEach( e => e.classList.remove( 'selected' ) );
2576
- entry.classList.add( "selected" );
2959
+ _showEntry();
2577
2960
 
2578
- this.root.querySelectorAll(".lexcontextmenu").forEach( e => e.remove() );
2579
- create_submenu( item, key, entry, -1 );
2961
+ this.focused = true;
2580
2962
  });
2581
2963
 
2582
- entry.addEventListener("mouseleave", () => {
2583
- this.root.querySelectorAll(".lexmenuentry").forEach( e => e.classList.remove( 'selected' ) );
2584
- this.root.querySelectorAll(".lexcontextmenu").forEach(e => e.remove());
2585
- });
2964
+ entry.addEventListener( "mouseover", (e) => {
2965
+
2966
+ if( this.focused && !entry.built )
2967
+ {
2968
+ _showEntry();
2969
+ }
2970
+ });
2971
+
2972
+ entry.addEventListener("blur", (e) => {
2973
+
2974
+ if( e.relatedTarget && e.relatedTarget.classList.contains( "lexmenubox" ) )
2975
+ {
2976
+ return;
2977
+ }
2978
+
2979
+ _resetMenubar();
2980
+ });
2586
2981
  }
2587
2982
  }
2588
2983
 
@@ -2600,20 +2995,24 @@ class Menubar {
2600
2995
  * @param {Object} item: parent item
2601
2996
  * @param {Array} tokens: split path strings
2602
2997
  */
2603
- getSubitem(item, tokens) {
2998
+ getSubitem( item, tokens ) {
2604
2999
 
2605
3000
  let subitem = null;
2606
- let path = tokens[0];
2607
- for(let i = 0; i < item.length; i++) {
2608
- if(item[i][path]) {
3001
+ let path = tokens[ 0 ];
2609
3002
 
2610
- if(tokens.length == 1) {
2611
- 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 ];
2612
3010
  return subitem;
2613
3011
  }
2614
- else {
2615
- tokens.splice(0,1);
2616
- return this.getSubitem(item[i][path], tokens);
3012
+ else
3013
+ {
3014
+ tokens.splice( 0, 1 );
3015
+ return this.getSubitem( item[ i ][ path ], tokens );
2617
3016
  }
2618
3017
 
2619
3018
  }
@@ -2640,11 +3039,12 @@ class Menubar {
2640
3039
  setButtonIcon( title, icon, callback, options = {} ) {
2641
3040
 
2642
3041
  const button = this.buttons[ title ];
2643
- if(button) {
2644
-
3042
+ if( button )
3043
+ {
2645
3044
  button.querySelector('a').className = "fa-solid" + " " + icon + " lexicon";
2646
3045
  }
2647
- else {
3046
+ else
3047
+ {
2648
3048
  let button = document.createElement('div');
2649
3049
  const disabled = options.disabled ?? false;
2650
3050
  button.className = "lexmenubutton" + (disabled ? " disabled" : "");
@@ -2654,15 +3054,21 @@ class Menubar {
2654
3054
  button.style.maxHeight = "calc(100% - 10px)";
2655
3055
  button.style.alignItems = "center";
2656
3056
 
2657
- if(options.float == "right")
3057
+ if( options.float == "right" )
3058
+ {
2658
3059
  button.right = true;
2659
- if(this.root.lastChild && this.root.lastChild.right) {
3060
+ }
3061
+
3062
+ if( this.root.lastChild && this.root.lastChild.right )
3063
+ {
2660
3064
  this.root.lastChild.before( button );
2661
3065
  }
2662
- else if(options.float == "left") {
2663
- this.root.prepend(button);
3066
+ else if( options.float == "left" )
3067
+ {
3068
+ this.root.prepend( button );
2664
3069
  }
2665
- else {
3070
+ else
3071
+ {
2666
3072
  this.root.appendChild( button );
2667
3073
  }
2668
3074
 
@@ -2682,11 +3088,12 @@ class Menubar {
2682
3088
 
2683
3089
  setButtonImage( title, src, callback, options = {} ) {
2684
3090
  const button = this.buttons[ title ];
2685
- if(button) {
2686
-
3091
+ if( button )
3092
+ {
2687
3093
  button.querySelector('a').className = "fa-solid" + " " + icon + " lexicon";
2688
3094
  }
2689
- else {
3095
+ else
3096
+ {
2690
3097
  let button = document.createElement('div');
2691
3098
  const disabled = options.disabled ?? false;
2692
3099
  button.className = "lexmenubutton" + (disabled ? " disabled" : "");
@@ -2695,15 +3102,21 @@ class Menubar {
2695
3102
  button.style.padding = "5px";
2696
3103
  button.style.alignItems = "center";
2697
3104
 
2698
- if(options.float == "right")
3105
+ if( options.float == "right" )
3106
+ {
2699
3107
  button.right = true;
2700
- if(this.root.lastChild && this.root.lastChild.right) {
3108
+ }
3109
+
3110
+ if( this.root.lastChild && this.root.lastChild.right )
3111
+ {
2701
3112
  this.root.lastChild.before( button );
2702
3113
  }
2703
- else if(options.float == "left") {
2704
- this.root.prepend(button);
3114
+ else if( options.float == "left" )
3115
+ {
3116
+ this.root.prepend( button );
2705
3117
  }
2706
- else {
3118
+ else
3119
+ {
2707
3120
  this.root.appendChild( button );
2708
3121
  }
2709
3122
 
@@ -2816,90 +3229,306 @@ LX.Menubar = Menubar;
2816
3229
 
2817
3230
  class SideBar {
2818
3231
 
2819
- 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 = {} ) {
2820
3253
 
2821
3254
  this.root = document.createElement( 'div' );
2822
3255
  this.root.className = "lexsidebar";
2823
3256
 
2824
- this.footer = document.createElement( 'div' );
2825
- this.footer.className = "lexsidebarfooter";
2826
- this.root.appendChild( this.footer );
3257
+ window.sidebar = this;
3258
+
3259
+ this.collapsable = options.collapsable ?? true;
3260
+ this.collapseWidth = ( options.collapseToIcons ?? true ) ? "58px" : "0px";
3261
+ this.collapsed = false;
3262
+
3263
+ doAsync( () => {
3264
+
3265
+ this.root.parentElement.ogWidth = this.root.parentElement.style.width;
3266
+ this.root.parentElement.style.transition = "width 0.25s ease-out";
3267
+
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
+ } );
3365
+
3366
+ const avatar = document.createElement( 'span' );
3367
+ avatar.className = "lexavatar";
3368
+ this.footer.appendChild( avatar );
3369
+
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)`;
2827
3404
 
2828
3405
  this.items = [ ];
3406
+ this.icons = { };
3407
+ this.groups = { };
2829
3408
  }
2830
3409
 
2831
3410
  /**
2832
- * @method add
2833
- * @param {*} options:
2834
- * callback: Function to call on each item
2835
- * bottom: Bool to set item at the bottom as helper button (not selectable)
2836
- * className: Add class to the entry DOM element
3411
+ * @method toggleCollapsed
2837
3412
  */
2838
3413
 
2839
- add( key, options = {} ) {
2840
-
2841
- if( options.constructor == Function )
2842
- options = { callback: options };
2843
-
2844
- let pKey = key.replace( /\s/g, '' ).replaceAll( '.', '' );
3414
+ toggleCollapsed() {
2845
3415
 
2846
- if( this.items.findIndex( (v, i) => v.key == pKey ) > -1 )
3416
+ if( !this.collapsable )
2847
3417
  {
2848
- console.warn( `'${key}' already created in Sidebar` );
2849
3418
  return;
2850
3419
  }
2851
3420
 
2852
- let entry = document.createElement( 'div' );
2853
- entry.className = "lexsidebarentry " + ( options.className ?? "" );
2854
- entry.id = pKey;
3421
+ this.collapsed = !this.collapsed;
2855
3422
 
2856
- if( options.bottom )
3423
+ if( this.collapsed )
2857
3424
  {
2858
- this.footer.appendChild( entry );
3425
+ this.root.classList.add( "collapsing" );
3426
+ this.root.parentElement.style.width = this.collapseWidth;
2859
3427
  }
2860
3428
  else
2861
3429
  {
2862
- 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;
2863
3433
  }
2864
3434
 
2865
- // Reappend footer in root
2866
- this.root.appendChild( this.footer );
3435
+ this.resizeObserver.observe( this.root.parentElement );
2867
3436
 
2868
- let button = document.createElement( 'button' );
2869
- button.innerHTML = "<i class='"+ (options.icon ?? "") + "'></i>";
2870
- entry.appendChild( button );
3437
+ doAsync( () => {
2871
3438
 
2872
- let desc = document.createElement( 'span' );
2873
- desc.className = 'lexsidebarentrydesc';
2874
- desc.innerHTML = key;
2875
- entry.appendChild( desc );
3439
+ this.root.classList.toggle( "collapsed", this.collapsed );
3440
+ this.resizeObserver.unobserve( this.root.parentElement );
2876
3441
 
2877
- button.addEventListener("mouseenter", () => {
2878
- setTimeout( () => {
2879
- desc.style.display = "unset";
2880
- }, 100 );
2881
- });
3442
+ }, 250 );
3443
+ }
2882
3444
 
2883
- button.addEventListener("mouseleave", () => {
2884
- setTimeout( () => {
2885
- desc.style.display = "none";
2886
- }, 100 );
2887
- });
3445
+ /**
3446
+ * @method separator
3447
+ */
3448
+
3449
+ separator() {
3450
+
3451
+ this.currentGroup = null;
3452
+
3453
+ this.add( "" );
3454
+ }
3455
+
3456
+ /**
3457
+ * @method group
3458
+ * @param {String} groupName
3459
+ * @param {Object} action: { icon, callback }
3460
+ */
3461
+
3462
+ group( groupName, action ) {
3463
+
3464
+ this.currentGroup = groupName;
3465
+
3466
+ this.groups[ groupName ] = action;
3467
+ }
3468
+
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;
2888
3492
 
2889
- entry.addEventListener("click", () => {
2890
3493
 
2891
- const f = options.callback;
2892
- if( f ) f.call( this, key, entry );
3494
+ let idx = 0;
3495
+
3496
+ const _insertEntry = ( token, list ) => {
2893
3497
 
2894
- // Manage selected
2895
- if( !options.bottom )
3498
+ if( token == undefined )
2896
3499
  {
2897
- this.root.querySelectorAll(".lexsidebarentry").forEach( e => e.classList.remove( 'selected' ) );
2898
- entry.classList.add( "selected" );
3500
+ return;
2899
3501
  }
2900
- });
2901
3502
 
2902
- 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 );
2903
3532
  }
2904
3533
 
2905
3534
  /**
@@ -2918,6 +3547,246 @@ class SideBar {
2918
3547
 
2919
3548
  entry.domEl.click();
2920
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
+ }
2921
3790
  };
2922
3791
 
2923
3792
  LX.SideBar = SideBar;
@@ -2935,29 +3804,32 @@ class Widget {
2935
3804
  static DROPDOWN = 4;
2936
3805
  static CHECKBOX = 5;
2937
3806
  static TOGGLE = 6;
2938
- static COLOR = 7;
2939
- static NUMBER = 8;
2940
- static TITLE = 9;
2941
- static VECTOR = 10;
2942
- static TREE = 11;
2943
- static PROGRESS = 12;
2944
- static FILE = 13;
2945
- static LAYERS = 14;
2946
- static ARRAY = 15;
2947
- static LIST = 16;
2948
- static TAGS = 17;
2949
- static CURVE = 18;
2950
- static CARD = 19;
2951
- static IMAGE = 20;
2952
- static CONTENT = 21;
2953
- static CUSTOM = 22;
2954
- static SEPARATOR = 23;
2955
- static KNOB = 24;
2956
- static SIZE = 25;
2957
- static PAD = 26;
2958
- static FORM = 27;
2959
- static DIAL = 28;
2960
- static COUNTER = 29;
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;
2961
3833
 
2962
3834
  static NO_CONTEXT_TYPES = [
2963
3835
  Widget.BUTTON,
@@ -2985,7 +3857,9 @@ class Widget {
2985
3857
  set( value, skipCallback = false, signalName = "" ) {
2986
3858
 
2987
3859
  if( this.onSetValue )
3860
+ {
2988
3861
  return this.onSetValue( value, skipCallback );
3862
+ }
2989
3863
 
2990
3864
  console.warn("Can't set value of " + this.typeName());
2991
3865
  }
@@ -3024,14 +3898,17 @@ class Widget {
3024
3898
 
3025
3899
  typeName() {
3026
3900
 
3027
- switch( this.type ) {
3901
+ switch( this.type )
3902
+ {
3028
3903
  case Widget.TEXT: return "Text";
3029
3904
  case Widget.TEXTAREA: return "TextArea";
3030
3905
  case Widget.BUTTON: return "Button";
3031
3906
  case Widget.DROPDOWN: return "Dropdown";
3032
3907
  case Widget.CHECKBOX: return "Checkbox";
3033
3908
  case Widget.TOGGLE: return "Toggle";
3909
+ case Widget.RADIO: return "Radio";
3034
3910
  case Widget.COLOR: return "Color";
3911
+ case Widget.RANGE: return "Range";
3035
3912
  case Widget.NUMBER: return "Number";
3036
3913
  case Widget.VECTOR: return "Vector";
3037
3914
  case Widget.TREE: return "Tree";
@@ -3048,6 +3925,7 @@ class Widget {
3048
3925
  case Widget.FORM: return "Form";
3049
3926
  case Widget.DIAL: return "Dial";
3050
3927
  case Widget.COUNTER: return "Counter";
3928
+ case Widget.TABLE: return "Table";
3051
3929
  case Widget.CUSTOM: return this.customName;
3052
3930
  }
3053
3931
 
@@ -3108,11 +3986,12 @@ function ADD_CUSTOM_WIDGET( custom_widget_name, options = {} )
3108
3986
  buttonName += "<a class='fa-solid " + (instance ? "fa-bars-staggered" : " ") + " menu' style='float:right; width:5%;'></a>";
3109
3987
 
3110
3988
  let buttonEl = this.addButton(null, buttonName, (value, event) => {
3111
-
3112
- if( instance ) {
3989
+ if( instance )
3990
+ {
3113
3991
  element.querySelector(".lexcustomitems").toggleAttribute('hidden');
3114
3992
  }
3115
- else {
3993
+ else
3994
+ {
3116
3995
  addContextMenu(null, event, c => {
3117
3996
  c.add("New " + custom_widget_name, () => {
3118
3997
  instance = {};
@@ -3299,16 +4178,20 @@ class NodeTree {
3299
4178
 
3300
4179
  // Add or remove
3301
4180
  const idx = this.selected.indexOf( node );
3302
- if( idx > -1 ) {
4181
+ if( idx > -1 )
4182
+ {
3303
4183
  item.classList.remove( 'selected' );
3304
4184
  this.selected.splice( idx, 1 );
3305
- }else {
4185
+ }
4186
+ else
4187
+ {
3306
4188
  item.classList.add( 'selected' );
3307
4189
  this.selected.push( node );
3308
4190
  }
3309
4191
 
3310
4192
  // Only Show children...
3311
- if( isParent && node.id.length > 1 /* Strange case... */) {
4193
+ if( isParent && node.id.length > 1 /* Strange case... */)
4194
+ {
3312
4195
  node.closed = false;
3313
4196
  if( that.onevent )
3314
4197
  {
@@ -3346,7 +4229,7 @@ class NodeTree {
3346
4229
 
3347
4230
  e.preventDefault();
3348
4231
 
3349
- if( that.onevent )
4232
+ if( !that.onevent )
3350
4233
  {
3351
4234
  return;
3352
4235
  }
@@ -3402,7 +4285,8 @@ class NodeTree {
3402
4285
  return;
3403
4286
  }
3404
4287
 
3405
- if( that.onevent ) {
4288
+ if( that.onevent )
4289
+ {
3406
4290
  const event = new TreeEvent( TreeEvent.NODE_DELETED, node, e );
3407
4291
  that.onevent( event );
3408
4292
  }
@@ -3429,7 +4313,8 @@ class NodeTree {
3429
4313
  if( e.key == "Delete" )
3430
4314
  {
3431
4315
  // Send event now so we have the info in selected array..
3432
- if( that.onevent ) {
4316
+ if( that.onevent )
4317
+ {
3433
4318
  const event = new TreeEvent( TreeEvent.NODE_DELETED, this.selected.length > 1 ? this.selected : node, e );
3434
4319
  event.multiple = this.selected.length > 1;
3435
4320
  that.onevent( event );
@@ -3460,22 +4345,24 @@ class NodeTree {
3460
4345
 
3461
4346
  // Node rename
3462
4347
 
3463
- let name_input = document.createElement('input');
3464
- name_input.toggleAttribute('hidden', !node.rename);
3465
- name_input.value = node.id;
3466
- 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);
3467
4352
 
3468
- if(node.rename) {
4353
+ if( node.rename )
4354
+ {
3469
4355
  item.classList.add('selected');
3470
- name_input.focus();
4356
+ nameInput.focus();
3471
4357
  }
3472
4358
 
3473
- name_input.addEventListener("keyup", function(e){
3474
- if(e.key == 'Enter') {
3475
-
4359
+ nameInput.addEventListener("keyup", function(e){
4360
+ if( e.key == "Enter" )
4361
+ {
3476
4362
  this.value = this.value.replace(/\s/g, '_');
3477
4363
 
3478
- if(that.onevent) {
4364
+ if( that.onevent )
4365
+ {
3479
4366
  const event = new TreeEvent(TreeEvent.NODE_RENAMED, node, this.value);
3480
4367
  that.onevent( event );
3481
4368
  }
@@ -3485,18 +4372,20 @@ class NodeTree {
3485
4372
  that.frefresh( node.id );
3486
4373
  list.querySelector("#" + node.id).classList.add('selected');
3487
4374
  }
3488
- if(e.key == 'Escape') {
4375
+ else if(e.key == "Escape")
4376
+ {
3489
4377
  delete node.rename;
3490
4378
  that.frefresh( node.id );
3491
4379
  }
3492
4380
  });
3493
4381
 
3494
- name_input.addEventListener("blur", function(e){
4382
+ nameInput.addEventListener("blur", function(e){
3495
4383
  delete node.rename;
3496
4384
  that.refresh();
3497
4385
  });
3498
4386
 
3499
- if(this.options.draggable ?? true) {
4387
+ if( this.options.draggable ?? true )
4388
+ {
3500
4389
  // Drag nodes
3501
4390
  if(parent) // Root doesn't move!
3502
4391
  {
@@ -3522,29 +4411,32 @@ class NodeTree {
3522
4411
  return;
3523
4412
  let target = node;
3524
4413
  // Can't drop to same node
3525
- if(dragged.id == target.id) {
4414
+ if( dragged.id == target.id )
4415
+ {
3526
4416
  console.warn("Cannot parent node to itself!");
3527
4417
  return;
3528
4418
  }
3529
4419
 
3530
4420
  // Can't drop to child node
3531
- const isChild = function(new_parent, node) {
4421
+ const isChild = function( newParent, node ) {
3532
4422
  var result = false;
3533
- for( var c of node.children ) {
3534
- if( c.id == new_parent.id )
3535
- return true;
3536
- 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 );
3537
4427
  }
3538
4428
  return result;
3539
4429
  };
3540
4430
 
3541
- if(isChild(target, dragged)) {
4431
+ if( isChild( target, dragged ))
4432
+ {
3542
4433
  console.warn("Cannot parent node to a current child!");
3543
4434
  return;
3544
4435
  }
3545
4436
 
3546
4437
  // Trigger node dragger event
3547
- if(that.onevent) {
4438
+ if( that.onevent )
4439
+ {
3548
4440
  const event = new TreeEvent(TreeEvent.NODE_DRAGGED, dragged, target);
3549
4441
  that.onevent( event );
3550
4442
  }
@@ -3560,7 +4452,8 @@ class NodeTree {
3560
4452
  let handled = false;
3561
4453
 
3562
4454
  // Show/hide children
3563
- if(isParent) {
4455
+ if( isParent )
4456
+ {
3564
4457
  item.querySelector('a.hierarchy').addEventListener("click", function(e) {
3565
4458
 
3566
4459
  handled = true;
@@ -3568,7 +4461,8 @@ class NodeTree {
3568
4461
  e.stopPropagation();
3569
4462
 
3570
4463
  node.closed = !node.closed;
3571
- if(that.onevent) {
4464
+ if( that.onevent )
4465
+ {
3572
4466
  const event = new TreeEvent(TreeEvent.NODE_CARETCHANGED, node, node.closed);
3573
4467
  that.onevent( event );
3574
4468
  }
@@ -3588,7 +4482,8 @@ class NodeTree {
3588
4482
  node.visible = node.visible === undefined ? false : !node.visible;
3589
4483
  this.className = "itemicon fa-solid fa-eye" + (!node.visible ? "-slash" : "");
3590
4484
  // Trigger visibility event
3591
- if(that.onevent) {
4485
+ if( that.onevent )
4486
+ {
3592
4487
  const event = new TreeEvent(TreeEvent.NODE_VISIBILITY, node, node.visible);
3593
4488
  that.onevent( event );
3594
4489
  }
@@ -3597,9 +4492,10 @@ class NodeTree {
3597
4492
  item.appendChild(visibility);
3598
4493
  }
3599
4494
 
3600
- if(node.actions)
4495
+ if( node.actions )
3601
4496
  {
3602
- for(var i = 0; i < node.actions.length; ++i) {
4497
+ for( var i = 0; i < node.actions.length; ++i )
4498
+ {
3603
4499
  let a = node.actions[i];
3604
4500
  var actionEl = document.createElement('a');
3605
4501
  actionEl.className = "itemicon " + a.icon;
@@ -3612,20 +4508,25 @@ class NodeTree {
3612
4508
  }
3613
4509
  }
3614
4510
 
3615
- if(selectedId != undefined && node.id == selectedId) {
3616
- this.selected = [node];
4511
+ if( selectedId != undefined && node.id == selectedId )
4512
+ {
4513
+ this.selected = [ node ];
3617
4514
  item.click();
3618
4515
  }
3619
4516
 
3620
- if(node.closed )
4517
+ if( node.closed )
4518
+ {
3621
4519
  return;
4520
+ }
3622
4521
 
3623
4522
  for( var i = 0; i < node.children.length; ++i )
3624
4523
  {
3625
- let child = node.children[i];
4524
+ let child = node.children[ i ];
3626
4525
 
3627
- if( this.options.onlyFolders && child.type != 'folder')
4526
+ if( this.options.onlyFolders && child.type != 'folder' )
4527
+ {
3628
4528
  continue;
4529
+ }
3629
4530
 
3630
4531
  this._create_item( node, child, level + 1 );
3631
4532
  }
@@ -3664,12 +4565,12 @@ class Panel {
3664
4565
  * style: CSS Style object to be applied to the panel
3665
4566
  */
3666
4567
 
3667
- constructor( options = {} ) {
4568
+ constructor( options = {} ) {
3668
4569
  var root = document.createElement('div');
3669
4570
  root.className = "lexpanel";
3670
- if(options.id)
4571
+ if( options.id )
3671
4572
  root.id = options.id;
3672
- if(options.className)
4573
+ if( options.className )
3673
4574
  root.className += " " + options.className;
3674
4575
 
3675
4576
  root.style.width = options.width || "calc( 100% - 6px )";
@@ -3746,23 +4647,31 @@ class Panel {
3746
4647
  this.branches = [];
3747
4648
  this.current_branch = null;
3748
4649
 
3749
- for(let w in this.widgets) {
3750
- 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
+ {
3751
4654
  const signal = this.widgets[w].options.signal;
3752
- for(let i = 0; i < LX.signals[signal].length; i++) {
3753
- 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
+ {
3754
4659
  LX.signals[signal] = [...LX.signals[signal].slice(0, i), ...LX.signals[signal].slice(i+1)];
3755
4660
  }
3756
4661
  }
3757
4662
  }
3758
4663
  }
3759
4664
 
3760
- if(this.signals) {
3761
- 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
+ {
3762
4669
  let widget = Object.values(this.signals[w])[0];
3763
4670
  let signal = widget.options.signal;
3764
- for(let i = 0; i < LX.signals[signal].length; i++) {
3765
- 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
+ {
3766
4675
  LX.signals[signal] = [...LX.signals[signal].slice(0, i), ...LX.signals[signal].slice(i+1)];
3767
4676
  }
3768
4677
  }
@@ -3800,10 +4709,12 @@ class Panel {
3800
4709
 
3801
4710
  this._inline_widgets_left = -1;
3802
4711
 
3803
- if(!this._inlineContainer) {
4712
+ if( !this._inlineContainer )
4713
+ {
3804
4714
  this._inlineContainer = document.createElement('div');
3805
4715
  this._inlineContainer.className = "lexinlinewidgets";
3806
- if(justifyContent)
4716
+
4717
+ if( justifyContent )
3807
4718
  {
3808
4719
  this._inlineContainer.style.justifyContent = justifyContent;
3809
4720
  }
@@ -3817,7 +4728,7 @@ class Panel {
3817
4728
  if(is_pair)
3818
4729
  {
3819
4730
  // eg. an array, inline items appended later to
3820
- if(this._inline_queued_container)
4731
+ if( this._inline_queued_container)
3821
4732
  this._inlineContainer.appendChild( item[0] );
3822
4733
  // eg. a dropdown, item is appended to parent, not to inline cont.
3823
4734
  else
@@ -3829,7 +4740,7 @@ class Panel {
3829
4740
 
3830
4741
  if(!this._inline_queued_container)
3831
4742
  {
3832
- if(this.current_branch)
4743
+ if( this.current_branch)
3833
4744
  this.current_branch.content.appendChild( this._inlineContainer );
3834
4745
  else
3835
4746
  this.root.appendChild( this._inlineContainer );
@@ -3868,7 +4779,7 @@ class Panel {
3868
4779
  this.current_branch = branch;
3869
4780
 
3870
4781
  // Append to panel
3871
- if(this.branches.length == 0)
4782
+ if( this.branches.length == 0)
3872
4783
  branch.root.classList.add('first');
3873
4784
 
3874
4785
  // This is the last!
@@ -3879,7 +4790,8 @@ class Panel {
3879
4790
  this.root.appendChild( branch.root );
3880
4791
 
3881
4792
  // Add widget filter
3882
- if(options.filter) {
4793
+ if( options.filter )
4794
+ {
3883
4795
  this._addFilter( options.filter, {callback: this._searchWidgets.bind(this, branch.name)} );
3884
4796
  }
3885
4797
 
@@ -3996,14 +4908,18 @@ class Panel {
3996
4908
  element.jsInstance = widget;
3997
4909
 
3998
4910
  const insert_widget = el => {
3999
- if(options.container)
4000
- options.container.appendChild(el);
4001
- else if(!this.queuedContainer) {
4002
-
4003
- 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 )
4004
4918
  {
4005
- if(!options.skipWidget)
4919
+ if( !options.skipWidget )
4920
+ {
4006
4921
  this.current_branch.widgets.push( widget );
4922
+ }
4007
4923
  this.current_branch.content.appendChild( el );
4008
4924
  }
4009
4925
  else
@@ -4013,40 +4929,47 @@ class Panel {
4013
4929
  }
4014
4930
  }
4015
4931
  // Append content to queued tab container
4016
- else {
4932
+ else
4933
+ {
4017
4934
  this.queuedContainer.appendChild( el );
4018
4935
  }
4019
4936
  };
4020
4937
 
4021
4938
  const store_widget = el => {
4022
4939
 
4023
- if(!this.queuedContainer) {
4940
+ if( !this.queuedContainer )
4941
+ {
4024
4942
  this._inlineWidgets.push( el );
4025
4943
  }
4026
4944
  // Append content to queued tab container
4027
- else {
4945
+ else
4946
+ {
4028
4947
  this._inlineWidgets.push( [el, this.queuedContainer] );
4029
4948
  }
4030
4949
  };
4031
4950
 
4032
4951
  // Process inline widgets
4033
- if(this._inline_widgets_left > 0 && !options.skipInlineCount)
4952
+ if( this._inline_widgets_left > 0 && !options.skipInlineCount )
4034
4953
  {
4035
- if(!this._inlineWidgets) {
4954
+ if( !this._inlineWidgets )
4955
+ {
4036
4956
  this._inlineWidgets = [];
4037
4957
  }
4038
4958
 
4039
4959
  // Store widget and its container
4040
- store_widget(element);
4960
+ store_widget( element );
4041
4961
 
4042
4962
  this._inline_widgets_left--;
4043
4963
 
4044
4964
  // Last widget
4045
- if(!this._inline_widgets_left) {
4965
+ if( !this._inline_widgets_left )
4966
+ {
4046
4967
  this.endLine();
4047
4968
  }
4048
- }else {
4049
- insert_widget(element);
4969
+ }
4970
+ else
4971
+ {
4972
+ insert_widget( element );
4050
4973
  }
4051
4974
 
4052
4975
  return widget;
@@ -4070,8 +4993,8 @@ class Panel {
4070
4993
 
4071
4994
  let searchIcon = document.createElement('a');
4072
4995
  searchIcon.className = "fa-solid fa-magnifying-glass";
4073
- element.appendChild(input);
4074
4996
  element.appendChild(searchIcon);
4997
+ element.appendChild(input);
4075
4998
 
4076
4999
  input.addEventListener("input", (e) => {
4077
5000
  if(options.callback)
@@ -4083,15 +5006,20 @@ class Panel {
4083
5006
 
4084
5007
  _searchWidgets(branchName, value) {
4085
5008
 
4086
- for( let b of this.branches ) {
4087
-
4088
- if(b.name !== branchName)
5009
+ for( let b of this.branches )
5010
+ {
5011
+ if( b.name !== branchName )
5012
+ {
4089
5013
  continue;
5014
+ }
4090
5015
 
4091
5016
  // remove all widgets
4092
- for( let w of b.widgets ) {
4093
- if(w.domEl.classList.contains('lexfilter'))
5017
+ for( let w of b.widgets )
5018
+ {
5019
+ if( w.domEl.classList.contains('lexfilter') )
5020
+ {
4094
5021
  continue;
5022
+ }
4095
5023
  w.domEl.remove();
4096
5024
  }
4097
5025
 
@@ -4101,9 +5029,9 @@ class Panel {
4101
5029
  const emptyFilter = !value.length;
4102
5030
 
4103
5031
  // add widgets
4104
- for( let w of b.widgets ) {
4105
-
4106
- if(!emptyFilter)
5032
+ for( let w of b.widgets )
5033
+ {
5034
+ if( !emptyFilter )
4107
5035
  {
4108
5036
  if(!w.name) continue;
4109
5037
  const filterWord = value.toLowerCase();
@@ -4123,26 +5051,28 @@ class Panel {
4123
5051
  }
4124
5052
  }
4125
5053
 
4126
- _search_options(options, value) {
4127
- // push to right container
5054
+ _filterOptions( options, value ) {
5055
+
5056
+ // Push to right container
4128
5057
  const emptyFilter = !value.length;
4129
5058
  let filteredOptions = [];
4130
- // add widgets
4131
- for( let i = 0; i < options.length; i++) {
4132
- let o = options[i];
4133
- if(!emptyFilter)
5059
+
5060
+ // Add widgets
5061
+ for( let i = 0; i < options.length; i++ )
5062
+ {
5063
+ let o = options[ i ];
5064
+ if( !emptyFilter )
4134
5065
  {
4135
- let toCompare = (typeof o == 'string') ? o : o.value;
4136
- ;
5066
+ let toCompare = ( typeof o == 'string' ) ? o : o.value;
4137
5067
  const filterWord = value.toLowerCase();
4138
5068
  const name = toCompare.toLowerCase();
4139
- if(!name.includes(filterWord)) continue;
5069
+ if( !name.includes( filterWord ) ) continue;
4140
5070
  }
4141
- // insert filtered widget
4142
- filteredOptions.push(o);
5071
+
5072
+ filteredOptions.push( o );
4143
5073
  }
4144
5074
 
4145
- this.refresh(filteredOptions);
5075
+ this.refresh( filteredOptions );
4146
5076
  }
4147
5077
 
4148
5078
  _trigger( event, callback ) {
@@ -4195,7 +5125,7 @@ class Panel {
4195
5125
 
4196
5126
  clearQueue() {
4197
5127
 
4198
- if(this._queue && this._queue.length)
5128
+ if( this._queue && this._queue.length)
4199
5129
  {
4200
5130
  this.queuedContainer = this._queue.pop();
4201
5131
  return;
@@ -4421,7 +5351,8 @@ class Panel {
4421
5351
  element.appendChild( container );
4422
5352
 
4423
5353
  // Remove branch padding and margins
4424
- if( !widget.name ) {
5354
+ if( !widget.name )
5355
+ {
4425
5356
  element.className += " noname";
4426
5357
  container.style.width = "100%";
4427
5358
  }
@@ -4521,7 +5452,8 @@ class Panel {
4521
5452
  element.appendChild(container);
4522
5453
 
4523
5454
  // Remove branch padding and margins
4524
- if(!widget.name) {
5455
+ if( !widget.name )
5456
+ {
4525
5457
  element.className += " noname";
4526
5458
  container.style.width = "100%";
4527
5459
  }
@@ -4626,69 +5558,94 @@ class Panel {
4626
5558
  /**
4627
5559
  * @method addComboButtons
4628
5560
  * @param {String} name Widget name
4629
- * @param {Array} values Each of the {value, callback} items
5561
+ * @param {Array} values Each of the {value, callback, selected, disabled} items
4630
5562
  * @param {*} options:
4631
5563
  * float: Justify content (left, center, right) [center]
5564
+ * @legacy selected: Selected item by default by value
4632
5565
  * noSelection: Buttons can be clicked, but they are not selectable
5566
+ * toggle: Buttons can be toggled insted of selecting only one
4633
5567
  */
4634
5568
 
4635
5569
  addComboButtons( name, values, options = {} ) {
4636
5570
 
4637
- let widget = this.create_widget(name, Widget.BUTTON, options);
5571
+ let widget = this.create_widget( name, Widget.BUTTON, options );
4638
5572
  let element = widget.domEl;
4639
5573
 
4640
5574
  let that = this;
4641
5575
  let container = document.createElement('div');
4642
5576
  container.className = "lexcombobuttons ";
4643
- if( options.float ) container.className += options.float;
5577
+
5578
+ if( options.float )
5579
+ {
5580
+ container.className += options.float;
5581
+ }
5582
+
4644
5583
  container.style.width = "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
4645
5584
 
4646
- let should_select = !(options.noSelection ?? false);
5585
+ let buttonsBox = document.createElement('div');
5586
+ buttonsBox.className = "lexcombobuttonsbox ";
5587
+
5588
+ const shouldSelect = !( options.noSelection ?? false );
5589
+ const shouldToggle = shouldSelect && ( options.toggle ?? false );
5590
+
4647
5591
  for( let b of values )
4648
5592
  {
4649
- if( !b.value ) throw("Set 'value' for each button!");
5593
+ if( !b.value )
5594
+ {
5595
+ throw( "Set 'value' for each button!" );
5596
+ }
4650
5597
 
4651
5598
  let buttonEl = document.createElement('button');
4652
5599
  buttonEl.className = "lexbutton combo";
4653
5600
  buttonEl.title = b.icon ? b.value : "";
4654
- if(options.buttonClass)
4655
- buttonEl.classList.add(options.buttonClass);
5601
+ buttonEl.id = b.id ?? "";
4656
5602
 
4657
- if(options.selected == b.value)
4658
- buttonEl.classList.add("selected");
5603
+ if( options.buttonClass )
5604
+ {
5605
+ buttonEl.classList.add( options.buttonClass );
5606
+ }
4659
5607
 
4660
- if(b.id)
4661
- buttonEl.id = b.id;
5608
+ if( shouldSelect && ( b.selected || options.selected == b.value ) )
5609
+ {
5610
+ buttonEl.classList.add("selected");
5611
+ }
4662
5612
 
4663
- buttonEl.innerHTML = (b.icon ? "<a class='" + b.icon +"'></a>" : "") + "<span>" + (b.icon ? "" : b.value) + "</span>";
5613
+ buttonEl.innerHTML = ( b.icon ? "<a class='" + b.icon +"'></a>" : "" ) + "<span>" + ( b.icon ? "" : b.value ) + "</span>";
4664
5614
 
4665
- if(options.disabled)
4666
- buttonEl.setAttribute("disabled", true);
5615
+ if( b.disabled )
5616
+ {
5617
+ buttonEl.setAttribute( "disabled", true );
5618
+ }
4667
5619
 
4668
- buttonEl.addEventListener("click", function(e) {
4669
- if(should_select) {
4670
- container.querySelectorAll('button').forEach( s => s.classList.remove('selected'));
4671
- this.classList.add('selected');
5620
+ buttonEl.addEventListener("click", function( e ) {
5621
+ if( shouldSelect )
5622
+ {
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
+ }
4672
5632
  }
4673
- that._trigger( new IEvent(name, b.value, e), b.callback );
4674
- });
4675
5633
 
4676
- container.appendChild(buttonEl);
5634
+ that._trigger( new IEvent( name, b.value, e ), b.callback );
5635
+ });
4677
5636
 
4678
- // Remove branch padding and margins
4679
- if(widget.name === undefined) {
4680
- buttonEl.className += " noname";
4681
- buttonEl.style.width = "100%";
4682
- }
5637
+ buttonsBox.appendChild( buttonEl );
4683
5638
  }
4684
5639
 
4685
5640
  // Remove branch padding and margins
4686
- if(widget.name !== undefined) {
5641
+ if( !widget.name )
5642
+ {
4687
5643
  element.className += " noname";
4688
5644
  container.style.width = "100%";
4689
5645
  }
4690
5646
 
4691
- element.appendChild(container);
5647
+ container.appendChild( buttonsBox );
5648
+ element.appendChild( container );
4692
5649
 
4693
5650
  return widget;
4694
5651
  }
@@ -4855,7 +5812,8 @@ class Panel {
4855
5812
 
4856
5813
  element.appendChild( container );
4857
5814
 
4858
- if( !widget.name || options.hideName ) {
5815
+ if( !widget.name || options.hideName )
5816
+ {
4859
5817
  element.className += " noname";
4860
5818
  container.style.width = "100%";
4861
5819
  }
@@ -4940,6 +5898,8 @@ class Panel {
4940
5898
  * filter: Add a search bar to the widget [false]
4941
5899
  * disabled: Make the widget disabled [false]
4942
5900
  * skipReset: Don't add the reset value button when value changes
5901
+ * placeholder: Placeholder for the filter input
5902
+ * emptyMsg: Custom message to show when no filtered results
4943
5903
  */
4944
5904
 
4945
5905
  addDropdown( name, values, value, callback, options = {} ) {
@@ -4985,17 +5945,72 @@ class Panel {
4985
5945
  let buttonName = value;
4986
5946
  buttonName += "<a class='fa-solid fa-angle-down' style='float:right; margin-right: 3px;'></a>";
4987
5947
 
4988
- this.queue(container);
5948
+ this.queue( container );
5949
+
5950
+ const _placeOptions = ( parent ) => {
5951
+
5952
+ const overflowContainer = parent.getParentArea();
5953
+ const rect = selectedOption.getBoundingClientRect();
5954
+ const nestedDialog = parent.parentElement.closest( "dialog" );
5955
+
5956
+ // Manage vertical aspect
5957
+ {
5958
+ const listHeight = parent.offsetHeight;
5959
+ let topPosition = rect.y;
5960
+
5961
+ let maxY = window.innerHeight;
5962
+
5963
+ if( overflowContainer )
5964
+ {
5965
+ const parentRect = overflowContainer.getBoundingClientRect();
5966
+ maxY = parentRect.y + parentRect.height;
5967
+ }
5968
+
5969
+ if( nestedDialog )
5970
+ {
5971
+ const rect = nestedDialog.getBoundingClientRect();
5972
+ topPosition -= rect.y;
5973
+ }
5974
+
5975
+ parent.style.top = ( topPosition + selectedOption.offsetHeight ) + 'px';
4989
5976
 
4990
- const _getMaxListWidth = () => {
5977
+ const showAbove = ( topPosition + listHeight ) > maxY;
5978
+ if( showAbove )
5979
+ {
5980
+ parent.style.top = ( topPosition - listHeight ) + 'px';
5981
+ parent.classList.add( "place-above" );
5982
+ }
5983
+ }
4991
5984
 
4992
- let maxWidth = 0;
4993
- for( let i of values )
5985
+ // Manage horizontal aspect
4994
5986
  {
4995
- const iString = String( i );
4996
- maxWidth = Math.max( iString.length, maxWidth );
5987
+ const listWidth = parent.offsetWidth;
5988
+ let leftPosition = rect.x;
5989
+
5990
+ parent.style.minWidth = ( rect.width ) + 'px';
5991
+
5992
+ if( nestedDialog )
5993
+ {
5994
+ const rect = nestedDialog.getBoundingClientRect();
5995
+ leftPosition -= rect.x;
5996
+ }
5997
+
5998
+ parent.style.left = ( leftPosition ) + 'px';
5999
+
6000
+ let maxX = window.innerWidth;
6001
+
6002
+ if( overflowContainer )
6003
+ {
6004
+ const parentRect = overflowContainer.getBoundingClientRect();
6005
+ maxX = parentRect.x + parentRect.width;
6006
+ }
6007
+
6008
+ const showLeft = ( leftPosition + listWidth ) > maxX;
6009
+ if( showLeft )
6010
+ {
6011
+ parent.style.left = ( leftPosition - ( listWidth - rect.width ) ) + 'px';
6012
+ }
4997
6013
  }
4998
- return Math.max( maxWidth * 10, 80 );
4999
6014
  };
5000
6015
 
5001
6016
  let selectedOption = this.addButton( null, buttonName, ( value, event ) => {
@@ -5005,52 +6020,50 @@ class Panel {
5005
6020
  return;
5006
6021
  }
5007
6022
 
5008
- list.toggleAttribute( "hidden" );
5009
- list.classList.remove( "place-above" );
5010
-
5011
- const listHeight = 26 * values.length;
5012
- const rect = selectedOption.getBoundingClientRect();
5013
- const topPosition = rect.y;
5014
-
5015
- let maxY = window.innerHeight;
5016
- let overflowContainer = list.getParentArea();
6023
+ listDialog.classList.remove( "place-above" );
6024
+ const opened = listDialog.hasAttribute( "open" );
5017
6025
 
5018
- if( overflowContainer )
6026
+ if( !opened )
5019
6027
  {
5020
- const parentRect = overflowContainer.getBoundingClientRect();
5021
- maxY = parentRect.y + parentRect.height;
6028
+ listDialog.show();
6029
+ _placeOptions( listDialog );
6030
+ }
6031
+ else
6032
+ {
6033
+ listDialog.close();
5022
6034
  }
5023
6035
 
5024
- list.style.top = ( topPosition + selectedOption.offsetHeight ) + 'px';
5025
-
5026
- const showAbove = ( topPosition + listHeight ) > maxY;
5027
- if( showAbove )
6036
+ if( filter )
5028
6037
  {
5029
- list.style.top = ( topPosition - listHeight ) + 'px';
5030
- list.classList.add( "place-above" );
6038
+ filter.querySelector( "input" ).focus();
5031
6039
  }
5032
6040
 
5033
- list.style.width = (event.currentTarget.clientWidth) + 'px';
5034
- list.style.minWidth = (_getMaxListWidth()) + 'px';
5035
- list.focus();
5036
- }, { buttonClass: "array", skipInlineCount: true });
6041
+ }, { buttonClass: "array", skipInlineCount: true, disabled: options.disabled });
5037
6042
 
5038
6043
  this.clearQueue();
5039
6044
 
5040
6045
  selectedOption.style.width = "100%";
5041
6046
 
5042
6047
  selectedOption.refresh = (v) => {
5043
- if(selectedOption.querySelector("span").innerText == "")
6048
+ if( selectedOption.querySelector("span").innerText == "" )
6049
+ {
5044
6050
  selectedOption.querySelector("span").innerText = v;
6051
+ }
5045
6052
  else
6053
+ {
5046
6054
  selectedOption.querySelector("span").innerHTML = selectedOption.querySelector("span").innerHTML.replaceAll(selectedOption.querySelector("span").innerText, v);
6055
+ }
5047
6056
  }
5048
6057
 
5049
6058
  // Add dropdown options container
6059
+
6060
+ const listDialog = document.createElement( 'dialog' );
6061
+ listDialog.className = "lexdropdownoptions";
6062
+
5050
6063
  let list = document.createElement( 'ul' );
5051
6064
  list.tabIndex = -1;
5052
6065
  list.className = "lexoptions";
5053
- list.hidden = true;
6066
+ listDialog.appendChild( list )
5054
6067
 
5055
6068
  list.addEventListener( 'focusout', function( e ) {
5056
6069
  e.stopPropagation();
@@ -5068,97 +6081,118 @@ class Panel {
5068
6081
  {
5069
6082
  return;
5070
6083
  }
5071
- this.toggleAttribute( 'hidden', true );
6084
+ listDialog.close();
5072
6085
  });
5073
6086
 
5074
6087
  // Add filter options
5075
6088
  let filter = null;
5076
- if(options.filter ?? false)
6089
+ if( options.filter ?? false )
5077
6090
  {
5078
- filter = this._addFilter("Search option", {container: list, callback: this._search_options.bind(list, values)});
5079
- }
5080
-
5081
- // Create option list to empty it easily..
5082
- const listOptions = document.createElement('span');
5083
- list.appendChild( listOptions );
6091
+ filter = this._addFilter( options.placeholder ?? "Search...", { container: list, callback: this._filterOptions.bind( list, values )} );
5084
6092
 
5085
- if( filter )
5086
- {
5087
- list.prepend( filter );
5088
- listOptions.style.height = "calc(100% - 25px)";
6093
+ list.appendChild( filter );
5089
6094
 
5090
6095
  filter.addEventListener('focusout', function( e ) {
5091
6096
  if (e.relatedTarget && e.relatedTarget.tagName == "UL" && e.relatedTarget.classList.contains("lexoptions"))
5092
6097
  {
5093
6098
  return;
5094
6099
  }
5095
- list.toggleAttribute( 'hidden', true );
6100
+ listDialog.close();
5096
6101
  });
5097
6102
  }
5098
6103
 
6104
+ // Create option list to empty it easily..
6105
+ const listOptions = document.createElement('span');
6106
+ listOptions.style.height = "calc(100% - 25px)";
6107
+ list.appendChild( listOptions );
6108
+
5099
6109
  // Add dropdown options list
5100
- list.refresh = options => {
6110
+ list.refresh = ( options ) => {
5101
6111
 
5102
6112
  // Empty list
5103
6113
  listOptions.innerHTML = "";
5104
6114
 
5105
- for(let i = 0; i < options.length; i++)
6115
+ if( !options.length )
6116
+ {
6117
+ let iValue = options.emptyMsg ?? "No options found.";
6118
+
6119
+ let option = document.createElement( "div" );
6120
+ option.className = "option";
6121
+ option.style.flexDirection = "unset";
6122
+ option.innerHTML = iValue;
6123
+
6124
+ let li = document.createElement( "li" );
6125
+ li.className = "lexdropdownitem empty";
6126
+ li.appendChild( option );
6127
+
6128
+ listOptions.appendChild( li );
6129
+ return;
6130
+ }
6131
+
6132
+ for( let i = 0; i < options.length; i++ )
5106
6133
  {
5107
- let iValue = options[i];
5108
- let li = document.createElement('li');
5109
- let option = document.createElement('div');
6134
+ let iValue = options[ i ];
6135
+ let li = document.createElement( "li" );
6136
+ let option = document.createElement( "div" );
5110
6137
  option.className = "option";
5111
- li.appendChild(option);
5112
- li.addEventListener("click", (e) => {
5113
- element.querySelector(".lexoptions").toggleAttribute('hidden', true);
5114
- const currentSelected = element.querySelector(".lexoptions .selected");
5115
- if(currentSelected) currentSelected.classList.remove("selected");
5116
- value = e.currentTarget.getAttribute("value");
5117
- e.currentTarget.toggleAttribute('hidden', false);
5118
- e.currentTarget.classList.add("selected");
6138
+ li.appendChild( option );
6139
+
6140
+ li.addEventListener( "click", e => {
6141
+ listDialog.close();
6142
+ const currentSelected = element.querySelector( ".lexoptions .selected" );
6143
+ if(currentSelected) currentSelected.classList.remove( "selected" );
6144
+ value = e.currentTarget.getAttribute( "value" );
6145
+ e.currentTarget.toggleAttribute( "hidden", false );
6146
+ e.currentTarget.classList.add( "selected" );
5119
6147
  selectedOption.refresh(value);
5120
6148
 
5121
- let btn = element.querySelector(".lexwidgetname .lexicon");
5122
- if(btn) btn.style.display = (value != wValue.iValue ? "block" : "none");
5123
- that._trigger( new IEvent(name, value, null), callback );
6149
+ let btn = element.querySelector( ".lexwidgetname .lexicon" );
6150
+ if( btn ) btn.style.display = (value != wValue.iValue ? "block" : "none");
6151
+ that._trigger( new IEvent( name, value, null ), callback );
5124
6152
 
5125
6153
  // Reset filter
5126
- if(filter)
6154
+ if( filter )
5127
6155
  {
5128
- filter.querySelector('input').value = "";
5129
- this._search_options.bind(list, values, "")();
6156
+ filter.querySelector( "input" ).value = "";
6157
+ this._filterOptions.bind( list, values, "" )();
5130
6158
  }
5131
6159
  });
5132
6160
 
5133
6161
  // Add string option
5134
- if( iValue.constructor != Object ) {
5135
- option.style.flexDirection = 'unset';
6162
+ if( iValue.constructor != Object )
6163
+ {
6164
+ option.style.flexDirection = "unset";
5136
6165
  option.innerHTML = "</a><span>" + iValue + "</span><a class='fa-solid fa-check'>";
5137
6166
  option.value = iValue;
5138
- li.setAttribute("value", iValue);
6167
+ li.setAttribute( "value", iValue );
5139
6168
  li.className = "lexdropdownitem";
5140
- if( i == (options.length - 1) ) li.className += " last";
5141
- if(iValue == value) {
5142
- li.classList.add("selected");
6169
+
6170
+ if( iValue == value )
6171
+ {
6172
+ li.classList.add( "selected" );
5143
6173
  wValue.innerHTML = iValue;
5144
6174
  }
5145
6175
  }
5146
- else {
6176
+ else
6177
+ {
5147
6178
  // Add image option
5148
- let img = document.createElement("img");
6179
+ let img = document.createElement( "img" );
5149
6180
  img.src = iValue.src;
5150
- li.setAttribute("value", iValue.value);
6181
+ li.setAttribute( "value", iValue.value );
5151
6182
  li.className = "lexlistitem";
5152
6183
  option.innerText = iValue.value;
5153
6184
  option.className += " media";
5154
- option.prepend(img);
6185
+ option.prepend( img );
5155
6186
 
5156
- option.setAttribute("value", iValue.value);
5157
- option.setAttribute("data-index", i);
5158
- option.setAttribute("data-src", iValue.src);
5159
- option.setAttribute("title", iValue.value);
5160
- if(value == iValue.value)
5161
- li.classList.add("selected");
6187
+ option.setAttribute( "value", iValue.value );
6188
+ option.setAttribute( "data-index", i );
6189
+ option.setAttribute( "data-src", iValue.src );
6190
+ option.setAttribute( "title", iValue.value );
6191
+
6192
+ if( value == iValue.value )
6193
+ {
6194
+ li.classList.add( "selected" );
6195
+ }
5162
6196
  }
5163
6197
 
5164
6198
  listOptions.appendChild( li );
@@ -5167,7 +6201,7 @@ class Panel {
5167
6201
 
5168
6202
  list.refresh( values );
5169
6203
 
5170
- container.appendChild( list );
6204
+ container.appendChild( listDialog );
5171
6205
  element.appendChild( container );
5172
6206
 
5173
6207
  // Remove branch padding and margins
@@ -5198,15 +6232,16 @@ class Panel {
5198
6232
 
5199
6233
  addCurve( name, values, callback, options = {} ) {
5200
6234
 
5201
- if(!name) {
5202
- throw("Set Widget Name!");
6235
+ if( !name )
6236
+ {
6237
+ throw( "Set Widget Name!" );
5203
6238
  }
5204
6239
 
5205
6240
  let that = this;
5206
- let widget = this.create_widget(name, Widget.CURVE, options);
6241
+ let widget = this.create_widget( name, Widget.CURVE, options );
5207
6242
 
5208
6243
  widget.onGetValue = () => {
5209
- return JSON.parse(JSON.stringify(curveInstance.element.value));
6244
+ return JSON.parse(JSON.stringify( curveInstance.element.value ));
5210
6245
  };
5211
6246
 
5212
6247
  widget.onSetValue = ( newValue, skipCallback ) => {
@@ -5351,7 +6386,8 @@ class Panel {
5351
6386
 
5352
6387
  addLayers( name, value, callback, options = {} ) {
5353
6388
 
5354
- if(!name) {
6389
+ if( !name )
6390
+ {
5355
6391
  throw("Set Widget Name!");
5356
6392
  }
5357
6393
 
@@ -5393,7 +6429,8 @@ class Panel {
5393
6429
  let binary = value.toString( 2 );
5394
6430
  let nbits = binary.length;
5395
6431
  // fill zeros
5396
- for(var i = 0; i < (16 - nbits); ++i) {
6432
+ for( var i = 0; i < (16 - nbits); ++i )
6433
+ {
5397
6434
  binary = '0' + binary;
5398
6435
  }
5399
6436
 
@@ -5446,8 +6483,9 @@ class Panel {
5446
6483
 
5447
6484
  addArray( name, values = [], callback, options = {} ) {
5448
6485
 
5449
- if(!name) {
5450
- throw("Set Widget Name!");
6486
+ if( !name )
6487
+ {
6488
+ throw( "Set Widget Name!" );
5451
6489
  }
5452
6490
 
5453
6491
  let widget = this.create_widget(name, Widget.ARRAY, options);
@@ -5628,7 +6666,8 @@ class Panel {
5628
6666
  widget.updateValues( values );
5629
6667
 
5630
6668
  // Remove branch padding and margins
5631
- if( !widget.name ) {
6669
+ if( !widget.name )
6670
+ {
5632
6671
  element.className += " noname";
5633
6672
  listContainer.style.width = "100%";
5634
6673
  }
@@ -5715,7 +6754,7 @@ class Panel {
5715
6754
  tagsContainer.appendChild( tagInput );
5716
6755
 
5717
6756
  tagInput.onkeydown = function( e ) {
5718
- const val = this.value.replace(/\s/g, '');
6757
+ const val = this.value.replace( /\s/g, '' );
5719
6758
  if( e.key == ' ' || e.key == 'Enter' )
5720
6759
  {
5721
6760
  e.preventDefault();
@@ -5753,15 +6792,16 @@ class Panel {
5753
6792
  * @param {Function} callback Callback function on change
5754
6793
  * @param {*} options:
5755
6794
  * disabled: Make the widget disabled [false]
6795
+ * label: Checkbox label
5756
6796
  * suboptions: Callback to add widgets in case of TRUE value
5757
- * className: Customize colors
6797
+ * className: Extra classes to customize style
5758
6798
  */
5759
6799
 
5760
6800
  addCheckbox( name, value, callback, options = {} ) {
5761
6801
 
5762
- if( !name )
6802
+ if( !name && !options.label )
5763
6803
  {
5764
- throw( "Set Widget Name!" );
6804
+ throw( "Set Widget Name or at least a label!" );
5765
6805
  }
5766
6806
 
5767
6807
  let widget = this.create_widget( name, Widget.CHECKBOX, options );
@@ -5781,10 +6821,13 @@ class Panel {
5781
6821
  let element = widget.domEl;
5782
6822
 
5783
6823
  // Add reset functionality
5784
- Panel._add_reset_property( element.domName, function() {
5785
- checkbox.checked = !checkbox.checked;
5786
- Panel._dispatch_event( checkbox, "change" );
5787
- });
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
+ }
5788
6831
 
5789
6832
  // Add widget value
5790
6833
 
@@ -5800,7 +6843,7 @@ class Panel {
5800
6843
 
5801
6844
  let valueName = document.createElement( 'span' );
5802
6845
  valueName.className = "checkboxtext";
5803
- valueName.innerHTML = "On";
6846
+ valueName.innerHTML = options.label ?? "On";
5804
6847
 
5805
6848
  container.appendChild( checkbox );
5806
6849
  container.appendChild( valueName );
@@ -5938,6 +6981,96 @@ class Panel {
5938
6981
  return widget;
5939
6982
  }
5940
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
+
5941
7074
  /**
5942
7075
  * @method addColor
5943
7076
  * @param {String} name Widget name
@@ -5950,7 +7083,8 @@ class Panel {
5950
7083
 
5951
7084
  addColor( name, value, callback, options = {} ) {
5952
7085
 
5953
- if( !name ) {
7086
+ if( !name )
7087
+ {
5954
7088
  throw( "Set Widget Name!" );
5955
7089
  }
5956
7090
 
@@ -5988,7 +7122,8 @@ class Panel {
5988
7122
  color.useRGB = options.useRGB ?? false;
5989
7123
  color.value = color.iValue = value.constructor === Array ? rgbToHex( value ) : value;
5990
7124
 
5991
- if( options.disabled ) {
7125
+ if( options.disabled )
7126
+ {
5992
7127
  color.disabled = true;
5993
7128
  }
5994
7129
 
@@ -5997,39 +7132,170 @@ class Panel {
5997
7132
 
5998
7133
  const skipCallback = e.detail;
5999
7134
 
6000
- // Change value (always hex)
6001
- if( !change_from_input )
6002
- text_widget.set( val );
7135
+ // Change value (always hex)
7136
+ if( !change_from_input )
7137
+ text_widget.set( val );
7138
+
7139
+ // Reset button (default value)
7140
+ if( !skipCallback )
7141
+ {
7142
+ let btn = element.querySelector( ".lexwidgetname .lexicon" );
7143
+ if( btn ) btn.style.display = val != color.iValue ? "block": "none";
7144
+ }
7145
+
7146
+ if( color.useRGB )
7147
+ val = hexToRgb( val );
7148
+
7149
+ if( !skipCallback ) this._trigger( new IEvent( name, val, e ), callback );
7150
+ }, false );
7151
+
7152
+ container.appendChild( color );
7153
+
7154
+ this.queue( container );
7155
+
7156
+ const text_widget = this.addText( null, color.value, v => {
7157
+ change_from_input = true;
7158
+ widget.set( v );
7159
+ change_from_input = false;
7160
+ }, { width: "calc( 100% - 32px )"});
7161
+
7162
+ text_widget.domEl.style.marginLeft = "4px";
7163
+
7164
+ this.clearQueue();
7165
+
7166
+ element.appendChild( container );
7167
+
7168
+ return widget;
7169
+ }
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;
6003
7246
 
6004
7247
  // Reset button (default value)
6005
7248
  if( !skipCallback )
6006
7249
  {
6007
7250
  let btn = element.querySelector( ".lexwidgetname .lexicon" );
6008
- if( btn ) btn.style.display = val != color.iValue ? "block": "none";
7251
+ if( btn ) btn.style.display = val != slider.iValue ? "block": "none";
6009
7252
  }
6010
7253
 
6011
- if( color.useRGB )
6012
- val = hexToRgb( val );
7254
+ if( options.left )
7255
+ {
7256
+ val = ( +slider.max ) - val + ( +slider.min );
7257
+ }
6013
7258
 
6014
7259
  if( !skipCallback ) this._trigger( new IEvent( name, val, e ), callback );
6015
- }, false );
6016
-
6017
- container.appendChild( color );
7260
+ }, { passive: false });
6018
7261
 
6019
- this.queue( container );
7262
+ slider.addEventListener( "mousedown", function( e ) {
7263
+ if( options.onPress )
7264
+ {
7265
+ options.onPress.bind( slider )( e, slider );
7266
+ }
7267
+ }, false );
6020
7268
 
6021
- const text_widget = this.addText( null, color.value, v => {
6022
- change_from_input = true;
6023
- widget.set( v );
6024
- change_from_input = false;
6025
- }, { width: "calc( 100% - 32px )"});
7269
+ slider.addEventListener( "mouseup", function( e ) {
7270
+ if( options.onRelease )
7271
+ {
7272
+ options.onRelease.bind( slider )( e, slider );
7273
+ }
7274
+ }, false );
6026
7275
 
6027
- text_widget.domEl.style.marginLeft = "4px";
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
+ };
6028
7283
 
6029
- this.clearQueue();
7284
+ if( value.constructor == Number )
7285
+ {
7286
+ value = clamp( value, +slider.min, +slider.max );
7287
+ }
6030
7288
 
7289
+ container.appendChild( slider );
6031
7290
  element.appendChild( container );
6032
7291
 
7292
+ // Remove branch padding and margins
7293
+ if( !widget.name )
7294
+ {
7295
+ element.className += " noname";
7296
+ container.style.width = "100%";
7297
+ }
7298
+
6033
7299
  return widget;
6034
7300
  }
6035
7301
 
@@ -6065,7 +7331,8 @@ class Panel {
6065
7331
  let element = widget.domEl;
6066
7332
 
6067
7333
  // add reset functionality
6068
- if( widget.name ) {
7334
+ if( widget.name )
7335
+ {
6069
7336
  Panel._add_reset_property( element.domName, function() {
6070
7337
  this.style.display = "none";
6071
7338
  vecinput.value = vecinput.iValue;
@@ -6333,7 +7600,8 @@ class Panel {
6333
7600
  return;
6334
7601
  }
6335
7602
 
6336
- for( let i = 0; i < inputs.length; ++i ) {
7603
+ for( let i = 0; i < inputs.length; ++i )
7604
+ {
6337
7605
  let value = newValue[ i ];
6338
7606
  inputs[ i ].value = round( value, options.precision ) ?? 0;
6339
7607
  Panel._dispatch_event( inputs[ i ], "change", skipCallback );
@@ -6345,7 +7613,8 @@ class Panel {
6345
7613
  // Add reset functionality
6346
7614
  Panel._add_reset_property( element.domName, function() {
6347
7615
  this.style.display = "none";
6348
- for( let v of element.querySelectorAll( ".vecinput" ) ) {
7616
+ for( let v of element.querySelectorAll( ".vecinput" ) )
7617
+ {
6349
7618
  v.value = v.iValue;
6350
7619
  Panel._dispatch_event( v, "change" );
6351
7620
  }
@@ -6357,8 +7626,8 @@ class Panel {
6357
7626
  container.className = "lexvector";
6358
7627
  container.style.width = "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
6359
7628
 
6360
- for( let i = 0; i < num_components; ++i ) {
6361
-
7629
+ for( let i = 0; i < num_components; ++i )
7630
+ {
6362
7631
  let box = document.createElement( 'div' );
6363
7632
  box.className = "vecbox";
6364
7633
  box.innerHTML = "<span class='" + Panel.VECTOR_COMPONENTS[ i ] + "'></span>";
@@ -6434,7 +7703,8 @@ class Panel {
6434
7703
 
6435
7704
  if( locker.locked )
6436
7705
  {
6437
- for( let v of element.querySelectorAll( ".vecinput" ) ) {
7706
+ for( let v of element.querySelectorAll( ".vecinput" ) )
7707
+ {
6438
7708
  v.value = val;
6439
7709
  value[ v.idx ] = val;
6440
7710
  }
@@ -6492,7 +7762,8 @@ class Panel {
6492
7762
 
6493
7763
  if( locker.locked )
6494
7764
  {
6495
- for( let v of element.querySelectorAll( ".vecinput" ) ) {
7765
+ for( let v of element.querySelectorAll( ".vecinput" ) )
7766
+ {
6496
7767
  v.value = round( +v.valueAsNumber + mult * dt, options.precision );
6497
7768
  Panel._dispatch_event( v, "change" );
6498
7769
  }
@@ -6618,7 +7889,7 @@ class Panel {
6618
7889
  const value = [];
6619
7890
  for( let i = 0; i < element.dimensions.length; ++i )
6620
7891
  {
6621
- value.push( element.dimensions[ i ].onGetValue() );
7892
+ value.push( element.dimensions[ i ].value() );
6622
7893
  }
6623
7894
  return value;
6624
7895
  };
@@ -6626,7 +7897,7 @@ class Panel {
6626
7897
  widget.onSetValue = ( newValue, skipCallback ) => {
6627
7898
  for( let i = 0; i < element.dimensions.length; ++i )
6628
7899
  {
6629
- element.dimensions[ i ].onSetValue( newValue[ i ], skipCallback );
7900
+ element.dimensions[ i ].set( newValue[ i ], skipCallback );
6630
7901
  }
6631
7902
  };
6632
7903
 
@@ -6641,14 +7912,14 @@ class Panel {
6641
7912
  {
6642
7913
  element.dimensions[ i ] = this.addNumber( null, value[ i ], ( v ) => {
6643
7914
 
6644
- const value = widget.onGetValue();
7915
+ const value = widget.value();
6645
7916
 
6646
7917
  if( element.locked )
6647
7918
  {
6648
7919
  const ar = ( i == 0 ? 1.0 / element.aspectRatio : element.aspectRatio );
6649
7920
  const index = ( 1 + i ) % 2;
6650
7921
  value[ index ] = v * ar;
6651
- element.dimensions[ index ].onSetValue( value[ index ], true );
7922
+ element.dimensions[ index ].set( value[ index ], true );
6652
7923
  }
6653
7924
 
6654
7925
  if( callback )
@@ -6691,7 +7962,7 @@ class Panel {
6691
7962
  this.classList.remove( "fa-lock-open" );
6692
7963
 
6693
7964
  // Recompute ratio
6694
- const value = widget.onGetValue();
7965
+ const value = widget.value();
6695
7966
  element.aspectRatio = value[ 0 ] / value[ 1 ];
6696
7967
  }
6697
7968
  else
@@ -7088,7 +8359,8 @@ class Panel {
7088
8359
  let container = document.createElement('div');
7089
8360
  container.className = "lextree";
7090
8361
 
7091
- if(name) {
8362
+ if( name )
8363
+ {
7092
8364
  let title = document.createElement('span');
7093
8365
  title.innerHTML = name;
7094
8366
  container.appendChild(title);
@@ -7100,8 +8372,8 @@ class Panel {
7100
8372
  toolsDiv.className += " notitle";
7101
8373
 
7102
8374
  // Tree icons
7103
- if(options.icons) {
7104
-
8375
+ if( options.icons )
8376
+ {
7105
8377
  for( let data of options.icons )
7106
8378
  {
7107
8379
  let iconEl = document.createElement('a');
@@ -7161,11 +8433,15 @@ class Panel {
7161
8433
  let widget = new Widget( null, Widget.SEPARATOR );
7162
8434
  widget.domEl = element;
7163
8435
 
7164
- if(this.current_branch) {
8436
+ if( this.current_branch )
8437
+ {
7165
8438
  this.current_branch.content.appendChild( element );
7166
8439
  this.current_branch.widgets.push( widget );
7167
- } else
7168
- this.root.appendChild(element);
8440
+ }
8441
+ else
8442
+ {
8443
+ this.root.appendChild( element );
8444
+ }
7169
8445
  }
7170
8446
 
7171
8447
  /**
@@ -7346,6 +8622,234 @@ class Panel {
7346
8622
 
7347
8623
  return widget;
7348
8624
  }
8625
+
8626
+ /**
8627
+ * @method addTable
8628
+ * @param {String} name Widget name
8629
+ * @param {Number} data Table data
8630
+ * @param {*} options:
8631
+ * head: Table headers (each of the headers per column)
8632
+ * body: Table body (data per row for each column)
8633
+ * rowActions: Allow to add actions per row
8634
+ * onMenuAction: Function callback to fill the "menu" context
8635
+ * selectable: Each row can be selected
8636
+ */
8637
+
8638
+ addTable( name, data, options = { } ) {
8639
+
8640
+ if( !data )
8641
+ {
8642
+ throw( "Data is needed to create a table!" );
8643
+ }
8644
+
8645
+ let widget = this.create_widget( name, Widget.TABLE, options );
8646
+
8647
+ widget.onGetValue = () => {
8648
+
8649
+ };
8650
+
8651
+ widget.onSetValue = ( newValue, skipCallback ) => {
8652
+
8653
+ };
8654
+
8655
+ let element = widget.domEl;
8656
+
8657
+ const container = document.createElement('div');
8658
+ container.className = "lextable";
8659
+ container.style.width = "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
8660
+
8661
+ const table = document.createElement( 'table' );
8662
+ container.appendChild( table );
8663
+
8664
+ data.head = data.head ?? [];
8665
+ data.body = data.body ?? [];
8666
+ data.orderMap = { };
8667
+ data.checkMap = { };
8668
+
8669
+ function compareFn( idx, order, a, b) {
8670
+ if (a[idx] < b[idx]) return -order;
8671
+ else if (a[idx] > b[idx]) return order;
8672
+ return 0;
8673
+ }
8674
+
8675
+ widget.refreshTable = () => {
8676
+
8677
+ table.innerHTML = "";
8678
+
8679
+ // Head
8680
+ {
8681
+ const head = document.createElement( 'thead' );
8682
+ head.className = "lextablehead";
8683
+ table.appendChild( head );
8684
+
8685
+ const hrow = document.createElement( 'tr' );
8686
+
8687
+ if( options.selectable )
8688
+ {
8689
+ const th = document.createElement( 'th' );
8690
+ const input = document.createElement( 'input' );
8691
+ input.type = "checkbox";
8692
+ input.className = "lexcheckbox";
8693
+ input.checked = data.checkMap[ ":root" ] ?? false;
8694
+ th.appendChild( input );
8695
+
8696
+ input.addEventListener( 'change', function() {
8697
+
8698
+ data.checkMap[ ":root" ] = this.checked;
8699
+
8700
+ const body = table.querySelector( "tbody" );
8701
+ for( const el of body.childNodes )
8702
+ {
8703
+ data.checkMap[ el.getAttribute( "rowId" ) ] = this.checked;
8704
+ el.querySelector( "input" ).checked = this.checked;
8705
+ }
8706
+ });
8707
+
8708
+ hrow.appendChild( th );
8709
+ }
8710
+
8711
+ for( const headData of data.head )
8712
+ {
8713
+ const th = document.createElement( 'th' );
8714
+ th.innerHTML = `${ headData } <a class="fa-solid fa-sort"></a>`;
8715
+
8716
+ th.querySelector( 'a' ).addEventListener( 'click', () => {
8717
+
8718
+ if( !data.orderMap[ headData ] )
8719
+ {
8720
+ data.orderMap[ headData ] = 1;
8721
+ }
8722
+
8723
+ const idx = data.head.indexOf(headData);
8724
+ data.body = data.body.sort( compareFn.bind( this, idx,data.orderMap[ headData ] ) );
8725
+ data.orderMap[ headData ] = -data.orderMap[ headData ];
8726
+
8727
+ widget.refreshTable();
8728
+
8729
+ });
8730
+
8731
+ hrow.appendChild( th );
8732
+ }
8733
+
8734
+ // Add empty header column
8735
+ if( options.rowActions )
8736
+ {
8737
+ const th = document.createElement( 'th' );
8738
+ th.className = "sm";
8739
+ hrow.appendChild( th );
8740
+ }
8741
+
8742
+ head.appendChild( hrow );
8743
+ }
8744
+
8745
+ // Body
8746
+ {
8747
+ const body = document.createElement( 'tbody' );
8748
+ body.className = "lextablebody";
8749
+ table.appendChild( body );
8750
+
8751
+ for( let r = 0; r < data.body.length; ++r )
8752
+ {
8753
+ const bodyData = data.body[ r ];
8754
+ const row = document.createElement( 'tr' );
8755
+ const rowId = LX.getSupportedDOMName( bodyData.join( '-' ) );
8756
+ row.setAttribute( "rowId", rowId );
8757
+
8758
+ if( options.selectable )
8759
+ {
8760
+ const td = document.createElement( 'td' );
8761
+ const input = document.createElement( 'input' );
8762
+ input.type = "checkbox";
8763
+ input.className = "lexcheckbox";
8764
+ input.checked = data.checkMap[ rowId ];
8765
+ td.appendChild( input );
8766
+
8767
+ input.addEventListener( 'change', function() {
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
+ }
8775
+ });
8776
+
8777
+ row.appendChild( td );
8778
+ }
8779
+
8780
+ for( const rowData of bodyData )
8781
+ {
8782
+ const td = document.createElement( 'td' );
8783
+ td.innerHTML = `${ rowData }`;
8784
+ row.appendChild( td );
8785
+ }
8786
+
8787
+ if( options.rowActions )
8788
+ {
8789
+ const td = document.createElement( 'td' );
8790
+ td.className = "sm";
8791
+
8792
+ const buttons = document.createElement( 'div' );
8793
+ buttons.className = "lextablebuttons";
8794
+ td.appendChild( buttons );
8795
+
8796
+ for( const action of options.rowActions )
8797
+ {
8798
+ const button = document.createElement( 'a' );
8799
+ button.className = "lexicon";
8800
+
8801
+ if( action == "delete" )
8802
+ {
8803
+ button.className += " fa-solid fa-trash-can";
8804
+ button.addEventListener( 'click', function() {
8805
+ // Don't need to refresh table..
8806
+ data.body.splice( r, 1 );
8807
+ row.remove();
8808
+ });
8809
+ }
8810
+ else if( action == "menu" )
8811
+ {
8812
+ button.className += " fa-solid fa-ellipsis";
8813
+ button.addEventListener( 'click', function( event ) {
8814
+ addContextMenu( null, event, c => {
8815
+ if( options.onMenuAction )
8816
+ {
8817
+ options.onMenuAction( c );
8818
+ return;
8819
+ }
8820
+ console.warn( "Using <Menu action> without action callbacks." );
8821
+ } );
8822
+ });
8823
+ }
8824
+ else // custom actions
8825
+ {
8826
+ console.assert( action.constructor == Object );
8827
+ button.className += ` ${ action.icon }`;
8828
+ }
8829
+
8830
+ buttons.appendChild( button );
8831
+ }
8832
+
8833
+ row.appendChild( td );
8834
+ }
8835
+
8836
+ body.appendChild( row );
8837
+ }
8838
+ }
8839
+ }
8840
+
8841
+ widget.refreshTable();
8842
+
8843
+ if( !widget.name )
8844
+ {
8845
+ element.className += " noname";
8846
+ container.style.width = "100%";
8847
+ }
8848
+
8849
+ element.appendChild( container );
8850
+
8851
+ return widget;
8852
+ }
7349
8853
  }
7350
8854
 
7351
8855
  LX.Panel = Panel;
@@ -7393,7 +8897,7 @@ class Branch {
7393
8897
  root.appendChild( title );
7394
8898
 
7395
8899
  var branchContent = document.createElement( 'div' );
7396
- branchContent.id = name.replace(/\s/g, '');
8900
+ branchContent.id = name.replace( /\s/g, '' );
7397
8901
  branchContent.className = "lexbranchcontent";
7398
8902
  root.appendChild(branchContent);
7399
8903
  this.content = branchContent;
@@ -7449,7 +8953,8 @@ class Branch {
7449
8953
 
7450
8954
  const dialog = new Dialog(this.name, p => {
7451
8955
  // add widgets
7452
- for( let w of this.widgets ) {
8956
+ for( let w of this.widgets )
8957
+ {
7453
8958
  p.root.appendChild( w.domEl );
7454
8959
  }
7455
8960
  });
@@ -7542,8 +9047,8 @@ class Branch {
7542
9047
  var size = this.grabber.style.marginLeft;
7543
9048
 
7544
9049
  // Update sizes of widgets inside
7545
- for(var i = 0; i < this.widgets.length; i++) {
7546
-
9050
+ for( var i = 0; i < this.widgets.length; i++ )
9051
+ {
7547
9052
  let widget = this.widgets[ i ];
7548
9053
  let element = widget.domEl;
7549
9054
 
@@ -7562,9 +9067,6 @@ class Branch {
7562
9067
  case Widget.FILE:
7563
9068
  padding = "10%";
7564
9069
  break;
7565
- case Widget.TEXT:
7566
- padding = "8px";
7567
- break;
7568
9070
  };
7569
9071
 
7570
9072
  value.style.width = "-moz-calc( 100% - " + size + " - " + padding + " )";
@@ -7702,16 +9204,15 @@ class Dialog {
7702
9204
  draggable = options.draggable ?? true,
7703
9205
  modal = options.modal ?? false;
7704
9206
 
7705
- if( modal )
7706
- {
7707
- LX.modal.toggle( false );
7708
- }
7709
-
7710
- var root = document.createElement('div');
7711
- root.className = "lexdialog " + (options.class ?? "");
9207
+ var root = document.createElement('dialog');
9208
+ root.className = "lexdialog " + (options.className ?? "");
7712
9209
  root.id = options.id ?? "dialog" + Dialog._last_id++;
7713
9210
  LX.root.appendChild( root );
7714
9211
 
9212
+ doAsync( () => {
9213
+ modal ? root.showModal() : root.show();
9214
+ }, 10 );
9215
+
7715
9216
  let that = this;
7716
9217
 
7717
9218
  var titleDiv = document.createElement('div');
@@ -7793,18 +9294,17 @@ class Dialog {
7793
9294
 
7794
9295
  if( !options.onclose )
7795
9296
  {
7796
- that.panel.clear();
7797
- root.remove();
9297
+ root.close();
9298
+
9299
+ doAsync( () => {
9300
+ that.panel.clear();
9301
+ root.remove();
9302
+ }, 150 );
7798
9303
  }
7799
9304
  else
7800
9305
  {
7801
9306
  options.onclose( this.root );
7802
9307
  }
7803
-
7804
- if( modal )
7805
- {
7806
- LX.modal.toggle( true );
7807
- }
7808
9308
  };
7809
9309
 
7810
9310
  var closeButton = document.createElement( 'a' );
@@ -7863,15 +9363,15 @@ class Dialog {
7863
9363
 
7864
9364
  root.style.width = size[ 0 ] ? (size[ 0 ]) : "25%";
7865
9365
  root.style.height = size[ 1 ] ? (size[ 1 ]) : "auto";
9366
+ root.style.translate = options.position ? "unset" : "-50% -50%";
7866
9367
 
7867
9368
  if( options.size )
7868
9369
  {
7869
9370
  this.size = size;
7870
9371
  }
7871
9372
 
7872
- let rect = root.getBoundingClientRect();
7873
- root.style.left = position[ 0 ] ? (position[ 0 ]) : "calc( 50% - " + ( rect.width * 0.5 ) + "px )";
7874
- 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%";
7875
9375
 
7876
9376
  panel.root.style.width = "calc( 100% - 30px )";
7877
9377
  panel.root.style.height = title ? "calc( 100% - " + ( titleDiv.offsetHeight + 30 ) + "px )" : "calc( 100% - 51px )";
@@ -7888,7 +9388,7 @@ class Dialog {
7888
9388
  this._oncreate.call(this, this.panel);
7889
9389
  }
7890
9390
 
7891
- setPosition(x, y) {
9391
+ setPosition( x, y ) {
7892
9392
 
7893
9393
  this.root.style.left = x + "px";
7894
9394
  this.root.style.top = y + "px";
@@ -7919,21 +9419,32 @@ class PocketDialog extends Dialog {
7919
9419
  options.draggable = options.draggable ?? false;
7920
9420
  options.closable = options.closable ?? false;
7921
9421
 
9422
+ const dragMargin = 3;
9423
+
7922
9424
  super( title, callback, options );
7923
9425
 
7924
9426
  let that = this;
7925
9427
  // Update margins on branch title closes/opens
7926
9428
  LX.addSignal("@on_branch_closed", this.panel, closed => {
7927
9429
  if( this.dock_pos == PocketDialog.BOTTOM )
7928
- this.root.style.top = "calc(100% - " + (this.root.offsetHeight + 6) + "px)";
9430
+ {
9431
+ this.root.style.top = "calc(100% - " + (this.root.offsetHeight + dragMargin) + "px)";
9432
+ }
7929
9433
  });
7930
9434
 
7931
9435
  // Custom
7932
9436
  this.root.classList.add( "pocket" );
7933
- if( !options.position ) {
7934
- this.root.style.left = "calc(100% - " + (this.root.offsetWidth + 6) + "px)";
7935
- this.root.style.top = "0px";
9437
+
9438
+ this.root.style.translate = "none";
9439
+ this.root.style.top = "0";
9440
+ this.root.style.left = "unset";
9441
+
9442
+ if( !options.position )
9443
+ {
9444
+ this.root.style.right = dragMargin + "px";
9445
+ this.root.style.top = dragMargin + "px";
7936
9446
  }
9447
+
7937
9448
  this.panel.root.style.width = "calc( 100% - 12px )";
7938
9449
  this.panel.root.style.height = "calc( 100% - 40px )";
7939
9450
  this.dock_pos = PocketDialog.TOP;
@@ -7941,6 +9452,11 @@ class PocketDialog extends Dialog {
7941
9452
  this.minimized = false;
7942
9453
  this.title.tabIndex = -1;
7943
9454
  this.title.addEventListener("click", e => {
9455
+ if( this.title._eventCatched )
9456
+ {
9457
+ this.title._eventCatched = false;
9458
+ return;
9459
+ }
7944
9460
 
7945
9461
  // Sized dialogs have to keep their size
7946
9462
  if( this.size )
@@ -7954,7 +9470,7 @@ class PocketDialog extends Dialog {
7954
9470
 
7955
9471
  if( this.dock_pos == PocketDialog.BOTTOM )
7956
9472
  that.root.style.top = this.root.classList.contains("minimized") ?
7957
- "calc(100% - " + (that.title.offsetHeight + 6) + "px)" : "calc(100% - " + (that.root.offsetHeight + 6) + "px)";
9473
+ "calc(100% - " + (that.title.offsetHeight + 6) + "px)" : "calc(100% - " + (that.root.offsetHeight + dragMargin) + "px)";
7958
9474
  });
7959
9475
 
7960
9476
  if( !options.draggable )
@@ -7969,26 +9485,42 @@ class PocketDialog extends Dialog {
7969
9485
  switch( t )
7970
9486
  {
7971
9487
  case 'b':
7972
- this.root.style.top = "calc(100% - " + (this.root.offsetHeight + 6) + "px)";
9488
+ this.root.style.top = "calc(100% - " + (this.root.offsetHeight + dragMargin) + "px)";
7973
9489
  break;
7974
9490
  case 'l':
7975
- this.root.style.left = options.position ? options.position[ 1 ] : "0px";
9491
+ this.root.style.right = "unset";
9492
+ this.root.style.left = options.position ? options.position[ 1 ] : ( dragMargin + "px" );
7976
9493
  break;
7977
9494
  }
7978
9495
  }
7979
9496
  }
7980
9497
 
7981
9498
  this.root.classList.add('dockable');
7982
- this.title.addEventListener("keydown", function(e) {
7983
- if( e.ctrlKey && e.key == 'ArrowLeft' ) {
9499
+
9500
+ this.title.addEventListener("keydown", function( e ) {
9501
+ if( !e.ctrlKey )
9502
+ {
9503
+ return;
9504
+ }
9505
+
9506
+ that.root.style.right = "unset";
9507
+
9508
+ if( e.key == 'ArrowLeft' )
9509
+ {
7984
9510
  that.root.style.left = '0px';
7985
- } else if( e.ctrlKey && e.key == 'ArrowRight' ) {
7986
- that.root.style.left = "calc(100% - " + (that.root.offsetWidth + 6) + "px)";
7987
- }else if( e.ctrlKey && e.key == 'ArrowUp' ) {
9511
+ }
9512
+ else if( e.key == 'ArrowRight' )
9513
+ {
9514
+ that.root.style.left = "calc(100% - " + (that.root.offsetWidth + dragMargin) + "px)";
9515
+ }
9516
+ else if( e.key == 'ArrowUp' )
9517
+ {
7988
9518
  that.root.style.top = "0px";
7989
9519
  that.dock_pos = PocketDialog.TOP;
7990
- }else if( e.ctrlKey && e.key == 'ArrowDown' ) {
7991
- that.root.style.top = "calc(100% - " + (that.root.offsetHeight + 6) + "px)";
9520
+ }
9521
+ else if( e.key == 'ArrowDown' )
9522
+ {
9523
+ that.root.style.top = "calc(100% - " + (that.root.offsetHeight + dragMargin) + "px)";
7992
9524
  that.dock_pos = PocketDialog.BOTTOM;
7993
9525
  }
7994
9526
  });
@@ -8007,12 +9539,12 @@ class ContextMenu {
8007
9539
  constructor( event, title, options = {} ) {
8008
9540
 
8009
9541
  // remove all context menus
8010
- document.body.querySelectorAll(".lexcontextmenubox").forEach(e => e.remove());
9542
+ document.body.querySelectorAll( ".lexcontextmenu" ).forEach( e => e.remove() );
8011
9543
 
8012
- this.root = document.createElement('div');
8013
- this.root.className = "lexcontextmenubox";
8014
- this.root.style.left = (event.x - 48 + document.scrollingElement.scrollLeft) + "px";
8015
- 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";
8016
9548
 
8017
9549
  this.root.addEventListener("mouseleave", function() {
8018
9550
  this.remove();
@@ -8025,8 +9557,8 @@ class ContextMenu {
8025
9557
  {
8026
9558
  const item = {};
8027
9559
  item[ title ] = [];
8028
- item[ 'className' ] = "cmtitle";
8029
- item[ 'icon' ] = options.icon;
9560
+ item[ "className" ] = "cmtitle";
9561
+ item[ "icon" ] = options.icon;
8030
9562
  this.items.push( item );
8031
9563
  }
8032
9564
  }
@@ -8074,16 +9606,16 @@ class ContextMenu {
8074
9606
 
8075
9607
  _create_submenu( o, k, c, d ) {
8076
9608
 
8077
- this.root.querySelectorAll(".lexcontextmenubox").forEach( cm => cm.remove() );
9609
+ this.root.querySelectorAll( ".lexcontextmenu" ).forEach( cm => cm.remove() );
8078
9610
 
8079
9611
  let contextmenu = document.createElement('div');
8080
- contextmenu.className = "lexcontextmenubox";
9612
+ contextmenu.className = "lexcontextmenu";
8081
9613
  c.appendChild( contextmenu );
8082
9614
 
8083
9615
  for( var i = 0; i < o[k].length; ++i )
8084
9616
  {
8085
- const subitem = o[k][i];
8086
- const subkey = Object.keys(subitem)[0];
9617
+ const subitem = o[ k ][ i ];
9618
+ const subkey = Object.keys( subitem )[ 0 ];
8087
9619
  this._create_entry(subitem, subkey, contextmenu, d);
8088
9620
  }
8089
9621
 
@@ -8099,22 +9631,25 @@ class ContextMenu {
8099
9631
 
8100
9632
  const hasSubmenu = o[ k ].length;
8101
9633
  let entry = document.createElement('div');
8102
- entry.className = "lexcontextmenuentry" + (o[ 'className' ] ? " " + o[ 'className' ] : "" );
9634
+ entry.className = "lexmenuboxentry" + (o[ 'className' ] ? " " + o[ 'className' ] : "" );
8103
9635
  entry.id = o.id ?? ("eId" + getSupportedDOMName( k ));
8104
9636
  entry.innerHTML = "";
8105
9637
  const icon = o[ 'icon' ];
8106
- if(icon) {
9638
+ if( icon )
9639
+ {
8107
9640
  entry.innerHTML += "<a class='" + icon + " fa-sm'></a>";
8108
9641
  }
8109
9642
  const disabled = o['disabled'];
8110
9643
  entry.innerHTML += "<div class='lexentryname" + (disabled ? " disabled" : "") + "'>" + k + "</div>";
8111
9644
  c.appendChild( entry );
8112
9645
 
8113
- if( this.colors[ k ] ) {
9646
+ if( this.colors[ k ] )
9647
+ {
8114
9648
  entry.style.borderColor = this.colors[ k ];
8115
9649
  }
8116
9650
 
8117
- if( k == "" ) {
9651
+ if( k == "" )
9652
+ {
8118
9653
  entry.className += " cmseparator";
8119
9654
  return;
8120
9655
  }
@@ -8127,7 +9662,8 @@ class ContextMenu {
8127
9662
  if(disabled) return;
8128
9663
 
8129
9664
  const f = o[ 'callback' ];
8130
- if(f) {
9665
+ if( f )
9666
+ {
8131
9667
  f.call( this, k, entry );
8132
9668
  this.root.remove();
8133
9669
  }
@@ -8159,8 +9695,7 @@ class ContextMenu {
8159
9695
 
8160
9696
  entry.addEventListener("mouseleave", () => {
8161
9697
  d = -1; // Reset depth
8162
- // delete entry.built;
8163
- c.querySelectorAll(".lexcontextmenubox").forEach(e => e.remove());
9698
+ c.querySelectorAll(".lexcontextmenu").forEach(e => e.remove());
8164
9699
  });
8165
9700
  }
8166
9701
 
@@ -8193,22 +9728,25 @@ class ContextMenu {
8193
9728
  if(key) found = o[ key ];
8194
9729
  } );
8195
9730
 
8196
- if(found) {
9731
+ if( found )
9732
+ {
8197
9733
  insert( tokens[idx++], found );
8198
9734
  }
8199
- else {
9735
+ else
9736
+ {
8200
9737
  let item = {};
8201
9738
  item[ token ] = [];
8202
- const next_token = tokens[idx++];
9739
+ const nextToken = tokens[idx++];
8203
9740
  // Check if last token -> add callback
8204
- if(!next_token) {
9741
+ if( !nextToken )
9742
+ {
8205
9743
  item[ 'id' ] = options.id;
8206
9744
  item[ 'callback' ] = options.callback;
8207
9745
  item[ 'disabled' ] = options.disabled ?? false;
8208
9746
  }
8209
9747
 
8210
9748
  list.push( item );
8211
- insert( next_token, item[ token ] );
9749
+ insert( nextToken, item[ token ] );
8212
9750
  }
8213
9751
  };
8214
9752
 
@@ -8232,7 +9770,8 @@ class ContextMenu {
8232
9770
  _item[ key ].unshift( parent );
8233
9771
  }
8234
9772
 
8235
- for( var child of _item[ key ] ) {
9773
+ for( var child of _item[ key ] )
9774
+ {
8236
9775
  let k = Object.keys(child)[0];
8237
9776
  for( var i = 0; i < child[k].length; ++i )
8238
9777
  setParent(child);
@@ -8246,7 +9785,7 @@ class ContextMenu {
8246
9785
 
8247
9786
  for( let item of this.items )
8248
9787
  {
8249
- let key = Object.keys(item)[0];
9788
+ let key = Object.keys( item )[ 0 ];
8250
9789
  let pKey = "eId" + getSupportedDOMName( key );
8251
9790
 
8252
9791
  // Item already created
@@ -8363,7 +9902,7 @@ class Curve {
8363
9902
 
8364
9903
  var r = [];
8365
9904
  var dx = (element.xrange[1] - element.xrange[ 0 ]) / samples;
8366
- 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 )
8367
9906
  {
8368
9907
  r.push( element.getValueAt(i) );
8369
9908
  }
@@ -8372,7 +9911,8 @@ class Curve {
8372
9911
 
8373
9912
  element.addValue = function(v) {
8374
9913
 
8375
- for(var i = 0; i < element.value; i++) {
9914
+ for( var i = 0; i < element.value; i++ )
9915
+ {
8376
9916
  var value = element.value[i];
8377
9917
  if(value[0] < v[0]) continue;
8378
9918
  element.value.splice(i,0,v);
@@ -8398,7 +9938,7 @@ class Curve {
8398
9938
 
8399
9939
  var selected = -1;
8400
9940
 
8401
- element.redraw = function( o = {} ) {
9941
+ element.redraw = function( o = {} ) {
8402
9942
 
8403
9943
  if( o.value ) element.value = o.value;
8404
9944
  if( o.xrange ) element.xrange = o.xrange;
@@ -8427,13 +9967,16 @@ class Curve {
8427
9967
  ctx.moveTo( pos[ 0 ], pos[ 1 ] );
8428
9968
  let values = [pos[ 0 ], pos[ 1 ]];
8429
9969
 
8430
- for(var i in element.value) {
8431
- var value = element.value[i];
8432
- pos = convert(value);
8433
- values.push(pos[ 0 ]);
8434
- values.push(pos[ 1 ]);
8435
- 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
+ {
8436
9978
  ctx.lineTo( pos[ 0 ], pos[ 1 ] );
9979
+ }
8437
9980
  }
8438
9981
 
8439
9982
  pos = convert([ element.xrange[ 1 ], element.defaulty ]);
@@ -8450,7 +9993,8 @@ class Curve {
8450
9993
  }
8451
9994
 
8452
9995
  // Draw points
8453
- for( var i = 0; i < element.value.length; i += 1 ) {
9996
+ for( var i = 0; i < element.value.length; i += 1 )
9997
+ {
8454
9998
  var value = element.value[ i ];
8455
9999
  pos = convert( value );
8456
10000
  if( selected == i )
@@ -8462,10 +10006,11 @@ class Curve {
8462
10006
  ctx.fill();
8463
10007
  }
8464
10008
 
8465
- if(element.show_samples) {
10009
+ if( element.show_samples )
10010
+ {
8466
10011
  var samples = element.resample(element.show_samples);
8467
10012
  ctx.fillStyle = "#888";
8468
- for(var i = 0; i < samples.length; i += 1)
10013
+ for( var i = 0; i < samples.length; i += 1)
8469
10014
  {
8470
10015
  var value = [ i * ((element.xrange[ 1 ] - element.xrange[ 0 ]) / element.show_samples) + element.xrange[ 0 ], samples[ i ] ];
8471
10016
  pos = convert(value);
@@ -8488,7 +10033,8 @@ class Curve {
8488
10033
 
8489
10034
  selected = computeSelected( mousex, canvas.height - mousey );
8490
10035
 
8491
- 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
+ {
8492
10038
  var v = unconvert([ mousex, canvas.height - mousey ]);
8493
10039
  element.value.push( v );
8494
10040
  sortValues();
@@ -8536,7 +10082,8 @@ class Curve {
8536
10082
  var dy = element.draggable_y ? last_mouse[ 1 ] - mousey : 0;
8537
10083
  var delta = unconvert([ -dx, dy ]);
8538
10084
 
8539
- if( selected != -1 ) {
10085
+ if( selected != -1 )
10086
+ {
8540
10087
  var minx = element.xrange[ 0 ];
8541
10088
  var maxx = element.xrange[ 1 ];
8542
10089
 
@@ -8693,7 +10240,7 @@ class Dial {
8693
10240
 
8694
10241
  var r = [];
8695
10242
  var dx = (element.xrange[1] - element.xrange[ 0 ]) / samples;
8696
- 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)
8697
10244
  {
8698
10245
  r.push( element.getValueAt(i) );
8699
10246
  }
@@ -8702,15 +10249,16 @@ class Dial {
8702
10249
 
8703
10250
  element.addValue = function(v) {
8704
10251
 
8705
- for(var i = 0; i < element.value; i++) {
8706
- var value = element.value[i];
8707
- if(value[0] < v[0]) continue;
8708
- 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 );
8709
10257
  redraw();
8710
10258
  return;
8711
10259
  }
8712
10260
 
8713
- element.value.push(v);
10261
+ element.value.push( v );
8714
10262
  redraw();
8715
10263
  }
8716
10264
 
@@ -8730,7 +10278,7 @@ class Dial {
8730
10278
 
8731
10279
  var selected = -1;
8732
10280
 
8733
- element.redraw = function( o = {} ) {
10281
+ element.redraw = function( o = {} ) {
8734
10282
 
8735
10283
  if( o.value ) element.value = o.value;
8736
10284
  if( o.xrange ) element.xrange = o.xrange;
@@ -8759,17 +10307,17 @@ class Dial {
8759
10307
  ctx.moveTo( pos[ 0 ], pos[ 1 ] );
8760
10308
  let values = [pos[ 0 ], pos[ 1 ]];
8761
10309
 
8762
- for(var i in element.value) {
8763
- var value = element.value[i];
8764
- pos = convert(value);
8765
- values.push(pos[ 0 ]);
8766
- values.push(pos[ 1 ]);
8767
-
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 ] );
8768
10316
  }
8769
10317
 
8770
10318
  pos = convert([ element.xrange[ 1 ], element.defaulty ]);
8771
- values.push(pos[ 0 ]);
8772
- values.push(pos[ 1 ]);
10319
+ values.push( pos[ 0 ] );
10320
+ values.push( pos[ 1 ] );
8773
10321
 
8774
10322
  // Draw points
8775
10323
  const center = [0,0];
@@ -8779,7 +10327,8 @@ class Dial {
8779
10327
  ctx.arc( pos[ 0 ], pos[ 1 ], 3, 0, Math.PI * 2);
8780
10328
  ctx.fill();
8781
10329
 
8782
- for( var i = 0; i < element.value.length; i += 1 ) {
10330
+ for( var i = 0; i < element.value.length; i += 1 )
10331
+ {
8783
10332
  var value = element.value[ i ];
8784
10333
  pos = convert( value );
8785
10334
  if( selected == i )
@@ -8791,10 +10340,11 @@ class Dial {
8791
10340
  ctx.fill();
8792
10341
  }
8793
10342
 
8794
- if(element.show_samples) {
10343
+ if( element.show_samples )
10344
+ {
8795
10345
  var samples = element.resample(element.show_samples);
8796
10346
  ctx.fillStyle = "#888";
8797
- for(var i = 0; i < samples.length; i += 1)
10347
+ for( var i = 0; i < samples.length; i += 1)
8798
10348
  {
8799
10349
  var value = [ i * ((element.xrange[ 1 ] - element.xrange[ 0 ]) / element.show_samples) + element.xrange[ 0 ], samples[ i ] ];
8800
10350
  pos = convert(value);
@@ -8817,7 +10367,8 @@ class Dial {
8817
10367
 
8818
10368
  selected = computeSelected( mousex, canvas.height - mousey );
8819
10369
 
8820
- 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
+ {
8821
10372
  var v = unconvert([ mousex, canvas.height - mousey ]);
8822
10373
  element.value.push( v );
8823
10374
  sortValues();
@@ -8865,7 +10416,8 @@ class Dial {
8865
10416
  var dy = element.draggable_y ? last_mouse[ 1 ] - mousey : 0;
8866
10417
  var delta = unconvert([ -dx, dy ]);
8867
10418
 
8868
- if( selected != -1 ) {
10419
+ if( selected != -1 )
10420
+ {
8869
10421
  var minx = element.xrange[ 0 ];
8870
10422
  var maxx = element.xrange[ 1 ];
8871
10423
 
@@ -8973,7 +10525,8 @@ class AssetViewEvent {
8973
10525
  }
8974
10526
 
8975
10527
  string() {
8976
- switch(this.type) {
10528
+ switch(this.type)
10529
+ {
8977
10530
  case AssetViewEvent.NONE: return "assetview_event_none";
8978
10531
  case AssetViewEvent.ASSET_SELECTED: return "assetview_event_selected";
8979
10532
  case AssetViewEvent.ASSET_DELETED: return "assetview_event_deleted";
@@ -9287,7 +10840,7 @@ class AssetView {
9287
10840
  }
9288
10841
 
9289
10842
  this.rightPanel.sameLine();
9290
- this.rightPanel.addDropdown( "Filter", this.allowedTypes, this.allowedTypes[ 0 ], v => this._refreshContent.call(this, null, v), { width: "20%", minWidth: "128px" } );
10843
+ this.rightPanel.addDropdown( "Filter", this.allowedTypes, this.allowedTypes[ 0 ], v => this._refreshContent.call(this, null, v), { width: "30%", minWidth: "128px" } );
9291
10844
  this.rightPanel.addText( null, this.searchValue ?? "", v => this._refreshContent.call(this, v, null), { placeholder: "Search assets.." } );
9292
10845
  this.rightPanel.addButton( null, "<a class='fa fa-arrow-up-short-wide'></a>", on_sort.bind(this), { className: "micro", title: "Sort" } );
9293
10846
  this.rightPanel.addButton( null, "<a class='fa-solid fa-grip'></a>", on_change_view.bind(this), { className: "micro", title: "View" } );
@@ -9327,8 +10880,8 @@ class AssetView {
9327
10880
  icon: "fa-solid fa-arrows-rotate",
9328
10881
  callback: domEl => { this._refreshContent(); }
9329
10882
  }
9330
- ], { width: "auto", noSelection: true } );
9331
- 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" } });
9332
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"})
9333
10886
  this.rightPanel.endLine();
9334
10887
  }
@@ -9454,8 +11007,8 @@ class AssetView {
9454
11007
  title.innerText = item.id;
9455
11008
  itemEl.appendChild( title );
9456
11009
 
9457
- if( !that.skipPreview ) {
9458
-
11010
+ if( !that.skipPreview )
11011
+ {
9459
11012
  let preview = null;
9460
11013
  const hasImage = item.src && (['png', 'jpg'].indexOf( getExtension( item.src ) ) > -1 || item.src.includes("data:image/") ); // Support b64 image as src
9461
11014
 
@@ -9482,7 +11035,8 @@ class AssetView {
9482
11035
  var newEmSize = charsPerLine / newLength;
9483
11036
  var textBaseSize = 64;
9484
11037
 
9485
- if(newEmSize < 1) {
11038
+ if( newEmSize < 1 )
11039
+ {
9486
11040
  var newFontSize = newEmSize * textBaseSize;
9487
11041
  textEl.style.fontSize = newFontSize + "px";
9488
11042
  preview.style.paddingTop = "calc(50% - " + (textEl.offsetHeight * 0.5 + 10) + "px)"
@@ -9711,7 +11265,8 @@ class AssetView {
9711
11265
 
9712
11266
  this.currentData.push( item );
9713
11267
 
9714
- if(i == (num_files - 1)) {
11268
+ if( i == (num_files - 1) )
11269
+ {
9715
11270
  this._refreshContent();
9716
11271
  if( !this.skipBrowser )
9717
11272
  this.tree.refresh();
@@ -9763,7 +11318,7 @@ class AssetView {
9763
11318
  this.currentData.splice( idx, 1 );
9764
11319
  this._refreshContent( this.searchValue, this.filter );
9765
11320
 
9766
- if(this.onevent)
11321
+ if( this.onevent)
9767
11322
  {
9768
11323
  const event = new AssetViewEvent( AssetViewEvent.ASSET_DELETED, item );
9769
11324
  this.onevent( event );
@@ -9839,7 +11394,7 @@ Object.assign(LX, {
9839
11394
  xhr.onload = function(load)
9840
11395
  {
9841
11396
  var response = this.response;
9842
- if(this.status != 200)
11397
+ if( this.status != 200)
9843
11398
  {
9844
11399
  var err = "Error " + this.status;
9845
11400
  if(request.error)
@@ -9887,7 +11442,7 @@ Object.assign(LX, {
9887
11442
  var data = new FormData();
9888
11443
  if( request.data )
9889
11444
  {
9890
- for(var i in request.data)
11445
+ for( var i in request.data)
9891
11446
  data.append(i,request.data[i]);
9892
11447
  }
9893
11448
 
@@ -9948,7 +11503,7 @@ Object.assign(LX, {
9948
11503
  var size = total;
9949
11504
  var loaded_scripts = [];
9950
11505
 
9951
- for(var i in url)
11506
+ for( var i in url)
9952
11507
  {
9953
11508
  var script = document.createElement('script');
9954
11509
  script.num = i;
@@ -10073,6 +11628,18 @@ Element.prototype.getParentArea = function() {
10073
11628
  }
10074
11629
  }
10075
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
+
10076
11643
  LX.UTILS = {
10077
11644
  getTime() { return new Date().getTime() },
10078
11645
  compareThreshold( v, p, n, t ) { return Math.abs(v - p) >= t || Math.abs(v - n) >= t },
@@ -10112,17 +11679,19 @@ LX.UTILS = {
10112
11679
  drawSpline( ctx, pts, t ) {
10113
11680
 
10114
11681
  ctx.save();
10115
- var cp=[]; // array of control points, as x0,y0,x1,y1,...
10116
- var n=pts.length;
11682
+ var cp = []; // array of control points, as x0,y0,x1,y1,...
11683
+ var n = pts.length;
10117
11684
 
10118
11685
  // Draw an open curve, not connected at the ends
10119
- for(var i=0;i<n-4;i+=2) {
10120
- 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));
10121
11689
  }
10122
11690
 
10123
- for(var i=2;i<pts.length-5;i+=2) {
11691
+ for( var i = 2; i < ( pts.length - 5 ); i += 2 )
11692
+ {
10124
11693
  ctx.beginPath();
10125
- ctx.moveTo(pts[i],pts[i+1]);
11694
+ ctx.moveTo(pts[i], pts[i+1]);
10126
11695
  ctx.bezierCurveTo(cp[2*i-2],cp[2*i-1],cp[2*i],cp[2*i+1],pts[i+2],pts[i+3]);
10127
11696
  ctx.stroke();
10128
11697
  ctx.closePath();
@@ -10130,14 +11699,14 @@ LX.UTILS = {
10130
11699
 
10131
11700
  // For open curves the first and last arcs are simple quadratics.
10132
11701
  ctx.beginPath();
10133
- ctx.moveTo(pts[0],pts[1]);
10134
- 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 ]);
10135
11704
  ctx.stroke();
10136
11705
  ctx.closePath();
10137
11706
 
10138
11707
  ctx.beginPath();
10139
- ctx.moveTo(pts[n-2],pts[n-1]);
10140
- 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 ]);
10141
11710
  ctx.stroke();
10142
11711
  ctx.closePath();
10143
11712