lexgui 0.6.4 → 0.6.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/lexgui.js CHANGED
@@ -14,7 +14,7 @@ console.warn( 'Script _build/lexgui.js_ is depracated and will be removed soon.
14
14
  */
15
15
 
16
16
  const LX = {
17
- version: "0.6.3",
17
+ version: "0.6.6",
18
18
  ready: false,
19
19
  components: [], // Specific pre-build components
20
20
  signals: {}, // Events and triggers
@@ -597,549 +597,408 @@ function setCommandbarState( value, resetEntries = true )
597
597
 
598
598
  LX.setCommandbarState = setCommandbarState;
599
599
 
600
- /**
601
- * @method message
602
- * @param {String} text
603
- * @param {String} title (Optional)
604
- * @param {Object} options
605
- * id: Id of the message dialog
606
- * position: Dialog position in screen [screen centered]
607
- * draggable: Dialog can be dragged [false]
608
- */
609
-
610
- function message( text, title, options = {} )
611
- {
612
- if( !text )
613
- {
614
- throw( "No message to show" );
615
- }
600
+ /*
601
+ * Events and Signals
602
+ */
616
603
 
617
- options.modal = true;
604
+ class IEvent {
618
605
 
619
- return new LX.Dialog( title, p => {
620
- p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
621
- }, options );
606
+ constructor( name, value, domEvent ) {
607
+ this.name = name;
608
+ this.value = value;
609
+ this.domEvent = domEvent;
610
+ }
622
611
  }
612
+ LX.IEvent = IEvent;
623
613
 
624
- LX.message = message;
614
+ class TreeEvent {
625
615
 
626
- /**
627
- * @method popup
628
- * @param {String} text
629
- * @param {String} title (Optional)
630
- * @param {Object} options
631
- * id: Id of the message dialog
632
- * timeout (Number): Delay time before it closes automatically (ms). Default: [3000]
633
- * position (Array): [x,y] Dialog position in screen. Default: [screen centered]
634
- * size (Array): [width, height]
635
- */
616
+ static NONE = 0;
617
+ static NODE_SELECTED = 1;
618
+ static NODE_DELETED = 2;
619
+ static NODE_DBLCLICKED = 3;
620
+ static NODE_CONTEXTMENU = 4;
621
+ static NODE_DRAGGED = 5;
622
+ static NODE_RENAMED = 6;
623
+ static NODE_VISIBILITY = 7;
624
+ static NODE_CARETCHANGED = 8;
636
625
 
637
- function popup( text, title, options = {} )
638
- {
639
- if( !text )
640
- {
641
- throw("No message to show");
626
+ constructor( type, node, value ) {
627
+ this.type = type || TreeEvent.NONE;
628
+ this.node = node;
629
+ this.value = value;
630
+ this.multiple = false; // Multiple selection
631
+ this.panel = null;
642
632
  }
643
633
 
644
- options.size = options.size ?? [ "max-content", "auto" ];
645
- options.class = "lexpopup";
646
-
647
- const time = options.timeout || 3000;
648
- const dialog = new LX.Dialog( title, p => {
649
- p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
650
- }, options );
651
-
652
- setTimeout( () => {
653
- dialog.close();
654
- }, Math.max( time, 150 ) );
655
-
656
- return dialog;
634
+ string() {
635
+ switch( this.type )
636
+ {
637
+ case TreeEvent.NONE: return "tree_event_none";
638
+ case TreeEvent.NODE_SELECTED: return "tree_event_selected";
639
+ case TreeEvent.NODE_DELETED: return "tree_event_deleted";
640
+ case TreeEvent.NODE_DBLCLICKED: return "tree_event_dblclick";
641
+ case TreeEvent.NODE_CONTEXTMENU: return "tree_event_contextmenu";
642
+ case TreeEvent.NODE_DRAGGED: return "tree_event_dragged";
643
+ case TreeEvent.NODE_RENAMED: return "tree_event_renamed";
644
+ case TreeEvent.NODE_VISIBILITY: return "tree_event_visibility";
645
+ case TreeEvent.NODE_CARETCHANGED: return "tree_event_caretchanged";
646
+ }
647
+ }
657
648
  }
649
+ LX.TreeEvent = TreeEvent;
658
650
 
659
- LX.popup = popup;
660
-
661
- /**
662
- * @method prompt
663
- * @param {String} text
664
- * @param {String} title (Optional)
665
- * @param {Object} options
666
- * id: Id of the prompt dialog
667
- * position: Dialog position in screen [screen centered]
668
- * draggable: Dialog can be dragged [false]
669
- * input: If false, no text input appears
670
- * accept: Accept text
671
- * required: Input has to be filled [true]. Default: false
672
- */
673
-
674
- function prompt( text, title, callback, options = {} )
651
+ function emit( signalName, value, options = {} )
675
652
  {
676
- options.modal = true;
677
- options.className = "prompt";
678
-
679
- let value = "";
653
+ const data = LX.signals[ signalName ];
680
654
 
681
- const dialog = new LX.Dialog( title, p => {
655
+ if( !data )
656
+ {
657
+ return;
658
+ }
682
659
 
683
- p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
660
+ const target = options.target;
684
661
 
685
- if( options.input ?? true )
662
+ if( target )
663
+ {
664
+ if( target[ signalName ])
686
665
  {
687
- p.addText( null, options.input || value, v => value = v, { placeholder: "..." } );
666
+ target[ signalName ].call( target, value );
688
667
  }
689
668
 
690
- p.sameLine( 2 );
669
+ return;
670
+ }
691
671
 
692
- p.addButton(null, "Cancel", () => {if(options.on_cancel) options.on_cancel(); dialog.close();} );
672
+ for( let obj of data )
673
+ {
674
+ if( obj instanceof LX.Widget )
675
+ {
676
+ obj.set( value, options.skipCallback ?? true );
677
+ }
678
+ else if( obj.constructor === Function )
679
+ {
680
+ const fn = obj;
681
+ fn( null, value );
682
+ }
683
+ else
684
+ {
685
+ // This is an element
686
+ const fn = obj[ signalName ];
687
+ console.assert( fn, `No callback registered with _${ signalName }_ signal` );
688
+ fn.bind( obj )( value );
689
+ }
690
+ }
691
+ }
693
692
 
694
- p.addButton( null, options.accept || "Continue", () => {
695
- if( options.required && value === '' )
696
- {
697
- text += text.includes("You must fill the input text.") ? "": "\nYou must fill the input text.";
698
- dialog.close();
699
- prompt( text, title, callback, options );
700
- }
701
- else
702
- {
703
- if( callback ) callback.call( this, value );
704
- dialog.close();
705
- }
706
- }, { buttonClass: "primary" });
693
+ LX.emit = emit;
707
694
 
708
- }, options );
695
+ function addSignal( name, obj, callback )
696
+ {
697
+ obj[ name ] = callback;
709
698
 
710
- // Focus text prompt
711
- if( options.input ?? true )
699
+ if( !LX.signals[ name ] )
712
700
  {
713
- dialog.root.querySelector( 'input' ).focus();
701
+ LX.signals[ name ] = [];
714
702
  }
715
703
 
716
- return dialog;
704
+ if( LX.signals[ name ].indexOf( obj ) > -1 )
705
+ {
706
+ return;
707
+ }
708
+
709
+ LX.signals[ name ].push( obj );
717
710
  }
718
711
 
719
- LX.prompt = prompt;
712
+ LX.addSignal = addSignal;
713
+
714
+ /*
715
+ * DOM Elements
716
+ */
720
717
 
721
718
  /**
722
- * @method toast
723
- * @param {String} title
724
- * @param {String} description (Optional)
725
- * @param {Object} options
726
- * action: Data of the custom action { name, callback }
727
- * closable: Allow closing the toast
728
- * timeout: Time in which the toast closed automatically, in ms. -1 means persistent. [3000]
719
+ * @class Popover
729
720
  */
730
721
 
731
- function toast( title, description, options = {} )
732
- {
733
- if( !title )
734
- {
735
- throw( "The toast needs at least a title!" );
736
- }
722
+ class Popover {
737
723
 
738
- console.assert( this.notifications );
724
+ static activeElement = false;
739
725
 
740
- const toast = document.createElement( "li" );
741
- toast.className = "lextoast";
742
- toast.style.translate = "0 calc(100% + 30px)";
743
- this.notifications.prepend( toast );
726
+ constructor( trigger, content, options = {} ) {
744
727
 
745
- LX.doAsync( () => {
728
+ console.assert( trigger, "Popover needs a DOM element as trigger!" );
746
729
 
747
- if( this.notifications.offsetWidth > this.notifications.iWidth )
730
+ if( Popover.activeElement )
748
731
  {
749
- this.notifications.iWidth = Math.min( this.notifications.offsetWidth, 480 );
750
- this.notifications.style.width = this.notifications.iWidth + "px";
732
+ Popover.activeElement.destroy();
733
+ return;
751
734
  }
752
735
 
753
- toast.dataset[ "open" ] = true;
754
- }, 10 );
736
+ this._trigger = trigger;
737
+ trigger.classList.add( "triggered" );
738
+ trigger.active = this;
755
739
 
756
- const content = document.createElement( "div" );
757
- content.className = "lextoastcontent";
758
- toast.appendChild( content );
740
+ this._windowPadding = 4;
741
+ this.side = options.side ?? "bottom";
742
+ this.align = options.align ?? "center";
743
+ this.avoidCollisions = options.avoidCollisions ?? true;
759
744
 
760
- const titleContent = document.createElement( "div" );
761
- titleContent.className = "title";
762
- titleContent.innerHTML = title;
763
- content.appendChild( titleContent );
745
+ this.root = document.createElement( "div" );
746
+ this.root.dataset["side"] = this.side;
747
+ this.root.tabIndex = "1";
748
+ this.root.className = "lexpopover";
749
+ LX.root.appendChild( this.root );
764
750
 
765
- if( description )
766
- {
767
- const desc = document.createElement( "div" );
768
- desc.className = "desc";
769
- desc.innerHTML = description;
770
- content.appendChild( desc );
771
- }
751
+ this.root.addEventListener( "keydown", (e) => {
752
+ if( e.key == "Escape" )
753
+ {
754
+ e.preventDefault();
755
+ e.stopPropagation();
756
+ this.destroy();
757
+ }
758
+ } );
772
759
 
773
- if( options.action )
774
- {
775
- const panel = new LX.Panel();
776
- panel.addButton(null, options.action.name ?? "Accept", options.action.callback.bind( this, toast ), { width: "auto", maxWidth: "150px", className: "right", buttonClass: "border" });
777
- toast.appendChild( panel.root.childNodes[ 0 ] );
778
- }
760
+ if( content )
761
+ {
762
+ content = [].concat( content );
763
+ content.forEach( e => {
764
+ const domNode = e.root ?? e;
765
+ this.root.appendChild( domNode );
766
+ if( e.onPopover )
767
+ {
768
+ e.onPopover();
769
+ }
770
+ } );
771
+ }
779
772
 
780
- const that = this;
773
+ Popover.activeElement = this;
781
774
 
782
- toast.close = function() {
783
- this.dataset[ "closed" ] = true;
784
775
  LX.doAsync( () => {
785
- this.remove();
786
- if( !that.notifications.childElementCount )
787
- {
788
- that.notifications.style.width = "unset";
789
- that.notifications.iWidth = 0;
790
- }
791
- }, 500 );
792
- };
776
+ this._adjustPosition();
793
777
 
794
- if( options.closable ?? true )
795
- {
796
- const closeIcon = LX.makeIcon( "X", { iconClass: "closer" } );
797
- closeIcon.addEventListener( "click", () => {
798
- toast.close();
799
- } );
800
- toast.appendChild( closeIcon );
801
- }
778
+ this.root.focus();
802
779
 
803
- const timeout = options.timeout ?? 3000;
780
+ this._onClick = e => {
781
+ if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
782
+ {
783
+ return;
784
+ }
785
+ this.destroy();
786
+ };
804
787
 
805
- if( timeout != -1 )
806
- {
807
- LX.doAsync( () => {
808
- toast.close();
809
- }, timeout );
788
+ document.body.addEventListener( "mousedown", this._onClick, true );
789
+ document.body.addEventListener( "focusin", this._onClick, true );
790
+ }, 10 );
810
791
  }
811
- }
812
792
 
813
- LX.toast = toast;
814
-
815
- /**
816
- * @method badge
817
- * @param {String} text
818
- * @param {String} className
819
- * @param {Object} options
820
- * style: Style attributes to override
821
- * asElement: Returns the badge as HTMLElement [false]
822
- */
793
+ destroy() {
823
794
 
824
- function badge( text, className, options = {} )
825
- {
826
- const container = document.createElement( "div" );
827
- container.innerHTML = text;
828
- container.className = "lexbadge " + ( className ?? "" );
829
- Object.assign( container.style, options.style ?? {} );
830
- return ( options.asElement ?? false ) ? container : container.outerHTML;
831
- }
795
+ this._trigger.classList.remove( "triggered" );
832
796
 
833
- LX.badge = badge;
797
+ delete this._trigger.active;
834
798
 
835
- /**
836
- * @method makeElement
837
- * @param {String} htmlType
838
- * @param {String} className
839
- * @param {String} innerHTML
840
- * @param {HTMLElement} parent
841
- * @param {Object} overrideStyle
842
- */
799
+ document.body.removeEventListener( "mousedown", this._onClick, true );
800
+ document.body.removeEventListener( "focusin", this._onClick, true );
843
801
 
844
- function makeElement( htmlType, className, innerHTML, parent, overrideStyle = {} )
845
- {
846
- const element = document.createElement( htmlType );
847
- element.className = className ?? "";
848
- element.innerHTML = innerHTML ?? "";
849
- Object.assign( element.style, overrideStyle );
802
+ this.root.remove();
850
803
 
851
- if( parent )
852
- {
853
- if( parent.attach ) // Use attach method if possible
854
- {
855
- parent.attach( element );
856
- }
857
- else // its a native HTMLElement
858
- {
859
- parent.appendChild( element );
860
- }
804
+ Popover.activeElement = null;
861
805
  }
862
806
 
863
- return element;
864
- }
865
-
866
- LX.makeElement = makeElement;
867
-
868
- /**
869
- * @method makeContainer
870
- * @param {Array} size
871
- * @param {String} className
872
- * @param {String} innerHTML
873
- * @param {HTMLElement} parent
874
- * @param {Object} overrideStyle
875
- */
876
-
877
- function makeContainer( size, className, innerHTML, parent, overrideStyle = {} )
878
- {
879
- const container = LX.makeElement( "div", "lexcontainer " + ( className ?? "" ), innerHTML, parent, overrideStyle );
880
- container.style.width = size && size[ 0 ] ? size[ 0 ] : "100%";
881
- container.style.height = size && size[ 1 ] ? size[ 1 ] : "100%";
882
- return container;
883
- }
884
-
885
- LX.makeContainer = makeContainer;
886
-
887
- /**
888
- * @method asTooltip
889
- * @param {HTMLElement} trigger
890
- * @param {String} content
891
- * @param {Object} options
892
- * side: Side of the tooltip
893
- * offset: Tooltip margin offset
894
- * active: Tooltip active by default [true]
895
- */
896
-
897
- function asTooltip( trigger, content, options = {} )
898
- {
899
- console.assert( trigger, "You need a trigger to generate a tooltip!" );
900
-
901
- trigger.dataset[ "disableTooltip" ] = !( options.active ?? true );
902
-
903
- let tooltipDom = null;
807
+ _adjustPosition() {
904
808
 
905
- trigger.addEventListener( "mouseenter", function(e) {
809
+ const position = [ 0, 0 ];
906
810
 
907
- if( trigger.dataset[ "disableTooltip" ] == "true" )
811
+ // Place menu using trigger position and user options
908
812
  {
909
- return;
910
- }
911
-
912
- LX.root.querySelectorAll( ".lextooltip" ).forEach( e => e.remove() );
913
-
914
- tooltipDom = document.createElement( "div" );
915
- tooltipDom.className = "lextooltip";
916
- tooltipDom.innerHTML = content;
917
-
918
- LX.doAsync( () => {
813
+ const rect = this._trigger.getBoundingClientRect();
919
814
 
920
- const position = [ 0, 0 ];
921
- const rect = this.getBoundingClientRect();
922
- const offset = options.offset ?? 6;
923
815
  let alignWidth = true;
924
816
 
925
- switch( options.side ?? "top" )
817
+ switch( this.side )
926
818
  {
927
819
  case "left":
928
- position[ 0 ] += ( rect.x - tooltipDom.offsetWidth - offset );
820
+ position[ 0 ] += ( rect.x - this.root.offsetWidth );
929
821
  alignWidth = false;
930
822
  break;
931
823
  case "right":
932
- position[ 0 ] += ( rect.x + rect.width + offset );
824
+ position[ 0 ] += ( rect.x + rect.width );
933
825
  alignWidth = false;
934
826
  break;
935
827
  case "top":
936
- position[ 1 ] += ( rect.y - tooltipDom.offsetHeight - offset );
828
+ position[ 1 ] += ( rect.y - this.root.offsetHeight );
937
829
  alignWidth = true;
938
830
  break;
939
831
  case "bottom":
940
- position[ 1 ] += ( rect.y + rect.height + offset );
832
+ position[ 1 ] += ( rect.y + rect.height );
941
833
  alignWidth = true;
942
834
  break;
943
835
  }
944
836
 
945
- if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - tooltipDom.offsetWidth * 0.5; }
946
- else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - tooltipDom.offsetHeight * 0.5; }
947
-
948
- // Avoid collisions
949
- position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - tooltipDom.offsetWidth - 4 );
950
- position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - tooltipDom.offsetHeight - 4 );
951
-
952
- tooltipDom.style.left = `${ position[ 0 ] }px`;
953
- tooltipDom.style.top = `${ position[ 1 ] }px`;
954
- } );
955
-
956
- LX.root.appendChild( tooltipDom );
957
- } );
837
+ switch( this.align )
838
+ {
839
+ case "start":
840
+ if( alignWidth ) { position[ 0 ] += rect.x; }
841
+ else { position[ 1 ] += rect.y; }
842
+ break;
843
+ case "center":
844
+ if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - this.root.offsetWidth * 0.5; }
845
+ else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - this.root.offsetHeight * 0.5; }
846
+ break;
847
+ case "end":
848
+ if( alignWidth ) { position[ 0 ] += rect.x - this.root.offsetWidth + rect.width; }
849
+ else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
850
+ break;
851
+ }
852
+ }
958
853
 
959
- trigger.addEventListener( "mouseleave", function(e) {
960
- if( tooltipDom )
854
+ if( this.avoidCollisions )
961
855
  {
962
- tooltipDom.remove();
856
+ position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - this.root.offsetWidth - this._windowPadding );
857
+ position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - this.root.offsetHeight - this._windowPadding );
963
858
  }
964
- } );
859
+
860
+ this.root.style.left = `${ position[ 0 ] }px`;
861
+ this.root.style.top = `${ position[ 1 ] }px`;
862
+ }
965
863
  }
864
+ LX.Popover = Popover;
966
865
 
967
- LX.asTooltip = asTooltip;
866
+ /**
867
+ * @class Sheet
868
+ */
968
869
 
969
- /*
970
- * Events and Signals
971
- */
870
+ class Sheet {
972
871
 
973
- class IEvent {
872
+ constructor( size, content, options = {} ) {
974
873
 
975
- constructor( name, value, domEvent ) {
976
- this.name = name;
977
- this.value = value;
978
- this.domEvent = domEvent;
979
- }
980
- }
981
- LX.IEvent = IEvent;
874
+ this.side = options.side ?? "left";
982
875
 
983
- class TreeEvent {
876
+ this.root = document.createElement( "div" );
877
+ this.root.dataset["side"] = this.side;
878
+ this.root.tabIndex = "1";
879
+ this.root.role = "dialog";
880
+ this.root.className = "lexsheet fixed z-100 bg-primary";
881
+ LX.root.appendChild( this.root );
984
882
 
985
- static NONE = 0;
986
- static NODE_SELECTED = 1;
987
- static NODE_DELETED = 2;
988
- static NODE_DBLCLICKED = 3;
989
- static NODE_CONTEXTMENU = 4;
990
- static NODE_DRAGGED = 5;
991
- static NODE_RENAMED = 6;
992
- static NODE_VISIBILITY = 7;
993
- static NODE_CARETCHANGED = 8;
883
+ this.root.addEventListener( "keydown", (e) => {
884
+ if( e.key == "Escape" )
885
+ {
886
+ e.preventDefault();
887
+ e.stopPropagation();
888
+ this.destroy();
889
+ }
890
+ } );
994
891
 
995
- constructor( type, node, value ) {
996
- this.type = type || TreeEvent.NONE;
997
- this.node = node;
998
- this.value = value;
999
- this.multiple = false; // Multiple selection
1000
- this.panel = null;
1001
- }
1002
-
1003
- string() {
1004
- switch( this.type )
892
+ if( content )
1005
893
  {
1006
- case TreeEvent.NONE: return "tree_event_none";
1007
- case TreeEvent.NODE_SELECTED: return "tree_event_selected";
1008
- case TreeEvent.NODE_DELETED: return "tree_event_deleted";
1009
- case TreeEvent.NODE_DBLCLICKED: return "tree_event_dblclick";
1010
- case TreeEvent.NODE_CONTEXTMENU: return "tree_event_contextmenu";
1011
- case TreeEvent.NODE_DRAGGED: return "tree_event_dragged";
1012
- case TreeEvent.NODE_RENAMED: return "tree_event_renamed";
1013
- case TreeEvent.NODE_VISIBILITY: return "tree_event_visibility";
1014
- case TreeEvent.NODE_CARETCHANGED: return "tree_event_caretchanged";
894
+ content = [].concat( content );
895
+ content.forEach( e => {
896
+ const domNode = e.root ?? e;
897
+ this.root.appendChild( domNode );
898
+ if( e.onSheet )
899
+ {
900
+ e.onSheet();
901
+ }
902
+ } );
1015
903
  }
1016
- }
1017
- }
1018
- LX.TreeEvent = TreeEvent;
1019
904
 
1020
- function emit( signalName, value, options = {} )
1021
- {
1022
- const data = LX.signals[ signalName ];
905
+ LX.doAsync( () => {
1023
906
 
1024
- if( !data )
1025
- {
1026
- return;
1027
- }
907
+ LX.modal.toggle( false );
1028
908
 
1029
- const target = options.target;
909
+ switch( this.side )
910
+ {
911
+ case "left":
912
+ this.root.style.left = 0;
913
+ this.root.style.width = size;
914
+ this.root.style.height = "100%";
915
+ break;
916
+ case "right":
917
+ this.root.style.right = 0;
918
+ this.root.style.width = size;
919
+ this.root.style.height = "100%";
920
+ break;
921
+ case "top":
922
+ this.root.style.top = 0;
923
+ this.root.style.width = "100%";
924
+ this.root.style.height = size;
925
+ break;
926
+ case "bottom":
927
+ this.root.style.bottom = 0;
928
+ this.root.style.width = "100%";
929
+ this.root.style.height = size;
930
+ break;
931
+ }
1030
932
 
1031
- if( target )
1032
- {
1033
- if( target[ signalName ])
1034
- {
1035
- target[ signalName ].call( target, value );
1036
- }
933
+ this.root.focus();
1037
934
 
1038
- return;
1039
- }
935
+ this._onClick = e => {
936
+ if( e.target && ( this.root.contains( e.target ) ) )
937
+ {
938
+ return;
939
+ }
940
+ this.destroy();
941
+ };
1040
942
 
1041
- for( let obj of data )
1042
- {
1043
- if( obj instanceof LX.Widget )
1044
- {
1045
- obj.set( value, options.skipCallback ?? true );
1046
- }
1047
- else if( obj.constructor === Function )
1048
- {
1049
- const fn = obj;
1050
- fn( null, value );
1051
- }
1052
- else
1053
- {
1054
- // This is an element
1055
- const fn = obj[ signalName ];
1056
- console.assert( fn, `No callback registered with _${ signalName }_ signal` );
1057
- fn.bind( obj )( value );
1058
- }
943
+ document.body.addEventListener( "mousedown", this._onClick, true );
944
+ document.body.addEventListener( "focusin", this._onClick, true );
945
+ }, 10 );
1059
946
  }
1060
- }
1061
947
 
1062
- LX.emit = emit;
948
+ destroy() {
1063
949
 
1064
- function addSignal( name, obj, callback )
1065
- {
1066
- obj[ name ] = callback;
950
+ document.body.removeEventListener( "mousedown", this._onClick, true );
951
+ document.body.removeEventListener( "focusin", this._onClick, true );
1067
952
 
1068
- if( !LX.signals[ name ] )
1069
- {
1070
- LX.signals[ name ] = [];
1071
- }
953
+ this.root.remove();
1072
954
 
1073
- if( LX.signals[ name ].indexOf( obj ) > -1 )
1074
- {
1075
- return;
955
+ LX.modal.toggle( true );
1076
956
  }
1077
-
1078
- LX.signals[ name ].push( obj );
1079
957
  }
1080
-
1081
- LX.addSignal = addSignal;
1082
-
1083
- /*
1084
- * DOM Elements
1085
- */
958
+ LX.Sheet = Sheet;
1086
959
 
1087
960
  /**
1088
- * @class Popover
961
+ * @class DropdownMenu
1089
962
  */
1090
963
 
1091
- class Popover {
964
+ class DropdownMenu {
1092
965
 
1093
- static activeElement = false;
966
+ static currentMenu = false;
1094
967
 
1095
- constructor( trigger, content, options = {} ) {
968
+ constructor( trigger, items, options = {} ) {
1096
969
 
1097
- console.assert( trigger, "Popover needs a DOM element as trigger!" );
970
+ console.assert( trigger, "DropdownMenu needs a DOM element as trigger!" );
1098
971
 
1099
- if( Popover.activeElement )
972
+ if( DropdownMenu.currentMenu || !items?.length )
1100
973
  {
1101
- Popover.activeElement.destroy();
974
+ DropdownMenu.currentMenu.destroy();
975
+ this.invalid = true;
1102
976
  return;
1103
977
  }
1104
978
 
1105
979
  this._trigger = trigger;
1106
980
  trigger.classList.add( "triggered" );
1107
- trigger.active = this;
981
+ trigger.ddm = this;
982
+
983
+ this._items = items;
1108
984
 
1109
985
  this._windowPadding = 4;
1110
986
  this.side = options.side ?? "bottom";
1111
987
  this.align = options.align ?? "center";
1112
988
  this.avoidCollisions = options.avoidCollisions ?? true;
989
+ this.onBlur = options.onBlur;
990
+ this.inPlace = false;
1113
991
 
1114
992
  this.root = document.createElement( "div" );
993
+ this.root.id = "root";
1115
994
  this.root.dataset["side"] = this.side;
1116
995
  this.root.tabIndex = "1";
1117
- this.root.className = "lexpopover";
996
+ this.root.className = "lexdropdownmenu";
1118
997
  LX.root.appendChild( this.root );
1119
998
 
1120
- this.root.addEventListener( "keydown", (e) => {
1121
- if( e.key == "Escape" )
1122
- {
1123
- e.preventDefault();
1124
- e.stopPropagation();
1125
- this.destroy();
1126
- }
1127
- } );
1128
-
1129
- if( content )
1130
- {
1131
- content = [].concat( content );
1132
- content.forEach( e => {
1133
- const domNode = e.root ?? e;
1134
- this.root.appendChild( domNode );
1135
- if( e.onPopover )
1136
- {
1137
- e.onPopover();
1138
- }
1139
- } );
1140
- }
999
+ this._create( this._items );
1141
1000
 
1142
- Popover.activeElement = this;
1001
+ DropdownMenu.currentMenu = this;
1143
1002
 
1144
1003
  LX.doAsync( () => {
1145
1004
  this._adjustPosition();
@@ -1147,11 +1006,14 @@ class Popover {
1147
1006
  this.root.focus();
1148
1007
 
1149
1008
  this._onClick = e => {
1150
- if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
1009
+
1010
+ // Check if the click is inside a menu or on the trigger
1011
+ if( e.target && ( e.target.closest( ".lexdropdownmenu" ) != undefined || e.target == this._trigger ) )
1151
1012
  {
1152
1013
  return;
1153
1014
  }
1154
- this.destroy();
1015
+
1016
+ this.destroy( true );
1155
1017
  };
1156
1018
 
1157
1019
  document.body.addEventListener( "mousedown", this._onClick, true );
@@ -1159,298 +1021,67 @@ class Popover {
1159
1021
  }, 10 );
1160
1022
  }
1161
1023
 
1162
- destroy() {
1024
+ destroy( blurEvent ) {
1163
1025
 
1164
1026
  this._trigger.classList.remove( "triggered" );
1165
1027
 
1166
- delete this._trigger.active;
1028
+ delete this._trigger.ddm;
1167
1029
 
1168
1030
  document.body.removeEventListener( "mousedown", this._onClick, true );
1169
1031
  document.body.removeEventListener( "focusin", this._onClick, true );
1170
1032
 
1171
- this.root.remove();
1033
+ LX.root.querySelectorAll( ".lexdropdownmenu" ).forEach( m => { m.remove(); } );
1172
1034
 
1173
- Popover.activeElement = null;
1174
- }
1035
+ DropdownMenu.currentMenu = null;
1175
1036
 
1176
- _adjustPosition() {
1037
+ if( blurEvent && this.onBlur )
1038
+ {
1039
+ this.onBlur();
1040
+ }
1041
+ }
1177
1042
 
1178
- const position = [ 0, 0 ];
1043
+ _create( items, parentDom ) {
1179
1044
 
1180
- // Place menu using trigger position and user options
1045
+ if( !parentDom )
1181
1046
  {
1182
- const rect = this._trigger.getBoundingClientRect();
1047
+ parentDom = this.root;
1048
+ }
1049
+ else
1050
+ {
1051
+ const parentRect = parentDom.getBoundingClientRect();
1183
1052
 
1184
- let alignWidth = true;
1053
+ let newParent = document.createElement( "div" );
1054
+ newParent.tabIndex = "1";
1055
+ newParent.className = "lexdropdownmenu";
1056
+ newParent.dataset["id"] = parentDom.dataset["id"];
1057
+ newParent.dataset["side"] = "right"; // submenus always come from the right
1058
+ LX.root.appendChild( newParent );
1185
1059
 
1186
- switch( this.side )
1060
+ newParent.currentParent = parentDom;
1061
+ parentDom = newParent;
1062
+
1063
+ LX.doAsync( () => {
1064
+ const position = [ parentRect.x + parentRect.width, parentRect.y ];
1065
+
1066
+ if( this.avoidCollisions )
1067
+ {
1068
+ position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - newParent.offsetWidth - this._windowPadding );
1069
+ position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - newParent.offsetHeight - this._windowPadding );
1070
+ }
1071
+
1072
+ newParent.style.left = `${ position[ 0 ] }px`;
1073
+ newParent.style.top = `${ position[ 1 ] }px`;
1074
+ }, 10 );
1075
+ }
1076
+
1077
+ let applyIconPadding = items.filter( i => { return ( i?.icon != undefined ) || ( i?.checked != undefined ) } ).length > 0;
1078
+
1079
+ for( let item of items )
1080
+ {
1081
+ if( !item )
1187
1082
  {
1188
- case "left":
1189
- position[ 0 ] += ( rect.x - this.root.offsetWidth );
1190
- alignWidth = false;
1191
- break;
1192
- case "right":
1193
- position[ 0 ] += ( rect.x + rect.width );
1194
- alignWidth = false;
1195
- break;
1196
- case "top":
1197
- position[ 1 ] += ( rect.y - this.root.offsetHeight );
1198
- alignWidth = true;
1199
- break;
1200
- case "bottom":
1201
- position[ 1 ] += ( rect.y + rect.height );
1202
- alignWidth = true;
1203
- break;
1204
- }
1205
-
1206
- switch( this.align )
1207
- {
1208
- case "start":
1209
- if( alignWidth ) { position[ 0 ] += rect.x; }
1210
- else { position[ 1 ] += rect.y; }
1211
- break;
1212
- case "center":
1213
- if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - this.root.offsetWidth * 0.5; }
1214
- else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - this.root.offsetHeight * 0.5; }
1215
- break;
1216
- case "end":
1217
- if( alignWidth ) { position[ 0 ] += rect.x - this.root.offsetWidth + rect.width; }
1218
- else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
1219
- break;
1220
- }
1221
- }
1222
-
1223
- if( this.avoidCollisions )
1224
- {
1225
- position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - this.root.offsetWidth - this._windowPadding );
1226
- position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - this.root.offsetHeight - this._windowPadding );
1227
- }
1228
-
1229
- this.root.style.left = `${ position[ 0 ] }px`;
1230
- this.root.style.top = `${ position[ 1 ] }px`;
1231
- }
1232
- }
1233
- LX.Popover = Popover;
1234
-
1235
- /**
1236
- * @class Sheet
1237
- */
1238
-
1239
- class Sheet {
1240
-
1241
- constructor( size, content, options = {} ) {
1242
-
1243
- this.side = options.side ?? "left";
1244
-
1245
- this.root = document.createElement( "div" );
1246
- this.root.dataset["side"] = this.side;
1247
- this.root.tabIndex = "1";
1248
- this.root.role = "dialog";
1249
- this.root.className = "lexsheet fixed z-100 bg-primary";
1250
- LX.root.appendChild( this.root );
1251
-
1252
- this.root.addEventListener( "keydown", (e) => {
1253
- if( e.key == "Escape" )
1254
- {
1255
- e.preventDefault();
1256
- e.stopPropagation();
1257
- this.destroy();
1258
- }
1259
- } );
1260
-
1261
- if( content )
1262
- {
1263
- content = [].concat( content );
1264
- content.forEach( e => {
1265
- const domNode = e.root ?? e;
1266
- this.root.appendChild( domNode );
1267
- if( e.onSheet )
1268
- {
1269
- e.onSheet();
1270
- }
1271
- } );
1272
- }
1273
-
1274
- LX.doAsync( () => {
1275
-
1276
- LX.modal.toggle( false );
1277
-
1278
- switch( this.side )
1279
- {
1280
- case "left":
1281
- this.root.style.left = 0;
1282
- this.root.style.width = size;
1283
- this.root.style.height = "100%";
1284
- break;
1285
- case "right":
1286
- this.root.style.right = 0;
1287
- this.root.style.width = size;
1288
- this.root.style.height = "100%";
1289
- break;
1290
- case "top":
1291
- this.root.style.top = 0;
1292
- this.root.style.width = "100%";
1293
- this.root.style.height = size;
1294
- break;
1295
- case "bottom":
1296
- this.root.style.bottom = 0;
1297
- this.root.style.width = "100%";
1298
- this.root.style.height = size;
1299
- break;
1300
- }
1301
-
1302
- this.root.focus();
1303
-
1304
- this._onClick = e => {
1305
- if( e.target && ( this.root.contains( e.target ) ) )
1306
- {
1307
- return;
1308
- }
1309
- this.destroy();
1310
- };
1311
-
1312
- document.body.addEventListener( "mousedown", this._onClick, true );
1313
- document.body.addEventListener( "focusin", this._onClick, true );
1314
- }, 10 );
1315
- }
1316
-
1317
- destroy() {
1318
-
1319
- document.body.removeEventListener( "mousedown", this._onClick, true );
1320
- document.body.removeEventListener( "focusin", this._onClick, true );
1321
-
1322
- this.root.remove();
1323
-
1324
- LX.modal.toggle( true );
1325
- }
1326
- }
1327
- LX.Sheet = Sheet;
1328
-
1329
- /**
1330
- * @class DropdownMenu
1331
- */
1332
-
1333
- class DropdownMenu {
1334
-
1335
- static currentMenu = false;
1336
-
1337
- constructor( trigger, items, options = {} ) {
1338
-
1339
- console.assert( trigger, "DropdownMenu needs a DOM element as trigger!" );
1340
-
1341
- if( DropdownMenu.currentMenu || !items?.length )
1342
- {
1343
- DropdownMenu.currentMenu.destroy();
1344
- this.invalid = true;
1345
- return;
1346
- }
1347
-
1348
- this._trigger = trigger;
1349
- trigger.classList.add( "triggered" );
1350
- trigger.ddm = this;
1351
-
1352
- this._items = items;
1353
-
1354
- this._windowPadding = 4;
1355
- this.side = options.side ?? "bottom";
1356
- this.align = options.align ?? "center";
1357
- this.avoidCollisions = options.avoidCollisions ?? true;
1358
- this.onBlur = options.onBlur;
1359
- this.inPlace = false;
1360
-
1361
- this.root = document.createElement( "div" );
1362
- this.root.id = "root";
1363
- this.root.dataset["side"] = this.side;
1364
- this.root.tabIndex = "1";
1365
- this.root.className = "lexdropdownmenu";
1366
- LX.root.appendChild( this.root );
1367
-
1368
- this._create( this._items );
1369
-
1370
- DropdownMenu.currentMenu = this;
1371
-
1372
- LX.doAsync( () => {
1373
- this._adjustPosition();
1374
-
1375
- this.root.focus();
1376
-
1377
- this._onClick = e => {
1378
-
1379
- // Check if the click is inside a menu or on the trigger
1380
- if( e.target && ( e.target.closest( ".lexdropdownmenu" ) != undefined || e.target == this._trigger ) )
1381
- {
1382
- return;
1383
- }
1384
-
1385
- this.destroy( true );
1386
- };
1387
-
1388
- document.body.addEventListener( "mousedown", this._onClick, true );
1389
- document.body.addEventListener( "focusin", this._onClick, true );
1390
- }, 10 );
1391
- }
1392
-
1393
- destroy( blurEvent ) {
1394
-
1395
- this._trigger.classList.remove( "triggered" );
1396
-
1397
- delete this._trigger.ddm;
1398
-
1399
- document.body.removeEventListener( "mousedown", this._onClick, true );
1400
- document.body.removeEventListener( "focusin", this._onClick, true );
1401
-
1402
- LX.root.querySelectorAll( ".lexdropdownmenu" ).forEach( m => { m.remove(); } );
1403
-
1404
- DropdownMenu.currentMenu = null;
1405
-
1406
- if( blurEvent && this.onBlur )
1407
- {
1408
- this.onBlur();
1409
- }
1410
- }
1411
-
1412
- _create( items, parentDom ) {
1413
-
1414
- if( !parentDom )
1415
- {
1416
- parentDom = this.root;
1417
- }
1418
- else
1419
- {
1420
- const parentRect = parentDom.getBoundingClientRect();
1421
-
1422
- let newParent = document.createElement( "div" );
1423
- newParent.tabIndex = "1";
1424
- newParent.className = "lexdropdownmenu";
1425
- newParent.dataset["id"] = parentDom.dataset["id"];
1426
- newParent.dataset["side"] = "right"; // submenus always come from the right
1427
- LX.root.appendChild( newParent );
1428
-
1429
- newParent.currentParent = parentDom;
1430
- parentDom = newParent;
1431
-
1432
- LX.doAsync( () => {
1433
- const position = [ parentRect.x + parentRect.width, parentRect.y ];
1434
-
1435
- if( this.avoidCollisions )
1436
- {
1437
- position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - newParent.offsetWidth - this._windowPadding );
1438
- position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - newParent.offsetHeight - this._windowPadding );
1439
- }
1440
-
1441
- newParent.style.left = `${ position[ 0 ] }px`;
1442
- newParent.style.top = `${ position[ 1 ] }px`;
1443
- }, 10 );
1444
- }
1445
-
1446
- let applyIconPadding = items.filter( i => { return ( i?.icon != undefined ) || ( i?.checked != undefined ) } ).length > 0;
1447
-
1448
- for( let item of items )
1449
- {
1450
- if( !item )
1451
- {
1452
- this._addSeparator( parentDom );
1453
- continue;
1083
+ this._addSeparator( parentDom );
1084
+ continue;
1454
1085
  }
1455
1086
 
1456
1087
  const key = item.name ?? item;
@@ -1520,6 +1151,7 @@ class DropdownMenu {
1520
1151
  {
1521
1152
  const checkbox = new LX.Checkbox( pKey + "_entryChecked", item.checked, (v) => {
1522
1153
  const f = item[ 'callback' ];
1154
+ item.checked = v;
1523
1155
  if( f )
1524
1156
  {
1525
1157
  f.call( this, key, v, menuItem );
@@ -2254,13 +1886,6 @@ class Calendar {
2254
1886
 
2255
1887
  LX.Calendar = Calendar;
2256
1888
 
2257
- function flushCss(element) {
2258
- // By reading the offsetHeight property, we are forcing
2259
- // the browser to flush the pending CSS changes (which it
2260
- // does to ensure the value obtained is accurate).
2261
- element.offsetHeight;
2262
- }
2263
-
2264
1889
  /**
2265
1890
  * @class Tabs
2266
1891
  */
@@ -2386,7 +2011,7 @@ class Tabs {
2386
2011
  this.thumb.style.transition = "none";
2387
2012
  this.thumb.style.transform = "translate( " + ( tabEl.childIndex * tabEl.offsetWidth ) + "px )";
2388
2013
  this.thumb.style.width = ( tabEl.offsetWidth ) + "px";
2389
- flushCss( this.thumb );
2014
+ LX.flushCss( this.thumb );
2390
2015
  this.thumb.style.transition = transition;
2391
2016
  });
2392
2017
 
@@ -3500,7 +3125,7 @@ class CanvasCurve {
3500
3125
  }
3501
3126
  else
3502
3127
  {
3503
- LX.UTILS.drawSpline( ctx, values, element.smooth );
3128
+ LX.drawSpline( ctx, values, element.smooth );
3504
3129
  }
3505
3130
 
3506
3131
  // Draw points
@@ -4442,254 +4067,6 @@ class CanvasMap2D {
4442
4067
 
4443
4068
  LX.CanvasMap2D = CanvasMap2D;
4444
4069
 
4445
- /*
4446
- * Requests
4447
- */
4448
-
4449
- Object.assign(LX, {
4450
-
4451
- /**
4452
- * Request file from url (it could be a binary, text, etc.). If you want a simplied version use
4453
- * @method request
4454
- * @param {Object} request object with all the parameters like data (for sending forms), dataType, success, error
4455
- * @param {Function} on_complete
4456
- **/
4457
- request( request ) {
4458
-
4459
- var dataType = request.dataType || "text";
4460
- if(dataType == "json") //parse it locally
4461
- dataType = "text";
4462
- else if(dataType == "xml") //parse it locally
4463
- dataType = "text";
4464
- else if (dataType == "binary")
4465
- {
4466
- //request.mimeType = "text/plain; charset=x-user-defined";
4467
- dataType = "arraybuffer";
4468
- request.mimeType = "application/octet-stream";
4469
- }
4470
-
4471
- //regular case, use AJAX call
4472
- var xhr = new XMLHttpRequest();
4473
- xhr.open( request.data ? 'POST' : 'GET', request.url, true);
4474
- if(dataType)
4475
- xhr.responseType = dataType;
4476
- if (request.mimeType)
4477
- xhr.overrideMimeType( request.mimeType );
4478
- if( request.nocache )
4479
- xhr.setRequestHeader('Cache-Control', 'no-cache');
4480
-
4481
- xhr.onload = function(load)
4482
- {
4483
- var response = this.response;
4484
- if( this.status != 200)
4485
- {
4486
- var err = "Error " + this.status;
4487
- if(request.error)
4488
- request.error(err);
4489
- return;
4490
- }
4491
-
4492
- if(request.dataType == "json") //chrome doesnt support json format
4493
- {
4494
- try
4495
- {
4496
- response = JSON.parse(response);
4497
- }
4498
- catch (err)
4499
- {
4500
- if(request.error)
4501
- request.error(err);
4502
- else
4503
- throw err;
4504
- }
4505
- }
4506
- else if(request.dataType == "xml")
4507
- {
4508
- try
4509
- {
4510
- var xmlparser = new DOMParser();
4511
- response = xmlparser.parseFromString(response,"text/xml");
4512
- }
4513
- catch (err)
4514
- {
4515
- if(request.error)
4516
- request.error(err);
4517
- else
4518
- throw err;
4519
- }
4520
- }
4521
- if(request.success)
4522
- request.success.call(this, response, this);
4523
- };
4524
- xhr.onerror = function(err) {
4525
- if(request.error)
4526
- request.error(err);
4527
- };
4528
-
4529
- var data = new FormData();
4530
- if( request.data )
4531
- {
4532
- for( var i in request.data)
4533
- data.append(i,request.data[ i ]);
4534
- }
4535
-
4536
- xhr.send( data );
4537
- return xhr;
4538
- },
4539
-
4540
- /**
4541
- * Request file from url
4542
- * @method requestText
4543
- * @param {String} url
4544
- * @param {Function} onComplete
4545
- * @param {Function} onError
4546
- **/
4547
- requestText( url, onComplete, onError ) {
4548
- return this.request({ url: url, dataType:"text", success: onComplete, error: onError });
4549
- },
4550
-
4551
- /**
4552
- * Request file from url
4553
- * @method requestJSON
4554
- * @param {String} url
4555
- * @param {Function} onComplete
4556
- * @param {Function} onError
4557
- **/
4558
- requestJSON( url, onComplete, onError ) {
4559
- return this.request({ url: url, dataType:"json", success: onComplete, error: onError });
4560
- },
4561
-
4562
- /**
4563
- * Request binary file from url
4564
- * @method requestBinary
4565
- * @param {String} url
4566
- * @param {Function} onComplete
4567
- * @param {Function} onError
4568
- **/
4569
- requestBinary( url, onComplete, onError ) {
4570
- return this.request({ url: url, dataType:"binary", success: onComplete, error: onError });
4571
- },
4572
-
4573
- /**
4574
- * Request script and inserts it in the DOM
4575
- * @method requireScript
4576
- * @param {String|Array} url the url of the script or an array containing several urls
4577
- * @param {Function} onComplete
4578
- * @param {Function} onError
4579
- * @param {Function} onProgress (if several files are required, onProgress is called after every file is added to the DOM)
4580
- **/
4581
- requireScript( url, onComplete, onError, onProgress, version ) {
4582
-
4583
- if(!url)
4584
- throw("invalid URL");
4585
-
4586
- if( url.constructor === String )
4587
- url = [url];
4588
-
4589
- var total = url.length;
4590
- var loaded_scripts = [];
4591
-
4592
- for( var i in url)
4593
- {
4594
- var script = document.createElement('script');
4595
- script.num = i;
4596
- script.type = 'text/javascript';
4597
- script.src = url[ i ] + ( version ? "?version=" + version : "" );
4598
- script.original_src = url[ i ];
4599
- script.async = false;
4600
- script.onload = function( e ) {
4601
- total--;
4602
- loaded_scripts.push(this);
4603
- if(total)
4604
- {
4605
- if( onProgress )
4606
- {
4607
- onProgress( this.original_src, this.num );
4608
- }
4609
- }
4610
- else if(onComplete)
4611
- onComplete( loaded_scripts );
4612
- };
4613
- if(onError)
4614
- script.onerror = function(err) {
4615
- onError(err, this.original_src, this.num );
4616
- };
4617
- document.getElementsByTagName('head')[ 0 ].appendChild(script);
4618
- }
4619
- },
4620
-
4621
- loadScriptSync( url ) {
4622
- return new Promise((resolve, reject) => {
4623
- const script = document.createElement( "script" );
4624
- script.src = url;
4625
- script.async = false;
4626
- script.onload = () => resolve();
4627
- script.onerror = () => reject(new Error(`Failed to load ${url}`));
4628
- document.head.appendChild( script );
4629
- });
4630
- },
4631
-
4632
- downloadURL( url, filename ) {
4633
-
4634
- const fr = new FileReader();
4635
-
4636
- const _download = function(_url) {
4637
- var link = document.createElement('a');
4638
- link.href = _url;
4639
- link.download = filename;
4640
- document.body.appendChild(link);
4641
- link.click();
4642
- document.body.removeChild(link);
4643
- };
4644
-
4645
- if( url.includes('http') )
4646
- {
4647
- LX.request({ url: url, dataType: 'blob', success: (f) => {
4648
- fr.readAsDataURL( f );
4649
- fr.onload = e => {
4650
- _download(e.currentTarget.result);
4651
- };
4652
- } });
4653
- }else
4654
- {
4655
- _download(url);
4656
- }
4657
-
4658
- },
4659
-
4660
- downloadFile: function( filename, data, dataType ) {
4661
- if(!data)
4662
- {
4663
- console.warn("No file provided to download");
4664
- return;
4665
- }
4666
-
4667
- if(!dataType)
4668
- {
4669
- if(data.constructor === String )
4670
- dataType = 'text/plain';
4671
- else
4672
- dataType = 'application/octet-stream';
4673
- }
4674
-
4675
- var file = null;
4676
- if(data.constructor !== File && data.constructor !== Blob)
4677
- file = new Blob( [ data ], {type : dataType});
4678
- else
4679
- file = data;
4680
-
4681
- var url = URL.createObjectURL( file );
4682
- var element = document.createElement("a");
4683
- element.setAttribute('href', url);
4684
- element.setAttribute('download', filename );
4685
- element.style.display = 'none';
4686
- document.body.appendChild(element);
4687
- element.click();
4688
- document.body.removeChild(element);
4689
- setTimeout( function(){ URL.revokeObjectURL( url ); }, 1000*60 ); //wait one minute to revoke url
4690
- }
4691
- });
4692
-
4693
4070
  Object.defineProperty(String.prototype, 'lastChar', {
4694
4071
  get: function() { return this[ this.length - 1 ]; },
4695
4072
  enumerable: true,
@@ -4967,19 +4344,43 @@ function doAsync( fn, ms ) {
4967
4344
  LX.doAsync = doAsync;
4968
4345
 
4969
4346
  /**
4970
- * @method getSupportedDOMName
4971
- * @description Convert a text string to a valid DOM name
4972
- * @param {String} text Original text
4347
+ * @method flushCss
4348
+ * @description By reading the offsetHeight property, we are forcing the browser to flush
4349
+ * the pending CSS changes (which it does to ensure the value obtained is accurate).
4350
+ * @param {HTMLElement} element
4973
4351
  */
4974
- function getSupportedDOMName( text )
4352
+ function flushCss( element )
4975
4353
  {
4976
- console.assert( typeof text == "string", "getSupportedDOMName: Text is not a string!" );
4977
-
4978
- let name = text.trim();
4354
+ element.offsetHeight;
4355
+ }
4979
4356
 
4980
- // Replace specific known symbols
4981
- name = name.replace( /@/g, '_at_' ).replace( /\+/g, '_plus_' ).replace( /\./g, '_dot_' );
4982
- name = name.replace( /[^a-zA-Z0-9_-]/g, '_' );
4357
+ LX.flushCss = flushCss;
4358
+
4359
+ /**
4360
+ * @method deleteElement
4361
+ * @param {HTMLElement} element
4362
+ */
4363
+ function deleteElement( element )
4364
+ {
4365
+ if( element !== undefined ) element.remove();
4366
+ }
4367
+
4368
+ LX.deleteElement = deleteElement;
4369
+
4370
+ /**
4371
+ * @method getSupportedDOMName
4372
+ * @description Convert a text string to a valid DOM name
4373
+ * @param {String} text Original text
4374
+ */
4375
+ function getSupportedDOMName( text )
4376
+ {
4377
+ console.assert( typeof text == "string", "getSupportedDOMName: Text is not a string!" );
4378
+
4379
+ let name = text.trim();
4380
+
4381
+ // Replace specific known symbols
4382
+ name = name.replace( /@/g, '_at_' ).replace( /\+/g, '_plus_' ).replace( /\./g, '_dot_' );
4383
+ name = name.replace( /[^a-zA-Z0-9_-]/g, '_' );
4983
4384
 
4984
4385
  // prefix with an underscore if needed
4985
4386
  if( /^[0-9]/.test( name ) )
@@ -5036,9 +4437,13 @@ LX.stripHTML = stripHTML;
5036
4437
  * @param {Number|String} size
5037
4438
  * @param {Number} total
5038
4439
  */
5039
- const parsePixelSize = ( size, total ) => {
5040
-
5041
- if( size.constructor === Number ) { return size; } // Assuming pixels..
4440
+ function parsePixelSize( size, total )
4441
+ {
4442
+ // Assuming pixels..
4443
+ if( size.constructor === Number )
4444
+ {
4445
+ return size;
4446
+ }
5042
4447
 
5043
4448
  if( size.constructor === String )
5044
4449
  {
@@ -5076,7 +4481,7 @@ const parsePixelSize = ( size, total ) => {
5076
4481
  }
5077
4482
 
5078
4483
  throw( "Bad size format!" );
5079
- };
4484
+ }
5080
4485
 
5081
4486
  LX.parsePixelSize = parsePixelSize;
5082
4487
 
@@ -5339,7 +4744,7 @@ function measureRealWidth( value, paddingPlusMargin = 8 )
5339
4744
  i.innerHTML = value;
5340
4745
  document.body.appendChild( i );
5341
4746
  var rect = i.getBoundingClientRect();
5342
- LX.UTILS.deleteElement( i );
4747
+ LX.deleteElement( i );
5343
4748
  return rect.width + paddingPlusMargin;
5344
4749
  }
5345
4750
 
@@ -5761,206 +5166,852 @@ function makeIcon( iconName, options = { } )
5761
5166
  svg.classList.add( c );
5762
5167
  } );
5763
5168
 
5764
- const attrs = data[ 5 ].svgAttributes;
5765
- attrs?.split( ' ' ).forEach( attr => {
5766
- const t = attr.split( '=' );
5767
- svg.setAttribute( t[ 0 ], t[ 1 ] );
5768
- } );
5769
- }
5169
+ const attrs = data[ 5 ].svgAttributes;
5170
+ attrs?.split( ' ' ).forEach( attr => {
5171
+ const t = attr.split( '=' );
5172
+ svg.setAttribute( t[ 0 ], t[ 1 ] );
5173
+ } );
5174
+ }
5175
+
5176
+ const path = document.createElement( "path" );
5177
+ path.setAttribute( "fill", "currentColor" );
5178
+ path.setAttribute( "d", data[ 4 ] );
5179
+ svg.appendChild( path );
5180
+
5181
+ if( data[ 5 ] )
5182
+ {
5183
+ const classes = data[ 5 ].pathClass;
5184
+ classes?.split( ' ' ).forEach( c => {
5185
+ path.classList.add( c );
5186
+ } );
5187
+
5188
+ const attrs = data[ 5 ].pathAttributes;
5189
+ attrs?.split( ' ' ).forEach( attr => {
5190
+ const t = attr.split( '=' );
5191
+ path.setAttribute( t[ 0 ], t[ 1 ] );
5192
+ } );
5193
+ }
5194
+
5195
+ const faLicense = `<!-- This icon might belong to a collection from Iconify - https://iconify.design/ - or !Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. -->`;
5196
+ svg.innerHTML += faLicense;
5197
+ return _createIconFromSVG( svg );
5198
+ }
5199
+ }
5200
+
5201
+ // Fallback to Lucide icon
5202
+ console.assert( lucideData, `No existing icon named _${ iconName }_` );
5203
+ svg = lucide.createElement( lucideData, options );
5204
+
5205
+ return _createIconFromSVG( svg );
5206
+ }
5207
+
5208
+ LX.makeIcon = makeIcon;
5209
+
5210
+ /**
5211
+ * @method registerIcon
5212
+ * @description Register an SVG icon to LX.ICONS
5213
+ * @param {String} iconName
5214
+ * @param {String} svgString
5215
+ * @param {String} variant
5216
+ * @param {Array} aliases
5217
+ */
5218
+ function registerIcon( iconName, svgString, variant = "none", aliases = [] )
5219
+ {
5220
+ const svg = new DOMParser().parseFromString( svgString, 'image/svg+xml' ).documentElement;
5221
+ const path = svg.querySelector( "path" );
5222
+ const viewBox = svg.getAttribute( "viewBox" ).split( ' ' );
5223
+ const pathData = path.getAttribute( 'd' );
5224
+
5225
+ let svgAttributes = [];
5226
+ let pathAttributes = [];
5227
+
5228
+ for( const attr of svg.attributes )
5229
+ {
5230
+ switch( attr.name )
5231
+ {
5232
+ case "transform":
5233
+ case "fill":
5234
+ case "stroke-width":
5235
+ case "stroke-linecap":
5236
+ case "stroke-linejoin":
5237
+ svgAttributes.push( `${ attr.name }=${ attr.value }` );
5238
+ break;
5239
+ }
5240
+ }
5241
+
5242
+ for( const attr of path.attributes )
5243
+ {
5244
+ switch( attr.name )
5245
+ {
5246
+ case "transform":
5247
+ case "fill":
5248
+ case "stroke-width":
5249
+ case "stroke-linecap":
5250
+ case "stroke-linejoin":
5251
+ pathAttributes.push( `${ attr.name }=${ attr.value }` );
5252
+ break;
5253
+ }
5254
+ }
5255
+
5256
+ const iconData = [
5257
+ parseInt( viewBox[ 2 ] ),
5258
+ parseInt( viewBox[ 3 ] ),
5259
+ aliases,
5260
+ variant,
5261
+ pathData,
5262
+ {
5263
+ svgAttributes: svgAttributes.length ? svgAttributes.join( ' ' ) : null,
5264
+ pathAttributes: pathAttributes.length ? pathAttributes.join( ' ' ) : null
5265
+ }
5266
+ ];
5267
+
5268
+ if( LX.ICONS[ iconName ] )
5269
+ {
5270
+ console.warn( `${ iconName } will be added/replaced in LX.ICONS` );
5271
+ }
5272
+
5273
+ LX.ICONS[ iconName ] = iconData;
5274
+ }
5275
+
5276
+ LX.registerIcon = registerIcon;
5277
+
5278
+ /**
5279
+ * @method registerCommandbarEntry
5280
+ * @description Adds an extra command bar entry
5281
+ * @param {String} name
5282
+ * @param {Function} callback
5283
+ */
5284
+ function registerCommandbarEntry( name, callback )
5285
+ {
5286
+ LX.extraCommandbarEntries.push( { name, callback } );
5287
+ }
5288
+
5289
+ LX.registerCommandbarEntry = registerCommandbarEntry;
5290
+
5291
+ /*
5292
+ Dialog and Notification Elements
5293
+ */
5294
+
5295
+ /**
5296
+ * @method message
5297
+ * @param {String} text
5298
+ * @param {String} title (Optional)
5299
+ * @param {Object} options
5300
+ * id: Id of the message dialog
5301
+ * position: Dialog position in screen [screen centered]
5302
+ * draggable: Dialog can be dragged [false]
5303
+ */
5304
+
5305
+ function message( text, title, options = {} )
5306
+ {
5307
+ if( !text )
5308
+ {
5309
+ throw( "No message to show" );
5310
+ }
5311
+
5312
+ options.modal = true;
5313
+
5314
+ return new LX.Dialog( title, p => {
5315
+ p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
5316
+ }, options );
5317
+ }
5318
+
5319
+ LX.message = message;
5320
+
5321
+ /**
5322
+ * @method popup
5323
+ * @param {String} text
5324
+ * @param {String} title (Optional)
5325
+ * @param {Object} options
5326
+ * id: Id of the message dialog
5327
+ * timeout (Number): Delay time before it closes automatically (ms). Default: [3000]
5328
+ * position (Array): [x,y] Dialog position in screen. Default: [screen centered]
5329
+ * size (Array): [width, height]
5330
+ */
5331
+
5332
+ function popup( text, title, options = {} )
5333
+ {
5334
+ if( !text )
5335
+ {
5336
+ throw("No message to show");
5337
+ }
5338
+
5339
+ options.size = options.size ?? [ "max-content", "auto" ];
5340
+ options.class = "lexpopup";
5341
+
5342
+ const time = options.timeout || 3000;
5343
+ const dialog = new LX.Dialog( title, p => {
5344
+ p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
5345
+ }, options );
5346
+
5347
+ setTimeout( () => {
5348
+ dialog.close();
5349
+ }, Math.max( time, 150 ) );
5350
+
5351
+ return dialog;
5352
+ }
5353
+
5354
+ LX.popup = popup;
5355
+
5356
+ /**
5357
+ * @method prompt
5358
+ * @param {String} text
5359
+ * @param {String} title (Optional)
5360
+ * @param {Object} options
5361
+ * id: Id of the prompt dialog
5362
+ * position: Dialog position in screen [screen centered]
5363
+ * draggable: Dialog can be dragged [false]
5364
+ * input: If false, no text input appears
5365
+ * accept: Accept text
5366
+ * required: Input has to be filled [true]. Default: false
5367
+ */
5368
+
5369
+ function prompt( text, title, callback, options = {} )
5370
+ {
5371
+ options.modal = true;
5372
+ options.className = "prompt";
5373
+
5374
+ let value = "";
5375
+
5376
+ const dialog = new LX.Dialog( title, p => {
5377
+
5378
+ p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
5379
+
5380
+ if( options.input ?? true )
5381
+ {
5382
+ p.addText( null, options.input || value, v => value = v, { placeholder: "..." } );
5383
+ }
5384
+
5385
+ p.sameLine( 2 );
5386
+
5387
+ p.addButton(null, "Cancel", () => {if(options.on_cancel) options.on_cancel(); dialog.close();} );
5388
+
5389
+ p.addButton( null, options.accept || "Continue", () => {
5390
+ if( options.required && value === '' )
5391
+ {
5392
+ text += text.includes("You must fill the input text.") ? "": "\nYou must fill the input text.";
5393
+ dialog.close();
5394
+ prompt( text, title, callback, options );
5395
+ }
5396
+ else
5397
+ {
5398
+ if( callback ) callback.call( this, value );
5399
+ dialog.close();
5400
+ }
5401
+ }, { buttonClass: "primary" });
5402
+
5403
+ }, options );
5404
+
5405
+ // Focus text prompt
5406
+ if( options.input ?? true )
5407
+ {
5408
+ dialog.root.querySelector( 'input' ).focus();
5409
+ }
5410
+
5411
+ return dialog;
5412
+ }
5413
+
5414
+ LX.prompt = prompt;
5415
+
5416
+ /**
5417
+ * @method toast
5418
+ * @param {String} title
5419
+ * @param {String} description (Optional)
5420
+ * @param {Object} options
5421
+ * action: Data of the custom action { name, callback }
5422
+ * closable: Allow closing the toast
5423
+ * timeout: Time in which the toast closed automatically, in ms. -1 means persistent. [3000]
5424
+ */
5425
+
5426
+ function toast( title, description, options = {} )
5427
+ {
5428
+ if( !title )
5429
+ {
5430
+ throw( "The toast needs at least a title!" );
5431
+ }
5432
+
5433
+ console.assert( this.notifications );
5434
+
5435
+ const toast = document.createElement( "li" );
5436
+ toast.className = "lextoast";
5437
+ toast.style.translate = "0 calc(100% + 30px)";
5438
+ this.notifications.prepend( toast );
5439
+
5440
+ LX.doAsync( () => {
5441
+
5442
+ if( this.notifications.offsetWidth > this.notifications.iWidth )
5443
+ {
5444
+ this.notifications.iWidth = Math.min( this.notifications.offsetWidth, 480 );
5445
+ this.notifications.style.width = this.notifications.iWidth + "px";
5446
+ }
5447
+
5448
+ toast.dataset[ "open" ] = true;
5449
+ }, 10 );
5450
+
5451
+ const content = document.createElement( "div" );
5452
+ content.className = "lextoastcontent";
5453
+ toast.appendChild( content );
5454
+
5455
+ const titleContent = document.createElement( "div" );
5456
+ titleContent.className = "title";
5457
+ titleContent.innerHTML = title;
5458
+ content.appendChild( titleContent );
5459
+
5460
+ if( description )
5461
+ {
5462
+ const desc = document.createElement( "div" );
5463
+ desc.className = "desc";
5464
+ desc.innerHTML = description;
5465
+ content.appendChild( desc );
5466
+ }
5467
+
5468
+ if( options.action )
5469
+ {
5470
+ const panel = new LX.Panel();
5471
+ panel.addButton(null, options.action.name ?? "Accept", options.action.callback.bind( this, toast ), { width: "auto", maxWidth: "150px", className: "right", buttonClass: "border" });
5472
+ toast.appendChild( panel.root.childNodes[ 0 ] );
5473
+ }
5474
+
5475
+ const that = this;
5476
+
5477
+ toast.close = function() {
5478
+ this.dataset[ "closed" ] = true;
5479
+ LX.doAsync( () => {
5480
+ this.remove();
5481
+ if( !that.notifications.childElementCount )
5482
+ {
5483
+ that.notifications.style.width = "unset";
5484
+ that.notifications.iWidth = 0;
5485
+ }
5486
+ }, 500 );
5487
+ };
5488
+
5489
+ if( options.closable ?? true )
5490
+ {
5491
+ const closeIcon = LX.makeIcon( "X", { iconClass: "closer" } );
5492
+ closeIcon.addEventListener( "click", () => {
5493
+ toast.close();
5494
+ } );
5495
+ toast.appendChild( closeIcon );
5496
+ }
5497
+
5498
+ const timeout = options.timeout ?? 3000;
5499
+
5500
+ if( timeout != -1 )
5501
+ {
5502
+ LX.doAsync( () => {
5503
+ toast.close();
5504
+ }, timeout );
5505
+ }
5506
+ }
5507
+
5508
+ LX.toast = toast;
5509
+
5510
+ /**
5511
+ * @method badge
5512
+ * @param {String} text
5513
+ * @param {String} className
5514
+ * @param {Object} options
5515
+ * style: Style attributes to override
5516
+ * asElement: Returns the badge as HTMLElement [false]
5517
+ */
5518
+
5519
+ function badge( text, className, options = {} )
5520
+ {
5521
+ const container = document.createElement( "div" );
5522
+ container.innerHTML = text;
5523
+ container.className = "lexbadge " + ( className ?? "" );
5524
+ Object.assign( container.style, options.style ?? {} );
5525
+ return ( options.asElement ?? false ) ? container : container.outerHTML;
5526
+ }
5527
+
5528
+ LX.badge = badge;
5529
+
5530
+ /**
5531
+ * @method makeElement
5532
+ * @param {String} htmlType
5533
+ * @param {String} className
5534
+ * @param {String} innerHTML
5535
+ * @param {HTMLElement} parent
5536
+ * @param {Object} overrideStyle
5537
+ */
5538
+
5539
+ function makeElement( htmlType, className, innerHTML, parent, overrideStyle = {} )
5540
+ {
5541
+ const element = document.createElement( htmlType );
5542
+ element.className = className ?? "";
5543
+ element.innerHTML = innerHTML ?? "";
5544
+ Object.assign( element.style, overrideStyle );
5545
+
5546
+ if( parent )
5547
+ {
5548
+ if( parent.attach ) // Use attach method if possible
5549
+ {
5550
+ parent.attach( element );
5551
+ }
5552
+ else // its a native HTMLElement
5553
+ {
5554
+ parent.appendChild( element );
5555
+ }
5556
+ }
5557
+
5558
+ return element;
5559
+ }
5560
+
5561
+ LX.makeElement = makeElement;
5562
+
5563
+ /**
5564
+ * @method makeContainer
5565
+ * @param {Array} size
5566
+ * @param {String} className
5567
+ * @param {String} innerHTML
5568
+ * @param {HTMLElement} parent
5569
+ * @param {Object} overrideStyle
5570
+ */
5571
+
5572
+ function makeContainer( size, className, innerHTML, parent, overrideStyle = {} )
5573
+ {
5574
+ const container = LX.makeElement( "div", "lexcontainer " + ( className ?? "" ), innerHTML, parent, overrideStyle );
5575
+ container.style.width = size && size[ 0 ] ? size[ 0 ] : "100%";
5576
+ container.style.height = size && size[ 1 ] ? size[ 1 ] : "100%";
5577
+ return container;
5578
+ }
5579
+
5580
+ LX.makeContainer = makeContainer;
5581
+
5582
+ /**
5583
+ * @method asTooltip
5584
+ * @param {HTMLElement} trigger
5585
+ * @param {String} content
5586
+ * @param {Object} options
5587
+ * side: Side of the tooltip
5588
+ * offset: Tooltip margin offset
5589
+ * active: Tooltip active by default [true]
5590
+ */
5591
+
5592
+ function asTooltip( trigger, content, options = {} )
5593
+ {
5594
+ console.assert( trigger, "You need a trigger to generate a tooltip!" );
5595
+
5596
+ trigger.dataset[ "disableTooltip" ] = !( options.active ?? true );
5597
+
5598
+ let tooltipDom = null;
5599
+
5600
+ trigger.addEventListener( "mouseenter", function(e) {
5601
+
5602
+ if( trigger.dataset[ "disableTooltip" ] == "true" )
5603
+ {
5604
+ return;
5605
+ }
5606
+
5607
+ LX.root.querySelectorAll( ".lextooltip" ).forEach( e => e.remove() );
5608
+
5609
+ tooltipDom = document.createElement( "div" );
5610
+ tooltipDom.className = "lextooltip";
5611
+ tooltipDom.innerHTML = content;
5612
+
5613
+ LX.doAsync( () => {
5614
+
5615
+ const position = [ 0, 0 ];
5616
+ const rect = this.getBoundingClientRect();
5617
+ const offset = options.offset ?? 6;
5618
+ let alignWidth = true;
5619
+
5620
+ switch( options.side ?? "top" )
5621
+ {
5622
+ case "left":
5623
+ position[ 0 ] += ( rect.x - tooltipDom.offsetWidth - offset );
5624
+ alignWidth = false;
5625
+ break;
5626
+ case "right":
5627
+ position[ 0 ] += ( rect.x + rect.width + offset );
5628
+ alignWidth = false;
5629
+ break;
5630
+ case "top":
5631
+ position[ 1 ] += ( rect.y - tooltipDom.offsetHeight - offset );
5632
+ alignWidth = true;
5633
+ break;
5634
+ case "bottom":
5635
+ position[ 1 ] += ( rect.y + rect.height + offset );
5636
+ alignWidth = true;
5637
+ break;
5638
+ }
5639
+
5640
+ if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - tooltipDom.offsetWidth * 0.5; }
5641
+ else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - tooltipDom.offsetHeight * 0.5; }
5642
+
5643
+ // Avoid collisions
5644
+ position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - tooltipDom.offsetWidth - 4 );
5645
+ position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - tooltipDom.offsetHeight - 4 );
5646
+
5647
+ tooltipDom.style.left = `${ position[ 0 ] }px`;
5648
+ tooltipDom.style.top = `${ position[ 1 ] }px`;
5649
+ } );
5650
+
5651
+ LX.root.appendChild( tooltipDom );
5652
+ } );
5653
+
5654
+ trigger.addEventListener( "mouseleave", function(e) {
5655
+ if( tooltipDom )
5656
+ {
5657
+ tooltipDom.remove();
5658
+ }
5659
+ } );
5660
+ }
5661
+
5662
+ LX.asTooltip = asTooltip;
5663
+
5664
+ /*
5665
+ * Requests
5666
+ */
5667
+
5668
+ Object.assign(LX, {
5669
+
5670
+ /**
5671
+ * Request file from url (it could be a binary, text, etc.). If you want a simplied version use
5672
+ * @method request
5673
+ * @param {Object} request object with all the parameters like data (for sending forms), dataType, success, error
5674
+ * @param {Function} on_complete
5675
+ **/
5676
+ request( request ) {
5677
+
5678
+ var dataType = request.dataType || "text";
5679
+ if(dataType == "json") //parse it locally
5680
+ dataType = "text";
5681
+ else if(dataType == "xml") //parse it locally
5682
+ dataType = "text";
5683
+ else if (dataType == "binary")
5684
+ {
5685
+ //request.mimeType = "text/plain; charset=x-user-defined";
5686
+ dataType = "arraybuffer";
5687
+ request.mimeType = "application/octet-stream";
5688
+ }
5689
+
5690
+ //regular case, use AJAX call
5691
+ var xhr = new XMLHttpRequest();
5692
+ xhr.open( request.data ? 'POST' : 'GET', request.url, true);
5693
+ if(dataType)
5694
+ xhr.responseType = dataType;
5695
+ if (request.mimeType)
5696
+ xhr.overrideMimeType( request.mimeType );
5697
+ if( request.nocache )
5698
+ xhr.setRequestHeader('Cache-Control', 'no-cache');
5699
+
5700
+ xhr.onload = function(load)
5701
+ {
5702
+ var response = this.response;
5703
+ if( this.status != 200)
5704
+ {
5705
+ var err = "Error " + this.status;
5706
+ if(request.error)
5707
+ request.error(err);
5708
+ return;
5709
+ }
5710
+
5711
+ if(request.dataType == "json") //chrome doesnt support json format
5712
+ {
5713
+ try
5714
+ {
5715
+ response = JSON.parse(response);
5716
+ }
5717
+ catch (err)
5718
+ {
5719
+ if(request.error)
5720
+ request.error(err);
5721
+ else
5722
+ throw err;
5723
+ }
5724
+ }
5725
+ else if(request.dataType == "xml")
5726
+ {
5727
+ try
5728
+ {
5729
+ var xmlparser = new DOMParser();
5730
+ response = xmlparser.parseFromString(response,"text/xml");
5731
+ }
5732
+ catch (err)
5733
+ {
5734
+ if(request.error)
5735
+ request.error(err);
5736
+ else
5737
+ throw err;
5738
+ }
5739
+ }
5740
+ if(request.success)
5741
+ request.success.call(this, response, this);
5742
+ };
5743
+ xhr.onerror = function(err) {
5744
+ if(request.error)
5745
+ request.error(err);
5746
+ };
5747
+
5748
+ var data = new FormData();
5749
+ if( request.data )
5750
+ {
5751
+ for( var i in request.data)
5752
+ data.append(i,request.data[ i ]);
5753
+ }
5754
+
5755
+ xhr.send( data );
5756
+ return xhr;
5757
+ },
5758
+
5759
+ /**
5760
+ * Request file from url
5761
+ * @method requestText
5762
+ * @param {String} url
5763
+ * @param {Function} onComplete
5764
+ * @param {Function} onError
5765
+ **/
5766
+ requestText( url, onComplete, onError ) {
5767
+ return this.request({ url: url, dataType:"text", success: onComplete, error: onError });
5768
+ },
5769
+
5770
+ /**
5771
+ * Request file from url
5772
+ * @method requestJSON
5773
+ * @param {String} url
5774
+ * @param {Function} onComplete
5775
+ * @param {Function} onError
5776
+ **/
5777
+ requestJSON( url, onComplete, onError ) {
5778
+ return this.request({ url: url, dataType:"json", success: onComplete, error: onError });
5779
+ },
5780
+
5781
+ /**
5782
+ * Request binary file from url
5783
+ * @method requestBinary
5784
+ * @param {String} url
5785
+ * @param {Function} onComplete
5786
+ * @param {Function} onError
5787
+ **/
5788
+ requestBinary( url, onComplete, onError ) {
5789
+ return this.request({ url: url, dataType:"binary", success: onComplete, error: onError });
5790
+ },
5791
+
5792
+ /**
5793
+ * Request script and inserts it in the DOM
5794
+ * @method requireScript
5795
+ * @param {String|Array} url the url of the script or an array containing several urls
5796
+ * @param {Function} onComplete
5797
+ * @param {Function} onError
5798
+ * @param {Function} onProgress (if several files are required, onProgress is called after every file is added to the DOM)
5799
+ **/
5800
+ requireScript( url, onComplete, onError, onProgress, version ) {
5770
5801
 
5771
- const path = document.createElement( "path" );
5772
- path.setAttribute( "fill", "currentColor" );
5773
- path.setAttribute( "d", data[ 4 ] );
5774
- svg.appendChild( path );
5802
+ if(!url)
5803
+ throw("invalid URL");
5775
5804
 
5776
- if( data[ 5 ] )
5777
- {
5778
- const classes = data[ 5 ].pathClass;
5779
- classes?.split( ' ' ).forEach( c => {
5780
- path.classList.add( c );
5781
- } );
5805
+ if( url.constructor === String )
5806
+ url = [url];
5782
5807
 
5783
- const attrs = data[ 5 ].pathAttributes;
5784
- attrs?.split( ' ' ).forEach( attr => {
5785
- const t = attr.split( '=' );
5786
- path.setAttribute( t[ 0 ], t[ 1 ] );
5787
- } );
5788
- }
5808
+ var total = url.length;
5809
+ var loaded_scripts = [];
5789
5810
 
5790
- const faLicense = `<!-- This icon might belong to a collection from Iconify - https://iconify.design/ - or !Font Awesome Free 6.7.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2025 Fonticons, Inc. -->`;
5791
- svg.innerHTML += faLicense;
5792
- return _createIconFromSVG( svg );
5811
+ for( var i in url)
5812
+ {
5813
+ var script = document.createElement('script');
5814
+ script.num = i;
5815
+ script.type = 'text/javascript';
5816
+ script.src = url[ i ] + ( version ? "?version=" + version : "" );
5817
+ script.original_src = url[ i ];
5818
+ script.async = false;
5819
+ script.onload = function( e ) {
5820
+ total--;
5821
+ loaded_scripts.push(this);
5822
+ if(total)
5823
+ {
5824
+ if( onProgress )
5825
+ {
5826
+ onProgress( this.original_src, this.num );
5827
+ }
5828
+ }
5829
+ else if(onComplete)
5830
+ onComplete( loaded_scripts );
5831
+ };
5832
+ if(onError)
5833
+ script.onerror = function(err) {
5834
+ onError(err, this.original_src, this.num );
5835
+ };
5836
+ document.getElementsByTagName('head')[ 0 ].appendChild(script);
5793
5837
  }
5794
- }
5795
-
5796
- // Fallback to Lucide icon
5797
- console.assert( lucideData, `No existing icon named _${ iconName }_` );
5798
- svg = lucide.createElement( lucideData, options );
5838
+ },
5799
5839
 
5800
- return _createIconFromSVG( svg );
5801
- }
5840
+ loadScriptSync( url ) {
5841
+ return new Promise((resolve, reject) => {
5842
+ const script = document.createElement( "script" );
5843
+ script.src = url;
5844
+ script.async = false;
5845
+ script.onload = () => resolve();
5846
+ script.onerror = () => reject(new Error(`Failed to load ${url}`));
5847
+ document.head.appendChild( script );
5848
+ });
5849
+ },
5802
5850
 
5803
- LX.makeIcon = makeIcon;
5851
+ downloadURL( url, filename ) {
5804
5852
 
5805
- /**
5806
- * @method registerIcon
5807
- * @description Register an SVG icon to LX.ICONS
5808
- * @param {String} iconName
5809
- * @param {String} svgString
5810
- * @param {String} variant
5811
- * @param {Array} aliases
5812
- */
5813
- function registerIcon( iconName, svgString, variant = "none", aliases = [] )
5814
- {
5815
- const svg = new DOMParser().parseFromString( svgString, 'image/svg+xml' ).documentElement;
5816
- const path = svg.querySelector( "path" );
5817
- const viewBox = svg.getAttribute( "viewBox" ).split( ' ' );
5818
- const pathData = path.getAttribute( 'd' );
5853
+ const fr = new FileReader();
5819
5854
 
5820
- let svgAttributes = [];
5821
- let pathAttributes = [];
5855
+ const _download = function(_url) {
5856
+ var link = document.createElement('a');
5857
+ link.href = _url;
5858
+ link.download = filename;
5859
+ document.body.appendChild(link);
5860
+ link.click();
5861
+ document.body.removeChild(link);
5862
+ };
5822
5863
 
5823
- for( const attr of svg.attributes )
5824
- {
5825
- switch( attr.name )
5864
+ if( url.includes('http') )
5826
5865
  {
5827
- case "transform":
5828
- case "fill":
5829
- case "stroke-width":
5830
- case "stroke-linecap":
5831
- case "stroke-linejoin":
5832
- svgAttributes.push( `${ attr.name }=${ attr.value }` );
5833
- break;
5866
+ LX.request({ url: url, dataType: 'blob', success: (f) => {
5867
+ fr.readAsDataURL( f );
5868
+ fr.onload = e => {
5869
+ _download(e.currentTarget.result);
5870
+ };
5871
+ } });
5872
+ }else
5873
+ {
5874
+ _download(url);
5834
5875
  }
5835
- }
5836
5876
 
5837
- for( const attr of path.attributes )
5838
- {
5839
- switch( attr.name )
5877
+ },
5878
+
5879
+ downloadFile: function( filename, data, dataType ) {
5880
+ if(!data)
5840
5881
  {
5841
- case "transform":
5842
- case "fill":
5843
- case "stroke-width":
5844
- case "stroke-linecap":
5845
- case "stroke-linejoin":
5846
- pathAttributes.push( `${ attr.name }=${ attr.value }` );
5847
- break;
5882
+ console.warn("No file provided to download");
5883
+ return;
5848
5884
  }
5849
- }
5850
5885
 
5851
- const iconData = [
5852
- parseInt( viewBox[ 2 ] ),
5853
- parseInt( viewBox[ 3 ] ),
5854
- aliases,
5855
- variant,
5856
- pathData,
5886
+ if(!dataType)
5857
5887
  {
5858
- svgAttributes: svgAttributes.length ? svgAttributes.join( ' ' ) : null,
5859
- pathAttributes: pathAttributes.length ? pathAttributes.join( ' ' ) : null
5888
+ if(data.constructor === String )
5889
+ dataType = 'text/plain';
5890
+ else
5891
+ dataType = 'application/octet-stream';
5860
5892
  }
5861
- ];
5862
5893
 
5863
- if( LX.ICONS[ iconName ] )
5864
- {
5865
- console.warn( `${ iconName } will be added/replaced in LX.ICONS` );
5894
+ var file = null;
5895
+ if(data.constructor !== File && data.constructor !== Blob)
5896
+ file = new Blob( [ data ], {type : dataType});
5897
+ else
5898
+ file = data;
5899
+
5900
+ var url = URL.createObjectURL( file );
5901
+ var element = document.createElement("a");
5902
+ element.setAttribute('href', url);
5903
+ element.setAttribute('download', filename );
5904
+ element.style.display = 'none';
5905
+ document.body.appendChild(element);
5906
+ element.click();
5907
+ document.body.removeChild(element);
5908
+ setTimeout( function(){ URL.revokeObjectURL( url ); }, 1000*60 ); //wait one minute to revoke url
5866
5909
  }
5910
+ });
5867
5911
 
5868
- LX.ICONS[ iconName ] = iconData;
5912
+ /**
5913
+ * @method compareThreshold
5914
+ * @param {String} url
5915
+ * @param {Function} onComplete
5916
+ * @param {Function} onError
5917
+ **/
5918
+ function compareThreshold( v, p, n, t )
5919
+ {
5920
+ return Math.abs( v - p ) >= t || Math.abs( v - n ) >= t;
5869
5921
  }
5870
5922
 
5871
- LX.registerIcon = registerIcon;
5923
+ LX.compareThreshold = compareThreshold;
5872
5924
 
5873
5925
  /**
5874
- * @method registerCommandbarEntry
5875
- * @description Adds an extra command bar entry
5876
- * @param {String} name
5877
- * @param {Function} callback
5878
- */
5879
- function registerCommandbarEntry( name, callback )
5926
+ * @method compareThresholdRange
5927
+ * @param {String} url
5928
+ * @param {Function} onComplete
5929
+ * @param {Function} onError
5930
+ **/
5931
+ function compareThresholdRange( v0, v1, t0, t1 )
5880
5932
  {
5881
- LX.extraCommandbarEntries.push( { name, callback } );
5933
+ return v0 >= t0 && v0 <= t1 || v1 >= t0 && v1 <= t1 || v0 <= t0 && v1 >= t1;
5882
5934
  }
5883
5935
 
5884
- LX.registerCommandbarEntry = registerCommandbarEntry;
5936
+ LX.compareThresholdRange = compareThresholdRange;
5885
5937
 
5886
5938
  /**
5887
- * Utils package
5888
- * @namespace LX.UTILS
5889
- * @description Collection of utility functions
5890
- */
5891
-
5892
- LX.UTILS = {
5893
-
5894
- compareThreshold( v, p, n, t ) { return Math.abs(v - p) >= t || Math.abs(v - n) >= t },
5895
- compareThresholdRange( v0, v1, t0, t1 ) { return v0 >= t0 && v0 <= t1 || v1 >= t0 && v1 <= t1 || v0 <= t0 && v1 >= t1},
5896
- deleteElement( el ) { if( el ) el.remove(); },
5897
- flushCss(element) {
5898
- // By reading the offsetHeight property, we are forcing
5899
- // the browser to flush the pending CSS changes (which it
5900
- // does to ensure the value obtained is accurate).
5901
- element.offsetHeight;
5902
- },
5903
- getControlPoints( x0, y0, x1, y1, x2, y2, t ) {
5904
-
5905
- // x0,y0,x1,y1 are the coordinates of the end (knot) pts of this segment
5906
- // x2,y2 is the next knot -- not connected here but needed to calculate p2
5907
- // p1 is the control point calculated here, from x1 back toward x0.
5908
- // p2 is the next control point, calculated here and returned to become the
5909
- // next segment's p1.
5910
- // t is the 'tension' which controls how far the control points spread.
5939
+ * @method getControlPoints
5940
+ * @param {String} url
5941
+ * @param {Function} onComplete
5942
+ * @param {Function} onError
5943
+ **/
5944
+ function getControlPoints( x0, y0, x1, y1, x2, y2, t )
5945
+ {
5946
+ // x0,y0,x1,y1 are the coordinates of the end (knot) pts of this segment
5947
+ // x2,y2 is the next knot -- not connected here but needed to calculate p2
5948
+ // p1 is the control point calculated here, from x1 back toward x0.
5949
+ // p2 is the next control point, calculated here and returned to become the
5950
+ // next segment's p1.
5951
+ // t is the 'tension' which controls how far the control points spread.
5911
5952
 
5912
- // Scaling factors: distances from this knot to the previous and following knots.
5913
- var d01=Math.sqrt(Math.pow(x1-x0,2)+Math.pow(y1-y0,2));
5914
- var d12=Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2));
5953
+ // Scaling factors: distances from this knot to the previous and following knots.
5954
+ var d01=Math.sqrt(Math.pow(x1-x0,2)+Math.pow(y1-y0,2));
5955
+ var d12=Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2));
5915
5956
 
5916
- var fa=t*d01/(d01+d12);
5917
- var fb=t-fa;
5957
+ var fa=t*d01/(d01+d12);
5958
+ var fb=t-fa;
5918
5959
 
5919
- var p1x=x1+fa*(x0-x2);
5920
- var p1y=y1+fa*(y0-y2);
5960
+ var p1x=x1+fa*(x0-x2);
5961
+ var p1y=y1+fa*(y0-y2);
5921
5962
 
5922
- var p2x=x1-fb*(x0-x2);
5923
- var p2y=y1-fb*(y0-y2);
5963
+ var p2x=x1-fb*(x0-x2);
5964
+ var p2y=y1-fb*(y0-y2);
5924
5965
 
5925
- return [p1x,p1y,p2x,p2y]
5926
- },
5927
- drawSpline( ctx, pts, t ) {
5966
+ return [p1x,p1y,p2x,p2y]
5967
+ }
5928
5968
 
5929
- ctx.save();
5930
- var cp = []; // array of control points, as x0,y0,x1,y1,...
5931
- var n = pts.length;
5969
+ LX.getControlPoints = getControlPoints;
5932
5970
 
5933
- // Draw an open curve, not connected at the ends
5934
- for( var i = 0; i < (n - 4); i += 2 )
5935
- {
5936
- cp = cp.concat(LX.UTILS.getControlPoints(pts[ i ],pts[i+1],pts[i+2],pts[i+3],pts[i+4],pts[i+5],t));
5937
- }
5971
+ /**
5972
+ * @method drawSpline
5973
+ * @param {CanvasRenderingContext2D} ctx
5974
+ * @param {Array} pts
5975
+ * @param {Number} t
5976
+ **/
5977
+ function drawSpline( ctx, pts, t )
5978
+ {
5979
+ ctx.save();
5980
+ var cp = []; // array of control points, as x0,y0,x1,y1,...
5981
+ var n = pts.length;
5938
5982
 
5939
- for( var i = 2; i < ( pts.length - 5 ); i += 2 )
5940
- {
5941
- ctx.beginPath();
5942
- ctx.moveTo(pts[ i ], pts[i+1]);
5943
- ctx.bezierCurveTo(cp[2*i-2],cp[2*i-1],cp[2*i],cp[2*i+1],pts[i+2],pts[i+3]);
5944
- ctx.stroke();
5945
- ctx.closePath();
5946
- }
5983
+ // Draw an open curve, not connected at the ends
5984
+ for( var i = 0; i < (n - 4); i += 2 )
5985
+ {
5986
+ cp = cp.concat(LX.getControlPoints(pts[ i ],pts[i+1],pts[i+2],pts[i+3],pts[i+4],pts[i+5],t));
5987
+ }
5947
5988
 
5948
- // For open curves the first and last arcs are simple quadratics.
5989
+ for( var i = 2; i < ( pts.length - 5 ); i += 2 )
5990
+ {
5949
5991
  ctx.beginPath();
5950
- ctx.moveTo( pts[ 0 ], pts[ 1 ] );
5951
- ctx.quadraticCurveTo( cp[ 0 ], cp[ 1 ], pts[ 2 ], pts[ 3 ]);
5992
+ ctx.moveTo(pts[ i ], pts[i+1]);
5993
+ ctx.bezierCurveTo(cp[2*i-2],cp[2*i-1],cp[2*i],cp[2*i+1],pts[i+2],pts[i+3]);
5952
5994
  ctx.stroke();
5953
5995
  ctx.closePath();
5996
+ }
5954
5997
 
5955
- ctx.beginPath();
5956
- ctx.moveTo( pts[ n-2 ], pts[ n-1 ] );
5957
- ctx.quadraticCurveTo( cp[ 2*n-10 ], cp[ 2*n-9 ], pts[ n-4 ], pts[ n-3 ]);
5958
- ctx.stroke();
5959
- ctx.closePath();
5998
+ // For open curves the first and last arcs are simple quadratics.
5999
+ ctx.beginPath();
6000
+ ctx.moveTo( pts[ 0 ], pts[ 1 ] );
6001
+ ctx.quadraticCurveTo( cp[ 0 ], cp[ 1 ], pts[ 2 ], pts[ 3 ]);
6002
+ ctx.stroke();
6003
+ ctx.closePath();
5960
6004
 
5961
- ctx.restore();
5962
- }
5963
- };
6005
+ ctx.beginPath();
6006
+ ctx.moveTo( pts[ n-2 ], pts[ n-1 ] );
6007
+ ctx.quadraticCurveTo( cp[ 2*n-10 ], cp[ 2*n-9 ], pts[ n-4 ], pts[ n-3 ]);
6008
+ ctx.stroke();
6009
+ ctx.closePath();
6010
+
6011
+ ctx.restore();
6012
+ }
6013
+
6014
+ LX.drawSpline = drawSpline;
5964
6015
 
5965
6016
  // area.js @jxarco
5966
6017
 
@@ -6685,7 +6736,7 @@ class Area {
6685
6736
 
6686
6737
  addSidebar( callback, options = {} ) {
6687
6738
 
6688
- let sidebar = new LX.Sidebar( options );
6739
+ let sidebar = new LX.Sidebar( { callback, ...options } );
6689
6740
 
6690
6741
  if( callback )
6691
6742
  {
@@ -13943,6 +13994,7 @@ class Sidebar {
13943
13994
 
13944
13995
  this.root = document.createElement( "div" );
13945
13996
  this.root.className = "lexsidebar " + ( options.className ?? "" );
13997
+ this.callback = options.callback ?? null;
13946
13998
 
13947
13999
  this._displaySelected = options.displaySelected ?? false;
13948
14000
 
@@ -13959,17 +14011,19 @@ class Sidebar {
13959
14011
  configurable: true
13960
14012
  });
13961
14013
 
14014
+ const mobile = navigator && /Android|iPhone/i.test( navigator.userAgent );
14015
+
13962
14016
  this.side = options.side ?? "left";
13963
14017
  this.collapsable = options.collapsable ?? true;
13964
14018
  this._collapseWidth = ( options.collapseToIcons ?? true ) ? "58px" : "0px";
13965
- this.collapsed = false;
14019
+ this.collapsed = options.collapsed ?? mobile;
13966
14020
 
13967
14021
  this.filterString = "";
13968
14022
 
13969
14023
  LX.doAsync( () => {
13970
14024
 
13971
14025
  this.root.parentElement.ogWidth = this.root.parentElement.style.width;
13972
- this.root.parentElement.style.transition = "width 0.25s ease-out";
14026
+ this.root.parentElement.style.transition = this.collapsed ? "" : "width 0.25s ease-out";
13973
14027
 
13974
14028
  this.resizeObserver = new ResizeObserver( entries => {
13975
14029
  for ( const entry of entries )
@@ -13978,6 +14032,24 @@ class Sidebar {
13978
14032
  }
13979
14033
  });
13980
14034
 
14035
+ if( this.collapsed )
14036
+ {
14037
+ this.root.classList.toggle( "collapsed", this.collapsed );
14038
+ this.root.parentElement.style.width = this._collapseWidth;
14039
+
14040
+ if( !this.resizeObserver )
14041
+ {
14042
+ throw( "Wait until ResizeObserver has been created!" );
14043
+ }
14044
+
14045
+ this.resizeObserver.observe( this.root.parentElement );
14046
+
14047
+ LX.doAsync( () => {
14048
+ this.resizeObserver.unobserve( this.root.parentElement );
14049
+ this.root.querySelectorAll( ".lexsidebarentrycontent" ).forEach( e => e.dataset[ "disableTooltip" ] = !this.collapsed );
14050
+ }, 10 );
14051
+ }
14052
+
13981
14053
  }, 10 );
13982
14054
 
13983
14055
  // Header
@@ -13993,11 +14065,29 @@ class Sidebar {
13993
14065
  const icon = LX.makeIcon( this.side == "left" ? "PanelLeft" : "PanelRight", { title: "Toggle Sidebar", iconClass: "toggler" } );
13994
14066
  this.header.appendChild( icon );
13995
14067
 
13996
- icon.addEventListener( "click", (e) => {
13997
- e.preventDefault();
13998
- e.stopPropagation();
13999
- this.toggleCollapsed();
14000
- } );
14068
+ if( mobile )
14069
+ {
14070
+ // create an area and append a sidebar:
14071
+ const area = new LX.Area({ skipAppend: true });
14072
+ const sheetSidebarOptions = LX.deepCopy( options );
14073
+ sheetSidebarOptions.collapsed = false;
14074
+ sheetSidebarOptions.collapsable = false;
14075
+ area.addSidebar( this.callback, sheetSidebarOptions );
14076
+
14077
+ icon.addEventListener( "click", e => {
14078
+ e.preventDefault();
14079
+ e.stopPropagation();
14080
+ new LX.Sheet("256px", [ area ], { side: this.side } );
14081
+ } );
14082
+ }
14083
+ else
14084
+ {
14085
+ icon.addEventListener( "click", e => {
14086
+ e.preventDefault();
14087
+ e.stopPropagation();
14088
+ this.toggleCollapsed();
14089
+ } );
14090
+ }
14001
14091
  }
14002
14092
  }
14003
14093
 
@@ -14437,20 +14527,17 @@ class Sidebar {
14437
14527
  return;
14438
14528
  }
14439
14529
 
14530
+ const f = options.callback;
14531
+ if( f ) f.call( this, key, item.value, e );
14532
+
14440
14533
  if( isCollapsable )
14441
14534
  {
14442
14535
  itemDom.querySelector( ".collapser" ).click();
14443
14536
  }
14444
- else
14537
+ else if( item.checkbox )
14445
14538
  {
14446
- const f = options.callback;
14447
- if( f ) f.call( this, key, item.value, e );
14448
-
14449
- if( item.checkbox )
14450
- {
14451
- item.value = !item.value;
14452
- item.checkbox.set( item.value, true );
14453
- }
14539
+ item.value = !item.value;
14540
+ item.checkbox.set( item.value, true );
14454
14541
  }
14455
14542
 
14456
14543
  // Manage selected