lexgui 0.5.8 → 0.5.9

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
@@ -12,7 +12,7 @@ console.warn( 'Script _build/lexgui.js_ is depracated and will be removed soon.
12
12
  */
13
13
 
14
14
  var LX = {
15
- version: "0.5.8",
15
+ version: "0.5.9",
16
16
  ready: false,
17
17
  components: [], // Specific pre-build components
18
18
  signals: {}, // Events and triggers
@@ -779,7 +779,7 @@ LX.makeKbd = makeKbd;
779
779
  * @description Gets an SVG element using one of LX.ICONS
780
780
  * @param {String} iconName
781
781
  * @param {Object} options
782
- * iconTitle
782
+ * title
783
783
  * extraClass
784
784
  * svgClass
785
785
  */
@@ -788,7 +788,7 @@ function makeIcon( iconName, options = { } )
788
788
  let data = LX.ICONS[ iconName ];
789
789
  console.assert( data, `No icon named _${ iconName }_` );
790
790
 
791
- const iconTitle = options.iconTitle;
791
+ const iconTitle = options.title;
792
792
  const iconClass = options.iconClass;
793
793
  const svgClass = options.svgClass;
794
794
 
@@ -1647,7 +1647,7 @@ function toast( title, description, options = {} )
1647
1647
  if( options.action )
1648
1648
  {
1649
1649
  const panel = new Panel();
1650
- panel.addButton(null, options.action.name ?? "Accept", options.action.callback.bind( this, toast ), { width: "auto", maxWidth: "150px", className: "right", buttonClass: "outline" });
1650
+ panel.addButton(null, options.action.name ?? "Accept", options.action.callback.bind( this, toast ), { width: "auto", maxWidth: "150px", className: "right", buttonClass: "border" });
1651
1651
  toast.appendChild( panel.root.childNodes[ 0 ] );
1652
1652
  }
1653
1653
 
@@ -1947,6 +1947,158 @@ LX.addSignal = addSignal;
1947
1947
  * DOM Elements
1948
1948
  */
1949
1949
 
1950
+ /**
1951
+ * @class Popover
1952
+ */
1953
+
1954
+ class Popover {
1955
+
1956
+ static activeElement = false;
1957
+
1958
+ constructor( trigger, content, options = {} ) {
1959
+
1960
+ console.assert( trigger, "Popover needs a DOM element as trigger!" );
1961
+
1962
+ if( Popover.activeElement )
1963
+ {
1964
+ Popover.activeElement.destroy();
1965
+ return;
1966
+ }
1967
+
1968
+ this._trigger = trigger;
1969
+ trigger.classList.add( "triggered" );
1970
+ trigger.active = this;
1971
+
1972
+ this._windowPadding = 4;
1973
+ this.side = options.side ?? "bottom";
1974
+ this.align = options.align ?? "center";
1975
+ this.avoidCollisions = options.avoidCollisions ?? true;
1976
+
1977
+ this.root = document.createElement( "div" );
1978
+ this.root.dataset["side"] = this.side;
1979
+ this.root.tabIndex = "1";
1980
+ this.root.className = "lexpopover";
1981
+ LX.root.appendChild( this.root );
1982
+
1983
+ this.root.addEventListener( "keydown", (e) => {
1984
+ if( e.key == "Escape" )
1985
+ {
1986
+ e.preventDefault();
1987
+ e.stopPropagation();
1988
+ this.destroy();
1989
+ }
1990
+ } )
1991
+
1992
+ if( content )
1993
+ {
1994
+ content = [].concat( content );
1995
+ content.forEach( e => {
1996
+ const domNode = e.root ?? e;
1997
+ this.root.appendChild( domNode );
1998
+ if( e.onPopover )
1999
+ {
2000
+ e.onPopover();
2001
+ }
2002
+ } );
2003
+ }
2004
+
2005
+ Popover.activeElement = this;
2006
+
2007
+ doAsync( () => {
2008
+ this._adjustPosition();
2009
+
2010
+ this.root.focus();
2011
+
2012
+ this._onClick = e => {
2013
+ if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
2014
+ {
2015
+ return;
2016
+ }
2017
+ this.destroy();
2018
+ };
2019
+
2020
+ document.body.addEventListener( "mousedown", this._onClick, true );
2021
+ document.body.addEventListener( "focusin", this._onClick, true );
2022
+ }, 10 );
2023
+ }
2024
+
2025
+ destroy() {
2026
+
2027
+ this._trigger.classList.remove( "triggered" );
2028
+
2029
+ delete this._trigger.active;
2030
+
2031
+ document.body.removeEventListener( "click", this._onClick );
2032
+
2033
+ this.root.remove();
2034
+
2035
+ Popover.activeElement = null;
2036
+ }
2037
+
2038
+ _adjustPosition() {
2039
+
2040
+ const position = [ 0, 0 ];
2041
+
2042
+ // Place menu using trigger position and user options
2043
+ {
2044
+ const rect = this._trigger.getBoundingClientRect();
2045
+
2046
+ let alignWidth = true;
2047
+
2048
+ switch( this.side )
2049
+ {
2050
+ case "left":
2051
+ position[ 0 ] += ( rect.x - this.root.offsetWidth );
2052
+ alignWidth = false;
2053
+ break;
2054
+ case "right":
2055
+ position[ 0 ] += ( rect.x + rect.width );
2056
+ alignWidth = false;
2057
+ break;
2058
+ case "top":
2059
+ position[ 1 ] += ( rect.y - this.root.offsetHeight );
2060
+ alignWidth = true;
2061
+ break;
2062
+ case "bottom":
2063
+ position[ 1 ] += ( rect.y + rect.height );
2064
+ alignWidth = true;
2065
+ break;
2066
+ default:
2067
+ break;
2068
+ }
2069
+
2070
+ switch( this.align )
2071
+ {
2072
+ case "start":
2073
+ if( alignWidth ) { position[ 0 ] += rect.x; }
2074
+ else { position[ 1 ] += rect.y; }
2075
+ break;
2076
+ case "center":
2077
+ if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - this.root.offsetWidth * 0.5; }
2078
+ else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - this.root.offsetHeight * 0.5; }
2079
+ break;
2080
+ case "end":
2081
+ if( alignWidth ) { position[ 0 ] += rect.x - this.root.offsetWidth + rect.width; }
2082
+ else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
2083
+ break;
2084
+ default:
2085
+ break;
2086
+ }
2087
+ }
2088
+
2089
+ if( this.avoidCollisions )
2090
+ {
2091
+ position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - this.root.offsetWidth - this._windowPadding );
2092
+ position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - this.root.offsetHeight - this._windowPadding );
2093
+ }
2094
+
2095
+ this.root.style.left = `${ position[ 0 ] }px`;
2096
+ this.root.style.top = `${ position[ 1 ] }px`;
2097
+ }
2098
+ };
2099
+
2100
+ LX.Popover = Popover;
2101
+
1950
2102
  /**
1951
2103
  * @class DropdownMenu
1952
2104
  */
@@ -2244,14 +2396,8 @@ class ColorPicker {
2244
2396
 
2245
2397
  static currentPicker = false;
2246
2398
 
2247
- constructor( hexValue, trigger, options = {} ) {
2248
-
2249
- console.assert( trigger, "ColorPicker needs a DOM element as trigger!" );
2399
+ constructor( hexValue, options = {} ) {
2250
2400
 
2251
- this._windowPadding = 4;
2252
- this.side = options.side ?? "bottom";
2253
- this.align = options.align ?? "center";
2254
- this.avoidCollisions = options.avoidCollisions ?? true;
2255
2401
  this.colorModel = options.colorModel ?? "Hex";
2256
2402
  this.useAlpha = options.useAlpha ?? false;
2257
2403
  this.callback = options.onChange;
@@ -2261,32 +2407,8 @@ class ColorPicker {
2261
2407
  console.warn( "Define a callback in _options.onChange_ to allow getting new Color values!" );
2262
2408
  }
2263
2409
 
2264
- if( ColorPicker.currentPicker )
2265
- {
2266
- ColorPicker.currentPicker.destroy();
2267
- return;
2268
- }
2269
-
2270
- this._trigger = trigger;
2271
- trigger.classList.add( "triggered" );
2272
- trigger.picker = this;
2273
-
2274
2410
  this.root = document.createElement( "div" );
2275
- this.root.tabIndex = "1";
2276
2411
  this.root.className = "lexcolorpicker";
2277
- this.root.dataset["side"] = this.side;
2278
- LX.root.appendChild( this.root );
2279
-
2280
- this.root.addEventListener( "keydown", (e) => {
2281
- if( e.key == "Escape" )
2282
- {
2283
- e.preventDefault();
2284
- e.stopPropagation();
2285
- this.destroy();
2286
- }
2287
- } )
2288
-
2289
- ColorPicker.currentPicker = this;
2290
2412
 
2291
2413
  this.markerHalfSize = 8;
2292
2414
  this.markerSize = this.markerHalfSize * 2;
@@ -2305,8 +2427,6 @@ class ColorPicker {
2305
2427
  this.intSatMarker.style.backgroundColor = this.currentColor.hex;
2306
2428
  this.colorPickerBackground.appendChild( this.intSatMarker );
2307
2429
 
2308
- doAsync( this._svToPosition.bind( this, this.currentColor.hsv.s, this.currentColor.hsv.v ) );
2309
-
2310
2430
  let pickerRect = null;
2311
2431
 
2312
2432
  let innerMouseDown = e => {
@@ -2386,11 +2506,6 @@ class ColorPicker {
2386
2506
  this.hueMarker.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2387
2507
  this.colorPickerTracker.appendChild( this.hueMarker );
2388
2508
 
2389
- doAsync( () => {
2390
- const hueLeft = LX.remapRange( this.currentColor.hsv.h, 0, 360, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2391
- this.hueMarker.style.left = hueLeft + "px";
2392
- } );
2393
-
2394
2509
  const _fromHueX = ( hueX ) => {
2395
2510
  this.hueMarker.style.left = hueX + "px";
2396
2511
  this.currentColor.hsv.h = LX.remapRange( hueX, 0, this.colorPickerTracker.offsetWidth - this.markerSize, 0, 360 );
@@ -2453,11 +2568,6 @@ class ColorPicker {
2453
2568
  this.alphaMarker.style.backgroundColor = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b },${ this.currentColor.css.a })`;
2454
2569
  this.alphaTracker.appendChild( this.alphaMarker );
2455
2570
 
2456
- doAsync( () => {
2457
- const alphaLeft = LX.remapRange( this.currentColor.hsv.a, 0, 1, 0, this.alphaTracker.offsetWidth - this.markerSize );
2458
- this.alphaMarker.style.left = alphaLeft + "px";
2459
- } );
2460
-
2461
2571
  const _fromAlphaX = ( alphaX ) => {
2462
2572
  this.alphaMarker.style.left = alphaX + "px";
2463
2573
  this.currentColor.hsv.a = LX.remapRange( alphaX, 0, this.alphaTracker.offsetWidth - this.markerSize, 0, 1 );
@@ -2535,62 +2645,34 @@ class ColorPicker {
2535
2645
 
2536
2646
  this._updateColorValue( hexValue, true );
2537
2647
 
2538
- doAsync( () => {
2539
- this._adjustPosition();
2540
-
2541
- this.root.focus();
2542
-
2543
- this._onClick = e => {
2544
- if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
2545
- {
2546
- return;
2547
- }
2548
- this.destroy();
2549
- };
2648
+ doAsync( this._placeMarkers.bind( this ) );
2550
2649
 
2551
- document.body.addEventListener( "mousedown", this._onClick, true );
2552
- document.body.addEventListener( "focusin", this._onClick, true );
2553
- }, 10 );
2650
+ this.onPopover = this._placeMarkers.bind( this );
2554
2651
  }
2555
2652
 
2556
- fromHexColor( hexColor ) {
2557
-
2558
- this.currentColor.setHex( hexColor );
2559
-
2560
- // Decompose into HSV
2561
- const { h, s, v } = this.currentColor.hsv;
2562
- this._svToPosition( s, v );
2653
+ _placeMarkers() {
2563
2654
 
2564
- const hueColor = new Color( { h, s: 1, v: 1 } );
2565
- this.hueMarker.style.backgroundColor = this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2566
- this.hueMarker.style.left = LX.remapRange( h, 0, 360, -this.markerHalfSize, this.colorPickerTracker.offsetWidth - this.markerHalfSize ) + "px";
2655
+ this._svToPosition( this.currentColor.hsv.s, this.currentColor.hsv.v );
2567
2656
 
2568
- this._updateColorValue( hexColor );
2569
- }
2570
-
2571
- destroy() {
2657
+ const hueLeft = LX.remapRange( this.currentColor.hsv.h, 0, 360, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2658
+ this.hueMarker.style.left = hueLeft + "px";
2572
2659
 
2573
- this._trigger.classList.remove( "triggered" );
2574
-
2575
- delete this._trigger.picker;
2576
-
2577
- document.body.removeEventListener( "mousedown", this._onClick, true );
2578
- document.body.removeEventListener( "focusin", this._onClick, true );
2579
-
2580
- LX.root.querySelectorAll( ".lexcolorpicker" ).forEach( m => { m.remove(); } );
2581
-
2582
- ColorPicker.currentPicker = null;
2660
+ if( this.useAlpha )
2661
+ {
2662
+ const alphaLeft = LX.remapRange( this.currentColor.hsv.a, 0, 1, 0, this.alphaTracker.offsetWidth - this.markerSize );
2663
+ this.alphaMarker.style.left = alphaLeft + "px";
2664
+ }
2583
2665
  }
2584
2666
 
2585
2667
  _svToPosition( s, v ) {
2586
2668
  this.intSatMarker.style.left = `${ LX.remapRange( s, 0, 1, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize ) }px`;
2587
2669
  this.intSatMarker.style.top = `${ LX.remapRange( 1 - v, 0, 1, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize ) }px`
2588
- };
2670
+ }
2589
2671
 
2590
2672
  _positionToSv( left, top ) {
2591
2673
  this.currentColor.hsv.s = LX.remapRange( left, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize, 0, 1 );
2592
2674
  this.currentColor.hsv.v = 1 - LX.remapRange( top, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize, 0, 1 );
2593
- };
2675
+ }
2594
2676
 
2595
2677
  _updateColorValue( newHexValue, skipCallback = false ) {
2596
2678
 
@@ -2633,71 +2715,274 @@ class ColorPicker {
2633
2715
  if( this.useAlpha ) components.push( toFixed( a ) );
2634
2716
  this.labelWidget.set( components.join( ' ' ) );
2635
2717
  }
2636
- };
2718
+ }
2637
2719
 
2638
- _adjustPosition() {
2720
+ fromHexColor( hexColor ) {
2639
2721
 
2640
- const position = [ 0, 0 ];
2722
+ this.currentColor.setHex( hexColor );
2641
2723
 
2642
- // Place menu using trigger position and user options
2724
+ // Decompose into HSV
2725
+ const { h, s, v } = this.currentColor.hsv;
2726
+ this._svToPosition( s, v );
2727
+
2728
+ const hueColor = new Color( { h, s: 1, v: 1 } );
2729
+ this.hueMarker.style.backgroundColor = this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2730
+ this.hueMarker.style.left = LX.remapRange( h, 0, 360, -this.markerHalfSize, this.colorPickerTracker.offsetWidth - this.markerHalfSize ) + "px";
2731
+
2732
+ this._updateColorValue( hexColor );
2733
+ }
2734
+ };
2735
+
2736
+ LX.ColorPicker = ColorPicker;
2737
+
2738
+ class Calendar {
2739
+
2740
+ /**
2741
+ * @constructor Calendar
2742
+ * @param {String} dateString D/M/Y
2743
+ * @param {Object} options
2744
+ * onChange: Function to call on date changes
2745
+ */
2746
+
2747
+ constructor( dateString, options = {} ) {
2748
+
2749
+ this.root = LX.makeContainer( ["256px", "auto"], "border p-3 bg-primary rounded-lg text-md" );
2750
+
2751
+ this.onChange = options.onChange;
2752
+ this.untilToday = options.untilToday;
2753
+ this.fromToday = options.fromToday;
2754
+
2755
+ if( dateString )
2643
2756
  {
2644
- const rect = this._trigger.getBoundingClientRect();
2757
+ this.fromDateString( dateString );
2758
+ }
2759
+ else
2760
+ {
2761
+ const date = new Date();
2762
+ this.month = date.getMonth() + 1;
2763
+ this.year = date.getFullYear();
2764
+ this.fromMonthYear( this.month, this.year );
2765
+ }
2766
+ }
2645
2767
 
2646
- let alignWidth = true;
2768
+ _getCurrentDate() {
2769
+ return {
2770
+ day: this.day,
2771
+ month: this.month,
2772
+ year: this.year,
2773
+ fullDate: this.getFullDate()
2774
+ }
2775
+ }
2647
2776
 
2648
- switch( this.side )
2777
+ _previousMonth() {
2778
+
2779
+ this.month = Math.max( 0, this.month - 1 );
2780
+
2781
+ if( this.month == 0 )
2782
+ {
2783
+ this.month = 12;
2784
+ this.year--;
2785
+ }
2786
+
2787
+ this.fromMonthYear( this.month, this.year );
2788
+ }
2789
+
2790
+ _nextMonth() {
2791
+
2792
+ this.month = Math.min( this.month + 1, 12 );
2793
+
2794
+ if( this.month == 12 )
2795
+ {
2796
+ this.month = 0;
2797
+ this.year++;
2798
+ }
2799
+
2800
+ this.fromMonthYear( this.month, this.year );
2801
+ }
2802
+
2803
+ refresh() {
2804
+
2805
+ this.root.innerHTML = "";
2806
+
2807
+ // Header
2808
+ {
2809
+ const header = LX.makeContainer( ["100%", "auto"], "flex flex-row p-1", "", this.root );
2810
+
2811
+ const prevMonthIcon = LX.makeIcon( "left", { title: "Previous Month", iconClass: "border p-1 rounded hover:bg-secondary", svgClass: "sm" } );
2812
+ header.appendChild( prevMonthIcon );
2813
+ prevMonthIcon.addEventListener( "click", () => {
2814
+ this._previousMonth();
2815
+ } );
2816
+
2817
+ const monthYearLabel = LX.makeContainer( ["100%", "auto"], "text-center font-medium select-none", `${ this.monthName } ${ this.year }`, header );
2818
+
2819
+ const nextMonthIcon = LX.makeIcon( "right", { title: "Next Month", iconClass: "border p-1 rounded hover:bg-secondary", svgClass: "sm" } );
2820
+ header.appendChild( nextMonthIcon );
2821
+ nextMonthIcon.addEventListener( "click", () => {
2822
+ this._nextMonth();
2823
+ } );
2824
+ }
2825
+
2826
+ // Body
2827
+ {
2828
+ const daysTable = document.createElement( "table" );
2829
+ daysTable.className = "w-full";
2830
+ this.root.appendChild( daysTable );
2831
+
2832
+ // Table Head
2649
2833
  {
2650
- case "left":
2651
- position[ 0 ] += ( rect.x - this.root.offsetWidth );
2652
- alignWidth = false;
2653
- break;
2654
- case "right":
2655
- position[ 0 ] += ( rect.x + rect.width );
2656
- alignWidth = false;
2657
- break;
2658
- case "top":
2659
- position[ 1 ] += ( rect.y - this.root.offsetHeight );
2660
- alignWidth = true;
2661
- break;
2662
- case "bottom":
2663
- position[ 1 ] += ( rect.y + rect.height );
2664
- alignWidth = true;
2665
- break;
2666
- default:
2667
- break;
2834
+ const head = document.createElement( 'thead' );
2835
+ daysTable.appendChild( head );
2836
+
2837
+ const hrow = document.createElement( 'tr' );
2838
+
2839
+ for( const headData of [ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ] )
2840
+ {
2841
+ const th = document.createElement( 'th' );
2842
+ th.className = "fg-tertiary text-sm font-normal select-none";
2843
+ th.innerHTML = `<span>${ headData }</span>`;
2844
+ hrow.appendChild( th );
2845
+ }
2846
+
2847
+ head.appendChild( hrow );
2668
2848
  }
2669
2849
 
2670
- switch( this.align )
2850
+ // Table Body
2671
2851
  {
2672
- case "start":
2673
- if( alignWidth ) { position[ 0 ] += rect.x; }
2674
- else { position[ 1 ] += rect.y; }
2675
- break;
2676
- case "center":
2677
- if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - this.root.offsetWidth * 0.5; }
2678
- else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - this.root.offsetHeight * 0.5; }
2679
- break;
2680
- case "end":
2681
- if( alignWidth ) { position[ 0 ] += rect.x - this.root.offsetWidth + rect.width; }
2682
- else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
2683
- break;
2684
- default:
2685
- break;
2852
+ const body = document.createElement( 'tbody' );
2853
+ daysTable.appendChild( body );
2854
+
2855
+ for( let week = 0; week < 6; week++ )
2856
+ {
2857
+ const hrow = document.createElement( 'tr' );
2858
+ const weekDays = this.calendarDays.slice( week * 7, week * 7 + 7 );
2859
+
2860
+ for( const dayData of weekDays )
2861
+ {
2862
+ const th = document.createElement( 'th' );
2863
+ th.className = "leading-loose font-normal rounded select-none cursor-pointer";
2864
+
2865
+ const dayDate = new Date( `${ this.month }/${ dayData.day }/${ this.year }` );
2866
+ const date = new Date();
2867
+ const beforeToday = this.untilToday ? ( dayDate.getTime() < date.getTime() ) : true;
2868
+ const afterToday = this.fromToday ? ( dayDate.getTime() > date.getTime() ) : true;
2869
+ const selectable = dayData.currentMonth && beforeToday && afterToday;
2870
+
2871
+ if( this.currentDate && ( dayData.day == this.currentDate.day ) && ( this.month == this.currentDate.month )
2872
+ && ( this.year == this.currentDate.year ) && dayData.currentMonth )
2873
+ {
2874
+ th.className += ` bg-contrast fg-contrast`;
2875
+ }
2876
+ else
2877
+ {
2878
+ th.className += ` ${ selectable ? "fg-primary" : "fg-tertiary" } hover:bg-secondary`;
2879
+ }
2880
+
2881
+ th.innerHTML = `<span>${ dayData.day }</span>`;
2882
+ hrow.appendChild( th );
2883
+
2884
+ if( selectable )
2885
+ {
2886
+ th.addEventListener( "click", () => {
2887
+ this.day = dayData.day;
2888
+ this.currentDate = this._getCurrentDate();
2889
+ if( this.onChange )
2890
+ {
2891
+ this.onChange( this.currentDate );
2892
+ }
2893
+ } );
2894
+ }
2895
+ }
2896
+
2897
+ body.appendChild( hrow );
2898
+ }
2686
2899
  }
2687
2900
  }
2901
+ }
2688
2902
 
2689
- if( this.avoidCollisions )
2903
+ fromDateString( dateString ) {
2904
+
2905
+ const tokens = dateString.split( '/' );
2906
+
2907
+ this.day = parseInt( tokens[ 0 ] );
2908
+ this.month = parseInt( tokens[ 1 ] );
2909
+ this.year = parseInt( tokens[ 2 ] );
2910
+
2911
+ this.currentDate = this._getCurrentDate();
2912
+
2913
+ this.fromMonthYear( this.month, this.year );
2914
+ }
2915
+
2916
+ fromMonthYear( month, year ) {
2917
+
2918
+ // Month is 0-based (0 = January, ... 11 = December)
2919
+ month--;
2920
+
2921
+ year = year ?? new Date().getFullYear();
2922
+
2923
+ const weekDay = new Date( year, month, 1 ).getDay();
2924
+ const firstDay = weekDay === 0 ? 6 : weekDay - 1; // 0 = Monday, 1 = Tuesday...
2925
+ const daysInMonth = new Date( year, month + 1, 0 ).getDate();
2926
+
2927
+ // Previous month
2928
+ const prevMonth = month === 0 ? 11 : month - 1;
2929
+ const prevYear = month === 0 ? year - 1 : year;
2930
+ const daysInPrevMonth = new Date( prevYear, prevMonth + 1, 0 ).getDate();
2931
+
2932
+ // Prepare full grid (up to 6 weeks = 42 days)
2933
+ const calendarDays = [];
2934
+
2935
+ // Fill in days from previous month
2936
+ for( let i = firstDay - 1; i >= 0; i--)
2690
2937
  {
2691
- position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - this.root.offsetWidth - this._windowPadding );
2692
- position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - this.root.offsetHeight - this._windowPadding );
2938
+ calendarDays.push( { day: daysInPrevMonth - i, currentMonth: false } );
2693
2939
  }
2694
2940
 
2695
- this.root.style.left = `${ position[ 0 ] }px`;
2696
- this.root.style.top = `${ position[ 1 ] }px`;
2941
+ // Fill in current month days
2942
+ for ( let i = 1; i <= daysInMonth; i++ )
2943
+ {
2944
+ calendarDays.push( { day: i, currentMonth: true } );
2945
+ }
2946
+
2947
+ // Fill in next month days to complete the grid (if needed)
2948
+ const remaining = 42 - calendarDays.length;
2949
+ for( let i = 1; i <= remaining; i++ )
2950
+ {
2951
+ calendarDays.push( { day: i, currentMonth: false } );
2952
+ }
2953
+
2954
+ this.monthName = this.getMonthName( month );
2955
+ this.firstDay = firstDay;
2956
+ this.daysInMonth = daysInMonth;
2957
+ this.calendarDays = calendarDays;
2958
+
2959
+ this.refresh();
2697
2960
  }
2698
- };
2699
2961
 
2700
- LX.ColorPicker = ColorPicker;
2962
+ getMonthName( monthIndex, locale = "en-US" ) {
2963
+ const formatter = new Intl.DateTimeFormat( locale, { month: "long" } );
2964
+ return formatter.format( new Date( 2000, monthIndex, 1 ) );
2965
+ }
2966
+
2967
+ getFullDate() {
2968
+ return `${ this.monthName } ${ this.day }${ this._getOrdinalSuffix( this.day ) }, ${ this.year }`;
2969
+ }
2970
+
2971
+ _getOrdinalSuffix( day ) {
2972
+ if ( day > 3 && day < 21 ) return "th";
2973
+ switch ( day % 10 )
2974
+ {
2975
+ case 1: return "st";
2976
+ case 2: return "nd";
2977
+ case 3: return "rd";
2978
+ default: return "th";
2979
+ }
2980
+ }
2981
+ }
2982
+
2983
+ LX.Calendar = Calendar;
2984
+
2985
+ /* Layout Classes */
2701
2986
 
2702
2987
  class Area {
2703
2988
 
@@ -5303,8 +5588,9 @@ class Widget {
5303
5588
  static COUNTER = 33;
5304
5589
  static TABLE = 34;
5305
5590
  static TABS = 35;
5306
- static LABEL = 36;
5307
- static BLANK = 37;
5591
+ static DATE = 36;
5592
+ static LABEL = 37;
5593
+ static BLANK = 38;
5308
5594
 
5309
5595
  static NO_CONTEXT_TYPES = [
5310
5596
  Widget.BUTTON,
@@ -5542,6 +5828,7 @@ class Widget {
5542
5828
  case Widget.COUNTER: return "Counter";
5543
5829
  case Widget.TABLE: return "Table";
5544
5830
  case Widget.TABS: return "Tabs";
5831
+ case Widget.DATE: return "Date";
5545
5832
  case Widget.LABEL: return "Label";
5546
5833
  case Widget.BLANK: return "Blank";
5547
5834
  case Widget.CUSTOM: return this.customName;
@@ -6142,8 +6429,11 @@ class NodeTree {
6142
6429
  actionEl.className = "lexicon " + a.icon;
6143
6430
  actionEl.title = a.name;
6144
6431
  actionEl.addEventListener("click", function( e ) {
6145
- a.callback( node, actionEl );
6146
- e.stopPropagation();
6432
+ if( a.callback )
6433
+ {
6434
+ a.callback( node, actionEl );
6435
+ e.stopPropagation();
6436
+ }
6147
6437
  });
6148
6438
 
6149
6439
  inputContainer.appendChild( actionEl );
@@ -6244,6 +6534,8 @@ class NodeTree {
6244
6534
  }
6245
6535
  }
6246
6536
 
6537
+ LX.NodeTree = NodeTree;
6538
+
6247
6539
  /**
6248
6540
  * @class Blank
6249
6541
  * @description Blank Widget
@@ -6634,6 +6926,7 @@ class Button extends Widget {
6634
6926
  wValue.querySelector( "a" ).classList.add( "swap-off" );
6635
6927
 
6636
6928
  const input = document.createElement( "input" );
6929
+ input.className = "p-0 border-0";
6637
6930
  input.type = "checkbox";
6638
6931
  wValue.prepend( input );
6639
6932
 
@@ -8194,6 +8487,16 @@ class ColorInput extends Widget {
8194
8487
  container.className = "lexcolor";
8195
8488
  this.root.appendChild( container );
8196
8489
 
8490
+ this.picker = new ColorPicker( value, {
8491
+ colorModel: options.useRGB ? "RGB" : "Hex",
8492
+ useAlpha,
8493
+ onChange: ( color ) => {
8494
+ this._fromColorPicker = true;
8495
+ this.set( color.hex );
8496
+ delete this._fromColorPicker;
8497
+ }
8498
+ } );
8499
+
8197
8500
  let sampleContainer = LX.makeContainer( ["18px", "18px"], "flex flex-row bg-contrast rounded overflow-hidden", "", container );
8198
8501
  sampleContainer.tabIndex = "1";
8199
8502
  sampleContainer.addEventListener( "click", e => {
@@ -8201,15 +8504,8 @@ class ColorInput extends Widget {
8201
8504
  {
8202
8505
  return;
8203
8506
  }
8204
- new ColorPicker( value, sampleContainer, {
8205
- colorModel: options.useRGB ? "RGB" : "Hex",
8206
- useAlpha,
8207
- onChange: ( color ) => {
8208
- this._fromColorPicker = true;
8209
- this.set( color.hex );
8210
- delete this._fromColorPicker;
8211
- }
8212
- } );
8507
+
8508
+ this._popover = new Popover( sampleContainer, [ this.picker ] );
8213
8509
  } );
8214
8510
 
8215
8511
  let colorSampleRGB = document.createElement( 'div' );
@@ -10202,6 +10498,75 @@ class Table extends Widget {
10202
10498
 
10203
10499
  LX.Table = Table;
10204
10500
 
10501
+ /**
10502
+ * @class DatePicker
10503
+ * @description DatePicker Widget
10504
+ */
10505
+
10506
+ class DatePicker extends Widget {
10507
+
10508
+ constructor( name, dateString, callback, options = { } ) {
10509
+
10510
+ super( Widget.DATE, name, null, options );
10511
+
10512
+ if( options.today )
10513
+ {
10514
+ const date = new Date();
10515
+ dateString = `${ date.getDate() }/${ date.getMonth() + 1 }/${ date.getFullYear() }`;
10516
+ }
10517
+
10518
+ this.onGetValue = () => {
10519
+ return dateString;
10520
+ }
10521
+
10522
+ this.onSetValue = ( newValue, skipCallback, event ) => {
10523
+
10524
+ dateString = newValue;
10525
+
10526
+ this.calendar.fromDateString( newValue );
10527
+
10528
+ refresh( this.calendar.getFullDate() );
10529
+
10530
+ if( !skipCallback )
10531
+ {
10532
+ this._trigger( new IEvent( name, newValue, event ), callback );
10533
+ }
10534
+ }
10535
+
10536
+ this.onResize = ( rect ) => {
10537
+ const realNameWidth = ( this.root.domName?.style.width ?? "0px" );
10538
+ container.style.width = `calc( 100% - ${ realNameWidth })`;
10539
+ };
10540
+
10541
+ const container = document.createElement('div');
10542
+ container.className = "lexdate";
10543
+ this.root.appendChild( container );
10544
+
10545
+ this.calendar = new Calendar( dateString, { onChange: ( date ) => {
10546
+ this.set( `${ date.day }/${ date.month }/${ date.year }` )
10547
+ }, ...options });
10548
+
10549
+ const refresh = ( currentDate ) => {
10550
+ container.innerHTML = "";
10551
+ const calendarIcon = LX.makeIcon( "calendar" );
10552
+ const calendarButton = new Button( null, currentDate ?? "Pick a date", () => {
10553
+ this._popover = new Popover( calendarButton.root, ( popoverRoot ) => {
10554
+ popoverRoot.appendChild( this.calendar.root );
10555
+ } );
10556
+ }, { buttonClass: `flex flex-row px-3 ${ currentDate ? "" : "fg-tertiary" } justify-between` } );
10557
+
10558
+ calendarButton.root.querySelector( "button" ).appendChild( calendarIcon );
10559
+ container.appendChild( calendarButton.root );
10560
+ };
10561
+
10562
+ refresh( dateString ? this.calendar.getFullDate(): null );
10563
+
10564
+ doAsync( this.onResize.bind( this ) );
10565
+ }
10566
+ }
10567
+
10568
+ LX.DatePicker = DatePicker;
10569
+
10205
10570
  /**
10206
10571
  * @class Panel
10207
10572
  */
@@ -11348,6 +11713,23 @@ class Panel {
11348
11713
  const widget = new Table( name, data, options );
11349
11714
  return this._attachWidget( widget );
11350
11715
  }
11716
+
11717
+ /**
11718
+ * @method addDate
11719
+ * @param {String} name Widget name
11720
+ * @param {String} dateString
11721
+ * @param {Function} callback
11722
+ * @param {Object} options:
11723
+ * hideName: Don't use name as label [false]
11724
+ * today: Set current day as selected by default
11725
+ * untilToday: Allow dates only until current day
11726
+ * fromToday: Allow dates only from current day
11727
+ */
11728
+
11729
+ addDate( name, dateString, callback, options = { } ) {
11730
+ const widget = new DatePicker( name, dateString, callback, options );
11731
+ return this._attachWidget( widget );
11732
+ }
11351
11733
  }
11352
11734
 
11353
11735
  LX.Panel = Panel;
@@ -14380,6 +14762,7 @@ LX.ICONS = {
14380
14762
  "sidebar": [512, 512, [], "regular", "M64 64h384a32 32 0 0 1 32 32v320a32 32 0 0 1-32 32H64a32 32 0 0 1-32-32V96a32 32 0 0 1 32-32zm128 0v384", null, "fill=none stroke-width=50 stroke-linejoin=round stroke-linecap=round"],
14381
14763
  "table-cells": [512, 512, [], "solid", "M64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32zm88 64l0 64-88 0 0-64 88 0zm56 0l88 0 0 64-88 0 0-64zm240 0l0 64-88 0 0-64 88 0zM64 224l88 0 0 64-88 0 0-64zm232 0l0 64-88 0 0-64 88 0zm64 0l88 0 0 64-88 0 0-64zM152 352l0 64-88 0 0-64 88 0zm56 0l88 0 0 64-88 0 0-64zm240 0l0 64-88 0 0-64 88 0z"],
14382
14764
  "table-cells-large": [512, 512, [], "solid", "M448 96l0 128-160 0 0-128 160 0zm0 192l0 128-160 0 0-128 160 0zM224 224L64 224 64 96l160 0 0 128zM64 288l160 0 0 128L64 416l0-128zM64 32C28.7 32 0 60.7 0 96L0 416c0 35.3 28.7 64 64 64l384 0c35.3 0 64-28.7 64-64l0-320c0-35.3-28.7-64-64-64L64 32z"],
14765
+ "frame": [24, 24, [], "solid", "M2 6h20M2 18h20M6 2v20M18 2v20", null, "fill=none stroke-width=2 stroke-linejoin=round stroke-linecap=round"],
14383
14766
  "lightbulb": [384, 512, [], "regular", "M297.2 248.9C311.6 228.3 320 203.2 320 176c0-70.7-57.3-128-128-128S64 105.3 64 176c0 27.2 8.4 52.3 22.8 72.9c3.7 5.3 8.1 11.3 12.8 17.7c0 0 0 0 0 0c12.9 17.7 28.3 38.9 39.8 59.8c10.4 19 15.7 38.8 18.3 57.5L109 384c-2.2-12-5.9-23.7-11.8-34.5c-9.9-18-22.2-34.9-34.5-51.8c0 0 0 0 0 0s0 0 0 0c-5.2-7.1-10.4-14.2-15.4-21.4C27.6 247.9 16 213.3 16 176C16 78.8 94.8 0 192 0s176 78.8 176 176c0 37.3-11.6 71.9-31.4 100.3c-5 7.2-10.2 14.3-15.4 21.4c0 0 0 0 0 0s0 0 0 0c-12.3 16.8-24.6 33.7-34.5 51.8c-5.9 10.8-9.6 22.5-11.8 34.5l-48.6 0c2.6-18.7 7.9-38.6 18.3-57.5c11.5-20.9 26.9-42.1 39.8-59.8c0 0 0 0 0 0s0 0 0 0s0 0 0 0c4.7-6.4 9-12.4 12.7-17.7zM192 128c-26.5 0-48 21.5-48 48c0 8.8-7.2 16-16 16s-16-7.2-16-16c0-44.2 35.8-80 80-80c8.8 0 16 7.2 16 16s-7.2 16-16 16zm0 384c-44.2 0-80-35.8-80-80l0-16 160 0 0 16c0 44.2-35.8 80-80 80z"],
14384
14767
  "flag": [448, 512, [], "regular", "M48 24C48 10.7 37.3 0 24 0S0 10.7 0 24L0 64 0 350.5 0 400l0 88c0 13.3 10.7 24 24 24s24-10.7 24-24l0-100 80.3-20.1c41.1-10.3 84.6-5.5 122.5 13.4c44.2 22.1 95.5 24.8 141.7 7.4l34.7-13c12.5-4.7 20.8-16.6 20.8-30l0-279.7c0-23-24.2-38-44.8-27.7l-9.6 4.8c-46.3 23.2-100.8 23.2-147.1 0c-35.1-17.6-75.4-22-113.5-12.5L48 52l0-28zm0 77.5l96.6-24.2c27-6.7 55.5-3.6 80.4 8.8c54.9 27.4 118.7 29.7 175 6.8l0 241.8-24.4 9.1c-33.7 12.6-71.2 10.7-103.4-5.4c-48.2-24.1-103.3-30.1-155.6-17.1L48 338.5l0-237z"],
14385
14768
  "newspaper": [512, 512, [], "regular", "M168 80c-13.3 0-24 10.7-24 24l0 304c0 8.4-1.4 16.5-4.1 24L440 432c13.3 0 24-10.7 24-24l0-304c0-13.3-10.7-24-24-24L168 80zM72 480c-39.8 0-72-32.2-72-72L0 112C0 98.7 10.7 88 24 88s24 10.7 24 24l0 296c0 13.3 10.7 24 24 24s24-10.7 24-24l0-304c0-39.8 32.2-72 72-72l272 0c39.8 0 72 32.2 72 72l0 304c0 39.8-32.2 72-72 72L72 480zM176 136c0-13.3 10.7-24 24-24l96 0c13.3 0 24 10.7 24 24l0 80c0 13.3-10.7 24-24 24l-96 0c-13.3 0-24-10.7-24-24l0-80zm200-24l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm0 80l32 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-32 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zM200 272l208 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-208 0c-13.3 0-24-10.7-24-24s10.7-24 24-24zm0 80l208 0c13.3 0 24 10.7 24 24s-10.7 24-24 24l-208 0c-13.3 0-24-10.7-24-24s10.7-24 24-24z"],