lexgui 0.5.8 → 0.5.10

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.10",
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
 
@@ -1239,7 +1239,7 @@ function _createCommandbar( root )
1239
1239
  if( LX.has('CodeEditor') )
1240
1240
  {
1241
1241
  const instances = LX.CodeEditor.getInstances();
1242
- if( !instances.length ) return;
1242
+ if( !instances.length || !instances[ 0 ].area.root.offsetHeight ) return;
1243
1243
 
1244
1244
  const languages = instances[ 0 ].languages;
1245
1245
 
@@ -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 = {} ) {
2393
+ constructor( hexValue, options = {} ) {
2242
2394
 
2243
- console.assert( trigger, "ColorPicker needs a DOM element as trigger!" );
2244
-
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();
2642
+ doAsync( this._placeMarkers.bind( this ) );
2534
2643
 
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
- };
2544
-
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 );
2557
-
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";
2647
+ _placeMarkers() {
2561
2648
 
2562
- this._updateColorValue( hexColor );
2563
- }
2649
+ this._svToPosition( this.currentColor.hsv.s, this.currentColor.hsv.v );
2564
2650
 
2565
- destroy() {
2566
-
2567
- this._trigger.classList.remove( "triggered" );
2651
+ const hueLeft = LX.remapRange( this.currentColor.hsv.h, 0, 360, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2652
+ this.hueMarker.style.left = hueLeft + "px";
2568
2653
 
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
 
@@ -4712,14 +4997,14 @@ class SideBar {
4712
4997
  this.root.appendChild( this.footer );
4713
4998
  }
4714
4999
 
4715
- // Set width depending on header/footer
4716
- doAsync( () => {
4717
- // This account for header, footer and all inner margins
4718
- const contentOffset = ( parseInt( this.header?.getComputedSize().height ) ?? 0 ) +
4719
- ( parseInt( this.filter?.getComputedSize().height ) ?? 0 ) +
4720
- ( parseInt( this.footer?.getComputedSize().height ) ?? 0 );
5000
+ const resizeObserver = new ResizeObserver( entries => {
5001
+ const contentOffset = ( this.header?.offsetHeight ?? 0 ) +
5002
+ ( this.filter?.offsetHeight ?? 0 ) +
5003
+ ( this.footer?.offsetHeight ?? 0 );
4721
5004
  this.content.style.height = `calc(100% - ${ contentOffset }px)`;
4722
- }, 10 );
5005
+ } );
5006
+
5007
+ resizeObserver.observe( this.root );
4723
5008
 
4724
5009
  this.items = [ ];
4725
5010
  this.icons = { };
@@ -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,73 @@ 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, [ this.calendar ] );
10548
+ }, { buttonClass: `flex flex-row px-3 ${ currentDate ? "" : "fg-tertiary" } justify-between` } );
10549
+
10550
+ calendarButton.root.querySelector( "button" ).appendChild( calendarIcon );
10551
+ container.appendChild( calendarButton.root );
10552
+ };
10553
+
10554
+ refresh( dateString ? this.calendar.getFullDate(): null );
10555
+
10556
+ doAsync( this.onResize.bind( this ) );
10557
+ }
10558
+ }
10559
+
10560
+ LX.DatePicker = DatePicker;
10561
+
10199
10562
  /**
10200
10563
  * @class Panel
10201
10564
  */
@@ -11342,6 +11705,23 @@ class Panel {
11342
11705
  const widget = new Table( name, data, options );
11343
11706
  return this._attachWidget( widget );
11344
11707
  }
11708
+
11709
+ /**
11710
+ * @method addDate
11711
+ * @param {String} name Widget name
11712
+ * @param {String} dateString
11713
+ * @param {Function} callback
11714
+ * @param {Object} options:
11715
+ * hideName: Don't use name as label [false]
11716
+ * today: Set current day as selected by default
11717
+ * untilToday: Allow dates only until current day
11718
+ * fromToday: Allow dates only from current day
11719
+ */
11720
+
11721
+ addDate( name, dateString, callback, options = { } ) {
11722
+ const widget = new DatePicker( name, dateString, callback, options );
11723
+ return this._attachWidget( widget );
11724
+ }
11345
11725
  }
11346
11726
 
11347
11727
  LX.Panel = Panel;
@@ -11668,7 +12048,6 @@ class Footer {
11668
12048
  parent.appendChild( root );
11669
12049
 
11670
12050
  // Set always at bottom
11671
- root.previousElementSibling.style.height = "unset";
11672
12051
  root.previousElementSibling.style.flexGrow = "1";
11673
12052
 
11674
12053
  this.root = root;
@@ -13064,8 +13443,8 @@ class AssetView {
13064
13443
  div.className = 'lexassetbrowser';
13065
13444
  this.root = div;
13066
13445
 
13067
- let area = new LX.Area({height: "100%"});
13068
- div.appendChild(area.root);
13446
+ let area = new LX.Area( { width: "100%", height: "100%" } );
13447
+ div.appendChild( area.root );
13069
13448
 
13070
13449
  let left, right, contentArea = area;
13071
13450
 
@@ -13078,6 +13457,9 @@ class AssetView {
13078
13457
  this.contextMenu = options.contextMenu ?? [];
13079
13458
  this.onRefreshContent = options.onRefreshContent;
13080
13459
 
13460
+ // Append temporarily to the dom
13461
+ document.body.appendChild( this.root );
13462
+
13081
13463
  if( !this.skipBrowser )
13082
13464
  {
13083
13465
  [left, right] = area.split({ type: "horizontal", sizes: ["15%", "85%"]});
@@ -13115,6 +13497,9 @@ class AssetView {
13115
13497
  {
13116
13498
  this.previewPanel = right.addPanel( {className: 'lexassetcontentpanel', style: { overflow: 'scroll' }} );
13117
13499
  }
13500
+
13501
+ // Clean up
13502
+ document.body.removeChild( this.root );
13118
13503
  }
13119
13504
 
13120
13505
  /**
@@ -14374,6 +14759,7 @@ LX.ICONS = {
14374
14759
  "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
14760
  "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
14761
  "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"],
14762
+ "frame": [24, 24, [], "solid", "M2 6h20M2 18h20M6 2v20M18 2v20", null, "fill=none stroke-width=2 stroke-linejoin=round stroke-linecap=round"],
14377
14763
  "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
14764
  "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
14765
  "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"],