lexgui 0.6.10 → 0.6.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/build/lexgui.js CHANGED
@@ -14,7 +14,7 @@ console.warn( 'Script _build/lexgui.js_ is depracated and will be removed soon.
14
14
  */
15
15
 
16
16
  const LX = {
17
- version: "0.6.10",
17
+ version: "0.6.12",
18
18
  ready: false,
19
19
  components: [], // Specific pre-build components
20
20
  signals: {}, // Events and triggers
@@ -725,8 +725,6 @@ class Popover {
725
725
 
726
726
  constructor( trigger, content, options = {} ) {
727
727
 
728
- console.assert( trigger, "Popover needs a DOM element as trigger!" );
729
-
730
728
  if( Popover.activeElement )
731
729
  {
732
730
  Popover.activeElement.destroy();
@@ -734,13 +732,20 @@ class Popover {
734
732
  }
735
733
 
736
734
  this._trigger = trigger;
737
- trigger.classList.add( "triggered" );
738
- trigger.active = this;
735
+
736
+ if( trigger )
737
+ {
738
+ trigger.classList.add( "triggered" );
739
+ trigger.active = this;
740
+ }
739
741
 
740
742
  this._windowPadding = 4;
741
743
  this.side = options.side ?? "bottom";
742
744
  this.align = options.align ?? "center";
745
+ this.sideOffset = options.sideOffset ?? 0;
746
+ this.alignOffset = options.alignOffset ?? 0;
743
747
  this.avoidCollisions = options.avoidCollisions ?? true;
748
+ this.reference = options.reference;
744
749
 
745
750
  this.root = document.createElement( "div" );
746
751
  this.root.dataset["side"] = this.side;
@@ -775,29 +780,35 @@ class Popover {
775
780
  LX.doAsync( () => {
776
781
  this._adjustPosition();
777
782
 
778
- this.root.focus();
783
+ if( this._trigger )
784
+ {
785
+ this.root.focus();
779
786
 
780
- this._onClick = e => {
781
- if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
782
- {
783
- return;
784
- }
785
- this.destroy();
786
- };
787
+ this._onClick = e => {
788
+ if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
789
+ {
790
+ return;
791
+ }
792
+ this.destroy();
793
+ };
794
+
795
+ document.body.addEventListener( "mousedown", this._onClick, true );
796
+ document.body.addEventListener( "focusin", this._onClick, true );
797
+ }
787
798
 
788
- document.body.addEventListener( "mousedown", this._onClick, true );
789
- document.body.addEventListener( "focusin", this._onClick, true );
790
799
  }, 10 );
791
800
  }
792
801
 
793
802
  destroy() {
794
803
 
795
- this._trigger.classList.remove( "triggered" );
796
-
797
- delete this._trigger.active;
804
+ if( this._trigger )
805
+ {
806
+ this._trigger.classList.remove( "triggered" );
807
+ delete this._trigger.active;
798
808
 
799
- document.body.removeEventListener( "mousedown", this._onClick, true );
800
- document.body.removeEventListener( "focusin", this._onClick, true );
809
+ document.body.removeEventListener( "mousedown", this._onClick, true );
810
+ document.body.removeEventListener( "focusin", this._onClick, true );
811
+ }
801
812
 
802
813
  this.root.remove();
803
814
 
@@ -810,26 +821,28 @@ class Popover {
810
821
 
811
822
  // Place menu using trigger position and user options
812
823
  {
813
- const rect = this._trigger.getBoundingClientRect();
824
+ const el = this.reference ?? this._trigger;
825
+ console.assert( el, "Popover needs a trigger or reference element!" );
826
+ const rect = el.getBoundingClientRect();
814
827
 
815
828
  let alignWidth = true;
816
829
 
817
830
  switch( this.side )
818
831
  {
819
832
  case "left":
820
- position[ 0 ] += ( rect.x - this.root.offsetWidth );
833
+ position[ 0 ] += ( rect.x - this.root.offsetWidth - this.sideOffset );
821
834
  alignWidth = false;
822
835
  break;
823
836
  case "right":
824
- position[ 0 ] += ( rect.x + rect.width );
837
+ position[ 0 ] += ( rect.x + rect.width + this.sideOffset );
825
838
  alignWidth = false;
826
839
  break;
827
840
  case "top":
828
- position[ 1 ] += ( rect.y - this.root.offsetHeight );
841
+ position[ 1 ] += ( rect.y - this.root.offsetHeight - this.sideOffset );
829
842
  alignWidth = true;
830
843
  break;
831
844
  case "bottom":
832
- position[ 1 ] += ( rect.y + rect.height );
845
+ position[ 1 ] += ( rect.y + rect.height + this.sideOffset );
833
846
  alignWidth = true;
834
847
  break;
835
848
  }
@@ -849,6 +862,9 @@ class Popover {
849
862
  else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
850
863
  break;
851
864
  }
865
+
866
+ if( alignWidth ) { position[ 0 ] += this.alignOffset; }
867
+ else { position[ 1 ] += this.alignOffset; }
852
868
  }
853
869
 
854
870
  if( this.avoidCollisions )
@@ -863,6 +879,58 @@ class Popover {
863
879
  }
864
880
  LX.Popover = Popover;
865
881
 
882
+ /**
883
+ * @class PopConfirm
884
+ */
885
+
886
+ class PopConfirm {
887
+
888
+ constructor( reference, options = {} ) {
889
+
890
+ const okText = options.confirmText ?? "Yes";
891
+ const cancelText = options.cancelText ?? "No";
892
+ const title = options.title ?? "Confirm";
893
+ const content = options.content ?? "Are you sure you want to proceed?";
894
+ const onConfirm = options.onConfirm;
895
+ const onCancel = options.onCancel;
896
+
897
+ const popoverContainer = LX.makeContainer( ["auto", "auto"], "tour-step-container" );
898
+
899
+ {
900
+ const headerDiv = LX.makeContainer( ["100%", "auto"], "flex flex-row", "", popoverContainer );
901
+ LX.makeContainer( ["100%", "auto"], "p-1 font-medium text-md", title, headerDiv );
902
+ }
903
+
904
+ LX.makeContainer( ["100%", "auto"], "p-1 text-md", content, popoverContainer, { maxWidth: "400px" } );
905
+ const footer = LX.makeContainer( ["100%", "auto"], "flex flex-row text-md", "", popoverContainer );
906
+ const footerButtons = LX.makeContainer( ["100%", "auto"], "text-md", "", footer );
907
+ const footerPanel = new LX.Panel();
908
+ footerButtons.appendChild( footerPanel.root );
909
+
910
+ footerPanel.sameLine( 2, "justify-end" );
911
+ footerPanel.addButton( null, cancelText, () => {
912
+ if( onCancel ) onCancel();
913
+ this._popover?.destroy();
914
+ }, { xbuttonClass: "contrast" } );
915
+ footerPanel.addButton( null, okText, () => {
916
+ if( onConfirm ) onConfirm();
917
+ this._popover?.destroy();
918
+ }, { buttonClass: "accent" } );
919
+
920
+ this._popover?.destroy();
921
+ this._popover = new LX.Popover( null, [ popoverContainer ], {
922
+ reference,
923
+ side: options.side ?? "top",
924
+ align: options.align,
925
+ sideOffset: options.sideOffset,
926
+ alignOffset: options.alignOffset,
927
+ } );
928
+
929
+ }
930
+ }
931
+
932
+ LX.PopConfirm = PopConfirm;
933
+
866
934
  /**
867
935
  * @class Sheet
868
936
  */
@@ -985,6 +1053,8 @@ class DropdownMenu {
985
1053
  this._windowPadding = 4;
986
1054
  this.side = options.side ?? "bottom";
987
1055
  this.align = options.align ?? "center";
1056
+ this.sideOffset = options.sideOffset ?? 0;
1057
+ this.alignOffset = options.alignOffset ?? 0;
988
1058
  this.avoidCollisions = options.avoidCollisions ?? true;
989
1059
  this.onBlur = options.onBlur;
990
1060
  this.inPlace = false;
@@ -1227,19 +1297,19 @@ class DropdownMenu {
1227
1297
  switch( this.side )
1228
1298
  {
1229
1299
  case "left":
1230
- position[ 0 ] += ( rect.x - this.root.offsetWidth );
1300
+ position[ 0 ] += ( rect.x - this.root.offsetWidth - this.sideOffset );
1231
1301
  alignWidth = false;
1232
1302
  break;
1233
1303
  case "right":
1234
- position[ 0 ] += ( rect.x + rect.width );
1304
+ position[ 0 ] += ( rect.x + rect.width + this.sideOffset );
1235
1305
  alignWidth = false;
1236
1306
  break;
1237
1307
  case "top":
1238
- position[ 1 ] += ( rect.y - this.root.offsetHeight );
1308
+ position[ 1 ] += ( rect.y - this.root.offsetHeight - this.sideOffset );
1239
1309
  alignWidth = true;
1240
1310
  break;
1241
1311
  case "bottom":
1242
- position[ 1 ] += ( rect.y + rect.height );
1312
+ position[ 1 ] += ( rect.y + rect.height + this.sideOffset );
1243
1313
  alignWidth = true;
1244
1314
  break;
1245
1315
  }
@@ -1259,6 +1329,9 @@ class DropdownMenu {
1259
1329
  else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
1260
1330
  break;
1261
1331
  }
1332
+
1333
+ if( alignWidth ) { position[ 0 ] += this.alignOffset; }
1334
+ else { position[ 1 ] += this.alignOffset; }
1262
1335
  }
1263
1336
 
1264
1337
  if( this.avoidCollisions )
@@ -1653,8 +1726,15 @@ class Calendar {
1653
1726
  this.root = LX.makeContainer( ["256px", "auto"], "p-1 text-md" );
1654
1727
 
1655
1728
  this.onChange = options.onChange;
1729
+ this.onPreviousMonth = options.onPreviousMonth;
1730
+ this.onNextMonth = options.onNextMonth;
1731
+
1656
1732
  this.untilToday = options.untilToday;
1657
1733
  this.fromToday = options.fromToday;
1734
+ this.range = options.range;
1735
+
1736
+ this.skipPrevMonth = options.skipPrevMonth;
1737
+ this.skipNextMonth = options.skipNextMonth;
1658
1738
 
1659
1739
  if( dateString )
1660
1740
  {
@@ -1678,7 +1758,7 @@ class Calendar {
1678
1758
  }
1679
1759
  }
1680
1760
 
1681
- _previousMonth() {
1761
+ _previousMonth( skipCallback ) {
1682
1762
 
1683
1763
  this.month = Math.max( 0, this.month - 1 );
1684
1764
 
@@ -1689,9 +1769,14 @@ class Calendar {
1689
1769
  }
1690
1770
 
1691
1771
  this.fromMonthYear( this.month, this.year );
1772
+
1773
+ if( !skipCallback && this.onPreviousMonth )
1774
+ {
1775
+ this.onPreviousMonth( this.currentDate );
1776
+ }
1692
1777
  }
1693
1778
 
1694
- _nextMonth() {
1779
+ _nextMonth( skipCallback ) {
1695
1780
 
1696
1781
  this.month = Math.min( this.month + 1, 12 );
1697
1782
 
@@ -1702,6 +1787,11 @@ class Calendar {
1702
1787
  }
1703
1788
 
1704
1789
  this.fromMonthYear( this.month, this.year );
1790
+
1791
+ if( !skipCallback && this.onNextMonth )
1792
+ {
1793
+ this.onNextMonth( this.currentDate );
1794
+ }
1705
1795
  }
1706
1796
 
1707
1797
  refresh() {
@@ -1712,19 +1802,25 @@ class Calendar {
1712
1802
  {
1713
1803
  const header = LX.makeContainer( ["100%", "auto"], "flex flex-row p-1", "", this.root );
1714
1804
 
1715
- const prevMonthIcon = LX.makeIcon( "Left", { title: "Previous Month", iconClass: "border p-1 rounded hover:bg-secondary", svgClass: "sm" } );
1716
- header.appendChild( prevMonthIcon );
1717
- prevMonthIcon.addEventListener( "click", () => {
1718
- this._previousMonth();
1719
- } );
1805
+ if( !this.skipPrevMonth )
1806
+ {
1807
+ const prevMonthIcon = LX.makeIcon( "Left", { title: "Previous Month", iconClass: "border p-1 rounded hover:bg-secondary", svgClass: "sm" } );
1808
+ header.appendChild( prevMonthIcon );
1809
+ prevMonthIcon.addEventListener( "click", () => {
1810
+ this._previousMonth();
1811
+ } );
1812
+ }
1720
1813
 
1721
1814
  LX.makeContainer( ["100%", "auto"], "text-center font-medium select-none", `${ this.monthName } ${ this.year }`, header );
1722
1815
 
1723
- const nextMonthIcon = LX.makeIcon( "Right", { title: "Next Month", iconClass: "border p-1 rounded hover:bg-secondary", svgClass: "sm" } );
1724
- header.appendChild( nextMonthIcon );
1725
- nextMonthIcon.addEventListener( "click", () => {
1726
- this._nextMonth();
1727
- } );
1816
+ if( !this.skipNextMonth )
1817
+ {
1818
+ const nextMonthIcon = LX.makeIcon( "Right", { title: "Next Month", iconClass: "border p-1 rounded hover:bg-secondary", svgClass: "sm" } );
1819
+ header.appendChild( nextMonthIcon );
1820
+ nextMonthIcon.addEventListener( "click", () => {
1821
+ this._nextMonth();
1822
+ } );
1823
+ }
1728
1824
  }
1729
1825
 
1730
1826
  // Body
@@ -1756,6 +1852,11 @@ class Calendar {
1756
1852
  const body = document.createElement( 'tbody' );
1757
1853
  daysTable.appendChild( body );
1758
1854
 
1855
+ let fromRangeDate = this.range ? LX.dateFromDateString( this.range[ 0 ] ) : null;
1856
+ let toRangeDate = this.range ? LX.dateFromDateString( this.range[ 1 ] ) : null;
1857
+
1858
+ this.currentDate ? new Date( `${ this.currentDate.month }/${ this.currentDate.day }/${ this.currentDate.year }` ) : null;
1859
+
1759
1860
  for( let week = 0; week < 6; week++ )
1760
1861
  {
1761
1862
  const hrow = document.createElement( 'tr' );
@@ -1768,15 +1869,28 @@ class Calendar {
1768
1869
 
1769
1870
  const dayDate = new Date( `${ this.month }/${ dayData.day }/${ this.year }` );
1770
1871
  const date = new Date();
1872
+ // today inclusives
1771
1873
  const beforeToday = this.untilToday ? ( dayDate.getTime() < date.getTime() ) : true;
1772
- const afterToday = this.fromToday ? ( dayDate.getTime() > date.getTime() ) : true;
1874
+ const afterToday = this.fromToday ? ( dayDate.getFullYear() > date.getFullYear() ||
1875
+ (dayDate.getFullYear() === date.getFullYear() && dayDate.getMonth() > date.getMonth()) ||
1876
+ (dayDate.getFullYear() === date.getFullYear() && dayDate.getMonth() === date.getMonth() && dayDate.getDate() >= date.getDate())
1877
+ ) : true;
1773
1878
  const selectable = dayData.currentMonth && beforeToday && afterToday;
1774
-
1775
- if( this.currentDate && ( dayData.day == this.currentDate.day ) && ( this.month == this.currentDate.month )
1776
- && ( this.year == this.currentDate.year ) && dayData.currentMonth )
1879
+ const currentDay = this.currentDate && ( dayData.day == this.currentDate.day ) && ( this.month == this.currentDate.month )
1880
+ && ( this.year == this.currentDate.year ) && dayData.currentMonth;
1881
+ const currentFromRange = selectable && fromRangeDate && ( dayData.day == fromRangeDate.getDate() ) && ( this.month == ( fromRangeDate.getMonth() + 1 ) )
1882
+ && ( this.year == fromRangeDate.getFullYear() );
1883
+ const currentToRange = selectable && toRangeDate && ( dayData.day == toRangeDate.getDate() ) && ( this.month == ( toRangeDate.getMonth() + 1 ) )
1884
+ && ( this.year == toRangeDate.getFullYear() );
1885
+
1886
+ if( ( !this.range && currentDay ) || this.range && ( currentFromRange || currentToRange ) )
1777
1887
  {
1778
1888
  th.className += ` bg-contrast fg-contrast`;
1779
1889
  }
1890
+ else if( this.range && selectable && ( dayDate > fromRangeDate ) && ( dayDate < toRangeDate ) )
1891
+ {
1892
+ th.className += ` bg-accent fg-contrast`;
1893
+ }
1780
1894
  else
1781
1895
  {
1782
1896
  th.className += ` ${ selectable ? "fg-primary" : "fg-tertiary" } hover:bg-secondary`;
@@ -1796,6 +1910,20 @@ class Calendar {
1796
1910
  }
1797
1911
  } );
1798
1912
  }
1913
+ // This event should only be applied in non current month days
1914
+ else if( this.range === undefined && !dayData.currentMonth )
1915
+ {
1916
+ th.addEventListener( "click", () => {
1917
+ if( dayData?.prevMonth )
1918
+ {
1919
+ this._previousMonth();
1920
+ }
1921
+ else
1922
+ {
1923
+ this._nextMonth();
1924
+ }
1925
+ } );
1926
+ }
1799
1927
  }
1800
1928
 
1801
1929
  body.appendChild( hrow );
@@ -1810,6 +1938,7 @@ class Calendar {
1810
1938
 
1811
1939
  this.day = parseInt( tokens[ 0 ] );
1812
1940
  this.month = parseInt( tokens[ 1 ] );
1941
+ this.monthName = this.getMonthName( this.month - 1 );
1813
1942
  this.year = parseInt( tokens[ 2 ] );
1814
1943
 
1815
1944
  this.currentDate = this._getCurrentDate();
@@ -1839,7 +1968,7 @@ class Calendar {
1839
1968
  // Fill in days from previous month
1840
1969
  for( let i = firstDay - 1; i >= 0; i--)
1841
1970
  {
1842
- calendarDays.push( { day: daysInPrevMonth - i, currentMonth: false } );
1971
+ calendarDays.push( { day: daysInPrevMonth - i, currentMonth: false, prevMonth: true } );
1843
1972
  }
1844
1973
 
1845
1974
  // Fill in current month days
@@ -1852,7 +1981,7 @@ class Calendar {
1852
1981
  const remaining = 42 - calendarDays.length;
1853
1982
  for( let i = 1; i <= remaining; i++ )
1854
1983
  {
1855
- calendarDays.push( { day: i, currentMonth: false } );
1984
+ calendarDays.push( { day: i, currentMonth: false, nextMonth: true } );
1856
1985
  }
1857
1986
 
1858
1987
  this.monthName = this.getMonthName( month );
@@ -1868,8 +1997,14 @@ class Calendar {
1868
1997
  return formatter.format( new Date( 2000, monthIndex, 1 ) );
1869
1998
  }
1870
1999
 
1871
- getFullDate() {
1872
- return `${ this.monthName } ${ this.day }${ this._getOrdinalSuffix( this.day ) }, ${ this.year }`;
2000
+ getFullDate( monthName, day, year ) {
2001
+ return `${ monthName ?? this.monthName } ${ day ?? this.day }${ this._getOrdinalSuffix( day ?? this.day ) }, ${ year ?? this.year }`;
2002
+ }
2003
+
2004
+ setRange( range ) {
2005
+ console.assert( range.constructor === Array, "Date Range must be in Array format" );
2006
+ this.range = range;
2007
+ this.refresh();
1873
2008
  }
1874
2009
 
1875
2010
  _getOrdinalSuffix( day ) {
@@ -1886,6 +2021,110 @@ class Calendar {
1886
2021
 
1887
2022
  LX.Calendar = Calendar;
1888
2023
 
2024
+ class CalendarRange {
2025
+
2026
+ /**
2027
+ * @constructor CalendarRange
2028
+ * @param {Array} range ["DD/MM/YYYY", "DD/MM/YYYY"]
2029
+ * @param {Object} options
2030
+ */
2031
+
2032
+ constructor( range, options = {} ) {
2033
+
2034
+ this.root = LX.makeContainer( ["auto", "auto"], "flex flex-row" );
2035
+
2036
+ console.assert( range && range.constructor === Array, "Range cannot be empty and has to be an Array!" );
2037
+
2038
+ let mustAdvanceMonth = false;
2039
+
2040
+ // Fix any issues with date range picking
2041
+ {
2042
+ const t0 = LX.dateFromDateString( range[ 0 ] );
2043
+ const t1 = LX.dateFromDateString( range[ 1 ] );
2044
+
2045
+ if( t0 > t1 )
2046
+ {
2047
+ const tmp = range[ 0 ];
2048
+ range[ 0 ] = range[ 1 ];
2049
+ range[ 1 ] = tmp;
2050
+ }
2051
+
2052
+ mustAdvanceMonth = ( t0.getMonth() == t1.getMonth() ) && ( t0.getFullYear() == t1.getFullYear() );
2053
+ }
2054
+
2055
+ this.from = range[ 0 ];
2056
+ this.to = range[ 1 ];
2057
+
2058
+ this._selectingRange = false;
2059
+
2060
+ const onChange = ( date ) => {
2061
+
2062
+ const newDateString = `${ date.day }/${ date.month }/${ date.year }`;
2063
+
2064
+ if( !this._selectingRange )
2065
+ {
2066
+ this.from = this.to = newDateString;
2067
+ this._selectingRange = true;
2068
+ }
2069
+ else
2070
+ {
2071
+ this.to = newDateString;
2072
+ this._selectingRange = false;
2073
+ }
2074
+
2075
+ const newRange = [ this.from, this.to ];
2076
+
2077
+ this.fromCalendar.setRange( newRange );
2078
+ this.toCalendar.setRange( newRange );
2079
+
2080
+ if( options.onChange )
2081
+ {
2082
+ options.onChange( newRange );
2083
+ }
2084
+
2085
+ };
2086
+
2087
+ this.fromCalendar = new LX.Calendar( this.from, {
2088
+ skipNextMonth: true,
2089
+ onChange,
2090
+ onPreviousMonth: () => {
2091
+ this.toCalendar._previousMonth();
2092
+ },
2093
+ range
2094
+ });
2095
+
2096
+ this.toCalendar = new LX.Calendar( this.to, {
2097
+ skipPrevMonth: true,
2098
+ onChange,
2099
+ onNextMonth: () => {
2100
+ this.fromCalendar._nextMonth();
2101
+ },
2102
+ range
2103
+ });
2104
+
2105
+ if( mustAdvanceMonth )
2106
+ {
2107
+ this.toCalendar._nextMonth( true );
2108
+ }
2109
+
2110
+ this.root.appendChild( this.fromCalendar.root );
2111
+ this.root.appendChild( this.toCalendar.root );
2112
+ }
2113
+
2114
+ getFullDate() {
2115
+
2116
+ const d0 = LX.dateFromDateString( this.from );
2117
+ const d0Month = this.fromCalendar.getMonthName( d0.getMonth() );
2118
+
2119
+ const d1 = LX.dateFromDateString( this.to );
2120
+ const d1Month = this.toCalendar.getMonthName( d1.getMonth() );
2121
+
2122
+ return `${ this.fromCalendar.getFullDate( d0Month, d0.getDate(), d0.getFullYear() ) } to ${ this.toCalendar.getFullDate( d1Month, d1.getDate(), d1.getFullYear() ) }`;
2123
+ }
2124
+ }
2125
+
2126
+ LX.CalendarRange = CalendarRange;
2127
+
1889
2128
  /**
1890
2129
  * @class Tabs
1891
2130
  */
@@ -4736,6 +4975,22 @@ function hsvToRgb( hsv )
4736
4975
 
4737
4976
  LX.hsvToRgb = hsvToRgb;
4738
4977
 
4978
+ /**
4979
+ * @method dateFromDateString
4980
+ * @description Get an instance of Date() from a Date in String format (DD/MM/YYYY)
4981
+ * @param {String} dateString
4982
+ */
4983
+ function dateFromDateString( dateString )
4984
+ {
4985
+ const tokens = dateString.split( '/' );
4986
+ const day = parseInt( tokens[ 0 ] );
4987
+ const month = parseInt( tokens[ 1 ] );
4988
+ const year = parseInt( tokens[ 2 ] );
4989
+ return new Date( `${ month }/${ day }/${ year }` );
4990
+ }
4991
+
4992
+ LX.dateFromDateString = dateFromDateString;
4993
+
4739
4994
  /**
4740
4995
  * @method measureRealWidth
4741
4996
  * @description Measure the pixel width of a text
@@ -8667,6 +8922,20 @@ class Button extends Widget {
8667
8922
  {
8668
8923
  wValue.querySelector( ".file-input" ).click();
8669
8924
  }
8925
+ else if( options.mustConfirm )
8926
+ {
8927
+ new LX.PopConfirm( wValue, {
8928
+ onConfirm: () => {
8929
+ this._trigger( new LX.IEvent( name, value, e ), callback );
8930
+ },
8931
+ side: options.confirmSide,
8932
+ align: options.confirmAlign,
8933
+ confirmText: options.confirmText,
8934
+ cancelText: options.confirmCancelText,
8935
+ title: options.confirmTitle,
8936
+ content: options.confirmContent
8937
+ } );
8938
+ }
8670
8939
  else
8671
8940
  {
8672
8941
  const swapInput = wValue.querySelector( "input" );
@@ -8962,16 +9231,17 @@ class Form extends Widget {
8962
9231
 
8963
9232
  if( entryData.constructor != Object )
8964
9233
  {
8965
- entryData = { };
9234
+ const oldValue = JSON.parse( JSON.stringify( entryData ) );
9235
+ entryData = { value: oldValue };
8966
9236
  data[ entry ] = entryData;
8967
9237
  }
8968
9238
 
8969
- entryData.placeholder = entryData.placeholder ?? entry;
9239
+ entryData.placeholder = entryData.placeholder ?? ( entryData.label ?? `Enter ${ entry }` );
8970
9240
  entryData.width = "100%";
8971
9241
 
8972
9242
  if( !( options.skipLabels ?? false ) )
8973
9243
  {
8974
- const label = new LX.TextInput( null, entry, null, { disabled: true, inputClass: "formlabel nobg" } );
9244
+ const label = new LX.TextInput( null, entryData.label ?? entry, null, { disabled: true, inputClass: "formlabel nobg" } );
8975
9245
  container.appendChild( label.root );
8976
9246
  }
8977
9247
 
@@ -10666,15 +10936,15 @@ class Vector extends Widget {
10666
10936
 
10667
10937
  for( let i = 0; i < vectorInputs.length; ++i )
10668
10938
  {
10669
- let value = newValue[ i ];
10670
- value = LX.clamp( value, +vectorInputs[ i ].min, +vectorInputs[ i ].max );
10671
- value = LX.round( value, options.precision ) ?? 0;
10672
- vectorInputs[ i ].value = newValue[ i ] = value;
10939
+ let vecValue = newValue[ i ];
10940
+ vecValue = LX.clamp( vecValue, +vectorInputs[ i ].min, +vectorInputs[ i ].max );
10941
+ vecValue = LX.round( vecValue, options.precision ) ?? 0;
10942
+ vectorInputs[ i ].value = value[ i ] = vecValue;
10673
10943
  }
10674
10944
 
10675
10945
  if( !skipCallback )
10676
10946
  {
10677
- this._trigger( new LX.IEvent( name, newValue, event ), callback );
10947
+ this._trigger( new LX.IEvent( name, value, event ), callback );
10678
10948
  }
10679
10949
  };
10680
10950
 
@@ -11264,12 +11534,18 @@ class Progress extends Widget {
11264
11534
  };
11265
11535
 
11266
11536
  this.onSetValue = ( newValue, skipCallback, event ) => {
11537
+ newValue = LX.clamp( newValue, progress.min, progress.max );
11267
11538
  this.root.querySelector("meter").value = newValue;
11268
11539
  _updateColor();
11269
11540
  if( this.root.querySelector("span") )
11270
11541
  {
11271
11542
  this.root.querySelector("span").innerText = newValue;
11272
11543
  }
11544
+
11545
+ if( !skipCallback )
11546
+ {
11547
+ this._trigger( new LX.IEvent( name, newValue, event ), options.callback );
11548
+ }
11273
11549
  };
11274
11550
 
11275
11551
  this.onResize = ( rect ) => {
@@ -11353,11 +11629,6 @@ class Progress extends Widget {
11353
11629
  const rect = progress.getBoundingClientRect();
11354
11630
  const newValue = LX.round( LX.remapRange( e.offsetX - rect.x, 0, rect.width, progress.min, progress.max ) );
11355
11631
  this.set( newValue, false, e );
11356
-
11357
- if( options.callback )
11358
- {
11359
- options.callback( newValue, e );
11360
- }
11361
11632
  }
11362
11633
 
11363
11634
  e.stopPropagation();
@@ -12420,25 +12691,30 @@ LX.Table = Table;
12420
12691
 
12421
12692
  class DatePicker extends Widget {
12422
12693
 
12423
- constructor( name, dateString, callback, options = { } ) {
12694
+ constructor( name, dateValue, callback, options = { } ) {
12424
12695
 
12425
12696
  super( Widget.DATE, name, null, options );
12426
12697
 
12427
- if( options.today )
12698
+ const dateAsRange = ( dateValue?.constructor === Array );
12699
+
12700
+ if( !dateAsRange && options.today )
12428
12701
  {
12429
12702
  const date = new Date();
12430
- dateString = `${ date.getDate() }/${ date.getMonth() + 1 }/${ date.getFullYear() }`;
12703
+ dateValue = `${ date.getDate() }/${ date.getMonth() + 1 }/${ date.getFullYear() }`;
12431
12704
  }
12432
12705
 
12433
12706
  this.onGetValue = () => {
12434
- return dateString;
12707
+ return dateValue;
12435
12708
  };
12436
12709
 
12437
12710
  this.onSetValue = ( newValue, skipCallback, event ) => {
12438
12711
 
12439
- dateString = newValue;
12712
+ if( !dateAsRange )
12713
+ {
12714
+ this.calendar.fromDateString( newValue );
12715
+ }
12440
12716
 
12441
- this.calendar.fromDateString( newValue );
12717
+ dateValue = newValue;
12442
12718
 
12443
12719
  refresh( this.calendar.getFullDate() );
12444
12720
 
@@ -12453,26 +12729,80 @@ class DatePicker extends Widget {
12453
12729
  container.style.width = `calc( 100% - ${ realNameWidth })`;
12454
12730
  };
12455
12731
 
12456
- const container = document.createElement('div');
12457
- container.className = "lexdate";
12732
+ const container = LX.makeContainer( [ "auto", "auto" ], "lexdate flex flex-row" );
12458
12733
  this.root.appendChild( container );
12459
12734
 
12460
- this.calendar = new LX.Calendar( dateString, { onChange: ( date ) => {
12461
- this.set( `${ date.day }/${ date.month }/${ date.year }` );
12462
- }, ...options });
12735
+ if( !dateAsRange )
12736
+ {
12737
+ this.calendar = new LX.Calendar( dateValue, {
12738
+ onChange: ( date ) => {
12739
+ const newDateString = `${ date.day }/${ date.month }/${ date.year }`;
12740
+ this.set( newDateString );
12741
+ },
12742
+ ...options
12743
+ });
12744
+ }
12745
+ else
12746
+ {
12747
+ this.calendar = new LX.CalendarRange( dateValue, {
12748
+ onChange: ( dateRange ) => {
12749
+ this.set( dateRange );
12750
+ },
12751
+ ...options
12752
+ });
12753
+ }
12463
12754
 
12464
12755
  const refresh = ( currentDate ) => {
12756
+
12757
+ const emptyDate = !!currentDate;
12758
+
12465
12759
  container.innerHTML = "";
12760
+
12761
+ currentDate = currentDate ?? "Pick a date";
12762
+
12763
+ const dts = currentDate.split( " to " );
12764
+ const d0 = dateAsRange ? dts[ 0 ] : currentDate;
12765
+
12466
12766
  const calendarIcon = LX.makeIcon( "Calendar" );
12467
- const calendarButton = new LX.Button( null, currentDate ?? "Pick a date", () => {
12767
+ const calendarButton = new LX.Button( null, d0, () => {
12468
12768
  this._popover = new LX.Popover( calendarButton.root, [ this.calendar ] );
12469
- }, { buttonClass: `flex flex-row px-3 ${ currentDate ? "" : "fg-tertiary" } justify-between` } );
12470
-
12769
+ if( dateAsRange )
12770
+ {
12771
+ Object.assign( this._popover.root.style, { display: "flex", width: "auto" } );
12772
+ }
12773
+ }, { buttonClass: `flex flex-row px-3 ${ emptyDate ? "" : "fg-tertiary" } justify-between` } );
12471
12774
  calendarButton.root.querySelector( "button" ).appendChild( calendarIcon );
12775
+ calendarButton.root.style.width = "100%";
12472
12776
  container.appendChild( calendarButton.root );
12777
+
12778
+ if( dateAsRange )
12779
+ {
12780
+ const arrowRightIcon = LX.makeIcon( "ArrowRight" );
12781
+ LX.makeContainer( ["32px", "auto"], "content-center", arrowRightIcon.innerHTML, container );
12782
+
12783
+ const d1 = dts[ 1 ];
12784
+ const calendarIcon = LX.makeIcon( "Calendar" );
12785
+ const calendarButton = new LX.Button( null, d1, () => {
12786
+ this._popover = new LX.Popover( calendarButton.root, [ this.calendar ] );
12787
+ if( dateAsRange )
12788
+ {
12789
+ Object.assign( this._popover.root.style, { display: "flex", width: "auto" } );
12790
+ }
12791
+ }, { buttonClass: `flex flex-row px-3 ${ emptyDate ? "" : "fg-tertiary" } justify-between` } );
12792
+ calendarButton.root.querySelector( "button" ).appendChild( calendarIcon );
12793
+ calendarButton.root.style.width = "100%";
12794
+ container.appendChild( calendarButton.root );
12795
+ }
12473
12796
  };
12474
12797
 
12475
- refresh( dateString ? this.calendar.getFullDate(): null );
12798
+ if( dateValue )
12799
+ {
12800
+ refresh( this.calendar.getFullDate() );
12801
+ }
12802
+ else
12803
+ {
12804
+ refresh();
12805
+ }
12476
12806
 
12477
12807
  LX.doAsync( this.onResize.bind( this ) );
12478
12808
  }
@@ -13138,6 +13468,7 @@ class Panel {
13138
13468
  * img: Path to image to show as button value
13139
13469
  * title: Text to show in native Element title
13140
13470
  * buttonClass: Class to add to the native button element
13471
+ * mustConfirm: User must confirm trigger in a popover
13141
13472
  */
13142
13473
 
13143
13474
  addButton( name, value, callback, options = {} ) {
@@ -13683,7 +14014,7 @@ class Panel {
13683
14014
  /**
13684
14015
  * @method addDate
13685
14016
  * @param {String} name Widget name
13686
- * @param {String} dateString
14017
+ * @param {String} dateValue
13687
14018
  * @param {Function} callback
13688
14019
  * @param {Object} options:
13689
14020
  * hideName: Don't use name as label [false]
@@ -13692,8 +14023,8 @@ class Panel {
13692
14023
  * fromToday: Allow dates only from current day
13693
14024
  */
13694
14025
 
13695
- addDate( name, dateString, callback, options = { } ) {
13696
- const widget = new LX.DatePicker( name, dateString, callback, options );
14026
+ addDate( name, dateValue, callback, options = { } ) {
14027
+ const widget = new LX.DatePicker( name, dateValue, callback, options );
13697
14028
  return this._attachWidget( widget );
13698
14029
  }
13699
14030
 
@@ -15828,4 +16159,303 @@ class AssetView {
15828
16159
 
15829
16160
  LX.AssetView = AssetView;
15830
16161
 
16162
+ // tour.js @jxarco
16163
+
16164
+ class Tour {
16165
+
16166
+ static ACTIVE_TOURS = [];
16167
+
16168
+ /**
16169
+ * @constructor Tour
16170
+ * @param {Array} steps
16171
+ * @param {Object} options
16172
+ * useModal: Use a modal to highlight the tour step [true]
16173
+ * offset: Horizontal and vertical margin offset [0]
16174
+ * horizontalOffset: Horizontal offset [0]
16175
+ * verticalOffset: Vertical offset [0]
16176
+ * radius: Radius for the tour step highlight [8]
16177
+ */
16178
+
16179
+ constructor( steps, options = {} ) {
16180
+
16181
+ this.steps = steps || [];
16182
+ this.currentStep = 0;
16183
+
16184
+ this.useModal = options.useModal ?? true;
16185
+ this.offset = options.offset ?? 8;
16186
+ this.horizontalOffset = options.horizontalOffset;
16187
+ this.verticalOffset = options.verticalOffset;
16188
+ this.radius = options.radius ?? 12;
16189
+
16190
+ this.tourContainer = document.querySelector( ".tour-container" );
16191
+ if( !this.tourContainer )
16192
+ {
16193
+ this.tourContainer = LX.makeContainer( ["100%", "100%"], "tour-container" );
16194
+ this.tourContainer.style.display = "none";
16195
+ document.body.appendChild( this.tourContainer );
16196
+
16197
+ window.addEventListener( "resize", () => {
16198
+
16199
+ for( const tour of Tour.ACTIVE_TOURS )
16200
+ {
16201
+ tour._showStep( 0 );
16202
+ }
16203
+ } );
16204
+ }
16205
+ }
16206
+
16207
+ /**
16208
+ * @method begin
16209
+ */
16210
+
16211
+ begin() {
16212
+
16213
+ this.currentStep = 0;
16214
+
16215
+ this.tourContainer.style.display = "block";
16216
+
16217
+ Tour.ACTIVE_TOURS.push( this );
16218
+
16219
+ this._showStep( 0 );
16220
+ }
16221
+
16222
+ /**
16223
+ * @method stop
16224
+ */
16225
+
16226
+ stop() {
16227
+
16228
+ if( this.useModal )
16229
+ {
16230
+ this.tourMask.remove();
16231
+ this.tourMask = undefined;
16232
+ }
16233
+
16234
+ this._popover?.destroy();
16235
+
16236
+ const index = Tour.ACTIVE_TOURS.indexOf( this );
16237
+ if( index !== -1 )
16238
+ {
16239
+ Tour.ACTIVE_TOURS.splice( index, 1 );
16240
+ }
16241
+
16242
+ this.tourContainer.innerHTML = "";
16243
+ this.tourContainer.style.display = "none";
16244
+ }
16245
+
16246
+ // Show the current step of the tour
16247
+ _showStep( stepOffset = 1 ) {
16248
+
16249
+ this.currentStep += stepOffset;
16250
+
16251
+ const step = this.steps[ this.currentStep ];
16252
+ if ( !step ) {
16253
+ this.stop();
16254
+ return;
16255
+ }
16256
+
16257
+ const prevStep = this.steps[ this.currentStep - 1 ];
16258
+ const nextStep = this.steps[ this.currentStep + 1 ];
16259
+
16260
+ if( this.useModal )
16261
+ {
16262
+ this._generateMask( step.reference );
16263
+ }
16264
+
16265
+ this._createHighlight( step, prevStep, nextStep );
16266
+ }
16267
+
16268
+ // Generate mask for the specific step reference
16269
+ // using a fullscreen SVG with "rect" elements
16270
+ _generateMask( reference ) {
16271
+
16272
+ this.tourContainer.innerHTML = ""; // Clear previous content
16273
+
16274
+ this.tourMask = LX.makeContainer( ["100%", "100%"], "tour-mask" );
16275
+ this.tourContainer.appendChild( this.tourMask );
16276
+
16277
+ const svg = document.createElementNS( "http://www.w3.org/2000/svg", "svg" );
16278
+ svg.style.width = "100%";
16279
+ svg.style.height = "100%";
16280
+ this.tourMask.appendChild( svg );
16281
+
16282
+ const clipPath = document.createElementNS( "http://www.w3.org/2000/svg", "clipPath" );
16283
+ clipPath.setAttribute( "id", "svgTourClipPath" );
16284
+ svg.appendChild( clipPath );
16285
+
16286
+ function ceilAndShiftRect( p, s ) {
16287
+ const cp = Math.ceil( p );
16288
+ const delta = cp - p;
16289
+ const ds = s - delta;
16290
+ return [ cp, ds ];
16291
+ }
16292
+
16293
+ const refBounding = reference.getBoundingClientRect();
16294
+ const [ boundingX, boundingWidth ] = ceilAndShiftRect( refBounding.x, refBounding.width );
16295
+ const [ boundingY, boundingHeight ] = ceilAndShiftRect( refBounding.y, refBounding.height );
16296
+
16297
+ const vOffset = this.verticalOffset ?? this.offset;
16298
+ const hOffset = this.horizontalOffset ?? this.offset;
16299
+
16300
+ // Left
16301
+ {
16302
+ const rect = document.createElementNS( "http://www.w3.org/2000/svg", "rect" );
16303
+ rect.setAttribute( "x", 0 );
16304
+ rect.setAttribute( "y", 0 );
16305
+ rect.setAttribute( "width", Math.max( 0, boundingX - hOffset ) );
16306
+ rect.setAttribute( "height", window.innerHeight );
16307
+ rect.setAttribute( "stroke", "none" );
16308
+ clipPath.appendChild( rect );
16309
+ }
16310
+
16311
+ // Top
16312
+ {
16313
+ const rect = document.createElementNS( "http://www.w3.org/2000/svg", "rect" );
16314
+ rect.setAttribute( "x", boundingX - hOffset );
16315
+ rect.setAttribute( "y", 0 );
16316
+ rect.setAttribute( "width", Math.max( 0, boundingWidth + hOffset * 2 ) );
16317
+ rect.setAttribute( "height", Math.max( 0, boundingY - vOffset ) );
16318
+ rect.setAttribute( "stroke", "none" );
16319
+ clipPath.appendChild( rect );
16320
+ }
16321
+
16322
+ // Bottom
16323
+ {
16324
+ const rect = document.createElementNS( "http://www.w3.org/2000/svg", "rect" );
16325
+ rect.setAttribute( "x", boundingX - hOffset );
16326
+ rect.setAttribute( "y", boundingY + boundingHeight + vOffset );
16327
+ rect.setAttribute( "width", Math.max( 0, boundingWidth + hOffset * 2 ) );
16328
+ rect.setAttribute( "height", Math.max( 0, window.innerHeight - boundingY - boundingHeight - vOffset ) );
16329
+ rect.setAttribute( "stroke", "none" );
16330
+ clipPath.appendChild( rect );
16331
+ }
16332
+
16333
+ // Right
16334
+ {
16335
+ const rect = document.createElementNS( "http://www.w3.org/2000/svg", "rect" );
16336
+ rect.setAttribute( "x", boundingX + boundingWidth + hOffset );
16337
+ rect.setAttribute( "y", 0 );
16338
+ rect.setAttribute( "width", Math.max( 0, window.innerWidth - boundingX - boundingWidth ) );
16339
+ rect.setAttribute( "height", Math.max( 0, window.innerHeight ) );
16340
+ rect.setAttribute( "stroke", "none" );
16341
+ clipPath.appendChild( rect );
16342
+ }
16343
+
16344
+ // Reference Highlight
16345
+ const refContainer = LX.makeContainer( ["0", "0"], "tour-ref-mask" );
16346
+ refContainer.style.left = `${ boundingX - hOffset - 1 }px`;
16347
+ refContainer.style.top = `${ boundingY - vOffset - 1 }px`;
16348
+ refContainer.style.width = `${ boundingWidth + hOffset * 2 + 2 }px`;
16349
+ refContainer.style.height = `${ boundingHeight + vOffset * 2 + 2}px`;
16350
+ this.tourContainer.appendChild( refContainer );
16351
+
16352
+ const referenceMask = document.createElementNS( "http://www.w3.org/2000/svg", "mask" );
16353
+ referenceMask.setAttribute( "id", "svgTourReferenceMask" );
16354
+ svg.appendChild( referenceMask );
16355
+
16356
+ // Reference Mask
16357
+ {
16358
+ const rectWhite = document.createElementNS( "http://www.w3.org/2000/svg", "rect" );
16359
+ rectWhite.setAttribute( "width", boundingWidth + hOffset * 2 + 2 );
16360
+ rectWhite.setAttribute( "height", boundingHeight + vOffset * 2 + 2);
16361
+ rectWhite.setAttribute( "stroke", "none" );
16362
+ rectWhite.setAttribute( "fill", "white" );
16363
+ referenceMask.appendChild( rectWhite );
16364
+
16365
+ const rectBlack = document.createElementNS( "http://www.w3.org/2000/svg", "rect" );
16366
+ rectBlack.setAttribute( "rx", this.radius );
16367
+ rectBlack.setAttribute( "width", boundingWidth + hOffset * 2 + 2);
16368
+ rectBlack.setAttribute( "height", boundingHeight + vOffset * 2 + 2);
16369
+ rectBlack.setAttribute( "stroke", "none" );
16370
+ rectBlack.setAttribute( "fill", "black" );
16371
+ referenceMask.appendChild( rectBlack );
16372
+ }
16373
+ }
16374
+
16375
+ // Create the container with the user hints
16376
+ _createHighlight( step, previousStep, nextStep ) {
16377
+
16378
+ const popoverContainer = LX.makeContainer( ["auto", "auto"], "tour-step-container" );
16379
+
16380
+ {
16381
+ const header = LX.makeContainer( ["100%", "auto"], "flex flex-row", "", popoverContainer );
16382
+ LX.makeContainer( ["70%", "auto"], "p-2 font-medium", step.title, header );
16383
+ const closer = LX.makeContainer( ["30%", "auto"], "flex flex-row p-2 justify-end", "", header );
16384
+ const closeIcon = LX.makeIcon( "X" );
16385
+ closer.appendChild( closeIcon );
16386
+
16387
+ closeIcon.listen( "click", () => {
16388
+ this.stop();
16389
+ } );
16390
+ }
16391
+
16392
+ LX.makeContainer( ["100%", "auto"], "p-2 text-md", step.content, popoverContainer, { maxWidth: "400px" } );
16393
+ const footer = LX.makeContainer( ["100%", "auto"], "flex flex-row text-md", "", popoverContainer );
16394
+
16395
+ {
16396
+ const footerSteps = LX.makeContainer( ["50%", "auto"], "p-2 gap-1 self-center flex flex-row text-md", "", footer );
16397
+ for( let i = 0; i < this.steps.length; i++ )
16398
+ {
16399
+ const stepIndicator = document.createElement( "span" );
16400
+ stepIndicator.className = "tour-step-indicator";
16401
+ if( i === this.currentStep )
16402
+ {
16403
+ stepIndicator.classList.add( "active" );
16404
+ }
16405
+ footerSteps.appendChild( stepIndicator );
16406
+ }
16407
+ }
16408
+
16409
+ const footerButtons = LX.makeContainer( ["50%", "auto"], "text-md", "", footer );
16410
+ const footerPanel = new LX.Panel();
16411
+
16412
+ let numButtons = 1;
16413
+
16414
+ if( previousStep )
16415
+ {
16416
+ numButtons++;
16417
+ }
16418
+
16419
+ if( numButtons > 1 )
16420
+ {
16421
+ footerPanel.sameLine( 2, "justify-end" );
16422
+ }
16423
+
16424
+ if( previousStep )
16425
+ {
16426
+ footerPanel.addButton( null, "Previous", () => {
16427
+ this._showStep( -1 );
16428
+ }, { buttonClass: "contrast" } );
16429
+ }
16430
+
16431
+ if( nextStep )
16432
+ {
16433
+ footerPanel.addButton( null, "Next", () => {
16434
+ this._showStep( 1 );
16435
+ }, { buttonClass: "accent" } );
16436
+ }
16437
+ else
16438
+ {
16439
+ footerPanel.addButton( null, "Finish", () => {
16440
+ this.stop();
16441
+ } );
16442
+ }
16443
+
16444
+ footerButtons.appendChild( footerPanel.root );
16445
+
16446
+ const sideOffset = ( step.side === "left" || step.side === "right" ? this.horizontalOffset : this.verticalOffset ) ?? this.offset;
16447
+ const alignOffset = ( step.align === "start" || step.align === "end" ? sideOffset : 0 );
16448
+
16449
+ this._popover?.destroy();
16450
+ this._popover = new LX.Popover( null, [ popoverContainer ], {
16451
+ reference: step.reference,
16452
+ side: step.side,
16453
+ align: step.align,
16454
+ sideOffset,
16455
+ alignOffset: step.align === "start" ? -alignOffset : alignOffset,
16456
+ } );
16457
+ }
16458
+ }
16459
+ LX.Tour = Tour;
16460
+
15831
16461
  })( typeof(window) != 'undefined' ? window : (typeof(self) != 'undefined' ? self : global ) );