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.
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  var LX = {
9
- version: "0.5.8",
9
+ version: "0.5.9",
10
10
  ready: false,
11
11
  components: [], // Specific pre-build components
12
12
  signals: {}, // Events and triggers
@@ -773,7 +773,7 @@ LX.makeKbd = makeKbd;
773
773
  * @description Gets an SVG element using one of LX.ICONS
774
774
  * @param {String} iconName
775
775
  * @param {Object} options
776
- * iconTitle
776
+ * title
777
777
  * extraClass
778
778
  * svgClass
779
779
  */
@@ -782,7 +782,7 @@ function makeIcon( iconName, options = { } )
782
782
  let data = LX.ICONS[ iconName ];
783
783
  console.assert( data, `No icon named _${ iconName }_` );
784
784
 
785
- const iconTitle = options.iconTitle;
785
+ const iconTitle = options.title;
786
786
  const iconClass = options.iconClass;
787
787
  const svgClass = options.svgClass;
788
788
 
@@ -1641,7 +1641,7 @@ function toast( title, description, options = {} )
1641
1641
  if( options.action )
1642
1642
  {
1643
1643
  const panel = new Panel();
1644
- panel.addButton(null, options.action.name ?? "Accept", options.action.callback.bind( this, toast ), { width: "auto", maxWidth: "150px", className: "right", buttonClass: "outline" });
1644
+ panel.addButton(null, options.action.name ?? "Accept", options.action.callback.bind( this, toast ), { width: "auto", maxWidth: "150px", className: "right", buttonClass: "border" });
1645
1645
  toast.appendChild( panel.root.childNodes[ 0 ] );
1646
1646
  }
1647
1647
 
@@ -1941,6 +1941,158 @@ LX.addSignal = addSignal;
1941
1941
  * DOM Elements
1942
1942
  */
1943
1943
 
1944
+ /**
1945
+ * @class Popover
1946
+ */
1947
+
1948
+ class Popover {
1949
+
1950
+ static activeElement = false;
1951
+
1952
+ constructor( trigger, content, options = {} ) {
1953
+
1954
+ console.assert( trigger, "Popover needs a DOM element as trigger!" );
1955
+
1956
+ if( Popover.activeElement )
1957
+ {
1958
+ Popover.activeElement.destroy();
1959
+ return;
1960
+ }
1961
+
1962
+ this._trigger = trigger;
1963
+ trigger.classList.add( "triggered" );
1964
+ trigger.active = this;
1965
+
1966
+ this._windowPadding = 4;
1967
+ this.side = options.side ?? "bottom";
1968
+ this.align = options.align ?? "center";
1969
+ this.avoidCollisions = options.avoidCollisions ?? true;
1970
+
1971
+ this.root = document.createElement( "div" );
1972
+ this.root.dataset["side"] = this.side;
1973
+ this.root.tabIndex = "1";
1974
+ this.root.className = "lexpopover";
1975
+ LX.root.appendChild( this.root );
1976
+
1977
+ this.root.addEventListener( "keydown", (e) => {
1978
+ if( e.key == "Escape" )
1979
+ {
1980
+ e.preventDefault();
1981
+ e.stopPropagation();
1982
+ this.destroy();
1983
+ }
1984
+ } )
1985
+
1986
+ if( content )
1987
+ {
1988
+ content = [].concat( content );
1989
+ content.forEach( e => {
1990
+ const domNode = e.root ?? e;
1991
+ this.root.appendChild( domNode );
1992
+ if( e.onPopover )
1993
+ {
1994
+ e.onPopover();
1995
+ }
1996
+ } );
1997
+ }
1998
+
1999
+ Popover.activeElement = this;
2000
+
2001
+ doAsync( () => {
2002
+ this._adjustPosition();
2003
+
2004
+ this.root.focus();
2005
+
2006
+ this._onClick = e => {
2007
+ if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
2008
+ {
2009
+ return;
2010
+ }
2011
+ this.destroy();
2012
+ };
2013
+
2014
+ document.body.addEventListener( "mousedown", this._onClick, true );
2015
+ document.body.addEventListener( "focusin", this._onClick, true );
2016
+ }, 10 );
2017
+ }
2018
+
2019
+ destroy() {
2020
+
2021
+ this._trigger.classList.remove( "triggered" );
2022
+
2023
+ delete this._trigger.active;
2024
+
2025
+ document.body.removeEventListener( "click", this._onClick );
2026
+
2027
+ this.root.remove();
2028
+
2029
+ Popover.activeElement = null;
2030
+ }
2031
+
2032
+ _adjustPosition() {
2033
+
2034
+ const position = [ 0, 0 ];
2035
+
2036
+ // Place menu using trigger position and user options
2037
+ {
2038
+ const rect = this._trigger.getBoundingClientRect();
2039
+
2040
+ let alignWidth = true;
2041
+
2042
+ switch( this.side )
2043
+ {
2044
+ case "left":
2045
+ position[ 0 ] += ( rect.x - this.root.offsetWidth );
2046
+ alignWidth = false;
2047
+ break;
2048
+ case "right":
2049
+ position[ 0 ] += ( rect.x + rect.width );
2050
+ alignWidth = false;
2051
+ break;
2052
+ case "top":
2053
+ position[ 1 ] += ( rect.y - this.root.offsetHeight );
2054
+ alignWidth = true;
2055
+ break;
2056
+ case "bottom":
2057
+ position[ 1 ] += ( rect.y + rect.height );
2058
+ alignWidth = true;
2059
+ break;
2060
+ default:
2061
+ break;
2062
+ }
2063
+
2064
+ switch( this.align )
2065
+ {
2066
+ case "start":
2067
+ if( alignWidth ) { position[ 0 ] += rect.x; }
2068
+ else { position[ 1 ] += rect.y; }
2069
+ break;
2070
+ case "center":
2071
+ if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - this.root.offsetWidth * 0.5; }
2072
+ else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - this.root.offsetHeight * 0.5; }
2073
+ break;
2074
+ case "end":
2075
+ if( alignWidth ) { position[ 0 ] += rect.x - this.root.offsetWidth + rect.width; }
2076
+ else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
2077
+ break;
2078
+ default:
2079
+ break;
2080
+ }
2081
+ }
2082
+
2083
+ if( this.avoidCollisions )
2084
+ {
2085
+ position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - this.root.offsetWidth - this._windowPadding );
2086
+ position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - this.root.offsetHeight - this._windowPadding );
2087
+ }
2088
+
2089
+ this.root.style.left = `${ position[ 0 ] }px`;
2090
+ this.root.style.top = `${ position[ 1 ] }px`;
2091
+ }
2092
+ };
2093
+
2094
+ LX.Popover = Popover;
2095
+
1944
2096
  /**
1945
2097
  * @class DropdownMenu
1946
2098
  */
@@ -2238,14 +2390,8 @@ class ColorPicker {
2238
2390
 
2239
2391
  static currentPicker = false;
2240
2392
 
2241
- constructor( hexValue, trigger, options = {} ) {
2242
-
2243
- console.assert( trigger, "ColorPicker needs a DOM element as trigger!" );
2393
+ constructor( hexValue, options = {} ) {
2244
2394
 
2245
- this._windowPadding = 4;
2246
- this.side = options.side ?? "bottom";
2247
- this.align = options.align ?? "center";
2248
- this.avoidCollisions = options.avoidCollisions ?? true;
2249
2395
  this.colorModel = options.colorModel ?? "Hex";
2250
2396
  this.useAlpha = options.useAlpha ?? false;
2251
2397
  this.callback = options.onChange;
@@ -2255,32 +2401,8 @@ class ColorPicker {
2255
2401
  console.warn( "Define a callback in _options.onChange_ to allow getting new Color values!" );
2256
2402
  }
2257
2403
 
2258
- if( ColorPicker.currentPicker )
2259
- {
2260
- ColorPicker.currentPicker.destroy();
2261
- return;
2262
- }
2263
-
2264
- this._trigger = trigger;
2265
- trigger.classList.add( "triggered" );
2266
- trigger.picker = this;
2267
-
2268
2404
  this.root = document.createElement( "div" );
2269
- this.root.tabIndex = "1";
2270
2405
  this.root.className = "lexcolorpicker";
2271
- this.root.dataset["side"] = this.side;
2272
- LX.root.appendChild( this.root );
2273
-
2274
- this.root.addEventListener( "keydown", (e) => {
2275
- if( e.key == "Escape" )
2276
- {
2277
- e.preventDefault();
2278
- e.stopPropagation();
2279
- this.destroy();
2280
- }
2281
- } )
2282
-
2283
- ColorPicker.currentPicker = this;
2284
2406
 
2285
2407
  this.markerHalfSize = 8;
2286
2408
  this.markerSize = this.markerHalfSize * 2;
@@ -2299,8 +2421,6 @@ class ColorPicker {
2299
2421
  this.intSatMarker.style.backgroundColor = this.currentColor.hex;
2300
2422
  this.colorPickerBackground.appendChild( this.intSatMarker );
2301
2423
 
2302
- doAsync( this._svToPosition.bind( this, this.currentColor.hsv.s, this.currentColor.hsv.v ) );
2303
-
2304
2424
  let pickerRect = null;
2305
2425
 
2306
2426
  let innerMouseDown = e => {
@@ -2380,11 +2500,6 @@ class ColorPicker {
2380
2500
  this.hueMarker.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2381
2501
  this.colorPickerTracker.appendChild( this.hueMarker );
2382
2502
 
2383
- doAsync( () => {
2384
- const hueLeft = LX.remapRange( this.currentColor.hsv.h, 0, 360, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2385
- this.hueMarker.style.left = hueLeft + "px";
2386
- } );
2387
-
2388
2503
  const _fromHueX = ( hueX ) => {
2389
2504
  this.hueMarker.style.left = hueX + "px";
2390
2505
  this.currentColor.hsv.h = LX.remapRange( hueX, 0, this.colorPickerTracker.offsetWidth - this.markerSize, 0, 360 );
@@ -2447,11 +2562,6 @@ class ColorPicker {
2447
2562
  this.alphaMarker.style.backgroundColor = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b },${ this.currentColor.css.a })`;
2448
2563
  this.alphaTracker.appendChild( this.alphaMarker );
2449
2564
 
2450
- doAsync( () => {
2451
- const alphaLeft = LX.remapRange( this.currentColor.hsv.a, 0, 1, 0, this.alphaTracker.offsetWidth - this.markerSize );
2452
- this.alphaMarker.style.left = alphaLeft + "px";
2453
- } );
2454
-
2455
2565
  const _fromAlphaX = ( alphaX ) => {
2456
2566
  this.alphaMarker.style.left = alphaX + "px";
2457
2567
  this.currentColor.hsv.a = LX.remapRange( alphaX, 0, this.alphaTracker.offsetWidth - this.markerSize, 0, 1 );
@@ -2529,62 +2639,34 @@ class ColorPicker {
2529
2639
 
2530
2640
  this._updateColorValue( hexValue, true );
2531
2641
 
2532
- doAsync( () => {
2533
- this._adjustPosition();
2534
-
2535
- this.root.focus();
2536
-
2537
- this._onClick = e => {
2538
- if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
2539
- {
2540
- return;
2541
- }
2542
- this.destroy();
2543
- };
2642
+ doAsync( this._placeMarkers.bind( this ) );
2544
2643
 
2545
- document.body.addEventListener( "mousedown", this._onClick, true );
2546
- document.body.addEventListener( "focusin", this._onClick, true );
2547
- }, 10 );
2644
+ this.onPopover = this._placeMarkers.bind( this );
2548
2645
  }
2549
2646
 
2550
- fromHexColor( hexColor ) {
2551
-
2552
- this.currentColor.setHex( hexColor );
2553
-
2554
- // Decompose into HSV
2555
- const { h, s, v } = this.currentColor.hsv;
2556
- this._svToPosition( s, v );
2647
+ _placeMarkers() {
2557
2648
 
2558
- const hueColor = new Color( { h, s: 1, v: 1 } );
2559
- this.hueMarker.style.backgroundColor = this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2560
- this.hueMarker.style.left = LX.remapRange( h, 0, 360, -this.markerHalfSize, this.colorPickerTracker.offsetWidth - this.markerHalfSize ) + "px";
2649
+ this._svToPosition( this.currentColor.hsv.s, this.currentColor.hsv.v );
2561
2650
 
2562
- this._updateColorValue( hexColor );
2563
- }
2564
-
2565
- destroy() {
2651
+ const hueLeft = LX.remapRange( this.currentColor.hsv.h, 0, 360, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2652
+ this.hueMarker.style.left = hueLeft + "px";
2566
2653
 
2567
- this._trigger.classList.remove( "triggered" );
2568
-
2569
- delete this._trigger.picker;
2570
-
2571
- document.body.removeEventListener( "mousedown", this._onClick, true );
2572
- document.body.removeEventListener( "focusin", this._onClick, true );
2573
-
2574
- LX.root.querySelectorAll( ".lexcolorpicker" ).forEach( m => { m.remove(); } );
2575
-
2576
- ColorPicker.currentPicker = null;
2654
+ if( this.useAlpha )
2655
+ {
2656
+ const alphaLeft = LX.remapRange( this.currentColor.hsv.a, 0, 1, 0, this.alphaTracker.offsetWidth - this.markerSize );
2657
+ this.alphaMarker.style.left = alphaLeft + "px";
2658
+ }
2577
2659
  }
2578
2660
 
2579
2661
  _svToPosition( s, v ) {
2580
2662
  this.intSatMarker.style.left = `${ LX.remapRange( s, 0, 1, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize ) }px`;
2581
2663
  this.intSatMarker.style.top = `${ LX.remapRange( 1 - v, 0, 1, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize ) }px`
2582
- };
2664
+ }
2583
2665
 
2584
2666
  _positionToSv( left, top ) {
2585
2667
  this.currentColor.hsv.s = LX.remapRange( left, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize, 0, 1 );
2586
2668
  this.currentColor.hsv.v = 1 - LX.remapRange( top, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize, 0, 1 );
2587
- };
2669
+ }
2588
2670
 
2589
2671
  _updateColorValue( newHexValue, skipCallback = false ) {
2590
2672
 
@@ -2627,71 +2709,274 @@ class ColorPicker {
2627
2709
  if( this.useAlpha ) components.push( toFixed( a ) );
2628
2710
  this.labelWidget.set( components.join( ' ' ) );
2629
2711
  }
2630
- };
2712
+ }
2631
2713
 
2632
- _adjustPosition() {
2714
+ fromHexColor( hexColor ) {
2633
2715
 
2634
- const position = [ 0, 0 ];
2716
+ this.currentColor.setHex( hexColor );
2635
2717
 
2636
- // Place menu using trigger position and user options
2718
+ // Decompose into HSV
2719
+ const { h, s, v } = this.currentColor.hsv;
2720
+ this._svToPosition( s, v );
2721
+
2722
+ const hueColor = new Color( { h, s: 1, v: 1 } );
2723
+ this.hueMarker.style.backgroundColor = this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2724
+ this.hueMarker.style.left = LX.remapRange( h, 0, 360, -this.markerHalfSize, this.colorPickerTracker.offsetWidth - this.markerHalfSize ) + "px";
2725
+
2726
+ this._updateColorValue( hexColor );
2727
+ }
2728
+ };
2729
+
2730
+ LX.ColorPicker = ColorPicker;
2731
+
2732
+ class Calendar {
2733
+
2734
+ /**
2735
+ * @constructor Calendar
2736
+ * @param {String} dateString D/M/Y
2737
+ * @param {Object} options
2738
+ * onChange: Function to call on date changes
2739
+ */
2740
+
2741
+ constructor( dateString, options = {} ) {
2742
+
2743
+ this.root = LX.makeContainer( ["256px", "auto"], "border p-3 bg-primary rounded-lg text-md" );
2744
+
2745
+ this.onChange = options.onChange;
2746
+ this.untilToday = options.untilToday;
2747
+ this.fromToday = options.fromToday;
2748
+
2749
+ if( dateString )
2637
2750
  {
2638
- const rect = this._trigger.getBoundingClientRect();
2751
+ this.fromDateString( dateString );
2752
+ }
2753
+ else
2754
+ {
2755
+ const date = new Date();
2756
+ this.month = date.getMonth() + 1;
2757
+ this.year = date.getFullYear();
2758
+ this.fromMonthYear( this.month, this.year );
2759
+ }
2760
+ }
2639
2761
 
2640
- let alignWidth = true;
2762
+ _getCurrentDate() {
2763
+ return {
2764
+ day: this.day,
2765
+ month: this.month,
2766
+ year: this.year,
2767
+ fullDate: this.getFullDate()
2768
+ }
2769
+ }
2641
2770
 
2642
- switch( this.side )
2771
+ _previousMonth() {
2772
+
2773
+ this.month = Math.max( 0, this.month - 1 );
2774
+
2775
+ if( this.month == 0 )
2776
+ {
2777
+ this.month = 12;
2778
+ this.year--;
2779
+ }
2780
+
2781
+ this.fromMonthYear( this.month, this.year );
2782
+ }
2783
+
2784
+ _nextMonth() {
2785
+
2786
+ this.month = Math.min( this.month + 1, 12 );
2787
+
2788
+ if( this.month == 12 )
2789
+ {
2790
+ this.month = 0;
2791
+ this.year++;
2792
+ }
2793
+
2794
+ this.fromMonthYear( this.month, this.year );
2795
+ }
2796
+
2797
+ refresh() {
2798
+
2799
+ this.root.innerHTML = "";
2800
+
2801
+ // Header
2802
+ {
2803
+ const header = LX.makeContainer( ["100%", "auto"], "flex flex-row p-1", "", this.root );
2804
+
2805
+ const prevMonthIcon = LX.makeIcon( "left", { title: "Previous Month", iconClass: "border p-1 rounded hover:bg-secondary", svgClass: "sm" } );
2806
+ header.appendChild( prevMonthIcon );
2807
+ prevMonthIcon.addEventListener( "click", () => {
2808
+ this._previousMonth();
2809
+ } );
2810
+
2811
+ const monthYearLabel = LX.makeContainer( ["100%", "auto"], "text-center font-medium select-none", `${ this.monthName } ${ this.year }`, header );
2812
+
2813
+ const nextMonthIcon = LX.makeIcon( "right", { title: "Next Month", iconClass: "border p-1 rounded hover:bg-secondary", svgClass: "sm" } );
2814
+ header.appendChild( nextMonthIcon );
2815
+ nextMonthIcon.addEventListener( "click", () => {
2816
+ this._nextMonth();
2817
+ } );
2818
+ }
2819
+
2820
+ // Body
2821
+ {
2822
+ const daysTable = document.createElement( "table" );
2823
+ daysTable.className = "w-full";
2824
+ this.root.appendChild( daysTable );
2825
+
2826
+ // Table Head
2643
2827
  {
2644
- case "left":
2645
- position[ 0 ] += ( rect.x - this.root.offsetWidth );
2646
- alignWidth = false;
2647
- break;
2648
- case "right":
2649
- position[ 0 ] += ( rect.x + rect.width );
2650
- alignWidth = false;
2651
- break;
2652
- case "top":
2653
- position[ 1 ] += ( rect.y - this.root.offsetHeight );
2654
- alignWidth = true;
2655
- break;
2656
- case "bottom":
2657
- position[ 1 ] += ( rect.y + rect.height );
2658
- alignWidth = true;
2659
- break;
2660
- default:
2661
- break;
2828
+ const head = document.createElement( 'thead' );
2829
+ daysTable.appendChild( head );
2830
+
2831
+ const hrow = document.createElement( 'tr' );
2832
+
2833
+ for( const headData of [ "Mo", "Tu", "We", "Th", "Fr", "Sa", "Su" ] )
2834
+ {
2835
+ const th = document.createElement( 'th' );
2836
+ th.className = "fg-tertiary text-sm font-normal select-none";
2837
+ th.innerHTML = `<span>${ headData }</span>`;
2838
+ hrow.appendChild( th );
2839
+ }
2840
+
2841
+ head.appendChild( hrow );
2662
2842
  }
2663
2843
 
2664
- switch( this.align )
2844
+ // Table Body
2665
2845
  {
2666
- case "start":
2667
- if( alignWidth ) { position[ 0 ] += rect.x; }
2668
- else { position[ 1 ] += rect.y; }
2669
- break;
2670
- case "center":
2671
- if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - this.root.offsetWidth * 0.5; }
2672
- else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - this.root.offsetHeight * 0.5; }
2673
- break;
2674
- case "end":
2675
- if( alignWidth ) { position[ 0 ] += rect.x - this.root.offsetWidth + rect.width; }
2676
- else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
2677
- break;
2678
- default:
2679
- break;
2846
+ const body = document.createElement( 'tbody' );
2847
+ daysTable.appendChild( body );
2848
+
2849
+ for( let week = 0; week < 6; week++ )
2850
+ {
2851
+ const hrow = document.createElement( 'tr' );
2852
+ const weekDays = this.calendarDays.slice( week * 7, week * 7 + 7 );
2853
+
2854
+ for( const dayData of weekDays )
2855
+ {
2856
+ const th = document.createElement( 'th' );
2857
+ th.className = "leading-loose font-normal rounded select-none cursor-pointer";
2858
+
2859
+ const dayDate = new Date( `${ this.month }/${ dayData.day }/${ this.year }` );
2860
+ const date = new Date();
2861
+ const beforeToday = this.untilToday ? ( dayDate.getTime() < date.getTime() ) : true;
2862
+ const afterToday = this.fromToday ? ( dayDate.getTime() > date.getTime() ) : true;
2863
+ const selectable = dayData.currentMonth && beforeToday && afterToday;
2864
+
2865
+ if( this.currentDate && ( dayData.day == this.currentDate.day ) && ( this.month == this.currentDate.month )
2866
+ && ( this.year == this.currentDate.year ) && dayData.currentMonth )
2867
+ {
2868
+ th.className += ` bg-contrast fg-contrast`;
2869
+ }
2870
+ else
2871
+ {
2872
+ th.className += ` ${ selectable ? "fg-primary" : "fg-tertiary" } hover:bg-secondary`;
2873
+ }
2874
+
2875
+ th.innerHTML = `<span>${ dayData.day }</span>`;
2876
+ hrow.appendChild( th );
2877
+
2878
+ if( selectable )
2879
+ {
2880
+ th.addEventListener( "click", () => {
2881
+ this.day = dayData.day;
2882
+ this.currentDate = this._getCurrentDate();
2883
+ if( this.onChange )
2884
+ {
2885
+ this.onChange( this.currentDate );
2886
+ }
2887
+ } );
2888
+ }
2889
+ }
2890
+
2891
+ body.appendChild( hrow );
2892
+ }
2680
2893
  }
2681
2894
  }
2895
+ }
2682
2896
 
2683
- if( this.avoidCollisions )
2897
+ fromDateString( dateString ) {
2898
+
2899
+ const tokens = dateString.split( '/' );
2900
+
2901
+ this.day = parseInt( tokens[ 0 ] );
2902
+ this.month = parseInt( tokens[ 1 ] );
2903
+ this.year = parseInt( tokens[ 2 ] );
2904
+
2905
+ this.currentDate = this._getCurrentDate();
2906
+
2907
+ this.fromMonthYear( this.month, this.year );
2908
+ }
2909
+
2910
+ fromMonthYear( month, year ) {
2911
+
2912
+ // Month is 0-based (0 = January, ... 11 = December)
2913
+ month--;
2914
+
2915
+ year = year ?? new Date().getFullYear();
2916
+
2917
+ const weekDay = new Date( year, month, 1 ).getDay();
2918
+ const firstDay = weekDay === 0 ? 6 : weekDay - 1; // 0 = Monday, 1 = Tuesday...
2919
+ const daysInMonth = new Date( year, month + 1, 0 ).getDate();
2920
+
2921
+ // Previous month
2922
+ const prevMonth = month === 0 ? 11 : month - 1;
2923
+ const prevYear = month === 0 ? year - 1 : year;
2924
+ const daysInPrevMonth = new Date( prevYear, prevMonth + 1, 0 ).getDate();
2925
+
2926
+ // Prepare full grid (up to 6 weeks = 42 days)
2927
+ const calendarDays = [];
2928
+
2929
+ // Fill in days from previous month
2930
+ for( let i = firstDay - 1; i >= 0; i--)
2684
2931
  {
2685
- position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - this.root.offsetWidth - this._windowPadding );
2686
- position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - this.root.offsetHeight - this._windowPadding );
2932
+ calendarDays.push( { day: daysInPrevMonth - i, currentMonth: false } );
2687
2933
  }
2688
2934
 
2689
- this.root.style.left = `${ position[ 0 ] }px`;
2690
- this.root.style.top = `${ position[ 1 ] }px`;
2935
+ // Fill in current month days
2936
+ for ( let i = 1; i <= daysInMonth; i++ )
2937
+ {
2938
+ calendarDays.push( { day: i, currentMonth: true } );
2939
+ }
2940
+
2941
+ // Fill in next month days to complete the grid (if needed)
2942
+ const remaining = 42 - calendarDays.length;
2943
+ for( let i = 1; i <= remaining; i++ )
2944
+ {
2945
+ calendarDays.push( { day: i, currentMonth: false } );
2946
+ }
2947
+
2948
+ this.monthName = this.getMonthName( month );
2949
+ this.firstDay = firstDay;
2950
+ this.daysInMonth = daysInMonth;
2951
+ this.calendarDays = calendarDays;
2952
+
2953
+ this.refresh();
2691
2954
  }
2692
- };
2693
2955
 
2694
- LX.ColorPicker = ColorPicker;
2956
+ getMonthName( monthIndex, locale = "en-US" ) {
2957
+ const formatter = new Intl.DateTimeFormat( locale, { month: "long" } );
2958
+ return formatter.format( new Date( 2000, monthIndex, 1 ) );
2959
+ }
2960
+
2961
+ getFullDate() {
2962
+ return `${ this.monthName } ${ this.day }${ this._getOrdinalSuffix( this.day ) }, ${ this.year }`;
2963
+ }
2964
+
2965
+ _getOrdinalSuffix( day ) {
2966
+ if ( day > 3 && day < 21 ) return "th";
2967
+ switch ( day % 10 )
2968
+ {
2969
+ case 1: return "st";
2970
+ case 2: return "nd";
2971
+ case 3: return "rd";
2972
+ default: return "th";
2973
+ }
2974
+ }
2975
+ }
2976
+
2977
+ LX.Calendar = Calendar;
2978
+
2979
+ /* Layout Classes */
2695
2980
 
2696
2981
  class Area {
2697
2982
 
@@ -5297,8 +5582,9 @@ class Widget {
5297
5582
  static COUNTER = 33;
5298
5583
  static TABLE = 34;
5299
5584
  static TABS = 35;
5300
- static LABEL = 36;
5301
- static BLANK = 37;
5585
+ static DATE = 36;
5586
+ static LABEL = 37;
5587
+ static BLANK = 38;
5302
5588
 
5303
5589
  static NO_CONTEXT_TYPES = [
5304
5590
  Widget.BUTTON,
@@ -5536,6 +5822,7 @@ class Widget {
5536
5822
  case Widget.COUNTER: return "Counter";
5537
5823
  case Widget.TABLE: return "Table";
5538
5824
  case Widget.TABS: return "Tabs";
5825
+ case Widget.DATE: return "Date";
5539
5826
  case Widget.LABEL: return "Label";
5540
5827
  case Widget.BLANK: return "Blank";
5541
5828
  case Widget.CUSTOM: return this.customName;
@@ -6136,8 +6423,11 @@ class NodeTree {
6136
6423
  actionEl.className = "lexicon " + a.icon;
6137
6424
  actionEl.title = a.name;
6138
6425
  actionEl.addEventListener("click", function( e ) {
6139
- a.callback( node, actionEl );
6140
- e.stopPropagation();
6426
+ if( a.callback )
6427
+ {
6428
+ a.callback( node, actionEl );
6429
+ e.stopPropagation();
6430
+ }
6141
6431
  });
6142
6432
 
6143
6433
  inputContainer.appendChild( actionEl );
@@ -6238,6 +6528,8 @@ class NodeTree {
6238
6528
  }
6239
6529
  }
6240
6530
 
6531
+ LX.NodeTree = NodeTree;
6532
+
6241
6533
  /**
6242
6534
  * @class Blank
6243
6535
  * @description Blank Widget
@@ -6628,6 +6920,7 @@ class Button extends Widget {
6628
6920
  wValue.querySelector( "a" ).classList.add( "swap-off" );
6629
6921
 
6630
6922
  const input = document.createElement( "input" );
6923
+ input.className = "p-0 border-0";
6631
6924
  input.type = "checkbox";
6632
6925
  wValue.prepend( input );
6633
6926
 
@@ -8188,6 +8481,16 @@ class ColorInput extends Widget {
8188
8481
  container.className = "lexcolor";
8189
8482
  this.root.appendChild( container );
8190
8483
 
8484
+ this.picker = new ColorPicker( value, {
8485
+ colorModel: options.useRGB ? "RGB" : "Hex",
8486
+ useAlpha,
8487
+ onChange: ( color ) => {
8488
+ this._fromColorPicker = true;
8489
+ this.set( color.hex );
8490
+ delete this._fromColorPicker;
8491
+ }
8492
+ } );
8493
+
8191
8494
  let sampleContainer = LX.makeContainer( ["18px", "18px"], "flex flex-row bg-contrast rounded overflow-hidden", "", container );
8192
8495
  sampleContainer.tabIndex = "1";
8193
8496
  sampleContainer.addEventListener( "click", e => {
@@ -8195,15 +8498,8 @@ class ColorInput extends Widget {
8195
8498
  {
8196
8499
  return;
8197
8500
  }
8198
- new ColorPicker( value, sampleContainer, {
8199
- colorModel: options.useRGB ? "RGB" : "Hex",
8200
- useAlpha,
8201
- onChange: ( color ) => {
8202
- this._fromColorPicker = true;
8203
- this.set( color.hex );
8204
- delete this._fromColorPicker;
8205
- }
8206
- } );
8501
+
8502
+ this._popover = new Popover( sampleContainer, [ this.picker ] );
8207
8503
  } );
8208
8504
 
8209
8505
  let colorSampleRGB = document.createElement( 'div' );
@@ -10196,6 +10492,75 @@ class Table extends Widget {
10196
10492
 
10197
10493
  LX.Table = Table;
10198
10494
 
10495
+ /**
10496
+ * @class DatePicker
10497
+ * @description DatePicker Widget
10498
+ */
10499
+
10500
+ class DatePicker extends Widget {
10501
+
10502
+ constructor( name, dateString, callback, options = { } ) {
10503
+
10504
+ super( Widget.DATE, name, null, options );
10505
+
10506
+ if( options.today )
10507
+ {
10508
+ const date = new Date();
10509
+ dateString = `${ date.getDate() }/${ date.getMonth() + 1 }/${ date.getFullYear() }`;
10510
+ }
10511
+
10512
+ this.onGetValue = () => {
10513
+ return dateString;
10514
+ }
10515
+
10516
+ this.onSetValue = ( newValue, skipCallback, event ) => {
10517
+
10518
+ dateString = newValue;
10519
+
10520
+ this.calendar.fromDateString( newValue );
10521
+
10522
+ refresh( this.calendar.getFullDate() );
10523
+
10524
+ if( !skipCallback )
10525
+ {
10526
+ this._trigger( new IEvent( name, newValue, event ), callback );
10527
+ }
10528
+ }
10529
+
10530
+ this.onResize = ( rect ) => {
10531
+ const realNameWidth = ( this.root.domName?.style.width ?? "0px" );
10532
+ container.style.width = `calc( 100% - ${ realNameWidth })`;
10533
+ };
10534
+
10535
+ const container = document.createElement('div');
10536
+ container.className = "lexdate";
10537
+ this.root.appendChild( container );
10538
+
10539
+ this.calendar = new Calendar( dateString, { onChange: ( date ) => {
10540
+ this.set( `${ date.day }/${ date.month }/${ date.year }` )
10541
+ }, ...options });
10542
+
10543
+ const refresh = ( currentDate ) => {
10544
+ container.innerHTML = "";
10545
+ const calendarIcon = LX.makeIcon( "calendar" );
10546
+ const calendarButton = new Button( null, currentDate ?? "Pick a date", () => {
10547
+ this._popover = new Popover( calendarButton.root, ( popoverRoot ) => {
10548
+ popoverRoot.appendChild( this.calendar.root );
10549
+ } );
10550
+ }, { buttonClass: `flex flex-row px-3 ${ currentDate ? "" : "fg-tertiary" } justify-between` } );
10551
+
10552
+ calendarButton.root.querySelector( "button" ).appendChild( calendarIcon );
10553
+ container.appendChild( calendarButton.root );
10554
+ };
10555
+
10556
+ refresh( dateString ? this.calendar.getFullDate(): null );
10557
+
10558
+ doAsync( this.onResize.bind( this ) );
10559
+ }
10560
+ }
10561
+
10562
+ LX.DatePicker = DatePicker;
10563
+
10199
10564
  /**
10200
10565
  * @class Panel
10201
10566
  */
@@ -11342,6 +11707,23 @@ class Panel {
11342
11707
  const widget = new Table( name, data, options );
11343
11708
  return this._attachWidget( widget );
11344
11709
  }
11710
+
11711
+ /**
11712
+ * @method addDate
11713
+ * @param {String} name Widget name
11714
+ * @param {String} dateString
11715
+ * @param {Function} callback
11716
+ * @param {Object} options:
11717
+ * hideName: Don't use name as label [false]
11718
+ * today: Set current day as selected by default
11719
+ * untilToday: Allow dates only until current day
11720
+ * fromToday: Allow dates only from current day
11721
+ */
11722
+
11723
+ addDate( name, dateString, callback, options = { } ) {
11724
+ const widget = new DatePicker( name, dateString, callback, options );
11725
+ return this._attachWidget( widget );
11726
+ }
11345
11727
  }
11346
11728
 
11347
11729
  LX.Panel = Panel;
@@ -14374,6 +14756,7 @@ LX.ICONS = {
14374
14756
  "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"],
14375
14757
  "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"],
14376
14758
  "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"],
14759
+ "frame": [24, 24, [], "solid", "M2 6h20M2 18h20M6 2v20M18 2v20", null, "fill=none stroke-width=2 stroke-linejoin=round stroke-linecap=round"],
14377
14760
  "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"],
14378
14761
  "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"],
14379
14762
  "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"],