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.
package/build/lexgui.js CHANGED
@@ -12,7 +12,7 @@ console.warn( 'Script _build/lexgui.js_ is depracated and will be removed soon.
12
12
  */
13
13
 
14
14
  var LX = {
15
- version: "0.5.5",
15
+ version: "0.5.6",
16
16
  ready: false,
17
17
  components: [], // Specific pre-build components
18
18
  signals: {}, // Events and triggers
@@ -222,37 +222,141 @@ LX.getBase64Image = getBase64Image;
222
222
 
223
223
  /**
224
224
  * @method hexToRgb
225
- * @description Convert a hexadecimal string to a valid RGB color array
226
- * @param {String} hexStr Hexadecimal color
225
+ * @description Convert a hexadecimal string to a valid RGB color
226
+ * @param {String} hex Hexadecimal color
227
227
  */
228
- function hexToRgb( hexStr )
228
+ function hexToRgb( hex )
229
229
  {
230
- const red = parseInt( hexStr.substring( 1, 3 ), 16 ) / 255;
231
- const green = parseInt( hexStr.substring( 3, 5 ), 16 ) / 255;
232
- const blue = parseInt( hexStr.substring( 5, 7 ), 16 ) / 255;
233
- return [ red, green, blue ];
230
+ const hexPattern = /^#(?:[A-Fa-f0-9]{3,4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/;
231
+ if( !hexPattern.test( hex ) )
232
+ {
233
+ throw( `Invalid Hex Color: ${ hex }` );
234
+ }
235
+
236
+ hex = hex.replace( /^#/, '' );
237
+
238
+ // Expand shorthand form (#RGB or #RGBA)
239
+ if( hex.length === 3 || hex.length === 4 )
240
+ {
241
+ hex = hex.split( '' ).map( c => c + c ).join( '' );
242
+ }
243
+
244
+ const bigint = parseInt( hex, 16 );
245
+
246
+ const r = ( ( bigint >> ( hex.length === 8 ? 24 : 16 ) ) & 255 ) / 255;
247
+ const g = ( ( bigint >> ( hex.length === 8 ? 16 : 8 ) ) & 255 ) / 255;
248
+ const b = ( ( bigint >> ( hex.length === 8 ? 8 : 0 ) ) & 255 ) / 255;
249
+ const a = ( hex.length === 8 ? ( bigint & 255 ) : ( hex.length === 4 ? parseInt( hex.slice( -2 ), 16 ) : 255 ) ) / 255;
250
+
251
+ return { r, g, b, a };
234
252
  }
235
253
 
236
254
  LX.hexToRgb = hexToRgb;
237
255
 
256
+ /**
257
+ * @method hexToHsv
258
+ * @description Convert a hexadecimal string to HSV (0..360|0..1|0..1)
259
+ * @param {String} hexStr Hexadecimal color
260
+ */
261
+ function hexToHsv( hexStr )
262
+ {
263
+ const rgb = hexToRgb( hexStr );
264
+ return rgbToHsv( rgb );
265
+ }
266
+
267
+ LX.hexToHsv = hexToHsv;
268
+
238
269
  /**
239
270
  * @method rgbToHex
240
- * @description Convert a RGB color array to a hexadecimal string
241
- * @param {Array} rgb Array containing R, G, B, A*
271
+ * @description Convert a RGB color to a hexadecimal string
272
+ * @param {Object} rgb Object containing RGB color
273
+ * @param {Number} scale Use 255 for 0..255 range or 1 for 0..1 range
242
274
  */
243
- function rgbToHex( rgb )
275
+ function rgbToHex( rgb, scale = 255 )
244
276
  {
245
- let hex = "#";
246
- for( let c of rgb )
247
- {
248
- c = Math.floor( c * 255 );
249
- hex += c.toString( 16 );
250
- }
251
- return hex;
277
+ const rgbArray = [ rgb.r, rgb.g, rgb.b ];
278
+ if( rgb.a != undefined ) rgbArray.push( rgb.a );
279
+
280
+ return (
281
+ "#" +
282
+ rgbArray.map( c => {
283
+ c = Math.floor( c * scale );
284
+ const hex = c.toString(16);
285
+ return hex.length === 1 ? ( '0' + hex ) : hex;
286
+ }).join("")
287
+ );
252
288
  }
253
289
 
254
290
  LX.rgbToHex = rgbToHex;
255
291
 
292
+ /**
293
+ * @method rgbToCss
294
+ * @description Convert a RGB color (0..1) to a CSS color format
295
+ * @param {Object} rgb Object containing RGB color
296
+ */
297
+ function rgbToCss( rgb )
298
+ {
299
+ return { r: Math.floor( rgb.r * 255 ), g: Math.floor( rgb.g * 255 ), b: Math.floor( rgb.b * 255 ), a: rgb.a };
300
+ }
301
+
302
+ LX.rgbToCss = rgbToCss;
303
+
304
+ /**
305
+ * @method rgbToHsv
306
+ * @description Convert a RGB color (0..1) array to HSV (0..360|0..1|0..1)
307
+ * @param {Object} rgb Array containing R, G, B
308
+ */
309
+ function rgbToHsv( rgb )
310
+ {
311
+ let { r, g, b, a } = rgb;
312
+ a = a ?? 1;
313
+
314
+ const max = Math.max(r, g, b);
315
+ const min = Math.min(r, g, b);
316
+ const d = max - min;
317
+ let h = 0;
318
+
319
+ if (d !== 0) {
320
+ if (max === r) { h = ((g - b) / d) % 6 }
321
+ else if (max === g) { h = (b - r) / d + 2 }
322
+ else { h = (r - g) / d + 4 }
323
+ h *= 60
324
+ if (h < 0) { h += 360 }
325
+ }
326
+
327
+ const s = max === 0 ? 0 : (d / max);
328
+ const v = max;
329
+
330
+ return { h, s, v, a };
331
+ }
332
+
333
+ LX.rgbToHsv = rgbToHsv;
334
+
335
+ /**
336
+ * @method hsvToRgb
337
+ * @description Convert an HSV color (0..360|0..1|0..1) array to RGB (0..1|0..255)
338
+ * @param {Array} hsv Array containing H, S, V
339
+ */
340
+ function hsvToRgb( hsv )
341
+ {
342
+ const { h, s, v, a } = hsv;
343
+ const c = v * s;
344
+ const x = c * (1 - Math.abs( ( (h / 60) % 2 ) - 1) )
345
+ const m = v - c;
346
+ let r = 0, g = 0, b = 0;
347
+
348
+ if( h < 60 ) { r = c; g = x; b = 0; }
349
+ else if ( h < 120 ) { r = x; g = c; b = 0; }
350
+ else if ( h < 180 ) { r = 0; g = c; b = x; }
351
+ else if ( h < 240 ) { r = 0; g = x; b = c; }
352
+ else if ( h < 300 ) { r = x; g = 0; b = c; }
353
+ else { r = c; g = 0; b = x; }
354
+
355
+ return { r: ( r + m ), g: ( g + m ), b: ( b + m ), a };
356
+ }
357
+
358
+ LX.hsvToRgb = hsvToRgb;
359
+
256
360
  /**
257
361
  * @method measureRealWidth
258
362
  * @description Measure the pixel width of a text
@@ -584,6 +688,42 @@ function makeCodeSnippet( code, size, options = { } )
584
688
 
585
689
  LX.makeCodeSnippet = makeCodeSnippet;
586
690
 
691
+ /**
692
+ * @method makeKbd
693
+ * @description Kbd element to display a keyboard key.
694
+ * @param {Array} keys
695
+ * @param {String} extraClass
696
+ */
697
+ function makeKbd( keys, extraClass = "" )
698
+ {
699
+ const specialKeys = {
700
+ "Ctrl": '⌃',
701
+ "Enter": '↩',
702
+ "Shift": '⇧',
703
+ "CapsLock": '⇪',
704
+ "Meta": '⌘',
705
+ "Option": '⌥',
706
+ "Alt": '⌥',
707
+ "Tab": '⇥',
708
+ "ArrowUp": '↑',
709
+ "ArrowDown": '↓',
710
+ "ArrowLeft": '←',
711
+ "ArrowRight": '→',
712
+ "Space": '␣'
713
+ };
714
+
715
+ const kbd = LX.makeContainer( ["auto", "auto"], "flex flex-row ml-auto" );
716
+
717
+ for( const k of keys )
718
+ {
719
+ LX.makeContainer( ["auto", "auto"], "self-center text-xs fg-secondary select-none", specialKeys[ k ] ?? k, kbd );
720
+ }
721
+
722
+ return kbd;
723
+ }
724
+
725
+ LX.makeKbd = makeKbd;
726
+
587
727
  /**
588
728
  * @method makeIcon
589
729
  * @description Gets an SVG element using one of LX.ICONS
@@ -722,7 +862,7 @@ function registerCommandbarEntry( name, callback )
722
862
 
723
863
  LX.registerCommandbarEntry = registerCommandbarEntry;
724
864
 
725
- // Math classes
865
+ // Utils classes
726
866
 
727
867
  class vec2 {
728
868
 
@@ -750,6 +890,77 @@ class vec2 {
750
890
 
751
891
  LX.vec2 = vec2;
752
892
 
893
+ class Color {
894
+
895
+ constructor( value ) {
896
+
897
+ Object.defineProperty( Color.prototype, "rgb", {
898
+ get: function() { return this._rgb; },
899
+ set: function( v ) { this._fromRGB( v ) }, enumerable: true, configurable: true
900
+ });
901
+
902
+ Object.defineProperty( Color.prototype, "hex", {
903
+ get: function() { return this._hex; },
904
+ set: function( v ) { this._fromHex( v ) }, enumerable: true, configurable: true
905
+ });
906
+
907
+ Object.defineProperty( Color.prototype, "hsv", {
908
+ get: function() { return this._hsv; },
909
+ set: function( v ) { this._fromHSV( v ) }, enumerable: true, configurable: true
910
+ });
911
+
912
+ this.set( value );
913
+ }
914
+
915
+ set( value ) {
916
+
917
+ if ( typeof value === 'string' && value.startsWith( '#' ) )
918
+ {
919
+ this._fromHex( value );
920
+ }
921
+ else if( 'r' in value && 'g' in value && 'b' in value)
922
+ {
923
+ value.a = value.a ?? 1.0;
924
+ this._fromRGB( value );
925
+ }
926
+ else if( 'h' in value && 's' in value && 'v' in value )
927
+ {
928
+ value.a = value.a ?? 1.0;
929
+ this._fromHSV( value );
930
+ }
931
+ else
932
+ {
933
+ throw( "Bad color model!", value );
934
+ }
935
+ }
936
+
937
+ setHSV( hsv ) { this._fromHSV( hsv ); }
938
+ setRGB( rgb ) { this._fromRGB( rgb ); }
939
+ setHex( hex ) { this._fromHex( hex ); }
940
+
941
+ _fromHex( hex ) {
942
+ this._fromRGB( hexToRgb( hex ) );
943
+ }
944
+
945
+ _fromRGB( rgb ) {
946
+ this._rgb = rgb;
947
+ this._hsv = rgbToHsv( rgb );
948
+ this._hex = rgbToHex( rgb );
949
+ this.css = rgbToCss( this._rgb );
950
+ }
951
+
952
+ _fromHSV( hsv ) {
953
+ this._hsv = hsv;
954
+ this._rgb = hsvToRgb( hsv );
955
+ this._hex = rgbToHex( this._rgb );
956
+ this.css = rgbToCss( this._rgb );
957
+ }
958
+ }
959
+
960
+ LX.Color = Color;
961
+
962
+ // Command bar creation
963
+
753
964
  function _createCommandbar( root )
754
965
  {
755
966
  let commandbar = document.createElement( "dialog" );
@@ -1807,6 +2018,13 @@ class DropdownMenu {
1807
2018
  submenuIcon.className = "fa-solid fa-angle-right fa-xs";
1808
2019
  menuItem.appendChild( submenuIcon );
1809
2020
  }
2021
+ else if( item.kbd )
2022
+ {
2023
+ item.kbd = [].concat( item.kbd );
2024
+
2025
+ const kbd = LX.makeKbd( item.kbd );
2026
+ menuItem.appendChild( kbd );
2027
+ }
1810
2028
 
1811
2029
  if( item.icon )
1812
2030
  {
@@ -1826,57 +2044,508 @@ class DropdownMenu {
1826
2044
  const input = checkbox.root.querySelector( "input" );
1827
2045
  menuItem.prepend( input );
1828
2046
 
1829
- menuItem.addEventListener( "click", (e) => {
1830
- if( e.target.type == "checkbox" ) return;
1831
- input.checked = !input.checked;
1832
- checkbox.set( input.checked );
1833
- } );
1834
- }
1835
- else
1836
- {
1837
- menuItem.addEventListener( "click", () => {
1838
- const f = item[ 'callback' ];
1839
- if( f )
1840
- {
1841
- f.call( this, key, menuItem );
1842
- }
1843
- this.destroy();
1844
- } );
1845
- }
2047
+ menuItem.addEventListener( "click", (e) => {
2048
+ if( e.target.type == "checkbox" ) return;
2049
+ input.checked = !input.checked;
2050
+ checkbox.set( input.checked );
2051
+ } );
2052
+ }
2053
+ else
2054
+ {
2055
+ menuItem.addEventListener( "click", () => {
2056
+ const f = item[ 'callback' ];
2057
+ if( f )
2058
+ {
2059
+ f.call( this, key, menuItem );
2060
+ }
2061
+ this.destroy();
2062
+ } );
2063
+ }
2064
+
2065
+ menuItem.addEventListener("mouseover", e => {
2066
+
2067
+ let path = menuItem.id;
2068
+ let p = parentDom;
2069
+
2070
+ while( p )
2071
+ {
2072
+ path += "/" + p.id;
2073
+ p = p.currentParent?.parentElement;
2074
+ }
2075
+
2076
+ LX.root.querySelectorAll( ".lexdropdownmenu" ).forEach( m => {
2077
+ if( !path.includes( m.id ) )
2078
+ {
2079
+ m.currentParent.built = false;
2080
+ m.remove();
2081
+ }
2082
+ } );
2083
+
2084
+ if( item.submenu )
2085
+ {
2086
+ if( menuItem.built )
2087
+ {
2088
+ return;
2089
+ }
2090
+ menuItem.built = true;
2091
+ this._create( item.submenu, menuItem );
2092
+ }
2093
+
2094
+ e.stopPropagation();
2095
+ });
2096
+ }
2097
+ }
2098
+
2099
+ _adjustPosition() {
2100
+
2101
+ const position = [ 0, 0 ];
2102
+
2103
+ // Place menu using trigger position and user options
2104
+ {
2105
+ const rect = this._trigger.getBoundingClientRect();
2106
+
2107
+ let alignWidth = true;
2108
+
2109
+ switch( this.side )
2110
+ {
2111
+ case "left":
2112
+ position[ 0 ] += ( rect.x - this.root.offsetWidth );
2113
+ alignWidth = false;
2114
+ break;
2115
+ case "right":
2116
+ position[ 0 ] += ( rect.x + rect.width );
2117
+ alignWidth = false;
2118
+ break;
2119
+ case "top":
2120
+ position[ 1 ] += ( rect.y - this.root.offsetHeight );
2121
+ alignWidth = true;
2122
+ break;
2123
+ case "bottom":
2124
+ position[ 1 ] += ( rect.y + rect.height );
2125
+ alignWidth = true;
2126
+ break;
2127
+ default:
2128
+ break;
2129
+ }
2130
+
2131
+ switch( this.align )
2132
+ {
2133
+ case "start":
2134
+ if( alignWidth ) { position[ 0 ] += rect.x; }
2135
+ else { position[ 1 ] += rect.y; }
2136
+ break;
2137
+ case "center":
2138
+ if( alignWidth ) { position[ 0 ] += ( rect.x + rect.width * 0.5 ) - this.root.offsetWidth * 0.5; }
2139
+ else { position[ 1 ] += ( rect.y + rect.height * 0.5 ) - this.root.offsetHeight * 0.5; }
2140
+ break;
2141
+ case "end":
2142
+ if( alignWidth ) { position[ 0 ] += rect.x - this.root.offsetWidth + rect.width; }
2143
+ else { position[ 1 ] += rect.y - this.root.offsetHeight + rect.height; }
2144
+ break;
2145
+ default:
2146
+ break;
2147
+ }
2148
+ }
2149
+
2150
+ if( this.avoidCollisions )
2151
+ {
2152
+ position[ 0 ] = LX.clamp( position[ 0 ], 0, window.innerWidth - this.root.offsetWidth - this._windowPadding );
2153
+ position[ 1 ] = LX.clamp( position[ 1 ], 0, window.innerHeight - this.root.offsetHeight - this._windowPadding );
2154
+ }
2155
+
2156
+ this.root.style.left = `${ position[ 0 ] }px`;
2157
+ this.root.style.top = `${ position[ 1 ] }px`;
2158
+ }
2159
+
2160
+ _addSeparator( parent ) {
2161
+ const separator = document.createElement('div');
2162
+ separator.className = "separator";
2163
+ parent = parent ?? this.root;
2164
+ parent.appendChild( separator );
2165
+ }
2166
+ };
2167
+
2168
+ LX.DropdownMenu = DropdownMenu;
2169
+
2170
+ /**
2171
+ * @class ColorPicker
2172
+ */
2173
+
2174
+ class ColorPicker {
2175
+
2176
+ static currentPicker = false;
2177
+
2178
+ constructor( hexValue, trigger, options = {} ) {
2179
+
2180
+ console.assert( trigger, "ColorPicker needs a DOM element as trigger!" );
2181
+
2182
+ this._windowPadding = 4;
2183
+ this.side = options.side ?? "bottom";
2184
+ this.align = options.align ?? "center";
2185
+ this.avoidCollisions = options.avoidCollisions ?? true;
2186
+ this.colorModel = options.colorModel ?? "Hex";
2187
+ this.useAlpha = options.useAlpha ?? false;
2188
+ this.callback = options.onChange;
2189
+
2190
+ if( !this.callback )
2191
+ {
2192
+ console.warn( "Define a callback in _options.onChange_ to allow getting new Color values!" );
2193
+ }
2194
+
2195
+ if( ColorPicker.currentPicker )
2196
+ {
2197
+ ColorPicker.currentPicker.destroy();
2198
+ return;
2199
+ }
2200
+
2201
+ this._trigger = trigger;
2202
+ trigger.classList.add( "triggered" );
2203
+ trigger.picker = this;
2204
+
2205
+ this.root = document.createElement( "div" );
2206
+ this.root.tabIndex = "1";
2207
+ this.root.className = "lexcolorpicker";
2208
+ this.root.dataset["side"] = this.side;
2209
+ LX.root.appendChild( this.root );
2210
+
2211
+ this.root.addEventListener( "keydown", (e) => {
2212
+ if( e.key == "Escape" )
2213
+ {
2214
+ e.preventDefault();
2215
+ e.stopPropagation();
2216
+ this.destroy();
2217
+ }
2218
+ } )
2219
+
2220
+ ColorPicker.currentPicker = this;
2221
+
2222
+ this.markerHalfSize = 8;
2223
+ this.markerSize = this.markerHalfSize * 2;
2224
+ this.currentColor = new Color( hexValue );
2225
+
2226
+ const hueColor = new Color( { h: this.currentColor.hsv.h, s: 1, v: 1 } );
2227
+
2228
+ // Intensity, Sat
2229
+ this.colorPickerBackground = document.createElement( 'div' );
2230
+ this.colorPickerBackground.className = "lexcolorpickerbg";
2231
+ this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2232
+ this.root.appendChild( this.colorPickerBackground );
2233
+
2234
+ this.intSatMarker = document.createElement( 'div' );
2235
+ this.intSatMarker.className = "lexcolormarker";
2236
+ this.intSatMarker.style.backgroundColor = this.currentColor.hex;
2237
+ this.colorPickerBackground.appendChild( this.intSatMarker );
2238
+
2239
+ doAsync( this._svToPosition.bind( this, this.currentColor.hsv.s, this.currentColor.hsv.v ) );
2240
+
2241
+ let innerMouseDown = e => {
2242
+ var doc = this.root.ownerDocument;
2243
+ doc.addEventListener( 'mousemove', innerMouseMove );
2244
+ doc.addEventListener( 'mouseup', innerMouseUp );
2245
+ document.body.classList.add( 'noevents' );
2246
+ e.stopImmediatePropagation();
2247
+ e.stopPropagation();
2248
+
2249
+ const currentLeft = ( e.offsetX - this.markerHalfSize );
2250
+ this.intSatMarker.style.left = currentLeft + "px";
2251
+ const currentTop = ( e.offsetY - this.markerHalfSize );
2252
+ this.intSatMarker.style.top = currentTop + "px";
2253
+ this._positionToSv( currentLeft, currentTop );
2254
+ this._updateColorValue();
2255
+ }
2256
+
2257
+ let innerMouseMove = e => {
2258
+ const dX = e.movementX;
2259
+ const dY = e.movementY;
2260
+
2261
+ const rect = this.colorPickerBackground.getBoundingClientRect();
2262
+ const mouseX = e.offsetX - rect.x;
2263
+ const mouseY = e.offsetY - rect.y;
2264
+
2265
+ if ( dX != 0 && ( mouseX >= 0 || dX < 0 ) && ( mouseX < this.colorPickerBackground.offsetWidth || dX > 0 ) )
2266
+ {
2267
+ this.intSatMarker.style.left = LX.clamp( parseInt( this.intSatMarker.style.left ) + dX, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize ) + "px";
2268
+ }
2269
+
2270
+ if ( dY != 0 && ( mouseY >= 0 || dY < 0 ) && ( mouseY < this.colorPickerBackground.offsetHeight || dY > 0 ) )
2271
+ {
2272
+ this.intSatMarker.style.top = LX.clamp( parseInt( this.intSatMarker.style.top ) + dY, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize ) + "px";
2273
+ }
2274
+
2275
+ this._positionToSv( parseInt( this.intSatMarker.style.left ), parseInt( this.intSatMarker.style.top ) );
2276
+ this._updateColorValue();
2277
+
2278
+ e.stopPropagation();
2279
+ e.preventDefault();
2280
+ }
2281
+
2282
+ let innerMouseUp = e => {
2283
+ var doc = this.root.ownerDocument;
2284
+ doc.removeEventListener( 'mousemove', innerMouseMove );
2285
+ doc.removeEventListener( 'mouseup', innerMouseUp );
2286
+ document.body.classList.remove( 'noevents' );
2287
+ }
2288
+
2289
+ this.colorPickerBackground.addEventListener( "mousedown", innerMouseDown );
2290
+
2291
+ const hueAlphaContainer = LX.makeContainer( ["100%", "auto"], "flex flex-row gap-1 items-center", "", this.root );
2292
+
2293
+ if( window.EyeDropper )
2294
+ {
2295
+ hueAlphaContainer.appendChild( new Button(null, "eyedrop", async () => {
2296
+ const eyeDropper = new EyeDropper()
2297
+ try {
2298
+ const result = await eyeDropper.open();
2299
+ this.fromHexColor( result.sRGBHex );
2300
+ } catch ( err ) {
2301
+ // console.error("EyeDropper cancelled or failed: ", err)
2302
+ }
2303
+ }, { icon: "eye-dropper", buttonClass: "bg-none", title: "Sample Color" }).root );
2304
+ }
2305
+
2306
+ const innerHueAlpha = LX.makeContainer( ["100%", "100%"], "flex flex-col gap-2", "", hueAlphaContainer );
2307
+
2308
+ // Hue
2309
+ this.colorPickerTracker = document.createElement( 'div' );
2310
+ this.colorPickerTracker.className = "lexhuetracker";
2311
+ innerHueAlpha.appendChild( this.colorPickerTracker );
2312
+
2313
+ this.hueMarker = document.createElement( 'div' );
2314
+ this.hueMarker.className = "lexcolormarker";
2315
+ this.hueMarker.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2316
+ this.colorPickerTracker.appendChild( this.hueMarker );
2317
+
2318
+ doAsync( () => {
2319
+ const hueLeft = LX.remapRange( this.currentColor.hsv.h, 0, 360, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2320
+ this.hueMarker.style.left = hueLeft + "px";
2321
+ } );
2322
+
2323
+ const _fromHueX = ( hueX ) => {
2324
+ this.hueMarker.style.left = hueX + "px";
2325
+ this.currentColor.hsv.h = LX.remapRange( hueX, 0, this.colorPickerTracker.offsetWidth - this.markerSize, 0, 360 );
2326
+
2327
+ const hueColor = new Color( { h: this.currentColor.hsv.h, s: 1, v: 1 } );
2328
+ this.hueMarker.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2329
+ this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2330
+ this._updateColorValue();
2331
+ };
2332
+
2333
+ let innerMouseDownHue = e => {
2334
+ const doc = this.root.ownerDocument;
2335
+ doc.addEventListener( 'mousemove', innerMouseMoveHue );
2336
+ doc.addEventListener( 'mouseup', innerMouseUpHue );
2337
+ document.body.classList.add( 'noevents' );
2338
+ e.stopImmediatePropagation();
2339
+ e.stopPropagation();
2340
+
2341
+ const hueX = clamp( e.offsetX - this.markerHalfSize, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2342
+ _fromHueX( hueX );
2343
+ }
2344
+
2345
+ let innerMouseMoveHue = e => {
2346
+ let dX = e.movementX;
2347
+
2348
+ const rect = this.colorPickerTracker.getBoundingClientRect();
2349
+ const mouseX = e.offsetX - rect.x;
2350
+
2351
+ if ( dX != 0 && ( mouseX >= 0 || dX < 0 ) && ( mouseX < this.colorPickerTracker.offsetWidth || dX > 0 ) )
2352
+ {
2353
+ const hueX = LX.clamp( parseInt( this.hueMarker.style.left ) + dX, 0, this.colorPickerTracker.offsetWidth - this.markerSize );
2354
+ _fromHueX( hueX )
2355
+ }
2356
+
2357
+ e.stopPropagation();
2358
+ e.preventDefault();
2359
+ }
2360
+
2361
+ let innerMouseUpHue = e => {
2362
+ var doc = this.root.ownerDocument;
2363
+ doc.removeEventListener( 'mousemove', innerMouseMoveHue );
2364
+ doc.removeEventListener( 'mouseup', innerMouseUpHue );
2365
+ document.body.classList.remove( 'noevents' );
2366
+ }
2367
+
2368
+ this.colorPickerTracker.addEventListener( "mousedown", innerMouseDownHue );
2369
+
2370
+ // Alpha
2371
+ if( this.useAlpha )
2372
+ {
2373
+ this.alphaTracker = document.createElement( 'div' );
2374
+ this.alphaTracker.className = "lexalphatracker";
2375
+ this.alphaTracker.style.color = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b })`;
2376
+ innerHueAlpha.appendChild( this.alphaTracker );
2377
+
2378
+ this.alphaMarker = document.createElement( 'div' );
2379
+ this.alphaMarker.className = "lexcolormarker";
2380
+ this.alphaMarker.style.backgroundColor = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b },${ this.currentColor.css.a })`;
2381
+ this.alphaTracker.appendChild( this.alphaMarker );
2382
+
2383
+ doAsync( () => {
2384
+ const alphaLeft = LX.remapRange( this.currentColor.hsv.a, 0, 1, 0, this.alphaTracker.offsetWidth - this.markerSize );
2385
+ this.alphaMarker.style.left = alphaLeft + "px";
2386
+ } );
2387
+
2388
+ const _fromAlphaX = ( alphaX ) => {
2389
+ this.alphaMarker.style.left = alphaX + "px";
2390
+ this.currentColor.hsv.a = LX.remapRange( alphaX, 0, this.alphaTracker.offsetWidth - this.markerSize, 0, 1 );
2391
+ this._updateColorValue();
2392
+ // Update alpha marker once the color is updated
2393
+ this.alphaMarker.style.backgroundColor = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b },${ this.currentColor.css.a })`;
2394
+ };
2395
+
2396
+ let innerMouseDownAlpha = e => {
2397
+ const doc = this.root.ownerDocument;
2398
+ doc.addEventListener( 'mousemove', innerMouseMoveAlpha );
2399
+ doc.addEventListener( 'mouseup', innerMouseUpAlpha );
2400
+ document.body.classList.add( 'noevents' );
2401
+ e.stopImmediatePropagation();
2402
+ e.stopPropagation();
2403
+ const alphaX = clamp( e.offsetX - this.markerHalfSize, 0, this.alphaTracker.offsetWidth - this.markerSize );
2404
+ _fromAlphaX( alphaX );
2405
+ }
2406
+
2407
+ let innerMouseMoveAlpha = e => {
2408
+ let dX = e.movementX;
2409
+
2410
+ const rect = this.alphaTracker.getBoundingClientRect();
2411
+ const mouseX = e.offsetX - rect.x;
2412
+
2413
+ if ( dX != 0 && ( mouseX >= 0 || dX < 0 ) && ( mouseX < this.alphaTracker.offsetWidth || dX > 0 ) )
2414
+ {
2415
+ const alphaX = LX.clamp( parseInt( this.alphaMarker.style.left ) + dX, 0, this.alphaTracker.offsetWidth - this.markerSize );
2416
+ _fromAlphaX( alphaX );
2417
+ }
2418
+
2419
+ e.stopPropagation();
2420
+ e.preventDefault();
2421
+ }
2422
+
2423
+ let innerMouseUpAlpha = e => {
2424
+ var doc = this.root.ownerDocument;
2425
+ doc.removeEventListener( 'mousemove', innerMouseMoveAlpha );
2426
+ doc.removeEventListener( 'mouseup', innerMouseUpAlpha );
2427
+ document.body.classList.remove( 'noevents' );
2428
+ }
2429
+
2430
+ this.alphaTracker.addEventListener( "mousedown", innerMouseDownAlpha );
2431
+ }
2432
+
2433
+ // Info display
2434
+ const colorLabel = LX.makeContainer( ["100%", "auto"], "flex flex-row gap-1", "", this.root );
2435
+
2436
+ colorLabel.appendChild( new Select( null, [ "CSS", "Hex", "HSV", "RGB" ], this.colorModel, v => {
2437
+ this.colorModel = v;
2438
+ this._updateColorValue( null, true );
2439
+ } ).root );
2440
+
2441
+ this.labelWidget = new TextInput( null, "", null, { inputClass: "bg-none", fit: true, disabled: true } );
2442
+ colorLabel.appendChild( this.labelWidget.root );
2443
+
2444
+ colorLabel.appendChild( new Button(null, "eyedrop", async () => {
2445
+ navigator.clipboard.writeText( this.labelWidget.value() );
2446
+ }, { icon: "copy", buttonClass: "bg-none", className: "ml-auto", title: "Copy" }).root );
2447
+
2448
+ this._updateColorValue( hexValue, true );
2449
+
2450
+ doAsync( () => {
2451
+ this._adjustPosition();
2452
+
2453
+ this.root.focus();
2454
+
2455
+ this._onClick = e => {
2456
+ if( e.target && ( this.root.contains( e.target ) || e.target == this._trigger ) )
2457
+ {
2458
+ return;
2459
+ }
2460
+ this.destroy();
2461
+ };
2462
+
2463
+ document.body.addEventListener( "mousedown", this._onClick, true );
2464
+ document.body.addEventListener( "focusin", this._onClick, true );
2465
+ }, 10 );
2466
+ }
2467
+
2468
+ fromHexColor( hexColor ) {
2469
+
2470
+ this.currentColor.setHex( hexColor );
2471
+
2472
+ // Decompose into HSV
2473
+ const { h, s, v } = this.currentColor.hsv;
2474
+ this._svToPosition( s, v );
2475
+
2476
+ const hueColor = new Color( { h, s: 1, v: 1 } );
2477
+ this.hueMarker.style.backgroundColor = this.colorPickerBackground.style.backgroundColor = `rgb(${ hueColor.css.r }, ${ hueColor.css.g }, ${ hueColor.css.b })`;
2478
+ this.hueMarker.style.left = LX.remapRange( h, 0, 360, -this.markerHalfSize, this.colorPickerTracker.offsetWidth - this.markerHalfSize ) + "px";
2479
+
2480
+ this._updateColorValue( hexColor );
2481
+ }
2482
+
2483
+ destroy() {
2484
+
2485
+ this._trigger.classList.remove( "triggered" );
2486
+
2487
+ delete this._trigger.picker;
2488
+
2489
+ document.body.removeEventListener( "mousedown", this._onClick, true );
2490
+ document.body.removeEventListener( "focusin", this._onClick, true );
2491
+
2492
+ LX.root.querySelectorAll( ".lexcolorpicker" ).forEach( m => { m.remove(); } );
2493
+
2494
+ ColorPicker.currentPicker = null;
2495
+ }
2496
+
2497
+ _svToPosition( s, v ) {
2498
+ this.intSatMarker.style.left = `${ LX.remapRange( s, 0, 1, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize ) }px`;
2499
+ this.intSatMarker.style.top = `${ LX.remapRange( 1 - v, 0, 1, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize ) }px`
2500
+ };
2501
+
2502
+ _positionToSv( left, top ) {
2503
+ this.currentColor.hsv.s = LX.remapRange( left, -this.markerHalfSize, this.colorPickerBackground.offsetWidth - this.markerHalfSize, 0, 1 );
2504
+ this.currentColor.hsv.v = 1 - LX.remapRange( top, -this.markerHalfSize, this.colorPickerBackground.offsetHeight - this.markerHalfSize, 0, 1 );
2505
+ };
2506
+
2507
+ _updateColorValue( newHexValue, skipCallback = false ) {
1846
2508
 
1847
- menuItem.addEventListener("mouseover", e => {
2509
+ this.currentColor.set( newHexValue ?? this.currentColor.hsv );
1848
2510
 
1849
- let path = menuItem.id;
1850
- let p = parentDom;
2511
+ if( this.callback && !skipCallback )
2512
+ {
2513
+ this.callback( this.currentColor );
2514
+ }
1851
2515
 
1852
- while( p )
1853
- {
1854
- path += "/" + p.id;
1855
- p = p.currentParent?.parentElement;
1856
- }
2516
+ this.intSatMarker.style.backgroundColor = this.currentColor.hex;
1857
2517
 
1858
- LX.root.querySelectorAll( ".lexdropdownmenu" ).forEach( m => {
1859
- if( !path.includes( m.id ) )
1860
- {
1861
- m.currentParent.built = false;
1862
- m.remove();
1863
- }
1864
- } );
2518
+ if( this.useAlpha )
2519
+ {
2520
+ this.alphaTracker.style.color = `rgb(${ this.currentColor.css.r }, ${ this.currentColor.css.g }, ${ this.currentColor.css.b },${ this.currentColor.css.a })`;
2521
+ }
1865
2522
 
1866
- if( item.submenu )
1867
- {
1868
- if( menuItem.built )
1869
- {
1870
- return;
1871
- }
1872
- menuItem.built = true;
1873
- this._create( item.submenu, menuItem );
1874
- }
2523
+ const toFixed = ( s, n = 2) => { return s.toFixed( n ).replace( /([0-9]+(\.[0-9]+[1-9])?)(\.?0+$)/, '$1' ) };
1875
2524
 
1876
- e.stopPropagation();
1877
- });
2525
+ if( this.colorModel == "CSS" )
2526
+ {
2527
+ const { r, g, b, a } = this.currentColor.css;
2528
+ this.labelWidget.set( `rgba(${ r },${ g },${ b }${ this.useAlpha ? ',' + toFixed( a ) : '' })` );
1878
2529
  }
1879
- }
2530
+ else if( this.colorModel == "Hex" )
2531
+ {
2532
+ this.labelWidget.set( ( this.useAlpha ? this.currentColor.hex : this.currentColor.hex.substr( 0, 7 ) ).toUpperCase() );
2533
+ }
2534
+ else if( this.colorModel == "HSV" )
2535
+ {
2536
+ const { h, s, v, a } = this.currentColor.hsv;
2537
+ const components = [ Math.floor( h ) + 'º', Math.floor( s * 100 ) + '%', Math.floor( v * 100 ) + '%' ];
2538
+ if( this.useAlpha ) components.push( toFixed( a ) );
2539
+ this.labelWidget.set( components.join( ' ' ) );
2540
+ }
2541
+ else // RGB
2542
+ {
2543
+ const { r, g, b, a } = this.currentColor.rgb;
2544
+ const components = [ toFixed( r ), toFixed( g ), toFixed( b ) ];
2545
+ if( this.useAlpha ) components.push( toFixed( a ) );
2546
+ this.labelWidget.set( components.join( ' ' ) );
2547
+ }
2548
+ };
1880
2549
 
1881
2550
  _adjustPosition() {
1882
2551
 
@@ -1938,16 +2607,9 @@ class DropdownMenu {
1938
2607
  this.root.style.left = `${ position[ 0 ] }px`;
1939
2608
  this.root.style.top = `${ position[ 1 ] }px`;
1940
2609
  }
1941
-
1942
- _addSeparator( parent ) {
1943
- const separator = document.createElement('div');
1944
- separator.className = "separator";
1945
- parent = parent ?? this.root;
1946
- parent.appendChild( separator );
1947
- }
1948
2610
  };
1949
2611
 
1950
- LX.DropdownMenu = DropdownMenu;
2612
+ LX.ColorPicker = ColorPicker;
1951
2613
 
1952
2614
  class Area {
1953
2615
 
@@ -3810,57 +4472,14 @@ class Menubar {
3810
4472
 
3811
4473
  for( let i = 0; i < buttons.length; ++i )
3812
4474
  {
3813
- let data = buttons[ i ];
3814
- let button = document.createElement( "label" );
4475
+ const data = buttons[ i ];
3815
4476
  const title = data.title;
3816
- let disabled = data.disabled ?? false;
3817
- button.className = "lexmenubutton" + (disabled ? " disabled" : "");
3818
- button.title = title ?? "";
3819
- this.buttonContainer.appendChild( button );
3820
-
3821
- const icon = document.createElement( "a" );
3822
- icon.className = data.icon + " lexicon";
3823
- button.appendChild( icon );
3824
-
3825
- let trigger = icon;
3826
-
3827
- if( data.swap )
3828
- {
3829
- button.classList.add( "swap" );
3830
- icon.classList.add( "swap-off" );
3831
-
3832
- const input = document.createElement( "input" );
3833
- input.type = "checkbox";
3834
- button.prepend( input );
3835
- trigger = input;
3836
-
3837
- const swapIcon = document.createElement( "a" );
3838
- swapIcon.className = data.swap + " swap-on lexicon";
3839
- button.appendChild( swapIcon );
3840
-
3841
- button.swap = function() {
3842
- const swapInput = this.querySelector( "input" );
3843
- swapInput.checked = !swapInput.checked;
3844
- };
3845
-
3846
- // Set if swap has to be performed
3847
- button.setState = function( v ) {
3848
- const swapInput = this.querySelector( "input" );
3849
- swapInput.checked = v;
3850
- };
3851
- }
3852
-
3853
- trigger.addEventListener("click", e => {
3854
- if( data.callback && !disabled )
3855
- {
3856
- const swapInput = button.querySelector( "input" );
3857
- data.callback.call( this, e, swapInput?.checked );
3858
- }
3859
- });
4477
+ const button = new Button( title, "", data.callback, { title, buttonClass: "bg-none", disabled: data.disabled, icon: data.icon, hideName: true, swap: data.swap } )
4478
+ this.buttonContainer.appendChild( button.root );
3860
4479
 
3861
4480
  if( title )
3862
4481
  {
3863
- this.buttons[ title ] = button;
4482
+ this.buttons[ title ] = button.root;
3864
4483
  }
3865
4484
  }
3866
4485
  }
@@ -4563,14 +5182,15 @@ class Widget {
4563
5182
  static SEPARATOR = 26;
4564
5183
  static KNOB = 27;
4565
5184
  static SIZE = 28;
4566
- static PAD = 29;
4567
- static FORM = 30;
4568
- static DIAL = 31;
4569
- static COUNTER = 32;
4570
- static TABLE = 33;
4571
- static TABS = 34;
4572
- static LABEL = 35;
4573
- static BLANK = 36;
5185
+ static OTP = 29;
5186
+ static PAD = 30;
5187
+ static FORM = 31;
5188
+ static DIAL = 32;
5189
+ static COUNTER = 33;
5190
+ static TABLE = 34;
5191
+ static TABS = 35;
5192
+ static LABEL = 36;
5193
+ static BLANK = 37;
4574
5194
 
4575
5195
  static NO_CONTEXT_TYPES = [
4576
5196
  Widget.BUTTON,
@@ -5815,40 +6435,17 @@ class Button extends Widget {
5815
6435
  super( Widget.BUTTON, name, null, options );
5816
6436
 
5817
6437
  this.onGetValue = () => {
5818
- return wValue.innerText;
6438
+ return wValue.querySelector( "input" )?.checked;
5819
6439
  };
5820
6440
 
5821
6441
  this.onSetValue = ( newValue, skipCallback, event ) => {
5822
6442
 
5823
- wValue.innerHTML = "";
5824
-
5825
- if( options.icon )
5826
- {
5827
- let icon = null;
5828
-
5829
- // @legacy
5830
- if( options.icon.includes( "fa-" ) )
5831
- {
5832
- icon = document.createElement( 'a' );
5833
- icon.className = options.icon;
5834
- }
5835
- else
5836
- {
5837
- icon = LX.makeIcon( options.icon );
5838
- }
5839
-
5840
- wValue.prepend( icon );
5841
- }
5842
- else if( options.img )
6443
+ if( !( options.swap ?? false ) )
5843
6444
  {
5844
- let img = document.createElement( 'img' );
5845
- img.src = options.img;
5846
- wValue.prepend( img );
5847
- }
5848
- else
5849
- {
5850
- wValue.innerHTML = `<span>${ ( newValue || "" ) }</span>`;
6445
+ return;
5851
6446
  }
6447
+
6448
+ this.root.setState( newValue, skipCallback );
5852
6449
  };
5853
6450
 
5854
6451
  this.onResize = ( rect ) => {
@@ -5872,25 +6469,88 @@ class Button extends Widget {
5872
6469
  wValue.classList.add( "selected" );
5873
6470
  }
5874
6471
 
5875
- this.onSetValue( value, true );
6472
+ if( options.icon )
6473
+ {
6474
+ let icon = null;
6475
+
6476
+ // @legacy
6477
+ if( options.icon.includes( "fa-" ) )
6478
+ {
6479
+ icon = document.createElement( 'a' );
6480
+ icon.className = options.icon + " lexicon";
6481
+ }
6482
+ else
6483
+ {
6484
+ icon = LX.makeIcon( options.icon );
6485
+ }
6486
+
6487
+ wValue.prepend( icon );
6488
+ }
6489
+ else if( options.img )
6490
+ {
6491
+ let img = document.createElement( 'img' );
6492
+ img.src = options.img;
6493
+ wValue.prepend( img );
6494
+ }
6495
+ else
6496
+ {
6497
+ wValue.innerHTML = `<span>${ ( value || "" ) }</span>`;
6498
+ }
5876
6499
 
5877
6500
  if( options.disabled )
5878
6501
  {
5879
6502
  wValue.setAttribute( "disabled", true );
5880
6503
  }
5881
6504
 
5882
- wValue.addEventListener( "click", e => {
6505
+ let trigger = wValue;
6506
+
6507
+ if( options.swap )
6508
+ {
6509
+ wValue.classList.add( "swap" );
6510
+ wValue.querySelector( "a" ).classList.add( "swap-off" );
6511
+
6512
+ const input = document.createElement( "input" );
6513
+ input.type = "checkbox";
6514
+ wValue.prepend( input );
6515
+ // trigger = input;
6516
+
6517
+ const swapIcon = document.createElement( "a" );
6518
+ swapIcon.className = options.swap + " swap-on lexicon";
6519
+ wValue.appendChild( swapIcon );
6520
+
6521
+ this.root.swap = function( skipCallback ) {
6522
+ const swapInput = wValue.querySelector( "input" );
6523
+ swapInput.checked = !swapInput.checked;
6524
+ if( !skipCallback )
6525
+ {
6526
+ trigger.click();
6527
+ }
6528
+ };
6529
+
6530
+ // Set if swap has to be performed
6531
+ this.root.setState = function( v, skipCallback ) {
6532
+ const swapInput = wValue.querySelector( "input" );
6533
+ swapInput.checked = v;
6534
+ if( !skipCallback )
6535
+ {
6536
+ trigger.click();
6537
+ }
6538
+ };
6539
+ }
6540
+
6541
+ trigger.addEventListener( "click", e => {
5883
6542
  if( options.selectable )
5884
6543
  {
5885
6544
  if( options.parent )
5886
6545
  {
5887
- options.parent.querySelectorAll(".lexbutton.selected").forEach( e => { if( e == wValue ) return; e.classList.remove( "selected" ) } );
6546
+ options.parent.querySelectorAll(".lexbutton.selected").forEach( b => { if( b == wValue ) return; b.classList.remove( "selected" ) } );
5888
6547
  }
5889
6548
 
5890
6549
  wValue.classList.toggle('selected');
5891
6550
  }
5892
6551
 
5893
- this._trigger( new IEvent( name, value, e ), callback );
6552
+ const swapInput = wValue.querySelector( "input" );
6553
+ this._trigger( new IEvent( name, swapInput?.checked ?? value, e ), callback );
5894
6554
  });
5895
6555
 
5896
6556
  if( options.tooltip )
@@ -6285,7 +6945,7 @@ class Select extends Widget {
6285
6945
 
6286
6946
  const selectRoot = selectedOption.root;
6287
6947
  const rect = selectRoot.getBoundingClientRect();
6288
- const nestedDialog = parent.parentElement.closest( "dialog" );
6948
+ const nestedDialog = parent.parentElement.closest( "dialog" ) ?? parent.parentElement.closest( ".lexcolorpicker" );
6289
6949
 
6290
6950
  // Manage vertical aspect
6291
6951
  {
@@ -7335,31 +7995,52 @@ class ColorInput extends Widget {
7335
7995
 
7336
7996
  constructor( name, value, callback, options = {} ) {
7337
7997
 
7338
- value = ( value.constructor === Array ) ? rgbToHex( value ) : value;
7998
+ const useAlpha = options.useAlpha ??
7999
+ ( ( value.constructor === Object && 'a' in value ) || ( value.constructor === String && [ 5, 9 ].includes( value.length ) ) );
8000
+
8001
+ const widgetColor = new Color( value );
8002
+
8003
+ // Force always hex internally
8004
+ value = useAlpha ? widgetColor.hex : widgetColor.hex.substr( 0, 7 );
7339
8005
 
7340
8006
  super( Widget.COLOR, name, value, options );
7341
8007
 
7342
8008
  this.onGetValue = () => {
7343
- return value;
8009
+ const currentColor = new Color( value );
8010
+ return options.useRGB ? currentColor.rgb : value;
7344
8011
  };
7345
8012
 
7346
8013
  this.onSetValue = ( newValue, skipCallback, event ) => {
7347
8014
 
7348
- if( color.useRGB )
8015
+ const newColor = new Color( newValue );
8016
+
8017
+ colorSampleRGB.style.color = value = newColor.hex.substr( 0, 7 );
8018
+
8019
+ if( useAlpha )
7349
8020
  {
7350
- newValue = hexToRgb( newValue );
8021
+ colorSampleAlpha.style.color = value = newColor.hex;
7351
8022
  }
7352
8023
 
7353
8024
  if( !this._skipTextUpdate )
7354
8025
  {
7355
- textWidget.set( newValue, true, event );
8026
+ textWidget.set( value, true, event );
7356
8027
  }
7357
8028
 
7358
- color.value = value = newValue;
7359
-
7360
8029
  if( !skipCallback )
7361
8030
  {
7362
- this._trigger( new IEvent( name, newValue, event ), callback );
8031
+ let retValue = value;
8032
+
8033
+ if( options.useRGB )
8034
+ {
8035
+ retValue = newColor.rgb;
8036
+
8037
+ if( !useAlpha )
8038
+ {
8039
+ delete retValue.a;
8040
+ }
8041
+ }
8042
+
8043
+ this._trigger( new IEvent( name, retValue, event ), callback );
7363
8044
  }
7364
8045
  };
7365
8046
 
@@ -7372,30 +8053,50 @@ class ColorInput extends Widget {
7372
8053
  container.className = "lexcolor";
7373
8054
  this.root.appendChild( container );
7374
8055
 
7375
- let color = document.createElement( 'input' );
7376
- color.style.width = "32px";
7377
- color.type = 'color';
7378
- color.className = "colorinput";
7379
- color.useRGB = options.useRGB ?? false;
7380
- color.value = value;
7381
- container.appendChild( color );
8056
+ let sampleContainer = LX.makeContainer( ["18px", "18px"], "flex flex-row bg-contrast rounded overflow-hidden", "", container );
8057
+ sampleContainer.tabIndex = "1";
8058
+ sampleContainer.addEventListener( "click", e => {
8059
+ if( ( options.disabled ?? false ) )
8060
+ {
8061
+ return;
8062
+ }
8063
+ new ColorPicker( value, sampleContainer, {
8064
+ colorModel: options.useRGB ? "RGB" : "Hex",
8065
+ useAlpha,
8066
+ onChange: ( color ) => {
8067
+ this._fromColorPicker = true;
8068
+ this.set( color.hex );
8069
+ delete this._fromColorPicker;
8070
+ }
8071
+ } );
8072
+ } );
8073
+
8074
+ let colorSampleRGB = document.createElement( 'div' );
8075
+ colorSampleRGB.className = "lexcolorsample";
8076
+ colorSampleRGB.style.color = value;
8077
+ sampleContainer.appendChild( colorSampleRGB );
7382
8078
 
7383
- if( options.disabled )
8079
+ let colorSampleAlpha = null;
8080
+
8081
+ if( useAlpha )
7384
8082
  {
7385
- color.disabled = true;
8083
+ colorSampleAlpha = document.createElement( 'div' );
8084
+ colorSampleAlpha.className = "lexcolorsample";
8085
+ colorSampleAlpha.style.color = value;
8086
+ sampleContainer.appendChild( colorSampleAlpha );
8087
+ }
8088
+ else
8089
+ {
8090
+ colorSampleRGB.style.width = "18px";
7386
8091
  }
7387
8092
 
7388
- color.addEventListener( "input", e => {
7389
- this.set( e.target.value, false, e );
7390
- }, false );
7391
-
7392
- const textWidget = new TextInput( null, color.value, v => {
8093
+ const textWidget = new TextInput( null, value, v => {
7393
8094
  this._skipTextUpdate = true;
7394
8095
  this.set( v );
7395
8096
  delete this._skipTextUpdate;
7396
- }, { width: "calc( 100% - 32px )", disabled: options.disabled });
8097
+ }, { width: "calc( 100% - 24px )", disabled: options.disabled });
7397
8098
 
7398
- textWidget.root.style.marginLeft = "4px";
8099
+ textWidget.root.style.marginLeft = "6px";
7399
8100
  container.appendChild( textWidget.root );
7400
8101
 
7401
8102
  doAsync( this.onResize.bind( this ) );
@@ -8103,6 +8804,164 @@ class SizeInput extends Widget {
8103
8804
 
8104
8805
  LX.SizeInput = SizeInput;
8105
8806
 
8807
+ /**
8808
+ * @class OTPInput
8809
+ * @description OTPInput Widget
8810
+ */
8811
+
8812
+ class OTPInput extends Widget {
8813
+
8814
+ constructor( name, value, callback, options = {} ) {
8815
+
8816
+ const pattern = options.pattern ?? "xxx-xxx";
8817
+ const patternSize = ( pattern.match(/x/g) || [] ).length;
8818
+
8819
+ value = String( value );
8820
+ if( !value.length )
8821
+ {
8822
+ value = "x".repeat( patternSize );
8823
+ }
8824
+
8825
+ super( Widget.OTP, name, value, options );
8826
+
8827
+ this.onGetValue = () => {
8828
+ return +value;
8829
+ };
8830
+
8831
+ this.onSetValue = ( newValue, skipCallback, event ) => {
8832
+
8833
+ value = newValue;
8834
+
8835
+ _refreshInput( value );
8836
+
8837
+ if( !skipCallback )
8838
+ {
8839
+ this._trigger( new IEvent( name, +newValue, event ), callback );
8840
+ }
8841
+ };
8842
+
8843
+ this.onResize = ( rect ) => {
8844
+ const realNameWidth = ( this.root.domName?.offsetWidth ?? 0 );
8845
+ container.style.width = `calc( 100% - ${ realNameWidth }px)`;
8846
+ };
8847
+
8848
+ this.disabled = options.disabled ?? false;
8849
+
8850
+ const container = document.createElement( 'div' );
8851
+ container.className = "lexotp flex flex-row items-center";
8852
+ this.root.appendChild( container );
8853
+
8854
+ const groups = pattern.split( '-' );
8855
+
8856
+ const _refreshInput = ( valueString ) => {
8857
+
8858
+ container.innerHTML = "";
8859
+
8860
+ let itemsCount = 0;
8861
+ let activeSlot = 0;
8862
+
8863
+ for( let i = 0; i < groups.length; ++i )
8864
+ {
8865
+ const g = groups[ i ];
8866
+
8867
+ for( let j = 0; j < g.length; ++j )
8868
+ {
8869
+ let number = valueString[ itemsCount++ ];
8870
+ number = ( number == 'x' ? '' : number );
8871
+
8872
+ const slotDom = LX.makeContainer( ["36px", "30px"],
8873
+ "lexotpslot border-top border-bottom border-left px-3 cursor-text select-none font-medium outline-none", number, container );
8874
+ slotDom.tabIndex = "1";
8875
+
8876
+ if( this.disabled )
8877
+ {
8878
+ slotDom.classList.add( "disabled" );
8879
+ }
8880
+
8881
+ const otpIndex = itemsCount;
8882
+
8883
+ if( j == 0 )
8884
+ {
8885
+ slotDom.className += " rounded-l";
8886
+ }
8887
+ else if( j == ( g.length - 1 ) )
8888
+ {
8889
+ slotDom.className += " rounded-r border-right";
8890
+ }
8891
+
8892
+ slotDom.addEventListener( "click", () => {
8893
+ if( this.disabled ) { return; }
8894
+ container.querySelectorAll( ".lexotpslot" ).forEach( s => s.classList.remove( "active" ) );
8895
+ const activeDom = container.querySelectorAll( ".lexotpslot" )[ activeSlot ];
8896
+ activeDom.classList.add( "active" );
8897
+ activeDom.focus();
8898
+ } );
8899
+
8900
+ slotDom.addEventListener( "blur", () => {
8901
+ if( this.disabled ) { return; }
8902
+ doAsync( () => {
8903
+ if( container.contains( document.activeElement ) ) { return; }
8904
+ container.querySelectorAll( ".lexotpslot" ).forEach( s => s.classList.remove( "active" ) );
8905
+ }, 10 );
8906
+ } );
8907
+
8908
+ slotDom.addEventListener( "keyup", e => {
8909
+ if( this.disabled ) { return; }
8910
+ if( !/[^0-9]+/g.test( e.key ) )
8911
+ {
8912
+ const number = e.key;
8913
+ console.assert( parseInt( number ) != NaN );
8914
+
8915
+ slotDom.innerHTML = number;
8916
+ valueString = valueString.substring( 0, otpIndex - 1 ) + number + valueString.substring( otpIndex );
8917
+
8918
+ const nexActiveDom = container.querySelectorAll( ".lexotpslot" )[ activeSlot + 1 ];
8919
+ if( nexActiveDom )
8920
+ {
8921
+ container.querySelectorAll( ".lexotpslot" )[ activeSlot ].classList.remove( "active" );
8922
+ nexActiveDom.classList.add( "active" );
8923
+ nexActiveDom.focus();
8924
+ activeSlot++;
8925
+ }
8926
+ else
8927
+ {
8928
+ this.set( valueString );
8929
+ }
8930
+ }
8931
+ else if( e.key == "ArrowLeft" || e.key == "ArrowRight" )
8932
+ {
8933
+ const dt = ( e.key == "ArrowLeft" ) ? -1 : 1;
8934
+ const newActiveDom = container.querySelectorAll( ".lexotpslot" )[ activeSlot + dt ];
8935
+ if( newActiveDom )
8936
+ {
8937
+ container.querySelectorAll( ".lexotpslot" )[ activeSlot ].classList.remove( "active" );
8938
+ newActiveDom.classList.add( "active" );
8939
+ newActiveDom.focus();
8940
+ activeSlot += dt;
8941
+ }
8942
+ }
8943
+ else if( e.key == "Enter" && !valueString.includes( 'x' ) )
8944
+ {
8945
+ this.set( valueString );
8946
+ }
8947
+ } );
8948
+ }
8949
+
8950
+ if( i < ( groups.length - 1 ) )
8951
+ {
8952
+ LX.makeContainer( ["auto", "auto"], "mx-2", `-`, container );
8953
+ }
8954
+ }
8955
+
8956
+ console.assert( itemsCount == valueString.length, "OTP Value/Pattern Mismatch!" )
8957
+ }
8958
+
8959
+ _refreshInput( value );
8960
+ }
8961
+ }
8962
+
8963
+ LX.OTPInput = OTPInput;
8964
+
8106
8965
  /**
8107
8966
  * @class Pad
8108
8967
  * @description Pad Widget
@@ -8870,7 +9729,7 @@ class Table extends Widget {
8870
9729
  for( const el of body.childNodes )
8871
9730
  {
8872
9731
  data.checkMap[ el.getAttribute( "rowId" ) ] = this.checked;
8873
- el.querySelector( "input" ).checked = this.checked;
9732
+ el.querySelector( "input[type='checkbox']" ).checked = this.checked;
8874
9733
  }
8875
9734
  });
8876
9735
 
@@ -9084,10 +9943,20 @@ class Table extends Widget {
9084
9943
  input.addEventListener( 'change', function() {
9085
9944
  data.checkMap[ rowId ] = this.checked;
9086
9945
 
9946
+ const headInput = table.querySelector( "thead input[type='checkbox']" );
9947
+
9087
9948
  if( !this.checked )
9088
9949
  {
9089
- const input = table.querySelector( "thead input[type='checkbox']" );
9090
- input.checked = data.checkMap[ ":root" ] = false;
9950
+ headInput.checked = data.checkMap[ ":root" ] = false;
9951
+ }
9952
+ else
9953
+ {
9954
+ const rowInputs = Array.from( table.querySelectorAll( "tbody input[type='checkbox']" ) );
9955
+ const uncheckedRowInputs = rowInputs.filter( i => { return !i.checked; } );
9956
+ if( !uncheckedRowInputs.length )
9957
+ {
9958
+ headInput.checked = data.checkMap[ ":root" ] = true;
9959
+ }
9091
9960
  }
9092
9961
  });
9093
9962
 
@@ -10198,6 +11067,22 @@ class Panel {
10198
11067
  return this._attachWidget( widget );
10199
11068
  }
10200
11069
 
11070
+ /**
11071
+ * @method addOTP
11072
+ * @param {String} name Widget name
11073
+ * @param {String} value Default numeric value in string format
11074
+ * @param {Function} callback Callback function on change
11075
+ * @param {Object} options:
11076
+ * hideName: Don't use name as label [false]
11077
+ * disabled: Make the widget disabled [false]
11078
+ * pattern: OTP numeric pattern
11079
+ */
11080
+
11081
+ addOTP( name, value, callback, options = {} ) {
11082
+ const widget = new OTPInput( name, value, callback, options );
11083
+ return this._attachWidget( widget );
11084
+ }
11085
+
10201
11086
  /**
10202
11087
  * @method addPad
10203
11088
  * @param {String} name Widget name
@@ -10430,7 +11315,7 @@ class Branch {
10430
11315
  // add widgets
10431
11316
  for( let w of this.widgets )
10432
11317
  {
10433
- p.root.appendChild( w.domEl );
11318
+ p.root.appendChild( w.root );
10434
11319
  }
10435
11320
  });
10436
11321
  dialog.widgets = this.widgets;
@@ -10734,7 +11619,7 @@ class Dialog {
10734
11619
 
10735
11620
  for( let w of that.widgets )
10736
11621
  {
10737
- branch.content.appendChild( w.domEl );
11622
+ branch.content.appendChild( w.root );
10738
11623
  }
10739
11624
 
10740
11625
  branch.widgets = that.widgets;
@@ -12375,7 +13260,7 @@ class AssetView {
12375
13260
 
12376
13261
  this.rightPanel.addText(null, this.path.join('/'), null, {
12377
13262
  inputClass: "nobg", disabled: true, signal: "@on_folder_change",
12378
- style: { fontWeight: "600", fontSize: "15px" }
13263
+ style: { fontWeight: "600", fontSize: "15px" }
12379
13264
  });
12380
13265
 
12381
13266
  this.rightPanel.endLine();
@@ -13112,7 +13997,7 @@ Element.prototype.addClass = function( className ) {
13112
13997
  }
13113
13998
 
13114
13999
  Element.prototype.getComputedSize = function() {
13115
- // Since we use "box-sizing: border-box" now,
14000
+ // Since we use "box-sizing: border-box" now,
13116
14001
  // it's all included in offsetWidth/offsetHeight
13117
14002
  return {
13118
14003
  width: this.offsetWidth,
@@ -13254,6 +14139,7 @@ LX.ICONS = {
13254
14139
  "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"],
13255
14140
  "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"],
13256
14141
  "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"],
14142
+ "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"],
13257
14143
  "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"],
13258
14144
  "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"],
13259
14145
  "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"],