lexgui 0.5.5 → 0.5.6

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.5",
9
+ version: "0.5.6",
10
10
  ready: false,
11
11
  components: [], // Specific pre-build components
12
12
  signals: {}, // Events and triggers
@@ -216,37 +216,141 @@ LX.getBase64Image = getBase64Image;
216
216
 
217
217
  /**
218
218
  * @method hexToRgb
219
- * @description Convert a hexadecimal string to a valid RGB color array
220
- * @param {String} hexStr Hexadecimal color
219
+ * @description Convert a hexadecimal string to a valid RGB color
220
+ * @param {String} hex Hexadecimal color
221
221
  */
222
- function hexToRgb( hexStr )
222
+ function hexToRgb( hex )
223
223
  {
224
- const red = parseInt( hexStr.substring( 1, 3 ), 16 ) / 255;
225
- const green = parseInt( hexStr.substring( 3, 5 ), 16 ) / 255;
226
- const blue = parseInt( hexStr.substring( 5, 7 ), 16 ) / 255;
227
- return [ red, green, blue ];
224
+ const hexPattern = /^#(?:[A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
225
+ if( !hexPattern.test( hex ) )
226
+ {
227
+ throw( `Invalid Hex Color: ${ hex }` );
228
+ }
229
+
230
+ hex = hex.replace( /^#/, '' );
231
+
232
+ // Expand shorthand form (#RGB or #RGBA)
233
+ if( hex.length === 3 || hex.length === 4 )
234
+ {
235
+ hex = hex.split( '' ).map( c => c + c ).join( '' );
236
+ }
237
+
238
+ const bigint = parseInt( hex, 16 );
239
+
240
+ const r = ( ( bigint >> ( hex.length === 8 ? 24 : 16 ) ) & 255 ) / 255;
241
+ const g = ( ( bigint >> ( hex.length === 8 ? 16 : 8 ) ) & 255 ) / 255;
242
+ const b = ( ( bigint >> ( hex.length === 8 ? 8 : 0 ) ) & 255 ) / 255;
243
+ const a = ( hex.length === 8 ? ( bigint & 255 ) : ( hex.length === 4 ? parseInt( hex.slice( -2 ), 16 ) : 255 ) ) / 255;
244
+
245
+ return { r, g, b, a };
228
246
  }
229
247
 
230
248
  LX.hexToRgb = hexToRgb;
231
249
 
250
+ /**
251
+ * @method hexToHsv
252
+ * @description Convert a hexadecimal string to HSV (0..360|0..1|0..1)
253
+ * @param {String} hexStr Hexadecimal color
254
+ */
255
+ function hexToHsv( hexStr )
256
+ {
257
+ const rgb = hexToRgb( hexStr );
258
+ return rgbToHsv( rgb );
259
+ }
260
+
261
+ LX.hexToHsv = hexToHsv;
262
+
232
263
  /**
233
264
  * @method rgbToHex
234
- * @description Convert a RGB color array to a hexadecimal string
235
- * @param {Array} rgb Array containing R, G, B, A*
265
+ * @description Convert a RGB color to a hexadecimal string
266
+ * @param {Object} rgb Object containing RGB color
267
+ * @param {Number} scale Use 255 for 0..255 range or 1 for 0..1 range
236
268
  */
237
- function rgbToHex( rgb )
269
+ function rgbToHex( rgb, scale = 255 )
238
270
  {
239
- let hex = "#";
240
- for( let c of rgb )
241
- {
242
- c = Math.floor( c * 255 );
243
- hex += c.toString( 16 );
244
- }
245
- return hex;
271
+ const rgbArray = [ rgb.r, rgb.g, rgb.b ];
272
+ if( rgb.a != undefined ) rgbArray.push( rgb.a );
273
+
274
+ return (
275
+ "#" +
276
+ rgbArray.map( c => {
277
+ c = Math.floor( c * scale );
278
+ const hex = c.toString(16);
279
+ return hex.length === 1 ? ( '0' + hex ) : hex;
280
+ }).join("")
281
+ );
246
282
  }
247
283
 
248
284
  LX.rgbToHex = rgbToHex;
249
285
 
286
+ /**
287
+ * @method rgbToCss
288
+ * @description Convert a RGB color (0..1) to a CSS color format
289
+ * @param {Object} rgb Object containing RGB color
290
+ */
291
+ function rgbToCss( rgb )
292
+ {
293
+ return { r: Math.floor( rgb.r * 255 ), g: Math.floor( rgb.g * 255 ), b: Math.floor( rgb.b * 255 ), a: rgb.a };
294
+ }
295
+
296
+ LX.rgbToCss = rgbToCss;
297
+
298
+ /**
299
+ * @method rgbToHsv
300
+ * @description Convert a RGB color (0..1) array to HSV (0..360|0..1|0..1)
301
+ * @param {Object} rgb Array containing R, G, B
302
+ */
303
+ function rgbToHsv( rgb )
304
+ {
305
+ let { r, g, b, a } = rgb;
306
+ a = a ?? 1;
307
+
308
+ const max = Math.max(r, g, b);
309
+ const min = Math.min(r, g, b);
310
+ const d = max - min;
311
+ let h = 0;
312
+
313
+ if (d !== 0) {
314
+ if (max === r) { h = ((g - b) / d) % 6 }
315
+ else if (max === g) { h = (b - r) / d + 2 }
316
+ else { h = (r - g) / d + 4 }
317
+ h *= 60
318
+ if (h < 0) { h += 360 }
319
+ }
320
+
321
+ const s = max === 0 ? 0 : (d / max);
322
+ const v = max;
323
+
324
+ return { h, s, v, a };
325
+ }
326
+
327
+ LX.rgbToHsv = rgbToHsv;
328
+
329
+ /**
330
+ * @method hsvToRgb
331
+ * @description Convert an HSV color (0..360|0..1|0..1) array to RGB (0..1|0..255)
332
+ * @param {Array} hsv Array containing H, S, V
333
+ */
334
+ function hsvToRgb( hsv )
335
+ {
336
+ const { h, s, v, a } = hsv;
337
+ const c = v * s;
338
+ const x = c * (1 - Math.abs( ( (h / 60) % 2 ) - 1) )
339
+ const m = v - c;
340
+ let r = 0, g = 0, b = 0;
341
+
342
+ if( h < 60 ) { r = c; g = x; b = 0; }
343
+ else if ( h < 120 ) { r = x; g = c; b = 0; }
344
+ else if ( h < 180 ) { r = 0; g = c; b = x; }
345
+ else if ( h < 240 ) { r = 0; g = x; b = c; }
346
+ else if ( h < 300 ) { r = x; g = 0; b = c; }
347
+ else { r = c; g = 0; b = x; }
348
+
349
+ return { r: ( r + m ), g: ( g + m ), b: ( b + m ), a };
350
+ }
351
+
352
+ LX.hsvToRgb = hsvToRgb;
353
+
250
354
  /**
251
355
  * @method measureRealWidth
252
356
  * @description Measure the pixel width of a text
@@ -578,6 +682,42 @@ function makeCodeSnippet( code, size, options = { } )
578
682
 
579
683
  LX.makeCodeSnippet = makeCodeSnippet;
580
684
 
685
+ /**
686
+ * @method makeKbd
687
+ * @description Kbd element to display a keyboard key.
688
+ * @param {Array} keys
689
+ * @param {String} extraClass
690
+ */
691
+ function makeKbd( keys, extraClass = "" )
692
+ {
693
+ const specialKeys = {
694
+ "Ctrl": '⌃',
695
+ "Enter": '↩',
696
+ "Shift": '⇧',
697
+ "CapsLock": '⇪',
698
+ "Meta": '⌘',
699
+ "Option": '⌥',
700
+ "Alt": '⌥',
701
+ "Tab": '⇥',
702
+ "ArrowUp": '↑',
703
+ "ArrowDown": '↓',
704
+ "ArrowLeft": '←',
705
+ "ArrowRight": '→',
706
+ "Space": '␣'
707
+ };
708
+
709
+ const kbd = LX.makeContainer( ["auto", "auto"], "flex flex-row ml-auto" );
710
+
711
+ for( const k of keys )
712
+ {
713
+ LX.makeContainer( ["auto", "auto"], "self-center text-xs fg-secondary select-none", specialKeys[ k ] ?? k, kbd );
714
+ }
715
+
716
+ return kbd;
717
+ }
718
+
719
+ LX.makeKbd = makeKbd;
720
+
581
721
  /**
582
722
  * @method makeIcon
583
723
  * @description Gets an SVG element using one of LX.ICONS
@@ -716,7 +856,7 @@ function registerCommandbarEntry( name, callback )
716
856
 
717
857
  LX.registerCommandbarEntry = registerCommandbarEntry;
718
858
 
719
- // Math classes
859
+ // Utils classes
720
860
 
721
861
  class vec2 {
722
862
 
@@ -744,6 +884,77 @@ class vec2 {
744
884
 
745
885
  LX.vec2 = vec2;
746
886
 
887
+ class Color {
888
+
889
+ constructor( value ) {
890
+
891
+ Object.defineProperty( Color.prototype, "rgb", {
892
+ get: function() { return this._rgb; },
893
+ set: function( v ) { this._fromRGB( v ) }, enumerable: true, configurable: true
894
+ });
895
+
896
+ Object.defineProperty( Color.prototype, "hex", {
897
+ get: function() { return this._hex; },
898
+ set: function( v ) { this._fromHex( v ) }, enumerable: true, configurable: true
899
+ });
900
+
901
+ Object.defineProperty( Color.prototype, "hsv", {
902
+ get: function() { return this._hsv; },
903
+ set: function( v ) { this._fromHSV( v ) }, enumerable: true, configurable: true
904
+ });
905
+
906
+ this.set( value );
907
+ }
908
+
909
+ set( value ) {
910
+
911
+ if ( typeof value === 'string' && value.startsWith( '#' ) )
912
+ {
913
+ this._fromHex( value );
914
+ }
915
+ else if( 'r' in value && 'g' in value && 'b' in value)
916
+ {
917
+ value.a = value.a ?? 1.0;
918
+ this._fromRGB( value );
919
+ }
920
+ else if( 'h' in value && 's' in value && 'v' in value )
921
+ {
922
+ value.a = value.a ?? 1.0;
923
+ this._fromHSV( value );
924
+ }
925
+ else
926
+ {
927
+ throw( "Bad color model!", value );
928
+ }
929
+ }
930
+
931
+ setHSV( hsv ) { this._fromHSV( hsv ); }
932
+ setRGB( rgb ) { this._fromRGB( rgb ); }
933
+ setHex( hex ) { this._fromHex( hex ); }
934
+
935
+ _fromHex( hex ) {
936
+ this._fromRGB( hexToRgb( hex ) );
937
+ }
938
+
939
+ _fromRGB( rgb ) {
940
+ this._rgb = rgb;
941
+ this._hsv = rgbToHsv( rgb );
942
+ this._hex = rgbToHex( rgb );
943
+ this.css = rgbToCss( this._rgb );
944
+ }
945
+
946
+ _fromHSV( hsv ) {
947
+ this._hsv = hsv;
948
+ this._rgb = hsvToRgb( hsv );
949
+ this._hex = rgbToHex( this._rgb );
950
+ this.css = rgbToCss( this._rgb );
951
+ }
952
+ }
953
+
954
+ LX.Color = Color;
955
+
956
+ // Command bar creation
957
+
747
958
  function _createCommandbar( root )
748
959
  {
749
960
  let commandbar = document.createElement( "dialog" );
@@ -1801,6 +2012,13 @@ class DropdownMenu {
1801
2012
  submenuIcon.className = "fa-solid fa-angle-right fa-xs";
1802
2013
  menuItem.appendChild( submenuIcon );
1803
2014
  }
2015
+ else if( item.kbd )
2016
+ {
2017
+ item.kbd = [].concat( item.kbd );
2018
+
2019
+ const kbd = LX.makeKbd( item.kbd );
2020
+ menuItem.appendChild( kbd );
2021
+ }
1804
2022
 
1805
2023
  if( item.icon )
1806
2024
  {
@@ -1820,57 +2038,508 @@ class DropdownMenu {
1820
2038
  const input = checkbox.root.querySelector( "input" );
1821
2039
  menuItem.prepend( input );
1822
2040
 
1823
- menuItem.addEventListener( "click", (e) => {
1824
- if( e.target.type == "checkbox" ) return;
1825
- input.checked = !input.checked;
1826
- checkbox.set( input.checked );
1827
- } );
1828
- }
1829
- else
1830
- {
1831
- menuItem.addEventListener( "click", () => {
1832
- const f = item[ 'callback' ];
1833
- if( f )
1834
- {
1835
- f.call( this, key, menuItem );
1836
- }
1837
- this.destroy();
1838
- } );
1839
- }
2041
+ menuItem.addEventListener( "click", (e) => {
2042
+ if( e.target.type == "checkbox" ) return;
2043
+ input.checked = !input.checked;
2044
+ checkbox.set( input.checked );
2045
+ } );
2046
+ }
2047
+ else
2048
+ {
2049
+ menuItem.addEventListener( "click", () => {
2050
+ const f = item[ 'callback' ];
2051
+ if( f )
2052
+ {
2053
+ f.call( this, key, menuItem );
2054
+ }
2055
+ this.destroy();
2056
+ } );
2057
+ }
2058
+
2059
+ menuItem.addEventListener("mouseover", e => {
2060
+
2061
+ let path = menuItem.id;
2062
+ let p = parentDom;
2063
+
2064
+ while( p )
2065
+ {
2066
+ path += "/" + p.id;
2067
+ p = p.currentParent?.parentElement;
2068
+ }
2069
+
2070
+ LX.root.querySelectorAll( ".lexdropdownmenu" ).forEach( m => {
2071
+ if( !path.includes( m.id ) )
2072
+ {
2073
+ m.currentParent.built = false;
2074
+ m.remove();
2075
+ }
2076
+ } );
2077
+
2078
+ if( item.submenu )
2079
+ {
2080
+ if( menuItem.built )
2081
+ {
2082
+ return;
2083
+ }
2084
+ menuItem.built = true;
2085
+ this._create( item.submenu, menuItem );
2086
+ }
2087
+
2088
+ e.stopPropagation();
2089
+ });
2090
+ }
2091
+ }
2092
+
2093
+ _adjustPosition() {
2094
+
2095
+ const position = [ 0, 0 ];
2096
+
2097
+ // Place menu using trigger position and user options
2098
+ {
2099
+ const rect = this._trigger.getBoundingClientRect();
2100
+
2101
+ let alignWidth = true;
2102
+
2103
+ switch( this.side )
2104
+ {
2105
+ case "left":
2106
+ position[ 0 ] += ( rect.x - this.root.offsetWidth );
2107
+ alignWidth = false;
2108
+ break;
2109
+ case "right":
2110
+ position[ 0 ] += ( rect.x + rect.width );
2111
+ alignWidth = false;
2112
+ break;
2113
+ case "top":
2114
+ position[ 1 ] += ( rect.y - this.root.offsetHeight );
2115
+ alignWidth = true;
2116
+ break;
2117
+ case "bottom":
2118
+ position[ 1 ] += ( rect.y + rect.height );
2119
+ alignWidth = true;
2120
+ break;
2121
+ default:
2122
+ break;
2123
+ }
2124
+
2125
+ switch( this.align )
2126
+ {
2127
+ case "start":
2128
+ if( alignWidth ) { position[ 0 ] += rect.x; }
2129
+ else { position[ 1 ] += rect.y; }
2130
+ break;
2131
+ case "center":
2132
+ if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - this.root.offsetWidth * 0.5; }
2133
+ else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - this.root.offsetHeight * 0.5; }
2134
+ break;
2135
+ case "end":
2136
+ if( alignWidth ) { position[ 0 ] += rect.x - this.root.offsetWidth + rect.width; }
2137
+ else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
2138
+ break;
2139
+ default:
2140
+ break;
2141
+ }
2142
+ }
2143
+
2144
+ if( this.avoidCollisions )
2145
+ {
2146
+ position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - this.root.offsetWidth - this._windowPadding );
2147
+ position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - this.root.offsetHeight - this._windowPadding );
2148
+ }
2149
+
2150
+ this.root.style.left = `${ position[ 0 ] }px`;
2151
+ this.root.style.top = `${ position[ 1 ] }px`;
2152
+ }
2153
+
2154
+ _addSeparator( parent ) {
2155
+ const separator = document.createElement('div');
2156
+ separator.className = "separator";
2157
+ parent = parent ?? this.root;
2158
+ parent.appendChild( separator );
2159
+ }
2160
+ };
2161
+
2162
+ LX.DropdownMenu = DropdownMenu;
2163
+
2164
+ /**
2165
+ * @class ColorPicker
2166
+ */
2167
+
2168
+ class ColorPicker {
2169
+
2170
+ static currentPicker = false;
2171
+
2172
+ constructor( hexValue, trigger, options = {} ) {
2173
+
2174
+ console.assert( trigger, "ColorPicker needs a DOM element as trigger!" );
2175
+
2176
+ this._windowPadding = 4;
2177
+ this.side = options.side ?? "bottom";
2178
+ this.align = options.align ?? "center";
2179
+ this.avoidCollisions = options.avoidCollisions ?? true;
2180
+ this.colorModel = options.colorModel ?? "Hex";
2181
+ this.useAlpha = options.useAlpha ?? false;
2182
+ this.callback = options.onChange;
2183
+
2184
+ if( !this.callback )
2185
+ {
2186
+ console.warn( "Define a callback in _options.onChange_ to allow getting new Color values!" );
2187
+ }
2188
+
2189
+ if( ColorPicker.currentPicker )
2190
+ {
2191
+ ColorPicker.currentPicker.destroy();
2192
+ return;
2193
+ }
2194
+
2195
+ this._trigger = trigger;
2196
+ trigger.classList.add( "triggered" );
2197
+ trigger.picker = this;
2198
+
2199
+ this.root = document.createElement( "div" );
2200
+ this.root.tabIndex = "1";
2201
+ this.root.className = "lexcolorpicker";
2202
+ this.root.dataset["side"] = this.side;
2203
+ LX.root.appendChild( this.root );
2204
+
2205
+ this.root.addEventListener( "keydown", (e) => {
2206
+ if( e.key == "Escape" )
2207
+ {
2208
+ e.preventDefault();
2209
+ e.stopPropagation();
2210
+ this.destroy();
2211
+ }
2212
+ } )
2213
+
2214
+ ColorPicker.currentPicker = this;
2215
+
2216
+ this.markerHalfSize = 8;
2217
+ this.markerSize = this.markerHalfSize * 2;
2218
+ this.currentColor = new Color( hexValue );
2219
+
2220
+ const hueColor = new Color( { h: this.currentColor.hsv.h, s: 1, v: 1 } );
2221
+
2222
+ // Intensity, Sat
2223
+ this.colorPickerBackground = document.createElement( 'div' );
2224
+ this.colorPickerBackground.className = "lexcolorpickerbg";
2225
+ this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2226
+ this.root.appendChild( this.colorPickerBackground );
2227
+
2228
+ this.intSatMarker = document.createElement( 'div' );
2229
+ this.intSatMarker.className = "lexcolormarker";
2230
+ this.intSatMarker.style.backgroundColor = this.currentColor.hex;
2231
+ this.colorPickerBackground.appendChild( this.intSatMarker );
2232
+
2233
+ doAsync( this._svToPosition.bind( this, this.currentColor.hsv.s, this.currentColor.hsv.v ) );
2234
+
2235
+ let innerMouseDown = e => {
2236
+ var doc = this.root.ownerDocument;
2237
+ doc.addEventListener( 'mousemove', innerMouseMove );
2238
+ doc.addEventListener( 'mouseup', innerMouseUp );
2239
+ document.body.classList.add( 'noevents' );
2240
+ e.stopImmediatePropagation();
2241
+ e.stopPropagation();
2242
+
2243
+ const currentLeft = ( e.offsetX - this.markerHalfSize );
2244
+ this.intSatMarker.style.left = currentLeft + "px";
2245
+ const currentTop = ( e.offsetY - this.markerHalfSize );
2246
+ this.intSatMarker.style.top = currentTop + "px";
2247
+ this._positionToSv( currentLeft, currentTop );
2248
+ this._updateColorValue();
2249
+ }
2250
+
2251
+ let innerMouseMove = e => {
2252
+ const dX = e.movementX;
2253
+ const dY = e.movementY;
2254
+
2255
+ const rect = this.colorPickerBackground.getBoundingClientRect();
2256
+ const mouseX = e.offsetX - rect.x;
2257
+ const mouseY = e.offsetY - rect.y;
2258
+
2259
+ if ( dX != 0 && ( mouseX >= 0 || dX < 0 ) && ( mouseX < this.colorPickerBackground.offsetWidth || dX > 0 ) )
2260
+ {
2261
+ this.intSatMarker.style.left = LX.clamp( parseInt( this.intSatMarker.style.left ) + dX, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize ) + "px";
2262
+ }
2263
+
2264
+ if ( dY != 0 && ( mouseY >= 0 || dY < 0 ) && ( mouseY < this.colorPickerBackground.offsetHeight || dY > 0 ) )
2265
+ {
2266
+ this.intSatMarker.style.top = LX.clamp( parseInt( this.intSatMarker.style.top ) + dY, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize ) + "px";
2267
+ }
2268
+
2269
+ this._positionToSv( parseInt( this.intSatMarker.style.left ), parseInt( this.intSatMarker.style.top ) );
2270
+ this._updateColorValue();
2271
+
2272
+ e.stopPropagation();
2273
+ e.preventDefault();
2274
+ }
2275
+
2276
+ let innerMouseUp = e => {
2277
+ var doc = this.root.ownerDocument;
2278
+ doc.removeEventListener( 'mousemove', innerMouseMove );
2279
+ doc.removeEventListener( 'mouseup', innerMouseUp );
2280
+ document.body.classList.remove( 'noevents' );
2281
+ }
2282
+
2283
+ this.colorPickerBackground.addEventListener( "mousedown", innerMouseDown );
2284
+
2285
+ const hueAlphaContainer = LX.makeContainer( ["100%", "auto"], "flex flex-row gap-1 items-center", "", this.root );
2286
+
2287
+ if( window.EyeDropper )
2288
+ {
2289
+ hueAlphaContainer.appendChild( new Button(null, "eyedrop", async () => {
2290
+ const eyeDropper = new EyeDropper()
2291
+ try {
2292
+ const result = await eyeDropper.open();
2293
+ this.fromHexColor( result.sRGBHex );
2294
+ } catch ( err ) {
2295
+ // console.error("EyeDropper cancelled or failed: ", err)
2296
+ }
2297
+ }, { icon: "eye-dropper", buttonClass: "bg-none", title: "Sample Color" }).root );
2298
+ }
2299
+
2300
+ const innerHueAlpha = LX.makeContainer( ["100%", "100%"], "flex flex-col gap-2", "", hueAlphaContainer );
2301
+
2302
+ // Hue
2303
+ this.colorPickerTracker = document.createElement( 'div' );
2304
+ this.colorPickerTracker.className = "lexhuetracker";
2305
+ innerHueAlpha.appendChild( this.colorPickerTracker );
2306
+
2307
+ this.hueMarker = document.createElement( 'div' );
2308
+ this.hueMarker.className = "lexcolormarker";
2309
+ this.hueMarker.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2310
+ this.colorPickerTracker.appendChild( this.hueMarker );
2311
+
2312
+ doAsync( () => {
2313
+ const hueLeft = LX.remapRange( this.currentColor.hsv.h, 0, 360, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2314
+ this.hueMarker.style.left = hueLeft + "px";
2315
+ } );
2316
+
2317
+ const _fromHueX = ( hueX ) => {
2318
+ this.hueMarker.style.left = hueX + "px";
2319
+ this.currentColor.hsv.h = LX.remapRange( hueX, 0, this.colorPickerTracker.offsetWidth - this.markerSize, 0, 360 );
2320
+
2321
+ const hueColor = new Color( { h: this.currentColor.hsv.h, s: 1, v: 1 } );
2322
+ this.hueMarker.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2323
+ this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2324
+ this._updateColorValue();
2325
+ };
2326
+
2327
+ let innerMouseDownHue = e => {
2328
+ const doc = this.root.ownerDocument;
2329
+ doc.addEventListener( 'mousemove', innerMouseMoveHue );
2330
+ doc.addEventListener( 'mouseup', innerMouseUpHue );
2331
+ document.body.classList.add( 'noevents' );
2332
+ e.stopImmediatePropagation();
2333
+ e.stopPropagation();
2334
+
2335
+ const hueX = clamp( e.offsetX - this.markerHalfSize, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2336
+ _fromHueX( hueX );
2337
+ }
2338
+
2339
+ let innerMouseMoveHue = e => {
2340
+ let dX = e.movementX;
2341
+
2342
+ const rect = this.colorPickerTracker.getBoundingClientRect();
2343
+ const mouseX = e.offsetX - rect.x;
2344
+
2345
+ if ( dX != 0 && ( mouseX >= 0 || dX < 0 ) && ( mouseX < this.colorPickerTracker.offsetWidth || dX > 0 ) )
2346
+ {
2347
+ const hueX = LX.clamp( parseInt( this.hueMarker.style.left ) + dX, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2348
+ _fromHueX( hueX )
2349
+ }
2350
+
2351
+ e.stopPropagation();
2352
+ e.preventDefault();
2353
+ }
2354
+
2355
+ let innerMouseUpHue = e => {
2356
+ var doc = this.root.ownerDocument;
2357
+ doc.removeEventListener( 'mousemove', innerMouseMoveHue );
2358
+ doc.removeEventListener( 'mouseup', innerMouseUpHue );
2359
+ document.body.classList.remove( 'noevents' );
2360
+ }
2361
+
2362
+ this.colorPickerTracker.addEventListener( "mousedown", innerMouseDownHue );
2363
+
2364
+ // Alpha
2365
+ if( this.useAlpha )
2366
+ {
2367
+ this.alphaTracker = document.createElement( 'div' );
2368
+ this.alphaTracker.className = "lexalphatracker";
2369
+ this.alphaTracker.style.color = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b })`;
2370
+ innerHueAlpha.appendChild( this.alphaTracker );
2371
+
2372
+ this.alphaMarker = document.createElement( 'div' );
2373
+ this.alphaMarker.className = "lexcolormarker";
2374
+ this.alphaMarker.style.backgroundColor = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b },${ this.currentColor.css.a })`;
2375
+ this.alphaTracker.appendChild( this.alphaMarker );
2376
+
2377
+ doAsync( () => {
2378
+ const alphaLeft = LX.remapRange( this.currentColor.hsv.a, 0, 1, 0, this.alphaTracker.offsetWidth - this.markerSize );
2379
+ this.alphaMarker.style.left = alphaLeft + "px";
2380
+ } );
2381
+
2382
+ const _fromAlphaX = ( alphaX ) => {
2383
+ this.alphaMarker.style.left = alphaX + "px";
2384
+ this.currentColor.hsv.a = LX.remapRange( alphaX, 0, this.alphaTracker.offsetWidth - this.markerSize, 0, 1 );
2385
+ this._updateColorValue();
2386
+ // Update alpha marker once the color is updated
2387
+ this.alphaMarker.style.backgroundColor = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b },${ this.currentColor.css.a })`;
2388
+ };
2389
+
2390
+ let innerMouseDownAlpha = e => {
2391
+ const doc = this.root.ownerDocument;
2392
+ doc.addEventListener( 'mousemove', innerMouseMoveAlpha );
2393
+ doc.addEventListener( 'mouseup', innerMouseUpAlpha );
2394
+ document.body.classList.add( 'noevents' );
2395
+ e.stopImmediatePropagation();
2396
+ e.stopPropagation();
2397
+ const alphaX = clamp( e.offsetX - this.markerHalfSize, 0, this.alphaTracker.offsetWidth - this.markerSize );
2398
+ _fromAlphaX( alphaX );
2399
+ }
2400
+
2401
+ let innerMouseMoveAlpha = e => {
2402
+ let dX = e.movementX;
2403
+
2404
+ const rect = this.alphaTracker.getBoundingClientRect();
2405
+ const mouseX = e.offsetX - rect.x;
2406
+
2407
+ if ( dX != 0 && ( mouseX >= 0 || dX < 0 ) && ( mouseX < this.alphaTracker.offsetWidth || dX > 0 ) )
2408
+ {
2409
+ const alphaX = LX.clamp( parseInt( this.alphaMarker.style.left ) + dX, 0, this.alphaTracker.offsetWidth - this.markerSize );
2410
+ _fromAlphaX( alphaX );
2411
+ }
2412
+
2413
+ e.stopPropagation();
2414
+ e.preventDefault();
2415
+ }
2416
+
2417
+ let innerMouseUpAlpha = e => {
2418
+ var doc = this.root.ownerDocument;
2419
+ doc.removeEventListener( 'mousemove', innerMouseMoveAlpha );
2420
+ doc.removeEventListener( 'mouseup', innerMouseUpAlpha );
2421
+ document.body.classList.remove( 'noevents' );
2422
+ }
2423
+
2424
+ this.alphaTracker.addEventListener( "mousedown", innerMouseDownAlpha );
2425
+ }
2426
+
2427
+ // Info display
2428
+ const colorLabel = LX.makeContainer( ["100%", "auto"], "flex flex-row gap-1", "", this.root );
2429
+
2430
+ colorLabel.appendChild( new Select( null, [ "CSS", "Hex", "HSV", "RGB" ], this.colorModel, v => {
2431
+ this.colorModel = v;
2432
+ this._updateColorValue( null, true );
2433
+ } ).root );
2434
+
2435
+ this.labelWidget = new TextInput( null, "", null, { inputClass: "bg-none", fit: true, disabled: true } );
2436
+ colorLabel.appendChild( this.labelWidget.root );
2437
+
2438
+ colorLabel.appendChild( new Button(null, "eyedrop", async () => {
2439
+ navigator.clipboard.writeText( this.labelWidget.value() );
2440
+ }, { icon: "copy", buttonClass: "bg-none", className: "ml-auto", title: "Copy" }).root );
2441
+
2442
+ this._updateColorValue( hexValue, true );
2443
+
2444
+ doAsync( () => {
2445
+ this._adjustPosition();
2446
+
2447
+ this.root.focus();
2448
+
2449
+ this._onClick = e => {
2450
+ if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
2451
+ {
2452
+ return;
2453
+ }
2454
+ this.destroy();
2455
+ };
2456
+
2457
+ document.body.addEventListener( "mousedown", this._onClick, true );
2458
+ document.body.addEventListener( "focusin", this._onClick, true );
2459
+ }, 10 );
2460
+ }
2461
+
2462
+ fromHexColor( hexColor ) {
2463
+
2464
+ this.currentColor.setHex( hexColor );
2465
+
2466
+ // Decompose into HSV
2467
+ const { h, s, v } = this.currentColor.hsv;
2468
+ this._svToPosition( s, v );
2469
+
2470
+ const hueColor = new Color( { h, s: 1, v: 1 } );
2471
+ this.hueMarker.style.backgroundColor = this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2472
+ this.hueMarker.style.left = LX.remapRange( h, 0, 360, -this.markerHalfSize, this.colorPickerTracker.offsetWidth - this.markerHalfSize ) + "px";
2473
+
2474
+ this._updateColorValue( hexColor );
2475
+ }
2476
+
2477
+ destroy() {
2478
+
2479
+ this._trigger.classList.remove( "triggered" );
2480
+
2481
+ delete this._trigger.picker;
2482
+
2483
+ document.body.removeEventListener( "mousedown", this._onClick, true );
2484
+ document.body.removeEventListener( "focusin", this._onClick, true );
2485
+
2486
+ LX.root.querySelectorAll( ".lexcolorpicker" ).forEach( m => { m.remove(); } );
2487
+
2488
+ ColorPicker.currentPicker = null;
2489
+ }
2490
+
2491
+ _svToPosition( s, v ) {
2492
+ this.intSatMarker.style.left = `${ LX.remapRange( s, 0, 1, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize ) }px`;
2493
+ this.intSatMarker.style.top = `${ LX.remapRange( 1 - v, 0, 1, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize ) }px`
2494
+ };
2495
+
2496
+ _positionToSv( left, top ) {
2497
+ this.currentColor.hsv.s = LX.remapRange( left, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize, 0, 1 );
2498
+ this.currentColor.hsv.v = 1 - LX.remapRange( top, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize, 0, 1 );
2499
+ };
2500
+
2501
+ _updateColorValue( newHexValue, skipCallback = false ) {
1840
2502
 
1841
- menuItem.addEventListener("mouseover", e => {
2503
+ this.currentColor.set( newHexValue ?? this.currentColor.hsv );
1842
2504
 
1843
- let path = menuItem.id;
1844
- let p = parentDom;
2505
+ if( this.callback && !skipCallback )
2506
+ {
2507
+ this.callback( this.currentColor );
2508
+ }
1845
2509
 
1846
- while( p )
1847
- {
1848
- path += "/" + p.id;
1849
- p = p.currentParent?.parentElement;
1850
- }
2510
+ this.intSatMarker.style.backgroundColor = this.currentColor.hex;
1851
2511
 
1852
- LX.root.querySelectorAll( ".lexdropdownmenu" ).forEach( m => {
1853
- if( !path.includes( m.id ) )
1854
- {
1855
- m.currentParent.built = false;
1856
- m.remove();
1857
- }
1858
- } );
2512
+ if( this.useAlpha )
2513
+ {
2514
+ this.alphaTracker.style.color = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b },${ this.currentColor.css.a })`;
2515
+ }
1859
2516
 
1860
- if( item.submenu )
1861
- {
1862
- if( menuItem.built )
1863
- {
1864
- return;
1865
- }
1866
- menuItem.built = true;
1867
- this._create( item.submenu, menuItem );
1868
- }
2517
+ const toFixed = ( s, n = 2) => { return s.toFixed( n ).replace( /([0-9]+(\.[0-9]+[1-9])?)(\.?0+$)/, '$1' ) };
1869
2518
 
1870
- e.stopPropagation();
1871
- });
2519
+ if( this.colorModel == "CSS" )
2520
+ {
2521
+ const { r, g, b, a } = this.currentColor.css;
2522
+ this.labelWidget.set( `rgba(${ r },${ g },${ b }${ this.useAlpha ? ',' + toFixed( a ) : '' })` );
1872
2523
  }
1873
- }
2524
+ else if( this.colorModel == "Hex" )
2525
+ {
2526
+ this.labelWidget.set( ( this.useAlpha ? this.currentColor.hex : this.currentColor.hex.substr( 0, 7 ) ).toUpperCase() );
2527
+ }
2528
+ else if( this.colorModel == "HSV" )
2529
+ {
2530
+ const { h, s, v, a } = this.currentColor.hsv;
2531
+ const components = [ Math.floor( h ) + 'º', Math.floor( s * 100 ) + '%', Math.floor( v * 100 ) + '%' ];
2532
+ if( this.useAlpha ) components.push( toFixed( a ) );
2533
+ this.labelWidget.set( components.join( ' ' ) );
2534
+ }
2535
+ else // RGB
2536
+ {
2537
+ const { r, g, b, a } = this.currentColor.rgb;
2538
+ const components = [ toFixed( r ), toFixed( g ), toFixed( b ) ];
2539
+ if( this.useAlpha ) components.push( toFixed( a ) );
2540
+ this.labelWidget.set( components.join( ' ' ) );
2541
+ }
2542
+ };
1874
2543
 
1875
2544
  _adjustPosition() {
1876
2545
 
@@ -1932,16 +2601,9 @@ class DropdownMenu {
1932
2601
  this.root.style.left = `${ position[ 0 ] }px`;
1933
2602
  this.root.style.top = `${ position[ 1 ] }px`;
1934
2603
  }
1935
-
1936
- _addSeparator( parent ) {
1937
- const separator = document.createElement('div');
1938
- separator.className = "separator";
1939
- parent = parent ?? this.root;
1940
- parent.appendChild( separator );
1941
- }
1942
2604
  };
1943
2605
 
1944
- LX.DropdownMenu = DropdownMenu;
2606
+ LX.ColorPicker = ColorPicker;
1945
2607
 
1946
2608
  class Area {
1947
2609
 
@@ -3804,57 +4466,14 @@ class Menubar {
3804
4466
 
3805
4467
  for( let i = 0; i < buttons.length; ++i )
3806
4468
  {
3807
- let data = buttons[ i ];
3808
- let button = document.createElement( "label" );
4469
+ const data = buttons[ i ];
3809
4470
  const title = data.title;
3810
- let disabled = data.disabled ?? false;
3811
- button.className = "lexmenubutton" + (disabled ? " disabled" : "");
3812
- button.title = title ?? "";
3813
- this.buttonContainer.appendChild( button );
3814
-
3815
- const icon = document.createElement( "a" );
3816
- icon.className = data.icon + " lexicon";
3817
- button.appendChild( icon );
3818
-
3819
- let trigger = icon;
3820
-
3821
- if( data.swap )
3822
- {
3823
- button.classList.add( "swap" );
3824
- icon.classList.add( "swap-off" );
3825
-
3826
- const input = document.createElement( "input" );
3827
- input.type = "checkbox";
3828
- button.prepend( input );
3829
- trigger = input;
3830
-
3831
- const swapIcon = document.createElement( "a" );
3832
- swapIcon.className = data.swap + " swap-on lexicon";
3833
- button.appendChild( swapIcon );
3834
-
3835
- button.swap = function() {
3836
- const swapInput = this.querySelector( "input" );
3837
- swapInput.checked = !swapInput.checked;
3838
- };
3839
-
3840
- // Set if swap has to be performed
3841
- button.setState = function( v ) {
3842
- const swapInput = this.querySelector( "input" );
3843
- swapInput.checked = v;
3844
- };
3845
- }
3846
-
3847
- trigger.addEventListener("click", e => {
3848
- if( data.callback && !disabled )
3849
- {
3850
- const swapInput = button.querySelector( "input" );
3851
- data.callback.call( this, e, swapInput?.checked );
3852
- }
3853
- });
4471
+ const button = new Button( title, "", data.callback, { title, buttonClass: "bg-none", disabled: data.disabled, icon: data.icon, hideName: true, swap: data.swap } )
4472
+ this.buttonContainer.appendChild( button.root );
3854
4473
 
3855
4474
  if( title )
3856
4475
  {
3857
- this.buttons[ title ] = button;
4476
+ this.buttons[ title ] = button.root;
3858
4477
  }
3859
4478
  }
3860
4479
  }
@@ -4557,14 +5176,15 @@ class Widget {
4557
5176
  static SEPARATOR = 26;
4558
5177
  static KNOB = 27;
4559
5178
  static SIZE = 28;
4560
- static PAD = 29;
4561
- static FORM = 30;
4562
- static DIAL = 31;
4563
- static COUNTER = 32;
4564
- static TABLE = 33;
4565
- static TABS = 34;
4566
- static LABEL = 35;
4567
- static BLANK = 36;
5179
+ static OTP = 29;
5180
+ static PAD = 30;
5181
+ static FORM = 31;
5182
+ static DIAL = 32;
5183
+ static COUNTER = 33;
5184
+ static TABLE = 34;
5185
+ static TABS = 35;
5186
+ static LABEL = 36;
5187
+ static BLANK = 37;
4568
5188
 
4569
5189
  static NO_CONTEXT_TYPES = [
4570
5190
  Widget.BUTTON,
@@ -5809,40 +6429,17 @@ class Button extends Widget {
5809
6429
  super( Widget.BUTTON, name, null, options );
5810
6430
 
5811
6431
  this.onGetValue = () => {
5812
- return wValue.innerText;
6432
+ return wValue.querySelector( "input" )?.checked;
5813
6433
  };
5814
6434
 
5815
6435
  this.onSetValue = ( newValue, skipCallback, event ) => {
5816
6436
 
5817
- wValue.innerHTML = "";
5818
-
5819
- if( options.icon )
5820
- {
5821
- let icon = null;
5822
-
5823
- // @legacy
5824
- if( options.icon.includes( "fa-" ) )
5825
- {
5826
- icon = document.createElement( 'a' );
5827
- icon.className = options.icon;
5828
- }
5829
- else
5830
- {
5831
- icon = LX.makeIcon( options.icon );
5832
- }
5833
-
5834
- wValue.prepend( icon );
5835
- }
5836
- else if( options.img )
6437
+ if( !( options.swap ?? false ) )
5837
6438
  {
5838
- let img = document.createElement( 'img' );
5839
- img.src = options.img;
5840
- wValue.prepend( img );
5841
- }
5842
- else
5843
- {
5844
- wValue.innerHTML = `<span>${ ( newValue || "" ) }</span>`;
6439
+ return;
5845
6440
  }
6441
+
6442
+ this.root.setState( newValue, skipCallback );
5846
6443
  };
5847
6444
 
5848
6445
  this.onResize = ( rect ) => {
@@ -5866,25 +6463,88 @@ class Button extends Widget {
5866
6463
  wValue.classList.add( "selected" );
5867
6464
  }
5868
6465
 
5869
- this.onSetValue( value, true );
6466
+ if( options.icon )
6467
+ {
6468
+ let icon = null;
6469
+
6470
+ // @legacy
6471
+ if( options.icon.includes( "fa-" ) )
6472
+ {
6473
+ icon = document.createElement( 'a' );
6474
+ icon.className = options.icon + " lexicon";
6475
+ }
6476
+ else
6477
+ {
6478
+ icon = LX.makeIcon( options.icon );
6479
+ }
6480
+
6481
+ wValue.prepend( icon );
6482
+ }
6483
+ else if( options.img )
6484
+ {
6485
+ let img = document.createElement( 'img' );
6486
+ img.src = options.img;
6487
+ wValue.prepend( img );
6488
+ }
6489
+ else
6490
+ {
6491
+ wValue.innerHTML = `<span>${ ( value || "" ) }</span>`;
6492
+ }
5870
6493
 
5871
6494
  if( options.disabled )
5872
6495
  {
5873
6496
  wValue.setAttribute( "disabled", true );
5874
6497
  }
5875
6498
 
5876
- wValue.addEventListener( "click", e => {
6499
+ let trigger = wValue;
6500
+
6501
+ if( options.swap )
6502
+ {
6503
+ wValue.classList.add( "swap" );
6504
+ wValue.querySelector( "a" ).classList.add( "swap-off" );
6505
+
6506
+ const input = document.createElement( "input" );
6507
+ input.type = "checkbox";
6508
+ wValue.prepend( input );
6509
+ // trigger = input;
6510
+
6511
+ const swapIcon = document.createElement( "a" );
6512
+ swapIcon.className = options.swap + " swap-on lexicon";
6513
+ wValue.appendChild( swapIcon );
6514
+
6515
+ this.root.swap = function( skipCallback ) {
6516
+ const swapInput = wValue.querySelector( "input" );
6517
+ swapInput.checked = !swapInput.checked;
6518
+ if( !skipCallback )
6519
+ {
6520
+ trigger.click();
6521
+ }
6522
+ };
6523
+
6524
+ // Set if swap has to be performed
6525
+ this.root.setState = function( v, skipCallback ) {
6526
+ const swapInput = wValue.querySelector( "input" );
6527
+ swapInput.checked = v;
6528
+ if( !skipCallback )
6529
+ {
6530
+ trigger.click();
6531
+ }
6532
+ };
6533
+ }
6534
+
6535
+ trigger.addEventListener( "click", e => {
5877
6536
  if( options.selectable )
5878
6537
  {
5879
6538
  if( options.parent )
5880
6539
  {
5881
- options.parent.querySelectorAll(".lexbutton.selected").forEach( e => { if( e == wValue ) return; e.classList.remove( "selected" ) } );
6540
+ options.parent.querySelectorAll(".lexbutton.selected").forEach( b => { if( b == wValue ) return; b.classList.remove( "selected" ) } );
5882
6541
  }
5883
6542
 
5884
6543
  wValue.classList.toggle('selected');
5885
6544
  }
5886
6545
 
5887
- this._trigger( new IEvent( name, value, e ), callback );
6546
+ const swapInput = wValue.querySelector( "input" );
6547
+ this._trigger( new IEvent( name, swapInput?.checked ?? value, e ), callback );
5888
6548
  });
5889
6549
 
5890
6550
  if( options.tooltip )
@@ -6279,7 +6939,7 @@ class Select extends Widget {
6279
6939
 
6280
6940
  const selectRoot = selectedOption.root;
6281
6941
  const rect = selectRoot.getBoundingClientRect();
6282
- const nestedDialog = parent.parentElement.closest( "dialog" );
6942
+ const nestedDialog = parent.parentElement.closest( "dialog" ) ?? parent.parentElement.closest( ".lexcolorpicker" );
6283
6943
 
6284
6944
  // Manage vertical aspect
6285
6945
  {
@@ -7329,31 +7989,52 @@ class ColorInput extends Widget {
7329
7989
 
7330
7990
  constructor( name, value, callback, options = {} ) {
7331
7991
 
7332
- value = ( value.constructor === Array ) ? rgbToHex( value ) : value;
7992
+ const useAlpha = options.useAlpha ??
7993
+ ( ( value.constructor === Object && 'a' in value ) || ( value.constructor === String && [ 5, 9 ].includes( value.length ) ) );
7994
+
7995
+ const widgetColor = new Color( value );
7996
+
7997
+ // Force always hex internally
7998
+ value = useAlpha ? widgetColor.hex : widgetColor.hex.substr( 0, 7 );
7333
7999
 
7334
8000
  super( Widget.COLOR, name, value, options );
7335
8001
 
7336
8002
  this.onGetValue = () => {
7337
- return value;
8003
+ const currentColor = new Color( value );
8004
+ return options.useRGB ? currentColor.rgb : value;
7338
8005
  };
7339
8006
 
7340
8007
  this.onSetValue = ( newValue, skipCallback, event ) => {
7341
8008
 
7342
- if( color.useRGB )
8009
+ const newColor = new Color( newValue );
8010
+
8011
+ colorSampleRGB.style.color = value = newColor.hex.substr( 0, 7 );
8012
+
8013
+ if( useAlpha )
7343
8014
  {
7344
- newValue = hexToRgb( newValue );
8015
+ colorSampleAlpha.style.color = value = newColor.hex;
7345
8016
  }
7346
8017
 
7347
8018
  if( !this._skipTextUpdate )
7348
8019
  {
7349
- textWidget.set( newValue, true, event );
8020
+ textWidget.set( value, true, event );
7350
8021
  }
7351
8022
 
7352
- color.value = value = newValue;
7353
-
7354
8023
  if( !skipCallback )
7355
8024
  {
7356
- this._trigger( new IEvent( name, newValue, event ), callback );
8025
+ let retValue = value;
8026
+
8027
+ if( options.useRGB )
8028
+ {
8029
+ retValue = newColor.rgb;
8030
+
8031
+ if( !useAlpha )
8032
+ {
8033
+ delete retValue.a;
8034
+ }
8035
+ }
8036
+
8037
+ this._trigger( new IEvent( name, retValue, event ), callback );
7357
8038
  }
7358
8039
  };
7359
8040
 
@@ -7366,30 +8047,50 @@ class ColorInput extends Widget {
7366
8047
  container.className = "lexcolor";
7367
8048
  this.root.appendChild( container );
7368
8049
 
7369
- let color = document.createElement( 'input' );
7370
- color.style.width = "32px";
7371
- color.type = 'color';
7372
- color.className = "colorinput";
7373
- color.useRGB = options.useRGB ?? false;
7374
- color.value = value;
7375
- container.appendChild( color );
8050
+ let sampleContainer = LX.makeContainer( ["18px", "18px"], "flex flex-row bg-contrast rounded overflow-hidden", "", container );
8051
+ sampleContainer.tabIndex = "1";
8052
+ sampleContainer.addEventListener( "click", e => {
8053
+ if( ( options.disabled ?? false ) )
8054
+ {
8055
+ return;
8056
+ }
8057
+ new ColorPicker( value, sampleContainer, {
8058
+ colorModel: options.useRGB ? "RGB" : "Hex",
8059
+ useAlpha,
8060
+ onChange: ( color ) => {
8061
+ this._fromColorPicker = true;
8062
+ this.set( color.hex );
8063
+ delete this._fromColorPicker;
8064
+ }
8065
+ } );
8066
+ } );
8067
+
8068
+ let colorSampleRGB = document.createElement( 'div' );
8069
+ colorSampleRGB.className = "lexcolorsample";
8070
+ colorSampleRGB.style.color = value;
8071
+ sampleContainer.appendChild( colorSampleRGB );
7376
8072
 
7377
- if( options.disabled )
8073
+ let colorSampleAlpha = null;
8074
+
8075
+ if( useAlpha )
7378
8076
  {
7379
- color.disabled = true;
8077
+ colorSampleAlpha = document.createElement( 'div' );
8078
+ colorSampleAlpha.className = "lexcolorsample";
8079
+ colorSampleAlpha.style.color = value;
8080
+ sampleContainer.appendChild( colorSampleAlpha );
8081
+ }
8082
+ else
8083
+ {
8084
+ colorSampleRGB.style.width = "18px";
7380
8085
  }
7381
8086
 
7382
- color.addEventListener( "input", e => {
7383
- this.set( e.target.value, false, e );
7384
- }, false );
7385
-
7386
- const textWidget = new TextInput( null, color.value, v => {
8087
+ const textWidget = new TextInput( null, value, v => {
7387
8088
  this._skipTextUpdate = true;
7388
8089
  this.set( v );
7389
8090
  delete this._skipTextUpdate;
7390
- }, { width: "calc( 100% - 32px )", disabled: options.disabled });
8091
+ }, { width: "calc( 100% - 24px )", disabled: options.disabled });
7391
8092
 
7392
- textWidget.root.style.marginLeft = "4px";
8093
+ textWidget.root.style.marginLeft = "6px";
7393
8094
  container.appendChild( textWidget.root );
7394
8095
 
7395
8096
  doAsync( this.onResize.bind( this ) );
@@ -8097,6 +8798,164 @@ class SizeInput extends Widget {
8097
8798
 
8098
8799
  LX.SizeInput = SizeInput;
8099
8800
 
8801
+ /**
8802
+ * @class OTPInput
8803
+ * @description OTPInput Widget
8804
+ */
8805
+
8806
+ class OTPInput extends Widget {
8807
+
8808
+ constructor( name, value, callback, options = {} ) {
8809
+
8810
+ const pattern = options.pattern ?? "xxx-xxx";
8811
+ const patternSize = ( pattern.match(/x/g) || [] ).length;
8812
+
8813
+ value = String( value );
8814
+ if( !value.length )
8815
+ {
8816
+ value = "x".repeat( patternSize );
8817
+ }
8818
+
8819
+ super( Widget.OTP, name, value, options );
8820
+
8821
+ this.onGetValue = () => {
8822
+ return +value;
8823
+ };
8824
+
8825
+ this.onSetValue = ( newValue, skipCallback, event ) => {
8826
+
8827
+ value = newValue;
8828
+
8829
+ _refreshInput( value );
8830
+
8831
+ if( !skipCallback )
8832
+ {
8833
+ this._trigger( new IEvent( name, +newValue, event ), callback );
8834
+ }
8835
+ };
8836
+
8837
+ this.onResize = ( rect ) => {
8838
+ const realNameWidth = ( this.root.domName?.offsetWidth ?? 0 );
8839
+ container.style.width = `calc( 100% - ${ realNameWidth }px)`;
8840
+ };
8841
+
8842
+ this.disabled = options.disabled ?? false;
8843
+
8844
+ const container = document.createElement( 'div' );
8845
+ container.className = "lexotp flex flex-row items-center";
8846
+ this.root.appendChild( container );
8847
+
8848
+ const groups = pattern.split( '-' );
8849
+
8850
+ const _refreshInput = ( valueString ) => {
8851
+
8852
+ container.innerHTML = "";
8853
+
8854
+ let itemsCount = 0;
8855
+ let activeSlot = 0;
8856
+
8857
+ for( let i = 0; i < groups.length; ++i )
8858
+ {
8859
+ const g = groups[ i ];
8860
+
8861
+ for( let j = 0; j < g.length; ++j )
8862
+ {
8863
+ let number = valueString[ itemsCount++ ];
8864
+ number = ( number == 'x' ? '' : number );
8865
+
8866
+ const slotDom = LX.makeContainer( ["36px", "30px"],
8867
+ "lexotpslot border-top border-bottom border-left px-3 cursor-text select-none font-medium outline-none", number, container );
8868
+ slotDom.tabIndex = "1";
8869
+
8870
+ if( this.disabled )
8871
+ {
8872
+ slotDom.classList.add( "disabled" );
8873
+ }
8874
+
8875
+ const otpIndex = itemsCount;
8876
+
8877
+ if( j == 0 )
8878
+ {
8879
+ slotDom.className += " rounded-l";
8880
+ }
8881
+ else if( j == ( g.length - 1 ) )
8882
+ {
8883
+ slotDom.className += " rounded-r border-right";
8884
+ }
8885
+
8886
+ slotDom.addEventListener( "click", () => {
8887
+ if( this.disabled ) { return; }
8888
+ container.querySelectorAll( ".lexotpslot" ).forEach( s => s.classList.remove( "active" ) );
8889
+ const activeDom = container.querySelectorAll( ".lexotpslot" )[ activeSlot ];
8890
+ activeDom.classList.add( "active" );
8891
+ activeDom.focus();
8892
+ } );
8893
+
8894
+ slotDom.addEventListener( "blur", () => {
8895
+ if( this.disabled ) { return; }
8896
+ doAsync( () => {
8897
+ if( container.contains( document.activeElement ) ) { return; }
8898
+ container.querySelectorAll( ".lexotpslot" ).forEach( s => s.classList.remove( "active" ) );
8899
+ }, 10 );
8900
+ } );
8901
+
8902
+ slotDom.addEventListener( "keyup", e => {
8903
+ if( this.disabled ) { return; }
8904
+ if( !/[^0-9]+/g.test( e.key ) )
8905
+ {
8906
+ const number = e.key;
8907
+ console.assert( parseInt( number ) != NaN );
8908
+
8909
+ slotDom.innerHTML = number;
8910
+ valueString = valueString.substring( 0, otpIndex - 1 ) + number + valueString.substring( otpIndex );
8911
+
8912
+ const nexActiveDom = container.querySelectorAll( ".lexotpslot" )[ activeSlot + 1 ];
8913
+ if( nexActiveDom )
8914
+ {
8915
+ container.querySelectorAll( ".lexotpslot" )[ activeSlot ].classList.remove( "active" );
8916
+ nexActiveDom.classList.add( "active" );
8917
+ nexActiveDom.focus();
8918
+ activeSlot++;
8919
+ }
8920
+ else
8921
+ {
8922
+ this.set( valueString );
8923
+ }
8924
+ }
8925
+ else if( e.key == "ArrowLeft" || e.key == "ArrowRight" )
8926
+ {
8927
+ const dt = ( e.key == "ArrowLeft" ) ? -1 : 1;
8928
+ const newActiveDom = container.querySelectorAll( ".lexotpslot" )[ activeSlot + dt ];
8929
+ if( newActiveDom )
8930
+ {
8931
+ container.querySelectorAll( ".lexotpslot" )[ activeSlot ].classList.remove( "active" );
8932
+ newActiveDom.classList.add( "active" );
8933
+ newActiveDom.focus();
8934
+ activeSlot += dt;
8935
+ }
8936
+ }
8937
+ else if( e.key == "Enter" && !valueString.includes( 'x' ) )
8938
+ {
8939
+ this.set( valueString );
8940
+ }
8941
+ } );
8942
+ }
8943
+
8944
+ if( i < ( groups.length - 1 ) )
8945
+ {
8946
+ LX.makeContainer( ["auto", "auto"], "mx-2", `-`, container );
8947
+ }
8948
+ }
8949
+
8950
+ console.assert( itemsCount == valueString.length, "OTP Value/Pattern Mismatch!" )
8951
+ }
8952
+
8953
+ _refreshInput( value );
8954
+ }
8955
+ }
8956
+
8957
+ LX.OTPInput = OTPInput;
8958
+
8100
8959
  /**
8101
8960
  * @class Pad
8102
8961
  * @description Pad Widget
@@ -8864,7 +9723,7 @@ class Table extends Widget {
8864
9723
  for( const el of body.childNodes )
8865
9724
  {
8866
9725
  data.checkMap[ el.getAttribute( "rowId" ) ] = this.checked;
8867
- el.querySelector( "input" ).checked = this.checked;
9726
+ el.querySelector( "input[type='checkbox']" ).checked = this.checked;
8868
9727
  }
8869
9728
  });
8870
9729
 
@@ -9078,10 +9937,20 @@ class Table extends Widget {
9078
9937
  input.addEventListener( 'change', function() {
9079
9938
  data.checkMap[ rowId ] = this.checked;
9080
9939
 
9940
+ const headInput = table.querySelector( "thead input[type='checkbox']" );
9941
+
9081
9942
  if( !this.checked )
9082
9943
  {
9083
- const input = table.querySelector( "thead input[type='checkbox']" );
9084
- input.checked = data.checkMap[ ":root" ] = false;
9944
+ headInput.checked = data.checkMap[ ":root" ] = false;
9945
+ }
9946
+ else
9947
+ {
9948
+ const rowInputs = Array.from( table.querySelectorAll( "tbody input[type='checkbox']" ) );
9949
+ const uncheckedRowInputs = rowInputs.filter( i => { return !i.checked; } );
9950
+ if( !uncheckedRowInputs.length )
9951
+ {
9952
+ headInput.checked = data.checkMap[ ":root" ] = true;
9953
+ }
9085
9954
  }
9086
9955
  });
9087
9956
 
@@ -10192,6 +11061,22 @@ class Panel {
10192
11061
  return this._attachWidget( widget );
10193
11062
  }
10194
11063
 
11064
+ /**
11065
+ * @method addOTP
11066
+ * @param {String} name Widget name
11067
+ * @param {String} value Default numeric value in string format
11068
+ * @param {Function} callback Callback function on change
11069
+ * @param {Object} options:
11070
+ * hideName: Don't use name as label [false]
11071
+ * disabled: Make the widget disabled [false]
11072
+ * pattern: OTP numeric pattern
11073
+ */
11074
+
11075
+ addOTP( name, value, callback, options = {} ) {
11076
+ const widget = new OTPInput( name, value, callback, options );
11077
+ return this._attachWidget( widget );
11078
+ }
11079
+
10195
11080
  /**
10196
11081
  * @method addPad
10197
11082
  * @param {String} name Widget name
@@ -10424,7 +11309,7 @@ class Branch {
10424
11309
  // add widgets
10425
11310
  for( let w of this.widgets )
10426
11311
  {
10427
- p.root.appendChild( w.domEl );
11312
+ p.root.appendChild( w.root );
10428
11313
  }
10429
11314
  });
10430
11315
  dialog.widgets = this.widgets;
@@ -10728,7 +11613,7 @@ class Dialog {
10728
11613
 
10729
11614
  for( let w of that.widgets )
10730
11615
  {
10731
- branch.content.appendChild( w.domEl );
11616
+ branch.content.appendChild( w.root );
10732
11617
  }
10733
11618
 
10734
11619
  branch.widgets = that.widgets;
@@ -12369,7 +13254,7 @@ class AssetView {
12369
13254
 
12370
13255
  this.rightPanel.addText(null, this.path.join('/'), null, {
12371
13256
  inputClass: "nobg", disabled: true, signal: "@on_folder_change",
12372
- style: { fontWeight: "600", fontSize: "15px" }
13257
+ style: { fontWeight: "600", fontSize: "15px" }
12373
13258
  });
12374
13259
 
12375
13260
  this.rightPanel.endLine();
@@ -13106,7 +13991,7 @@ Element.prototype.addClass = function( className ) {
13106
13991
  }
13107
13992
 
13108
13993
  Element.prototype.getComputedSize = function() {
13109
- // Since we use "box-sizing: border-box" now,
13994
+ // Since we use "box-sizing: border-box" now,
13110
13995
  // it's all included in offsetWidth/offsetHeight
13111
13996
  return {
13112
13997
  width: this.offsetWidth,
@@ -13248,6 +14133,7 @@ LX.ICONS = {
13248
14133
  "copy": [448, 512, [], "regular", "M384 336l-192 0c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l140.1 0L400 115.9 400 320c0 8.8-7.2 16-16 16zM192 384l192 0c35.3 0 64-28.7 64-64l0-204.1c0-12.7-5.1-24.9-14.1-33.9L366.1 14.1c-9-9-21.2-14.1-33.9-14.1L192 0c-35.3 0-64 28.7-64 64l0 256c0 35.3 28.7 64 64 64zM64 128c-35.3 0-64 28.7-64 64L0 448c0 35.3 28.7 64 64 64l192 0c35.3 0 64-28.7 64-64l0-32-48 0 0 32c0 8.8-7.2 16-16 16L64 464c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l32 0 0-48-32 0z"],
13249
14134
  "paste": [512, 512, [], "regular", "M104.6 48L64 48C28.7 48 0 76.7 0 112L0 384c0 35.3 28.7 64 64 64l96 0 0-48-96 0c-8.8 0-16-7.2-16-16l0-272c0-8.8 7.2-16 16-16l16 0c0 17.7 14.3 32 32 32l72.4 0C202 108.4 227.6 96 256 96l62 0c-7.1-27.6-32.2-48-62-48l-40.6 0C211.6 20.9 188.2 0 160 0s-51.6 20.9-55.4 48zM144 56a16 16 0 1 1 32 0 16 16 0 1 1 -32 0zM448 464l-192 0c-8.8 0-16-7.2-16-16l0-256c0-8.8 7.2-16 16-16l140.1 0L464 243.9 464 448c0 8.8-7.2 16-16 16zM256 512l192 0c35.3 0 64-28.7 64-64l0-204.1c0-12.7-5.1-24.9-14.1-33.9l-67.9-67.9c-9-9-21.2-14.1-33.9-14.1L256 128c-35.3 0-64 28.7-64 64l0 256c0 35.3 28.7 64 64 64z"],
13250
14135
  "clipboard": [384, 512, [], "regular", "M280 64l40 0c35.3 0 64 28.7 64 64l0 320c0 35.3-28.7 64-64 64L64 512c-35.3 0-64-28.7-64-64L0 128C0 92.7 28.7 64 64 64l40 0 9.6 0C121 27.5 153.3 0 192 0s71 27.5 78.4 64l9.6 0zM64 112c-8.8 0-16 7.2-16 16l0 320c0 8.8 7.2 16 16 16l256 0c8.8 0 16-7.2 16-16l0-320c0-8.8-7.2-16-16-16l-16 0 0 24c0 13.3-10.7 24-24 24l-88 0-88 0c-13.3 0-24-10.7-24-24l0-24-16 0zm128-8a24 24 0 1 0 0-48 24 24 0 1 0 0 48z"],
14136
+ "eye-dropper": [512, 512, [], "solid", "M341.6 29.2L240.1 130.8l-9.4-9.4c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3l160 160c12.5 12.5 32.8 12.5 45.3 0s12.5-32.8 0-45.3l-9.4-9.4L482.8 170.4c39-39 39-102.2 0-141.1s-102.2-39-141.1 0zM55.4 323.3c-15 15-23.4 35.4-23.4 56.6l0 42.4L5.4 462.2c-8.5 12.7-6.8 29.6 4 40.4s27.7 12.5 40.4 4L89.7 480l42.4 0c21.2 0 41.6-8.4 56.6-23.4L309.4 335.9l-45.3-45.3L143.4 411.3c-3 3-7.1 4.7-11.3 4.7L96 416l0-36.1c0-4.2 1.7-8.3 4.7-11.3L221.4 247.9l-45.3-45.3L55.4 323.3z"],
13251
14137
  "edit": [512, 512, [], "regular", "M441 58.9L453.1 71c9.4 9.4 9.4 24.6 0 33.9L424 134.1 377.9 88 407 58.9c9.4-9.4 24.6-9.4 33.9 0zM209.8 256.2L344 121.9 390.1 168 255.8 302.2c-2.9 2.9-6.5 5-10.4 6.1l-58.5 16.7 16.7-58.5c1.1-3.9 3.2-7.5 6.1-10.4zM373.1 25L175.8 222.2c-8.7 8.7-15 19.4-18.3 31.1l-28.6 100c-2.4 8.4-.1 17.4 6.1 23.6s15.2 8.5 23.6 6.1l100-28.6c11.8-3.4 22.5-9.7 31.1-18.3L487 138.9c28.1-28.1 28.1-73.7 0-101.8L474.9 25C446.8-3.1 401.2-3.1 373.1 25zM88 64C39.4 64 0 103.4 0 152L0 424c0 48.6 39.4 88 88 88l272 0c48.6 0 88-39.4 88-88l0-112c0-13.3-10.7-24-24-24s-24 10.7-24 24l0 112c0 22.1-17.9 40-40 40L88 464c-22.1 0-40-17.9-40-40l0-272c0-22.1 17.9-40 40-40l112 0c13.3 0 24-10.7 24-24s-10.7-24-24-24L88 64z"],
13252
14138
  "envelope": [512, 512, [], "regular", "M64 112c-8.8 0-16 7.2-16 16l0 22.1L220.5 291.7c20.7 17 50.4 17 71.1 0L464 150.1l0-22.1c0-8.8-7.2-16-16-16L64 112zM48 212.2L48 384c0 8.8 7.2 16 16 16l384 0c8.8 0 16-7.2 16-16l0-171.8L322 328.8c-38.4 31.5-93.7 31.5-132 0L48 212.2zM0 128C0 92.7 28.7 64 64 64l384 0c35.3 0 64 28.7 64 64l0 256c0 35.3-28.7 64-64 64L64 448c-35.3 0-64-28.7-64-64L0 128z"],
13253
14139
  "envelope-open": [512, 512, [], "regular", "M255.4 48.2c.2-.1 .4-.2 .6-.2s.4 .1 .6 .2L460.6 194c2.1 1.5 3.4 3.9 3.4 6.5l0 13.6L291.5 355.7c-20.7 17-50.4 17-71.1 0L48 214.1l0-13.6c0-2.6 1.2-5 3.4-6.5L255.4 48.2zM48 276.2L190 392.8c38.4 31.5 93.7 31.5 132 0L464 276.2 464 456c0 4.4-3.6 8-8 8L56 464c-4.4 0-8-3.6-8-8l0-179.8zM256 0c-10.2 0-20.2 3.2-28.5 9.1L23.5 154.9C8.7 165.4 0 182.4 0 200.5L0 456c0 30.9 25.1 56 56 56l400 0c30.9 0 56-25.1 56-56l0-255.5c0-18.1-8.7-35.1-23.4-45.6L284.5 9.1C276.2 3.2 266.2 0 256 0z"],