lexgui 0.6.4 → 0.6.5

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.5",
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;
@@ -2254,13 +1885,6 @@ class Calendar {
2254
1885
 
2255
1886
  LX.Calendar = Calendar;
2256
1887
 
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
1888
  /**
2265
1889
  * @class Tabs
2266
1890
  */
@@ -2386,7 +2010,7 @@ class Tabs {
2386
2010
  this.thumb.style.transition = "none";
2387
2011
  this.thumb.style.transform = "translate( " + ( tabEl.childIndex * tabEl.offsetWidth ) + "px )";
2388
2012
  this.thumb.style.width = ( tabEl.offsetWidth ) + "px";
2389
- flushCss( this.thumb );
2013
+ LX.flushCss( this.thumb );
2390
2014
  this.thumb.style.transition = transition;
2391
2015
  });
2392
2016
 
@@ -3500,7 +3124,7 @@ class CanvasCurve {
3500
3124
  }
3501
3125
  else
3502
3126
  {
3503
- LX.UTILS.drawSpline( ctx, values, element.smooth );
3127
+ LX.drawSpline( ctx, values, element.smooth );
3504
3128
  }
3505
3129
 
3506
3130
  // Draw points
@@ -4442,254 +4066,6 @@ class CanvasMap2D {
4442
4066
 
4443
4067
  LX.CanvasMap2D = CanvasMap2D;
4444
4068
 
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
4069
  Object.defineProperty(String.prototype, 'lastChar', {
4694
4070
  get: function() { return this[ this.length - 1 ]; },
4695
4071
  enumerable: true,
@@ -4967,19 +4343,43 @@ function doAsync( fn, ms ) {
4967
4343
  LX.doAsync = doAsync;
4968
4344
 
4969
4345
  /**
4970
- * @method getSupportedDOMName
4971
- * @description Convert a text string to a valid DOM name
4972
- * @param {String} text Original text
4346
+ * @method flushCss
4347
+ * @description By reading the offsetHeight property, we are forcing the browser to flush
4348
+ * the pending CSS changes (which it does to ensure the value obtained is accurate).
4349
+ * @param {HTMLElement} element
4973
4350
  */
4974
- function getSupportedDOMName( text )
4351
+ function flushCss( element )
4975
4352
  {
4976
- console.assert( typeof text == "string", "getSupportedDOMName: Text is not a string!" );
4977
-
4978
- let name = text.trim();
4353
+ element.offsetHeight;
4354
+ }
4979
4355
 
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, '_' );
4356
+ LX.flushCss = flushCss;
4357
+
4358
+ /**
4359
+ * @method deleteElement
4360
+ * @param {HTMLElement} element
4361
+ */
4362
+ function deleteElement( element )
4363
+ {
4364
+ if( element !== undefined ) element.remove();
4365
+ }
4366
+
4367
+ LX.deleteElement = deleteElement;
4368
+
4369
+ /**
4370
+ * @method getSupportedDOMName
4371
+ * @description Convert a text string to a valid DOM name
4372
+ * @param {String} text Original text
4373
+ */
4374
+ function getSupportedDOMName( text )
4375
+ {
4376
+ console.assert( typeof text == "string", "getSupportedDOMName: Text is not a string!" );
4377
+
4378
+ let name = text.trim();
4379
+
4380
+ // Replace specific known symbols
4381
+ name = name.replace( /@/g, '_at_' ).replace( /\+/g, '_plus_' ).replace( /\./g, '_dot_' );
4382
+ name = name.replace( /[^a-zA-Z0-9_-]/g, '_' );
4983
4383
 
4984
4384
  // prefix with an underscore if needed
4985
4385
  if( /^[0-9]/.test( name ) )
@@ -5036,9 +4436,13 @@ LX.stripHTML = stripHTML;
5036
4436
  * @param {Number|String} size
5037
4437
  * @param {Number} total
5038
4438
  */
5039
- const parsePixelSize = ( size, total ) => {
5040
-
5041
- if( size.constructor === Number ) { return size; } // Assuming pixels..
4439
+ function parsePixelSize( size, total )
4440
+ {
4441
+ // Assuming pixels..
4442
+ if( size.constructor === Number )
4443
+ {
4444
+ return size;
4445
+ }
5042
4446
 
5043
4447
  if( size.constructor === String )
5044
4448
  {
@@ -5076,7 +4480,7 @@ const parsePixelSize = ( size, total ) => {
5076
4480
  }
5077
4481
 
5078
4482
  throw( "Bad size format!" );
5079
- };
4483
+ }
5080
4484
 
5081
4485
  LX.parsePixelSize = parsePixelSize;
5082
4486
 
@@ -5339,7 +4743,7 @@ function measureRealWidth( value, paddingPlusMargin = 8 )
5339
4743
  i.innerHTML = value;
5340
4744
  document.body.appendChild( i );
5341
4745
  var rect = i.getBoundingClientRect();
5342
- LX.UTILS.deleteElement( i );
4746
+ LX.deleteElement( i );
5343
4747
  return rect.width + paddingPlusMargin;
5344
4748
  }
5345
4749
 
@@ -5761,206 +5165,852 @@ function makeIcon( iconName, options = { } )
5761
5165
  svg.classList.add( c );
5762
5166
  } );
5763
5167
 
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
- }
5168
+ const attrs = data[ 5 ].svgAttributes;
5169
+ attrs?.split( ' ' ).forEach( attr => {
5170
+ const t = attr.split( '=' );
5171
+ svg.setAttribute( t[ 0 ], t[ 1 ] );
5172
+ } );
5173
+ }
5174
+
5175
+ const path = document.createElement( "path" );
5176
+ path.setAttribute( "fill", "currentColor" );
5177
+ path.setAttribute( "d", data[ 4 ] );
5178
+ svg.appendChild( path );
5179
+
5180
+ if( data[ 5 ] )
5181
+ {
5182
+ const classes = data[ 5 ].pathClass;
5183
+ classes?.split( ' ' ).forEach( c => {
5184
+ path.classList.add( c );
5185
+ } );
5186
+
5187
+ const attrs = data[ 5 ].pathAttributes;
5188
+ attrs?.split( ' ' ).forEach( attr => {
5189
+ const t = attr.split( '=' );
5190
+ path.setAttribute( t[ 0 ], t[ 1 ] );
5191
+ } );
5192
+ }
5193
+
5194
+ 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. -->`;
5195
+ svg.innerHTML += faLicense;
5196
+ return _createIconFromSVG( svg );
5197
+ }
5198
+ }
5199
+
5200
+ // Fallback to Lucide icon
5201
+ console.assert( lucideData, `No existing icon named _${ iconName }_` );
5202
+ svg = lucide.createElement( lucideData, options );
5203
+
5204
+ return _createIconFromSVG( svg );
5205
+ }
5206
+
5207
+ LX.makeIcon = makeIcon;
5208
+
5209
+ /**
5210
+ * @method registerIcon
5211
+ * @description Register an SVG icon to LX.ICONS
5212
+ * @param {String} iconName
5213
+ * @param {String} svgString
5214
+ * @param {String} variant
5215
+ * @param {Array} aliases
5216
+ */
5217
+ function registerIcon( iconName, svgString, variant = "none", aliases = [] )
5218
+ {
5219
+ const svg = new DOMParser().parseFromString( svgString, 'image/svg+xml' ).documentElement;
5220
+ const path = svg.querySelector( "path" );
5221
+ const viewBox = svg.getAttribute( "viewBox" ).split( ' ' );
5222
+ const pathData = path.getAttribute( 'd' );
5223
+
5224
+ let svgAttributes = [];
5225
+ let pathAttributes = [];
5226
+
5227
+ for( const attr of svg.attributes )
5228
+ {
5229
+ switch( attr.name )
5230
+ {
5231
+ case "transform":
5232
+ case "fill":
5233
+ case "stroke-width":
5234
+ case "stroke-linecap":
5235
+ case "stroke-linejoin":
5236
+ svgAttributes.push( `${ attr.name }=${ attr.value }` );
5237
+ break;
5238
+ }
5239
+ }
5240
+
5241
+ for( const attr of path.attributes )
5242
+ {
5243
+ switch( attr.name )
5244
+ {
5245
+ case "transform":
5246
+ case "fill":
5247
+ case "stroke-width":
5248
+ case "stroke-linecap":
5249
+ case "stroke-linejoin":
5250
+ pathAttributes.push( `${ attr.name }=${ attr.value }` );
5251
+ break;
5252
+ }
5253
+ }
5254
+
5255
+ const iconData = [
5256
+ parseInt( viewBox[ 2 ] ),
5257
+ parseInt( viewBox[ 3 ] ),
5258
+ aliases,
5259
+ variant,
5260
+ pathData,
5261
+ {
5262
+ svgAttributes: svgAttributes.length ? svgAttributes.join( ' ' ) : null,
5263
+ pathAttributes: pathAttributes.length ? pathAttributes.join( ' ' ) : null
5264
+ }
5265
+ ];
5266
+
5267
+ if( LX.ICONS[ iconName ] )
5268
+ {
5269
+ console.warn( `${ iconName } will be added/replaced in LX.ICONS` );
5270
+ }
5271
+
5272
+ LX.ICONS[ iconName ] = iconData;
5273
+ }
5274
+
5275
+ LX.registerIcon = registerIcon;
5276
+
5277
+ /**
5278
+ * @method registerCommandbarEntry
5279
+ * @description Adds an extra command bar entry
5280
+ * @param {String} name
5281
+ * @param {Function} callback
5282
+ */
5283
+ function registerCommandbarEntry( name, callback )
5284
+ {
5285
+ LX.extraCommandbarEntries.push( { name, callback } );
5286
+ }
5287
+
5288
+ LX.registerCommandbarEntry = registerCommandbarEntry;
5289
+
5290
+ /*
5291
+ Dialog and Notification Elements
5292
+ */
5293
+
5294
+ /**
5295
+ * @method message
5296
+ * @param {String} text
5297
+ * @param {String} title (Optional)
5298
+ * @param {Object} options
5299
+ * id: Id of the message dialog
5300
+ * position: Dialog position in screen [screen centered]
5301
+ * draggable: Dialog can be dragged [false]
5302
+ */
5303
+
5304
+ function message( text, title, options = {} )
5305
+ {
5306
+ if( !text )
5307
+ {
5308
+ throw( "No message to show" );
5309
+ }
5310
+
5311
+ options.modal = true;
5312
+
5313
+ return new LX.Dialog( title, p => {
5314
+ p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
5315
+ }, options );
5316
+ }
5317
+
5318
+ LX.message = message;
5319
+
5320
+ /**
5321
+ * @method popup
5322
+ * @param {String} text
5323
+ * @param {String} title (Optional)
5324
+ * @param {Object} options
5325
+ * id: Id of the message dialog
5326
+ * timeout (Number): Delay time before it closes automatically (ms). Default: [3000]
5327
+ * position (Array): [x,y] Dialog position in screen. Default: [screen centered]
5328
+ * size (Array): [width, height]
5329
+ */
5330
+
5331
+ function popup( text, title, options = {} )
5332
+ {
5333
+ if( !text )
5334
+ {
5335
+ throw("No message to show");
5336
+ }
5337
+
5338
+ options.size = options.size ?? [ "max-content", "auto" ];
5339
+ options.class = "lexpopup";
5340
+
5341
+ const time = options.timeout || 3000;
5342
+ const dialog = new LX.Dialog( title, p => {
5343
+ p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
5344
+ }, options );
5345
+
5346
+ setTimeout( () => {
5347
+ dialog.close();
5348
+ }, Math.max( time, 150 ) );
5349
+
5350
+ return dialog;
5351
+ }
5352
+
5353
+ LX.popup = popup;
5354
+
5355
+ /**
5356
+ * @method prompt
5357
+ * @param {String} text
5358
+ * @param {String} title (Optional)
5359
+ * @param {Object} options
5360
+ * id: Id of the prompt dialog
5361
+ * position: Dialog position in screen [screen centered]
5362
+ * draggable: Dialog can be dragged [false]
5363
+ * input: If false, no text input appears
5364
+ * accept: Accept text
5365
+ * required: Input has to be filled [true]. Default: false
5366
+ */
5367
+
5368
+ function prompt( text, title, callback, options = {} )
5369
+ {
5370
+ options.modal = true;
5371
+ options.className = "prompt";
5372
+
5373
+ let value = "";
5374
+
5375
+ const dialog = new LX.Dialog( title, p => {
5376
+
5377
+ p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
5378
+
5379
+ if( options.input ?? true )
5380
+ {
5381
+ p.addText( null, options.input || value, v => value = v, { placeholder: "..." } );
5382
+ }
5383
+
5384
+ p.sameLine( 2 );
5385
+
5386
+ p.addButton(null, "Cancel", () => {if(options.on_cancel) options.on_cancel(); dialog.close();} );
5387
+
5388
+ p.addButton( null, options.accept || "Continue", () => {
5389
+ if( options.required && value === '' )
5390
+ {
5391
+ text += text.includes("You must fill the input text.") ? "": "\nYou must fill the input text.";
5392
+ dialog.close();
5393
+ prompt( text, title, callback, options );
5394
+ }
5395
+ else
5396
+ {
5397
+ if( callback ) callback.call( this, value );
5398
+ dialog.close();
5399
+ }
5400
+ }, { buttonClass: "primary" });
5401
+
5402
+ }, options );
5403
+
5404
+ // Focus text prompt
5405
+ if( options.input ?? true )
5406
+ {
5407
+ dialog.root.querySelector( 'input' ).focus();
5408
+ }
5409
+
5410
+ return dialog;
5411
+ }
5412
+
5413
+ LX.prompt = prompt;
5414
+
5415
+ /**
5416
+ * @method toast
5417
+ * @param {String} title
5418
+ * @param {String} description (Optional)
5419
+ * @param {Object} options
5420
+ * action: Data of the custom action { name, callback }
5421
+ * closable: Allow closing the toast
5422
+ * timeout: Time in which the toast closed automatically, in ms. -1 means persistent. [3000]
5423
+ */
5424
+
5425
+ function toast( title, description, options = {} )
5426
+ {
5427
+ if( !title )
5428
+ {
5429
+ throw( "The toast needs at least a title!" );
5430
+ }
5431
+
5432
+ console.assert( this.notifications );
5433
+
5434
+ const toast = document.createElement( "li" );
5435
+ toast.className = "lextoast";
5436
+ toast.style.translate = "0 calc(100% + 30px)";
5437
+ this.notifications.prepend( toast );
5438
+
5439
+ LX.doAsync( () => {
5440
+
5441
+ if( this.notifications.offsetWidth > this.notifications.iWidth )
5442
+ {
5443
+ this.notifications.iWidth = Math.min( this.notifications.offsetWidth, 480 );
5444
+ this.notifications.style.width = this.notifications.iWidth + "px";
5445
+ }
5446
+
5447
+ toast.dataset[ "open" ] = true;
5448
+ }, 10 );
5449
+
5450
+ const content = document.createElement( "div" );
5451
+ content.className = "lextoastcontent";
5452
+ toast.appendChild( content );
5453
+
5454
+ const titleContent = document.createElement( "div" );
5455
+ titleContent.className = "title";
5456
+ titleContent.innerHTML = title;
5457
+ content.appendChild( titleContent );
5458
+
5459
+ if( description )
5460
+ {
5461
+ const desc = document.createElement( "div" );
5462
+ desc.className = "desc";
5463
+ desc.innerHTML = description;
5464
+ content.appendChild( desc );
5465
+ }
5466
+
5467
+ if( options.action )
5468
+ {
5469
+ const panel = new LX.Panel();
5470
+ panel.addButton(null, options.action.name ?? "Accept", options.action.callback.bind( this, toast ), { width: "auto", maxWidth: "150px", className: "right", buttonClass: "border" });
5471
+ toast.appendChild( panel.root.childNodes[ 0 ] );
5472
+ }
5473
+
5474
+ const that = this;
5475
+
5476
+ toast.close = function() {
5477
+ this.dataset[ "closed" ] = true;
5478
+ LX.doAsync( () => {
5479
+ this.remove();
5480
+ if( !that.notifications.childElementCount )
5481
+ {
5482
+ that.notifications.style.width = "unset";
5483
+ that.notifications.iWidth = 0;
5484
+ }
5485
+ }, 500 );
5486
+ };
5487
+
5488
+ if( options.closable ?? true )
5489
+ {
5490
+ const closeIcon = LX.makeIcon( "X", { iconClass: "closer" } );
5491
+ closeIcon.addEventListener( "click", () => {
5492
+ toast.close();
5493
+ } );
5494
+ toast.appendChild( closeIcon );
5495
+ }
5496
+
5497
+ const timeout = options.timeout ?? 3000;
5498
+
5499
+ if( timeout != -1 )
5500
+ {
5501
+ LX.doAsync( () => {
5502
+ toast.close();
5503
+ }, timeout );
5504
+ }
5505
+ }
5506
+
5507
+ LX.toast = toast;
5508
+
5509
+ /**
5510
+ * @method badge
5511
+ * @param {String} text
5512
+ * @param {String} className
5513
+ * @param {Object} options
5514
+ * style: Style attributes to override
5515
+ * asElement: Returns the badge as HTMLElement [false]
5516
+ */
5517
+
5518
+ function badge( text, className, options = {} )
5519
+ {
5520
+ const container = document.createElement( "div" );
5521
+ container.innerHTML = text;
5522
+ container.className = "lexbadge " + ( className ?? "" );
5523
+ Object.assign( container.style, options.style ?? {} );
5524
+ return ( options.asElement ?? false ) ? container : container.outerHTML;
5525
+ }
5526
+
5527
+ LX.badge = badge;
5528
+
5529
+ /**
5530
+ * @method makeElement
5531
+ * @param {String} htmlType
5532
+ * @param {String} className
5533
+ * @param {String} innerHTML
5534
+ * @param {HTMLElement} parent
5535
+ * @param {Object} overrideStyle
5536
+ */
5537
+
5538
+ function makeElement( htmlType, className, innerHTML, parent, overrideStyle = {} )
5539
+ {
5540
+ const element = document.createElement( htmlType );
5541
+ element.className = className ?? "";
5542
+ element.innerHTML = innerHTML ?? "";
5543
+ Object.assign( element.style, overrideStyle );
5544
+
5545
+ if( parent )
5546
+ {
5547
+ if( parent.attach ) // Use attach method if possible
5548
+ {
5549
+ parent.attach( element );
5550
+ }
5551
+ else // its a native HTMLElement
5552
+ {
5553
+ parent.appendChild( element );
5554
+ }
5555
+ }
5556
+
5557
+ return element;
5558
+ }
5559
+
5560
+ LX.makeElement = makeElement;
5561
+
5562
+ /**
5563
+ * @method makeContainer
5564
+ * @param {Array} size
5565
+ * @param {String} className
5566
+ * @param {String} innerHTML
5567
+ * @param {HTMLElement} parent
5568
+ * @param {Object} overrideStyle
5569
+ */
5570
+
5571
+ function makeContainer( size, className, innerHTML, parent, overrideStyle = {} )
5572
+ {
5573
+ const container = LX.makeElement( "div", "lexcontainer " + ( className ?? "" ), innerHTML, parent, overrideStyle );
5574
+ container.style.width = size && size[ 0 ] ? size[ 0 ] : "100%";
5575
+ container.style.height = size && size[ 1 ] ? size[ 1 ] : "100%";
5576
+ return container;
5577
+ }
5578
+
5579
+ LX.makeContainer = makeContainer;
5580
+
5581
+ /**
5582
+ * @method asTooltip
5583
+ * @param {HTMLElement} trigger
5584
+ * @param {String} content
5585
+ * @param {Object} options
5586
+ * side: Side of the tooltip
5587
+ * offset: Tooltip margin offset
5588
+ * active: Tooltip active by default [true]
5589
+ */
5590
+
5591
+ function asTooltip( trigger, content, options = {} )
5592
+ {
5593
+ console.assert( trigger, "You need a trigger to generate a tooltip!" );
5594
+
5595
+ trigger.dataset[ "disableTooltip" ] = !( options.active ?? true );
5596
+
5597
+ let tooltipDom = null;
5598
+
5599
+ trigger.addEventListener( "mouseenter", function(e) {
5600
+
5601
+ if( trigger.dataset[ "disableTooltip" ] == "true" )
5602
+ {
5603
+ return;
5604
+ }
5605
+
5606
+ LX.root.querySelectorAll( ".lextooltip" ).forEach( e => e.remove() );
5607
+
5608
+ tooltipDom = document.createElement( "div" );
5609
+ tooltipDom.className = "lextooltip";
5610
+ tooltipDom.innerHTML = content;
5611
+
5612
+ LX.doAsync( () => {
5613
+
5614
+ const position = [ 0, 0 ];
5615
+ const rect = this.getBoundingClientRect();
5616
+ const offset = options.offset ?? 6;
5617
+ let alignWidth = true;
5618
+
5619
+ switch( options.side ?? "top" )
5620
+ {
5621
+ case "left":
5622
+ position[ 0 ] += ( rect.x - tooltipDom.offsetWidth - offset );
5623
+ alignWidth = false;
5624
+ break;
5625
+ case "right":
5626
+ position[ 0 ] += ( rect.x + rect.width + offset );
5627
+ alignWidth = false;
5628
+ break;
5629
+ case "top":
5630
+ position[ 1 ] += ( rect.y - tooltipDom.offsetHeight - offset );
5631
+ alignWidth = true;
5632
+ break;
5633
+ case "bottom":
5634
+ position[ 1 ] += ( rect.y + rect.height + offset );
5635
+ alignWidth = true;
5636
+ break;
5637
+ }
5638
+
5639
+ if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - tooltipDom.offsetWidth * 0.5; }
5640
+ else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - tooltipDom.offsetHeight * 0.5; }
5641
+
5642
+ // Avoid collisions
5643
+ position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - tooltipDom.offsetWidth - 4 );
5644
+ position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - tooltipDom.offsetHeight - 4 );
5645
+
5646
+ tooltipDom.style.left = `${ position[ 0 ] }px`;
5647
+ tooltipDom.style.top = `${ position[ 1 ] }px`;
5648
+ } );
5649
+
5650
+ LX.root.appendChild( tooltipDom );
5651
+ } );
5652
+
5653
+ trigger.addEventListener( "mouseleave", function(e) {
5654
+ if( tooltipDom )
5655
+ {
5656
+ tooltipDom.remove();
5657
+ }
5658
+ } );
5659
+ }
5660
+
5661
+ LX.asTooltip = asTooltip;
5662
+
5663
+ /*
5664
+ * Requests
5665
+ */
5666
+
5667
+ Object.assign(LX, {
5668
+
5669
+ /**
5670
+ * Request file from url (it could be a binary, text, etc.). If you want a simplied version use
5671
+ * @method request
5672
+ * @param {Object} request object with all the parameters like data (for sending forms), dataType, success, error
5673
+ * @param {Function} on_complete
5674
+ **/
5675
+ request( request ) {
5676
+
5677
+ var dataType = request.dataType || "text";
5678
+ if(dataType == "json") //parse it locally
5679
+ dataType = "text";
5680
+ else if(dataType == "xml") //parse it locally
5681
+ dataType = "text";
5682
+ else if (dataType == "binary")
5683
+ {
5684
+ //request.mimeType = "text/plain; charset=x-user-defined";
5685
+ dataType = "arraybuffer";
5686
+ request.mimeType = "application/octet-stream";
5687
+ }
5688
+
5689
+ //regular case, use AJAX call
5690
+ var xhr = new XMLHttpRequest();
5691
+ xhr.open( request.data ? 'POST' : 'GET', request.url, true);
5692
+ if(dataType)
5693
+ xhr.responseType = dataType;
5694
+ if (request.mimeType)
5695
+ xhr.overrideMimeType( request.mimeType );
5696
+ if( request.nocache )
5697
+ xhr.setRequestHeader('Cache-Control', 'no-cache');
5698
+
5699
+ xhr.onload = function(load)
5700
+ {
5701
+ var response = this.response;
5702
+ if( this.status != 200)
5703
+ {
5704
+ var err = "Error " + this.status;
5705
+ if(request.error)
5706
+ request.error(err);
5707
+ return;
5708
+ }
5709
+
5710
+ if(request.dataType == "json") //chrome doesnt support json format
5711
+ {
5712
+ try
5713
+ {
5714
+ response = JSON.parse(response);
5715
+ }
5716
+ catch (err)
5717
+ {
5718
+ if(request.error)
5719
+ request.error(err);
5720
+ else
5721
+ throw err;
5722
+ }
5723
+ }
5724
+ else if(request.dataType == "xml")
5725
+ {
5726
+ try
5727
+ {
5728
+ var xmlparser = new DOMParser();
5729
+ response = xmlparser.parseFromString(response,"text/xml");
5730
+ }
5731
+ catch (err)
5732
+ {
5733
+ if(request.error)
5734
+ request.error(err);
5735
+ else
5736
+ throw err;
5737
+ }
5738
+ }
5739
+ if(request.success)
5740
+ request.success.call(this, response, this);
5741
+ };
5742
+ xhr.onerror = function(err) {
5743
+ if(request.error)
5744
+ request.error(err);
5745
+ };
5746
+
5747
+ var data = new FormData();
5748
+ if( request.data )
5749
+ {
5750
+ for( var i in request.data)
5751
+ data.append(i,request.data[ i ]);
5752
+ }
5753
+
5754
+ xhr.send( data );
5755
+ return xhr;
5756
+ },
5757
+
5758
+ /**
5759
+ * Request file from url
5760
+ * @method requestText
5761
+ * @param {String} url
5762
+ * @param {Function} onComplete
5763
+ * @param {Function} onError
5764
+ **/
5765
+ requestText( url, onComplete, onError ) {
5766
+ return this.request({ url: url, dataType:"text", success: onComplete, error: onError });
5767
+ },
5768
+
5769
+ /**
5770
+ * Request file from url
5771
+ * @method requestJSON
5772
+ * @param {String} url
5773
+ * @param {Function} onComplete
5774
+ * @param {Function} onError
5775
+ **/
5776
+ requestJSON( url, onComplete, onError ) {
5777
+ return this.request({ url: url, dataType:"json", success: onComplete, error: onError });
5778
+ },
5779
+
5780
+ /**
5781
+ * Request binary file from url
5782
+ * @method requestBinary
5783
+ * @param {String} url
5784
+ * @param {Function} onComplete
5785
+ * @param {Function} onError
5786
+ **/
5787
+ requestBinary( url, onComplete, onError ) {
5788
+ return this.request({ url: url, dataType:"binary", success: onComplete, error: onError });
5789
+ },
5790
+
5791
+ /**
5792
+ * Request script and inserts it in the DOM
5793
+ * @method requireScript
5794
+ * @param {String|Array} url the url of the script or an array containing several urls
5795
+ * @param {Function} onComplete
5796
+ * @param {Function} onError
5797
+ * @param {Function} onProgress (if several files are required, onProgress is called after every file is added to the DOM)
5798
+ **/
5799
+ requireScript( url, onComplete, onError, onProgress, version ) {
5770
5800
 
5771
- const path = document.createElement( "path" );
5772
- path.setAttribute( "fill", "currentColor" );
5773
- path.setAttribute( "d", data[ 4 ] );
5774
- svg.appendChild( path );
5801
+ if(!url)
5802
+ throw("invalid URL");
5775
5803
 
5776
- if( data[ 5 ] )
5777
- {
5778
- const classes = data[ 5 ].pathClass;
5779
- classes?.split( ' ' ).forEach( c => {
5780
- path.classList.add( c );
5781
- } );
5804
+ if( url.constructor === String )
5805
+ url = [url];
5782
5806
 
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
- }
5807
+ var total = url.length;
5808
+ var loaded_scripts = [];
5789
5809
 
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 );
5810
+ for( var i in url)
5811
+ {
5812
+ var script = document.createElement('script');
5813
+ script.num = i;
5814
+ script.type = 'text/javascript';
5815
+ script.src = url[ i ] + ( version ? "?version=" + version : "" );
5816
+ script.original_src = url[ i ];
5817
+ script.async = false;
5818
+ script.onload = function( e ) {
5819
+ total--;
5820
+ loaded_scripts.push(this);
5821
+ if(total)
5822
+ {
5823
+ if( onProgress )
5824
+ {
5825
+ onProgress( this.original_src, this.num );
5826
+ }
5827
+ }
5828
+ else if(onComplete)
5829
+ onComplete( loaded_scripts );
5830
+ };
5831
+ if(onError)
5832
+ script.onerror = function(err) {
5833
+ onError(err, this.original_src, this.num );
5834
+ };
5835
+ document.getElementsByTagName('head')[ 0 ].appendChild(script);
5793
5836
  }
5794
- }
5795
-
5796
- // Fallback to Lucide icon
5797
- console.assert( lucideData, `No existing icon named _${ iconName }_` );
5798
- svg = lucide.createElement( lucideData, options );
5837
+ },
5799
5838
 
5800
- return _createIconFromSVG( svg );
5801
- }
5839
+ loadScriptSync( url ) {
5840
+ return new Promise((resolve, reject) => {
5841
+ const script = document.createElement( "script" );
5842
+ script.src = url;
5843
+ script.async = false;
5844
+ script.onload = () => resolve();
5845
+ script.onerror = () => reject(new Error(`Failed to load ${url}`));
5846
+ document.head.appendChild( script );
5847
+ });
5848
+ },
5802
5849
 
5803
- LX.makeIcon = makeIcon;
5850
+ downloadURL( url, filename ) {
5804
5851
 
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' );
5852
+ const fr = new FileReader();
5819
5853
 
5820
- let svgAttributes = [];
5821
- let pathAttributes = [];
5854
+ const _download = function(_url) {
5855
+ var link = document.createElement('a');
5856
+ link.href = _url;
5857
+ link.download = filename;
5858
+ document.body.appendChild(link);
5859
+ link.click();
5860
+ document.body.removeChild(link);
5861
+ };
5822
5862
 
5823
- for( const attr of svg.attributes )
5824
- {
5825
- switch( attr.name )
5863
+ if( url.includes('http') )
5826
5864
  {
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;
5865
+ LX.request({ url: url, dataType: 'blob', success: (f) => {
5866
+ fr.readAsDataURL( f );
5867
+ fr.onload = e => {
5868
+ _download(e.currentTarget.result);
5869
+ };
5870
+ } });
5871
+ }else
5872
+ {
5873
+ _download(url);
5834
5874
  }
5835
- }
5836
5875
 
5837
- for( const attr of path.attributes )
5838
- {
5839
- switch( attr.name )
5876
+ },
5877
+
5878
+ downloadFile: function( filename, data, dataType ) {
5879
+ if(!data)
5840
5880
  {
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;
5881
+ console.warn("No file provided to download");
5882
+ return;
5848
5883
  }
5849
- }
5850
5884
 
5851
- const iconData = [
5852
- parseInt( viewBox[ 2 ] ),
5853
- parseInt( viewBox[ 3 ] ),
5854
- aliases,
5855
- variant,
5856
- pathData,
5885
+ if(!dataType)
5857
5886
  {
5858
- svgAttributes: svgAttributes.length ? svgAttributes.join( ' ' ) : null,
5859
- pathAttributes: pathAttributes.length ? pathAttributes.join( ' ' ) : null
5887
+ if(data.constructor === String )
5888
+ dataType = 'text/plain';
5889
+ else
5890
+ dataType = 'application/octet-stream';
5860
5891
  }
5861
- ];
5862
5892
 
5863
- if( LX.ICONS[ iconName ] )
5864
- {
5865
- console.warn( `${ iconName } will be added/replaced in LX.ICONS` );
5893
+ var file = null;
5894
+ if(data.constructor !== File && data.constructor !== Blob)
5895
+ file = new Blob( [ data ], {type : dataType});
5896
+ else
5897
+ file = data;
5898
+
5899
+ var url = URL.createObjectURL( file );
5900
+ var element = document.createElement("a");
5901
+ element.setAttribute('href', url);
5902
+ element.setAttribute('download', filename );
5903
+ element.style.display = 'none';
5904
+ document.body.appendChild(element);
5905
+ element.click();
5906
+ document.body.removeChild(element);
5907
+ setTimeout( function(){ URL.revokeObjectURL( url ); }, 1000*60 ); //wait one minute to revoke url
5866
5908
  }
5909
+ });
5867
5910
 
5868
- LX.ICONS[ iconName ] = iconData;
5911
+ /**
5912
+ * @method compareThreshold
5913
+ * @param {String} url
5914
+ * @param {Function} onComplete
5915
+ * @param {Function} onError
5916
+ **/
5917
+ function compareThreshold( v, p, n, t )
5918
+ {
5919
+ return Math.abs( v - p ) >= t || Math.abs( v - n ) >= t;
5869
5920
  }
5870
5921
 
5871
- LX.registerIcon = registerIcon;
5922
+ LX.compareThreshold = compareThreshold;
5872
5923
 
5873
5924
  /**
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 )
5925
+ * @method compareThresholdRange
5926
+ * @param {String} url
5927
+ * @param {Function} onComplete
5928
+ * @param {Function} onError
5929
+ **/
5930
+ function compareThresholdRange( v0, v1, t0, t1 )
5880
5931
  {
5881
- LX.extraCommandbarEntries.push( { name, callback } );
5932
+ return v0 >= t0 && v0 <= t1 || v1 >= t0 && v1 <= t1 || v0 <= t0 && v1 >= t1;
5882
5933
  }
5883
5934
 
5884
- LX.registerCommandbarEntry = registerCommandbarEntry;
5935
+ LX.compareThresholdRange = compareThresholdRange;
5885
5936
 
5886
5937
  /**
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.
5938
+ * @method getControlPoints
5939
+ * @param {String} url
5940
+ * @param {Function} onComplete
5941
+ * @param {Function} onError
5942
+ **/
5943
+ function getControlPoints( x0, y0, x1, y1, x2, y2, t )
5944
+ {
5945
+ // x0,y0,x1,y1 are the coordinates of the end (knot) pts of this segment
5946
+ // x2,y2 is the next knot -- not connected here but needed to calculate p2
5947
+ // p1 is the control point calculated here, from x1 back toward x0.
5948
+ // p2 is the next control point, calculated here and returned to become the
5949
+ // next segment's p1.
5950
+ // t is the 'tension' which controls how far the control points spread.
5911
5951
 
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));
5952
+ // Scaling factors: distances from this knot to the previous and following knots.
5953
+ var d01=Math.sqrt(Math.pow(x1-x0,2)+Math.pow(y1-y0,2));
5954
+ var d12=Math.sqrt(Math.pow(x2-x1,2)+Math.pow(y2-y1,2));
5915
5955
 
5916
- var fa=t*d01/(d01+d12);
5917
- var fb=t-fa;
5956
+ var fa=t*d01/(d01+d12);
5957
+ var fb=t-fa;
5918
5958
 
5919
- var p1x=x1+fa*(x0-x2);
5920
- var p1y=y1+fa*(y0-y2);
5959
+ var p1x=x1+fa*(x0-x2);
5960
+ var p1y=y1+fa*(y0-y2);
5921
5961
 
5922
- var p2x=x1-fb*(x0-x2);
5923
- var p2y=y1-fb*(y0-y2);
5962
+ var p2x=x1-fb*(x0-x2);
5963
+ var p2y=y1-fb*(y0-y2);
5924
5964
 
5925
- return [p1x,p1y,p2x,p2y]
5926
- },
5927
- drawSpline( ctx, pts, t ) {
5965
+ return [p1x,p1y,p2x,p2y]
5966
+ }
5928
5967
 
5929
- ctx.save();
5930
- var cp = []; // array of control points, as x0,y0,x1,y1,...
5931
- var n = pts.length;
5968
+ LX.getControlPoints = getControlPoints;
5932
5969
 
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
- }
5970
+ /**
5971
+ * @method drawSpline
5972
+ * @param {CanvasRenderingContext2D} ctx
5973
+ * @param {Array} pts
5974
+ * @param {Number} t
5975
+ **/
5976
+ function drawSpline( ctx, pts, t )
5977
+ {
5978
+ ctx.save();
5979
+ var cp = []; // array of control points, as x0,y0,x1,y1,...
5980
+ var n = pts.length;
5938
5981
 
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
- }
5982
+ // Draw an open curve, not connected at the ends
5983
+ for( var i = 0; i < (n - 4); i += 2 )
5984
+ {
5985
+ cp = cp.concat(LX.getControlPoints(pts[ i ],pts[i+1],pts[i+2],pts[i+3],pts[i+4],pts[i+5],t));
5986
+ }
5947
5987
 
5948
- // For open curves the first and last arcs are simple quadratics.
5988
+ for( var i = 2; i < ( pts.length - 5 ); i += 2 )
5989
+ {
5949
5990
  ctx.beginPath();
5950
- ctx.moveTo( pts[ 0 ], pts[ 1 ] );
5951
- ctx.quadraticCurveTo( cp[ 0 ], cp[ 1 ], pts[ 2 ], pts[ 3 ]);
5991
+ ctx.moveTo(pts[ i ], pts[i+1]);
5992
+ ctx.bezierCurveTo(cp[2*i-2],cp[2*i-1],cp[2*i],cp[2*i+1],pts[i+2],pts[i+3]);
5952
5993
  ctx.stroke();
5953
5994
  ctx.closePath();
5995
+ }
5954
5996
 
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();
5997
+ // For open curves the first and last arcs are simple quadratics.
5998
+ ctx.beginPath();
5999
+ ctx.moveTo( pts[ 0 ], pts[ 1 ] );
6000
+ ctx.quadraticCurveTo( cp[ 0 ], cp[ 1 ], pts[ 2 ], pts[ 3 ]);
6001
+ ctx.stroke();
6002
+ ctx.closePath();
5960
6003
 
5961
- ctx.restore();
5962
- }
5963
- };
6004
+ ctx.beginPath();
6005
+ ctx.moveTo( pts[ n-2 ], pts[ n-1 ] );
6006
+ ctx.quadraticCurveTo( cp[ 2*n-10 ], cp[ 2*n-9 ], pts[ n-4 ], pts[ n-3 ]);
6007
+ ctx.stroke();
6008
+ ctx.closePath();
6009
+
6010
+ ctx.restore();
6011
+ }
6012
+
6013
+ LX.drawSpline = drawSpline;
5964
6014
 
5965
6015
  // area.js @jxarco
5966
6016
 
@@ -6685,7 +6735,7 @@ class Area {
6685
6735
 
6686
6736
  addSidebar( callback, options = {} ) {
6687
6737
 
6688
- let sidebar = new LX.Sidebar( options );
6738
+ let sidebar = new LX.Sidebar( { callback, ...options } );
6689
6739
 
6690
6740
  if( callback )
6691
6741
  {
@@ -13943,6 +13993,7 @@ class Sidebar {
13943
13993
 
13944
13994
  this.root = document.createElement( "div" );
13945
13995
  this.root.className = "lexsidebar " + ( options.className ?? "" );
13996
+ this.callback = options.callback ?? null;
13946
13997
 
13947
13998
  this._displaySelected = options.displaySelected ?? false;
13948
13999
 
@@ -13959,17 +14010,19 @@ class Sidebar {
13959
14010
  configurable: true
13960
14011
  });
13961
14012
 
14013
+ const mobile = navigator && /Android|iPhone/i.test( navigator.userAgent );
14014
+
13962
14015
  this.side = options.side ?? "left";
13963
14016
  this.collapsable = options.collapsable ?? true;
13964
14017
  this._collapseWidth = ( options.collapseToIcons ?? true ) ? "58px" : "0px";
13965
- this.collapsed = false;
14018
+ this.collapsed = options.collapsed ?? mobile;
13966
14019
 
13967
14020
  this.filterString = "";
13968
14021
 
13969
14022
  LX.doAsync( () => {
13970
14023
 
13971
14024
  this.root.parentElement.ogWidth = this.root.parentElement.style.width;
13972
- this.root.parentElement.style.transition = "width 0.25s ease-out";
14025
+ this.root.parentElement.style.transition = this.collapsed ? "" : "width 0.25s ease-out";
13973
14026
 
13974
14027
  this.resizeObserver = new ResizeObserver( entries => {
13975
14028
  for ( const entry of entries )
@@ -13978,6 +14031,24 @@ class Sidebar {
13978
14031
  }
13979
14032
  });
13980
14033
 
14034
+ if( this.collapsed )
14035
+ {
14036
+ this.root.classList.toggle( "collapsed", this.collapsed );
14037
+ this.root.parentElement.style.width = this._collapseWidth;
14038
+
14039
+ if( !this.resizeObserver )
14040
+ {
14041
+ throw( "Wait until ResizeObserver has been created!" );
14042
+ }
14043
+
14044
+ this.resizeObserver.observe( this.root.parentElement );
14045
+
14046
+ LX.doAsync( () => {
14047
+ this.resizeObserver.unobserve( this.root.parentElement );
14048
+ this.root.querySelectorAll( ".lexsidebarentrycontent" ).forEach( e => e.dataset[ "disableTooltip" ] = !this.collapsed );
14049
+ }, 10 );
14050
+ }
14051
+
13981
14052
  }, 10 );
13982
14053
 
13983
14054
  // Header
@@ -13993,11 +14064,29 @@ class Sidebar {
13993
14064
  const icon = LX.makeIcon( this.side == "left" ? "PanelLeft" : "PanelRight", { title: "Toggle Sidebar", iconClass: "toggler" } );
13994
14065
  this.header.appendChild( icon );
13995
14066
 
13996
- icon.addEventListener( "click", (e) => {
13997
- e.preventDefault();
13998
- e.stopPropagation();
13999
- this.toggleCollapsed();
14000
- } );
14067
+ if( mobile )
14068
+ {
14069
+ // create an area and append a sidebar:
14070
+ const area = new LX.Area({ skipAppend: true });
14071
+ const sheetSidebarOptions = LX.deepCopy( options );
14072
+ sheetSidebarOptions.collapsed = false;
14073
+ sheetSidebarOptions.collapsable = false;
14074
+ area.addSidebar( this.callback, sheetSidebarOptions );
14075
+
14076
+ icon.addEventListener( "click", e => {
14077
+ e.preventDefault();
14078
+ e.stopPropagation();
14079
+ new LX.Sheet("256px", [ area ], { side: this.side } );
14080
+ } );
14081
+ }
14082
+ else
14083
+ {
14084
+ icon.addEventListener( "click", e => {
14085
+ e.preventDefault();
14086
+ e.stopPropagation();
14087
+ this.toggleCollapsed();
14088
+ } );
14089
+ }
14001
14090
  }
14002
14091
  }
14003
14092