lexgui 0.1.44 → 0.1.46

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.
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  var LX = {
11
- version: "0.1.44",
11
+ version: "0.1.46",
12
12
  ready: false,
13
13
  components: [], // specific pre-build components
14
14
  signals: {} // events and triggers
@@ -32,34 +32,118 @@ LX.clamp = clamp;
32
32
  LX.round = round;
33
33
  LX.remapRange = remapRange;
34
34
 
35
- function getSupportedDOMName( string )
35
+ // Timer that works everywhere (from litegraph.js)
36
+ if ( typeof performance != "undefined" )
37
+ {
38
+ LX.getTime = performance.now.bind( performance );
39
+ }
40
+ else if( typeof Date != "undefined" && Date.now )
41
+ {
42
+ LX.getTime = Date.now.bind( Date );
43
+ }
44
+ else if ( typeof process != "undefined" )
45
+ {
46
+ LX.getTime = function() {
47
+ var t = process.hrtime();
48
+ return t[ 0 ] * 0.001 + t[ 1 ] * 1e-6;
49
+ };
50
+ }
51
+ else
52
+ {
53
+ LX.getTime = function() {
54
+ return new Date().getTime();
55
+ };
56
+ }
57
+
58
+ let ASYNC_ENABLED = true;
59
+
60
+ /**
61
+ * @method doAsync
62
+ * @description Call a function asynchronously
63
+ * @param {Function} fn Function to call
64
+ * @param {Number} ms Time to wait until calling the function (in milliseconds)
65
+ */
66
+ function doAsync( fn, ms ) {
67
+ if( ASYNC_ENABLED )
68
+ {
69
+ setTimeout( fn, ms ?? 0 );
70
+ }
71
+ else
72
+ {
73
+ fn();
74
+ }
75
+ }
76
+
77
+ LX.doAsync = doAsync;
78
+
79
+ /**
80
+ * @method getSupportedDOMName
81
+ * @description Convert a text string to a valid DOM name
82
+ * @param {String} text Original text
83
+ */
84
+ function getSupportedDOMName( text )
36
85
  {
37
- return string.replace(/\s/g, '').replaceAll('@', '_').replaceAll('+', '_plus_').replaceAll('.', '');
86
+ return text.replace(/\s/g, '').replaceAll('@', '_').replaceAll('+', '_plus_').replaceAll('.', '');
38
87
  }
39
88
 
40
89
  LX.getSupportedDOMName = getSupportedDOMName;
41
90
 
42
- function has( component_name )
91
+ /**
92
+ * @method has
93
+ * @description Ask if LexGUI is using a specific component
94
+ * @param {String} componentName Name of the LexGUI component
95
+ */
96
+ function has( componentName )
43
97
  {
44
- return (LX.components.indexOf( component_name ) > -1);
98
+ return ( LX.components.indexOf( componentName ) > -1 );
45
99
  }
46
100
 
47
101
  LX.has = has;
48
102
 
49
- function getExtension( s )
103
+ /**
104
+ * @method getExtension
105
+ * @description Get a extension from a path/url/filename
106
+ * @param {String} name
107
+ */
108
+ function getExtension( name )
50
109
  {
51
- return s.includes('.') ? s.split('.').pop() : null;
110
+ return name.includes('.') ? name.split('.').pop() : null;
52
111
  }
53
112
 
54
113
  LX.getExtension = getExtension;
55
114
 
56
- function deepCopy( o )
115
+ /**
116
+ * @method deepCopy
117
+ * @description Create a deep copy with no references from an object
118
+ * @param {Object} obj
119
+ */
120
+ function deepCopy( obj )
57
121
  {
58
- return JSON.parse(JSON.stringify(o))
122
+ return JSON.parse( JSON.stringify( obj ) )
59
123
  }
60
124
 
61
125
  LX.deepCopy = deepCopy;
62
126
 
127
+ /**
128
+ * @method setTheme
129
+ * @description Set dark or light theme
130
+ * @param {String} colorScheme Name of the scheme
131
+ */
132
+ function setTheme( colorScheme )
133
+ {
134
+ colorScheme = ( colorScheme == "light" ) ? "light" : "dark";
135
+ document.documentElement.setAttribute( "data-theme", colorScheme );
136
+ LX.emit( "@on_new_color_scheme", colorScheme );
137
+ }
138
+
139
+ LX.setTheme = setTheme;
140
+
141
+ /**
142
+ * @method setThemeColor
143
+ * @description Sets a new value for one of the main theme variables
144
+ * @param {String} colorName Name of the theme variable
145
+ * @param {String} color Color in rgba/hex
146
+ */
63
147
  function setThemeColor( colorName, color )
64
148
  {
65
149
  var r = document.querySelector( ':root' );
@@ -68,6 +152,11 @@ function setThemeColor( colorName, color )
68
152
 
69
153
  LX.setThemeColor = setThemeColor;
70
154
 
155
+ /**
156
+ * @method getThemeColor
157
+ * @description Get the value for one of the main theme variables
158
+ * @param {String} colorName Name of the theme variable
159
+ */
71
160
  function getThemeColor( colorName )
72
161
  {
73
162
  const r = getComputedStyle( document.querySelector( ':root' ) );
@@ -75,7 +164,9 @@ function getThemeColor( colorName )
75
164
 
76
165
  if( value.includes( "light-dark" ) && window.matchMedia )
77
166
  {
78
- if( window.matchMedia( "(prefers-color-scheme: light)" ).matches )
167
+ const currentScheme = r.getPropertyValue( "color-scheme" );
168
+
169
+ if( ( window.matchMedia( "(prefers-color-scheme: light)" ).matches ) || ( currentScheme == "light" ) )
79
170
  {
80
171
  return value.substring( value.indexOf( '(' ) + 1, value.indexOf( ',' ) ).replace( /\s/g, '' );
81
172
  }
@@ -90,7 +181,13 @@ function getThemeColor( colorName )
90
181
 
91
182
  LX.getThemeColor = getThemeColor;
92
183
 
93
- function getBase64Image( img ) {
184
+ /**
185
+ * @method getBase64Image
186
+ * @description Convert an image to a base64 string
187
+ * @param {Image} img
188
+ */
189
+ function getBase64Image( img )
190
+ {
94
191
  var canvas = document.createElement( 'canvas' );
95
192
  canvas.width = img.width;
96
193
  canvas.height = img.height;
@@ -101,7 +198,13 @@ function getBase64Image( img ) {
101
198
 
102
199
  LX.getBase64Image = getBase64Image;
103
200
 
104
- function hexToRgb( hexStr ) {
201
+ /**
202
+ * @method hexToRgb
203
+ * @description Convert a hexadecimal string to a valid RGB color array
204
+ * @param {String} hexStr Hexadecimal color
205
+ */
206
+ function hexToRgb( hexStr )
207
+ {
105
208
  const red = parseInt( hexStr.substring( 1, 3 ), 16 ) / 255;
106
209
  const green = parseInt( hexStr.substring( 3, 5 ), 16 ) / 255;
107
210
  const blue = parseInt( hexStr.substring( 5, 7 ), 16 ) / 255;
@@ -110,7 +213,13 @@ function hexToRgb( hexStr ) {
110
213
 
111
214
  LX.hexToRgb = hexToRgb;
112
215
 
113
- function rgbToHex( rgb ) {
216
+ /**
217
+ * @method rgbToHex
218
+ * @description Convert a RGB color array to a hexadecimal string
219
+ * @param {Array} rgb Array containing R, G, B, A*
220
+ */
221
+ function rgbToHex( rgb )
222
+ {
114
223
  let hex = "#";
115
224
  for( let c of rgb ) {
116
225
  c = Math.floor( c * 255 );
@@ -121,7 +230,14 @@ function rgbToHex( rgb ) {
121
230
 
122
231
  LX.rgbToHex = rgbToHex;
123
232
 
124
- function measureRealWidth( value, paddingPlusMargin = 8 ) {
233
+ /**
234
+ * @method measureRealWidth
235
+ * @description Measure the pixel width of a text
236
+ * @param {Object} value Text to measure
237
+ * @param {Number} paddingPlusMargin Padding offset
238
+ */
239
+ function measureRealWidth( value, paddingPlusMargin = 8 )
240
+ {
125
241
  var i = document.createElement( "span" );
126
242
  i.className = "lexinputmeasure";
127
243
  i.innerHTML = value;
@@ -133,7 +249,12 @@ function measureRealWidth( value, paddingPlusMargin = 8 ) {
133
249
 
134
250
  LX.measureRealWidth = measureRealWidth;
135
251
 
136
- function simple_guidGenerator() {
252
+ /**
253
+ * @method simple_guidGenerator
254
+ * @description Get a random unique id
255
+ */
256
+ function simple_guidGenerator()
257
+ {
137
258
  var S4 = function() {
138
259
  return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
139
260
  };
@@ -142,67 +263,40 @@ function simple_guidGenerator() {
142
263
 
143
264
  LX.guidGenerator = simple_guidGenerator;
144
265
 
145
- // Timer that works everywhere (from litegraph.js)
146
- if (typeof performance != "undefined") {
147
- LX.getTime = performance.now.bind(performance);
148
- } else if (typeof Date != "undefined" && Date.now) {
149
- LX.getTime = Date.now.bind(Date);
150
- } else if (typeof process != "undefined") {
151
- LX.getTime = function() {
152
- var t = process.hrtime();
153
- return t[0] * 0.001 + t[1] * 1e-6;
154
- };
155
- } else {
156
- LX.getTime = function getTime() {
157
- return new Date().getTime();
158
- };
159
- }
160
-
161
- let ASYNC_ENABLED = true;
162
-
163
- function doAsync( fn, ms ) {
164
- if( ASYNC_ENABLED )
165
- {
166
- setTimeout( fn, ms ?? 0 );
167
- }
168
- else
169
- {
170
- fn();
171
- }
266
+ /**
267
+ * @method buildTextPattern
268
+ * @description Create a validation pattern using specific options
269
+ * @param {Object} options
270
+ * lowercase (Boolean): Text must contain a lowercase char
271
+ * uppercase (Boolean): Text must contain an uppercase char
272
+ * digit (Boolean): Text must contain a digit
273
+ * specialChar (Boolean): Text must contain a special char
274
+ * noSpaces (Boolean): Do not allow spaces in text
275
+ * minLength (Number): Text minimum length
276
+ * maxLength (Number): Text maximum length
277
+ * asRegExp (Boolean): Return pattern as Regular Expression instance
278
+ */
279
+ function buildTextPattern( options = {} )
280
+ {
281
+ let patterns = [];
282
+ if ( options.lowercase ) patterns.push("(?=.*[a-z])");
283
+ if ( options.uppercase ) patterns.push("(?=.*[A-Z])");
284
+ if ( options.digit ) patterns.push("(?=.*\\d)");
285
+ if ( options.specialChar ) patterns.push("(?=.*[@#$%^&+=!])");
286
+ if ( options.noSpaces ) patterns.push("(?!.*\\s)");
287
+
288
+ let minLength = options.minLength || 0;
289
+ let maxLength = options.maxLength || ""; // Empty means no max length restriction
290
+
291
+ let pattern = `^${ patterns.join("") }.{${ minLength },${ maxLength }}$`;
292
+ return options.asRegExp ? new RegExp( pattern ) : pattern;
172
293
  }
173
294
 
174
- // Math classes
175
-
176
- class vec2 {
177
-
178
- constructor( x, y ) {
179
- this.x = x ?? 0;
180
- this.y = y ?? ( x ?? 0 );
181
- }
182
-
183
- get xy() { return [ this.x, this.y ]; }
184
- get yx() { return [ this.y, this.x ]; }
185
-
186
- set ( x, y ) { this.x = x; this.y = y; }
187
- add ( v, v0 = new vec2() ) { v0.set( this.x + v.x, this.y + v.y ); return v0; }
188
- sub ( v, v0 = new vec2() ) { v0.set( this.x - v.x, this.y - v.y ); return v0; }
189
- mul ( v, v0 = new vec2() ) { if( v.constructor == Number ) { v = new vec2( v ) } v0.set( this.x * v.x, this.y * v.y ); return v0; }
190
- div ( v, v0 = new vec2() ) { if( v.constructor == Number ) { v = new vec2( v ) } v0.set( this.x / v.x, this.y / v.y ); return v0; }
191
- abs ( v0 = new vec2() ) { v0.set( Math.abs( this.x ), Math.abs( this.y ) ); return v0; }
192
- dot ( v ) { return this.x * v.x + this.y * v.y; }
193
- len2 () { return this.dot( this ) }
194
- len () { return Math.sqrt( this.len2() ); }
195
- nrm ( v0 = new vec2() ) { v0.set( this.x, this.y ); return v0.mul( 1.0 / this.len(), v0 ); }
196
- dst ( v ) { return v.sub( this ).len(); }
197
- clp ( min, max, v0 = new vec2() ) { v0.set( clamp( this.x, min, max ), clamp( this.y, min, max ) ); return v0; }
198
- };
199
-
200
- LX.vec2 = vec2;
201
-
202
- // Other utils
295
+ LX.buildTextPattern = buildTextPattern;
203
296
 
204
297
  /**
205
298
  * @method makeDraggable
299
+ * @description Allow an element to be dragged
206
300
  * @param {Element} domEl
207
301
  * @param {Object} options
208
302
  * autoAdjust (Bool): Sets in a correct position at the beggining
@@ -210,8 +304,8 @@ LX.vec2 = vec2;
210
304
  * onMove (Function): Called each move event
211
305
  * onDragStart (Function): Called when drag event starts
212
306
  */
213
- function makeDraggable( domEl, options = { } ) {
214
-
307
+ function makeDraggable( domEl, options = { } )
308
+ {
215
309
  let offsetX = 0;
216
310
  let offsetY = 0;
217
311
  let currentTarget = null;
@@ -301,8 +395,131 @@ function makeDraggable( domEl, options = { } ) {
301
395
 
302
396
  LX.makeDraggable = makeDraggable;
303
397
 
304
- function create_global_searchbar( root ) {
398
+ /**
399
+ * @method makeCodeSnippet
400
+ * @description Create a code snippet in a specific language
401
+ * @param {String} code
402
+ * @param {Array} size
403
+ * @param {Object} options
404
+ * language (String):
405
+ * windowMode (Boolean):
406
+ * lineNumbers (Boolean):
407
+ * tabName (String):
408
+ */
409
+ function makeCodeSnippet( code, size, options = { } )
410
+ {
411
+ if( !LX.has('CodeEditor') )
412
+ {
413
+ console.error( "Import the CodeEditor component to create snippets!" );
414
+ return;
415
+ }
416
+
417
+ const snippet = document.createElement( "div" );
418
+ snippet.className = "lexcodesnippet";
419
+ snippet.style.width = size[ 0 ];
420
+ snippet.style.height = size[ 1 ];
421
+ const area = new Area( { noAppend: true } );
422
+ let editor = new LX.CodeEditor( area, {
423
+ skipInfo: true,
424
+ disableEdition: true,
425
+ allowAddScripts: false,
426
+ name: options.tabName,
427
+ // showTab: options.showTab ?? true,
428
+ // lineNumbers: options.lineNumbers ?? true
429
+ } );
430
+ editor.setText( code, options.language ?? "Plain Text" );
431
+
432
+ if( options.linesAdded )
433
+ {
434
+ const code = editor.root.querySelector( ".code" );
435
+ for( let l of options.linesAdded )
436
+ {
437
+ if( l.constructor == Number )
438
+ {
439
+ code.childNodes[ l - 1 ].classList.add( "added" );
440
+ }
441
+ else if( l.constructor == Array ) // It's a range
442
+ {
443
+ for( let i = ( l[0] - 1 ); i <= ( l[1] - 1 ); i++ )
444
+ {
445
+ code.childNodes[ i ].classList.add( "added" );
446
+ }
447
+ }
448
+ }
449
+ }
450
+
451
+ if( options.linesRemoved )
452
+ {
453
+ const code = editor.root.querySelector( ".code" );
454
+ for( let l of options.linesRemoved )
455
+ {
456
+ if( l.constructor == Number )
457
+ {
458
+ code.childNodes[ l - 1 ].classList.add( "removed" );
459
+ }
460
+ else if( l.constructor == Array ) // It's a range
461
+ {
462
+ for( let i = ( l[0] - 1 ); i <= ( l[1] - 1 ); i++ )
463
+ {
464
+ code.childNodes[ i ].classList.add( "removed" );
465
+ }
466
+ }
467
+ }
468
+ }
469
+
470
+ if( options.windowMode )
471
+ {
472
+ const windowActionButtons = document.createElement( "div" );
473
+ windowActionButtons.className = "lexwindowbuttons";
474
+ const aButton = document.createElement( "span" );
475
+ aButton.style.background = "#ee4f50";
476
+ const bButton = document.createElement( "span" );
477
+ bButton.style.background = "#f5b720";
478
+ const cButton = document.createElement( "span" );
479
+ cButton.style.background = "#53ca29";
480
+ windowActionButtons.appendChild( aButton );
481
+ windowActionButtons.appendChild( bButton );
482
+ windowActionButtons.appendChild( cButton );
483
+ const tabs = editor.root.querySelector( ".lexareatabs" );
484
+ tabs.prepend( windowActionButtons );
485
+ }
486
+
487
+ snippet.appendChild( area.root );
488
+ return snippet;
489
+ }
490
+
491
+ LX.makeCodeSnippet = makeCodeSnippet;
492
+
493
+ // Math classes
494
+
495
+ class vec2 {
496
+
497
+ constructor( x, y ) {
498
+ this.x = x ?? 0;
499
+ this.y = y ?? ( x ?? 0 );
500
+ }
501
+
502
+ get xy() { return [ this.x, this.y ]; }
503
+ get yx() { return [ this.y, this.x ]; }
504
+
505
+ set ( x, y ) { this.x = x; this.y = y; }
506
+ add ( v, v0 = new vec2() ) { v0.set( this.x + v.x, this.y + v.y ); return v0; }
507
+ sub ( v, v0 = new vec2() ) { v0.set( this.x - v.x, this.y - v.y ); return v0; }
508
+ mul ( v, v0 = new vec2() ) { if( v.constructor == Number ) { v = new vec2( v ) } v0.set( this.x * v.x, this.y * v.y ); return v0; }
509
+ div ( v, v0 = new vec2() ) { if( v.constructor == Number ) { v = new vec2( v ) } v0.set( this.x / v.x, this.y / v.y ); return v0; }
510
+ abs ( v0 = new vec2() ) { v0.set( Math.abs( this.x ), Math.abs( this.y ) ); return v0; }
511
+ dot ( v ) { return this.x * v.x + this.y * v.y; }
512
+ len2 () { return this.dot( this ) }
513
+ len () { return Math.sqrt( this.len2() ); }
514
+ nrm ( v0 = new vec2() ) { v0.set( this.x, this.y ); return v0.mul( 1.0 / this.len(), v0 ); }
515
+ dst ( v ) { return v.sub( this ).len(); }
516
+ clp ( min, max, v0 = new vec2() ) { v0.set( clamp( this.x, min, max ), clamp( this.y, min, max ) ); return v0; }
517
+ };
518
+
519
+ LX.vec2 = vec2;
305
520
 
521
+ function create_global_searchbar( root )
522
+ {
306
523
  let globalSearch = document.createElement("div");
307
524
  globalSearch.id = "global-search";
308
525
  globalSearch.className = "hidden";
@@ -756,7 +973,7 @@ function prompt( text, title, callback, options = {} )
756
973
  if( callback ) callback.call( this, value );
757
974
  dialog.close();
758
975
  }
759
- }, { buttonClass: "accept" });
976
+ }, { buttonClass: "primary" });
760
977
 
761
978
  p.addButton(null, "Cancel", () => {if(options.on_cancel) options.on_cancel(); dialog.close();} );
762
979
 
@@ -2205,18 +2422,18 @@ class Menubar {
2205
2422
  entry.className = "lexmenuentry";
2206
2423
  entry.id = pKey;
2207
2424
  entry.innerHTML = "<span>" + key + "</span>";
2208
- if(options.position == "left") {
2209
- this.root.prepend( entry );
2210
- }
2211
- else {
2212
- if(options.position == "right")
2213
- entry.right = true;
2214
- if(this.root.lastChild && this.root.lastChild.right) {
2215
- this.root.lastChild.before( entry );
2216
- }
2217
- else {
2218
- this.root.appendChild( entry );
2219
- }
2425
+ if(options.position == "left") {
2426
+ this.root.prepend( entry );
2427
+ }
2428
+ else {
2429
+ if(options.position == "right")
2430
+ entry.right = true;
2431
+ if(this.root.lastChild && this.root.lastChild.right) {
2432
+ this.root.lastChild.before( entry );
2433
+ }
2434
+ else {
2435
+ this.root.appendChild( entry );
2436
+ }
2220
2437
  }
2221
2438
 
2222
2439
  const create_submenu = function( o, k, c, d ) {
@@ -2437,16 +2654,16 @@ class Menubar {
2437
2654
  button.style.maxHeight = "calc(100% - 10px)";
2438
2655
  button.style.alignItems = "center";
2439
2656
 
2440
- if(options.float == "right")
2441
- button.right = true;
2442
- if(this.root.lastChild && this.root.lastChild.right) {
2443
- this.root.lastChild.before( button );
2657
+ if(options.float == "right")
2658
+ button.right = true;
2659
+ if(this.root.lastChild && this.root.lastChild.right) {
2660
+ this.root.lastChild.before( button );
2444
2661
  }
2445
2662
  else if(options.float == "left") {
2446
2663
  this.root.prepend(button);
2447
- }
2448
- else {
2449
- this.root.appendChild( button );
2664
+ }
2665
+ else {
2666
+ this.root.appendChild( button );
2450
2667
  }
2451
2668
 
2452
2669
  const _b = button.querySelector('a');
@@ -2478,16 +2695,16 @@ class Menubar {
2478
2695
  button.style.padding = "5px";
2479
2696
  button.style.alignItems = "center";
2480
2697
 
2481
- if(options.float == "right")
2482
- button.right = true;
2483
- if(this.root.lastChild && this.root.lastChild.right) {
2484
- this.root.lastChild.before( button );
2485
- }
2698
+ if(options.float == "right")
2699
+ button.right = true;
2700
+ if(this.root.lastChild && this.root.lastChild.right) {
2701
+ this.root.lastChild.before( button );
2702
+ }
2486
2703
  else if(options.float == "left") {
2487
2704
  this.root.prepend(button);
2488
2705
  }
2489
- else {
2490
- this.root.appendChild( button );
2706
+ else {
2707
+ this.root.appendChild( button );
2491
2708
  }
2492
2709
 
2493
2710
  const _b = button.querySelector('a');
@@ -2507,44 +2724,86 @@ class Menubar {
2507
2724
 
2508
2725
  addButtons( buttons, options = {} ) {
2509
2726
 
2510
- if(!buttons)
2511
- throw("No buttons to add!");
2727
+ if( !buttons )
2728
+ {
2729
+ throw( "No buttons to add!" );
2730
+ }
2512
2731
 
2513
- if(!this.buttonContainer)
2732
+ if( !this.buttonContainer )
2514
2733
  {
2515
- this.buttonContainer = document.createElement('div');
2734
+ this.buttonContainer = document.createElement( "div" );
2516
2735
  this.buttonContainer.className = "lexmenubuttons";
2517
- this.buttonContainer.classList.add(options.float ?? 'center');
2518
- if(options.position == "right")
2519
- this.buttonContainer.right = true;
2520
- if(this.root.lastChild && this.root.lastChild.right) {
2521
- this.root.lastChild.before( this.buttonContainer );
2522
- }
2523
- else {
2524
- this.root.appendChild( this.buttonContainer );
2736
+ this.buttonContainer.classList.add( options.float ?? "center" );
2737
+
2738
+ if( options.position == "right" )
2739
+ {
2740
+ this.buttonContainer.right = true;
2741
+ }
2742
+
2743
+ if( this.root.lastChild && this.root.lastChild.right )
2744
+ {
2745
+ this.root.lastChild.before( this.buttonContainer );
2746
+ }
2747
+ else
2748
+ {
2749
+ this.root.appendChild( this.buttonContainer );
2525
2750
  }
2526
2751
  }
2527
2752
 
2528
2753
  for( let i = 0; i < buttons.length; ++i )
2529
2754
  {
2530
- let data = buttons[i];
2531
- let button = document.createElement('div');
2755
+ let data = buttons[ i ];
2756
+ let button = document.createElement( "label" );
2532
2757
  const title = data.title;
2533
2758
  let disabled = data.disabled ?? false;
2534
2759
  button.className = "lexmenubutton" + (disabled ? " disabled" : "");
2535
2760
  button.title = title ?? "";
2536
- button.innerHTML = "<a class='" + data.icon + " lexicon'></a>";
2537
2761
  this.buttonContainer.appendChild( button );
2538
2762
 
2539
- const _b = button.querySelector('a');
2540
- _b.addEventListener("click", (e) => {
2541
- disabled = e.target.parentElement.classList.contains("disabled");
2542
- if(data.callback && !disabled)
2543
- data.callback.call( this, _b, e );
2763
+ const icon = document.createElement( "a" );
2764
+ icon.className = data.icon + " lexicon";
2765
+ button.appendChild( icon );
2766
+
2767
+ let trigger = icon;
2768
+
2769
+ if( data.swap )
2770
+ {
2771
+ button.classList.add( "swap" );
2772
+ icon.classList.add( "swap-off" );
2773
+
2774
+ const input = document.createElement( "input" );
2775
+ input.type = "checkbox";
2776
+ button.prepend( input );
2777
+ trigger = input;
2778
+
2779
+ const swapIcon = document.createElement( "a" );
2780
+ swapIcon.className = data.swap + " swap-on lexicon";
2781
+ button.appendChild( swapIcon );
2782
+
2783
+ button.swap = function() {
2784
+ const swapInput = this.querySelector( "input" );
2785
+ swapInput.checked = !swapInput.checked;
2786
+ };
2787
+
2788
+ // Set if swap has to be performed
2789
+ button.setState = function( v ) {
2790
+ const swapInput = this.querySelector( "input" );
2791
+ swapInput.checked = v;
2792
+ };
2793
+ }
2794
+
2795
+ trigger.addEventListener("click", e => {
2796
+ if( data.callback && !disabled )
2797
+ {
2798
+ const swapInput = button.querySelector( "input" );
2799
+ data.callback.call( this, e, swapInput?.checked );
2800
+ }
2544
2801
  });
2545
2802
 
2546
- if(title)
2803
+ if( title )
2804
+ {
2547
2805
  this.buttons[ title ] = button;
2806
+ }
2548
2807
  }
2549
2808
  }
2550
2809
  };
@@ -2698,6 +2957,7 @@ class Widget {
2698
2957
  static PAD = 26;
2699
2958
  static FORM = 27;
2700
2959
  static DIAL = 28;
2960
+ static COUNTER = 29;
2701
2961
 
2702
2962
  static NO_CONTEXT_TYPES = [
2703
2963
  Widget.BUTTON,
@@ -2787,6 +3047,7 @@ class Widget {
2787
3047
  case Widget.PAD: return "Pad";
2788
3048
  case Widget.FORM: return "Form";
2789
3049
  case Widget.DIAL: return "Dial";
3050
+ case Widget.COUNTER: return "Counter";
2790
3051
  case Widget.CUSTOM: return this.customName;
2791
3052
  }
2792
3053
 
@@ -4017,7 +4278,9 @@ class Panel {
4017
4278
  * @param {Function} callback Callback function on change
4018
4279
  * @param {*} options:
4019
4280
  * disabled: Make the widget disabled [false]
4281
+ * required: Make the input required
4020
4282
  * placeholder: Add input placeholder
4283
+ * pattern: Regular expression that value must match
4021
4284
  * trigger: Choose onchange trigger (default, input) [default]
4022
4285
  * inputWidth: Width of the text input
4023
4286
  * skipReset: Don't add the reset value button when value changes
@@ -4032,11 +4295,18 @@ class Panel {
4032
4295
  widget.onGetValue = () => {
4033
4296
  return wValue.value;
4034
4297
  };
4298
+
4035
4299
  widget.onSetValue = ( newValue, skipCallback ) => {
4036
4300
  this.disabled ? wValue.innerText = newValue : wValue.value = newValue;
4037
4301
  Panel._dispatch_event( wValue, "focusout", skipCallback );
4038
4302
  };
4039
4303
 
4304
+ widget.valid = () => {
4305
+ if( wValue.pattern == "" ) { return true; }
4306
+ const regexp = new RegExp( wValue.pattern );
4307
+ return regexp.test( wValue.value );
4308
+ };
4309
+
4040
4310
  let element = widget.domEl;
4041
4311
 
4042
4312
  // Add reset functionality
@@ -4071,14 +4341,33 @@ class Panel {
4071
4341
  wValue.style.width = "100%";
4072
4342
  wValue.style.textAlign = options.float ?? "";
4073
4343
 
4074
- if( options.placeholder )
4075
- wValue.setAttribute( "placeholder", options.placeholder );
4344
+ wValue.setAttribute( "placeholder", options.placeholder ?? "" );
4345
+
4346
+ if( options.required )
4347
+ {
4348
+ wValue.setAttribute( "required", options.required );
4349
+ }
4350
+
4351
+ if( options.pattern )
4352
+ {
4353
+ wValue.setAttribute( "pattern", options.pattern );
4354
+ }
4076
4355
 
4077
4356
  var resolve = ( function( val, event ) {
4357
+
4358
+ if( !widget.valid() )
4359
+ {
4360
+ return;
4361
+ }
4362
+
4078
4363
  const skipCallback = event.detail;
4079
4364
  let btn = element.querySelector( ".lexwidgetname .lexicon" );
4080
4365
  if( btn ) btn.style.display = ( val != wValue.iValue ? "block" : "none" );
4081
- if( !skipCallback ) this._trigger( new IEvent( name, val, event ), callback );
4366
+ if( !skipCallback )
4367
+ {
4368
+ this._trigger( new IEvent( name, val, event ), callback );
4369
+ }
4370
+
4082
4371
  }).bind( this );
4083
4372
 
4084
4373
  const trigger = options.trigger ?? 'default';
@@ -4290,18 +4579,13 @@ class Panel {
4290
4579
 
4291
4580
  var wValue = document.createElement( 'button' );
4292
4581
  wValue.title = options.title ?? "";
4293
- wValue.className = "lexbutton";
4582
+ wValue.className = "lexbutton " + ( options.buttonClass ?? "" );
4294
4583
 
4295
4584
  if( options.selected )
4296
4585
  {
4297
4586
  wValue.classList.add( "selected" );
4298
4587
  }
4299
4588
 
4300
- if( options.buttonClass )
4301
- {
4302
- wValue.classList.add( options.buttonClass );
4303
- }
4304
-
4305
4589
  wValue.innerHTML =
4306
4590
  (options.icon ? "<a class='" + options.icon + "'></a>" :
4307
4591
  ( options.img ? "<img src='" + options.img + "'>" : "<span>" + (value || "") + "</span>" ));
@@ -4540,7 +4824,7 @@ class Panel {
4540
4824
 
4541
4825
  this.addLabel( entry, { textClass: "formlabel" } );
4542
4826
 
4543
- this.addText( null, entryData.constructor == Object ? entryData.value : entryData, ( value ) => {
4827
+ entryData.textWidget = this.addText( null, entryData.constructor == Object ? entryData.value : entryData, ( value ) => {
4544
4828
  container.formData[ entry ] = value;
4545
4829
  }, entryData );
4546
4830
 
@@ -4550,11 +4834,22 @@ class Panel {
4550
4834
  this.addBlank( );
4551
4835
 
4552
4836
  this.addButton( null, options.actionName ?? "Submit", ( value, event ) => {
4837
+
4838
+ for( let entry in data )
4839
+ {
4840
+ let entryData = data[ entry ];
4841
+
4842
+ if( !entryData.textWidget.valid() )
4843
+ {
4844
+ return;
4845
+ }
4846
+ }
4847
+
4553
4848
  if( callback )
4554
4849
  {
4555
4850
  callback( container.formData, event );
4556
4851
  }
4557
- }, { buttonClass: "accept", width: "calc(100% - 10px)" } );
4852
+ }, { buttonClass: "primary", width: "calc(100% - 10px)" } );
4558
4853
 
4559
4854
  this.clearQueue();
4560
4855
 
@@ -4709,13 +5004,36 @@ class Panel {
4709
5004
  delete list.unfocus_event;
4710
5005
  return;
4711
5006
  }
4712
- const topPosition = selectedOption.getBoundingClientRect().y;
4713
- list.style.top = (topPosition + selectedOption.offsetHeight) + 'px';
5007
+
5008
+ list.toggleAttribute( "hidden" );
5009
+ list.classList.remove( "place-above" );
5010
+
5011
+ const listHeight = 26 * values.length;
5012
+ const rect = selectedOption.getBoundingClientRect();
5013
+ const topPosition = rect.y;
5014
+
5015
+ let maxY = window.innerHeight;
5016
+ let overflowContainer = list.getParentArea();
5017
+
5018
+ if( overflowContainer )
5019
+ {
5020
+ const parentRect = overflowContainer.getBoundingClientRect();
5021
+ maxY = parentRect.y + parentRect.height;
5022
+ }
5023
+
5024
+ list.style.top = ( topPosition + selectedOption.offsetHeight ) + 'px';
5025
+
5026
+ const showAbove = ( topPosition + listHeight ) > maxY;
5027
+ if( showAbove )
5028
+ {
5029
+ list.style.top = ( topPosition - listHeight ) + 'px';
5030
+ list.classList.add( "place-above" );
5031
+ }
5032
+
4714
5033
  list.style.width = (event.currentTarget.clientWidth) + 'px';
4715
5034
  list.style.minWidth = (_getMaxListWidth()) + 'px';
4716
- list.toggleAttribute('hidden');
4717
5035
  list.focus();
4718
- }, { buttonClass: 'array', skipInlineCount: true });
5036
+ }, { buttonClass: "array", skipInlineCount: true });
4719
5037
 
4720
5038
  this.clearQueue();
4721
5039
 
@@ -5361,40 +5679,45 @@ class Panel {
5361
5679
 
5362
5680
  // Show tags
5363
5681
 
5364
- let tags_container = document.createElement('div');
5365
- tags_container.className = "lextags";
5366
- tags_container.style.width = "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
5682
+ const tagsContainer = document.createElement('div');
5683
+ tagsContainer.className = "lextags";
5684
+ tagsContainer.style.width = "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
5367
5685
 
5368
5686
  const create_tags = () => {
5369
5687
 
5370
- tags_container.innerHTML = "";
5688
+ tagsContainer.innerHTML = "";
5371
5689
 
5372
5690
  for( let i = 0; i < value.length; ++i )
5373
5691
  {
5374
- let tag_name = value[i];
5375
- let tag = document.createElement('span');
5692
+ const tagName = value[i];
5693
+ const tag = document.createElement('span');
5376
5694
  tag.className = "lextag";
5377
- tag.innerHTML = tag_name;
5695
+ tag.innerHTML = tagName;
5696
+
5697
+ const removeButton = document.createElement('a');
5698
+ removeButton.className = "lextagrmb fa-solid fa-xmark lexicon";
5699
+ tag.appendChild( removeButton );
5378
5700
 
5379
- tag.addEventListener('click', function( e ) {
5380
- this.remove();
5381
- value.splice( value.indexOf( tag_name ), 1 );
5701
+ removeButton.addEventListener( 'click', e => {
5702
+ tag.remove();
5703
+ value.splice( value.indexOf( tagName ), 1 );
5382
5704
  let btn = element.querySelector( ".lexwidgetname .lexicon" );
5383
5705
  if( btn ) btn.style.display = ( value != defaultValue ? "block" : "none" );
5384
5706
  that._trigger( new IEvent( name, value, e ), callback );
5385
- });
5707
+ } );
5386
5708
 
5387
- tags_container.appendChild( tag );
5709
+ tagsContainer.appendChild( tag );
5388
5710
  }
5389
5711
 
5390
- let tag_input = document.createElement( 'input' );
5391
- tag_input.value = "";
5392
- tag_input.placeholder = "Tag...";
5393
- tags_container.insertChildAtIndex( tag_input, 0 );
5712
+ let tagInput = document.createElement( 'input' );
5713
+ tagInput.value = "";
5714
+ tagInput.placeholder = "Add tag...";
5715
+ tagsContainer.appendChild( tagInput );
5394
5716
 
5395
- tag_input.onkeydown = function( e ) {
5717
+ tagInput.onkeydown = function( e ) {
5396
5718
  const val = this.value.replace(/\s/g, '');
5397
- if( e.key == ' ') {
5719
+ if( e.key == ' ' || e.key == 'Enter' )
5720
+ {
5398
5721
  e.preventDefault();
5399
5722
  if( !val.length || value.indexOf( val ) > -1 )
5400
5723
  return;
@@ -5406,18 +5729,19 @@ class Panel {
5406
5729
  }
5407
5730
  };
5408
5731
 
5409
- tag_input.focus();
5732
+ tagInput.focus();
5410
5733
  }
5411
5734
 
5412
5735
  create_tags();
5413
5736
 
5414
5737
  // Remove branch padding and margins
5415
- if(!widget.name) {
5738
+ if( !widget.name )
5739
+ {
5416
5740
  element.className += " noname";
5417
- tags_container.style.width = "100%";
5741
+ tagsContainer.style.width = "100%";
5418
5742
  }
5419
5743
 
5420
- element.appendChild(tags_container);
5744
+ element.appendChild( tagsContainer );
5421
5745
 
5422
5746
  return widget;
5423
5747
  }
@@ -6604,7 +6928,7 @@ class Panel {
6604
6928
  progress.classList.add( "editable" );
6605
6929
  progress.addEventListener( "mousedown", inner_mousedown );
6606
6930
 
6607
- var that = this;
6931
+ const that = this;
6608
6932
 
6609
6933
  function inner_mousedown( e )
6610
6934
  {
@@ -6612,24 +6936,28 @@ class Panel {
6612
6936
  doc.addEventListener( 'mousemove', inner_mousemove );
6613
6937
  doc.addEventListener( 'mouseup', inner_mouseup );
6614
6938
  document.body.classList.add( 'noevents' );
6939
+ progress.classList.add( "grabbing" );
6615
6940
  e.stopImmediatePropagation();
6616
6941
  e.stopPropagation();
6942
+
6943
+ const rect = progress.getBoundingClientRect();
6944
+ const newValue = round( remapRange( e.offsetX, 0, rect.width, progress.min, progress.max ) );
6945
+ that.setValue( name, newValue );
6617
6946
  }
6618
6947
 
6619
6948
  function inner_mousemove( e )
6620
6949
  {
6621
- let dt = -e.movementX;
6950
+ let dt = e.movementX;
6622
6951
 
6623
6952
  if ( dt != 0 )
6624
6953
  {
6625
- let v = that.getValue( name, value );
6626
- v += e.movementX / 100;
6627
- v = round( v );
6628
- that.setValue( name, v );
6954
+ const rect = progress.getBoundingClientRect();
6955
+ const newValue = round( remapRange( e.offsetX - rect.x, 0, rect.width, progress.min, progress.max ) );
6956
+ that.setValue( name, newValue );
6629
6957
 
6630
6958
  if( options.callback )
6631
6959
  {
6632
- options.callback( v, e );
6960
+ options.callback( newValue, e );
6633
6961
  }
6634
6962
  }
6635
6963
 
@@ -6643,6 +6971,7 @@ class Panel {
6643
6971
  doc.removeEventListener( 'mousemove', inner_mousemove );
6644
6972
  doc.removeEventListener( 'mouseup', inner_mouseup );
6645
6973
  document.body.classList.remove( 'noevents' );
6974
+ progress.classList.remove( "grabbing" );
6646
6975
  }
6647
6976
  }
6648
6977
 
@@ -6932,6 +7261,91 @@ class Panel {
6932
7261
 
6933
7262
  this.addSeparator();
6934
7263
  }
7264
+
7265
+ /**
7266
+ * @method addCounter
7267
+ * @param {String} name Widget name
7268
+ * @param {Number} value Counter value
7269
+ * @param {Function} callback Callback function on change
7270
+ * @param {*} options:
7271
+ * disabled: Make the widget disabled [false]
7272
+ * min, max: Min and Max values
7273
+ * step: Step for adding/substracting
7274
+ * label: Text to show below the counter
7275
+ */
7276
+
7277
+ addCounter( name, value, callback, options = { } ) {
7278
+
7279
+ let widget = this.create_widget( name, Widget.COUNTER, options );
7280
+
7281
+ widget.onGetValue = () => {
7282
+ return counterText.count;
7283
+ };
7284
+
7285
+ widget.onSetValue = ( newValue, skipCallback ) => {
7286
+ _onChange( newValue, skipCallback );
7287
+ };
7288
+
7289
+ let element = widget.domEl;
7290
+
7291
+ const min = options.min ?? 0;
7292
+ const max = options.max ?? 100;
7293
+ const step = options.step ?? 1;
7294
+
7295
+ const _onChange = ( value, skipCallback, event ) => {
7296
+ value = clamp( value, min, max );
7297
+ counterText.count = value;
7298
+ counterText.innerHTML = value;
7299
+ if( !skipCallback )
7300
+ {
7301
+ this._trigger( new IEvent( name, value, event ), callback );
7302
+ }
7303
+ }
7304
+
7305
+ const container = document.createElement( 'div' );
7306
+ container.className = "lexcounter";
7307
+ element.appendChild( container );
7308
+
7309
+ this.queue( container );
7310
+
7311
+ this.addButton(null, "<a style='margin-top: 0px;' class='fa-solid fa-minus'></a>", (value, e) => {
7312
+ let mult = step ?? 1;
7313
+ if( e.shiftKey ) mult *= 10;
7314
+ _onChange( counterText.count - mult, false, e );
7315
+ }, { className: "micro", skipInlineCount: true, title: "Minus" });
7316
+
7317
+ this.clearQueue();
7318
+
7319
+ const containerBox = document.createElement( 'div' );
7320
+ containerBox.className = "lexcounterbox";
7321
+ container.appendChild( containerBox );
7322
+
7323
+ const counterText = document.createElement( 'span' );
7324
+ counterText.className = "lexcountervalue";
7325
+ counterText.innerHTML = value;
7326
+ counterText.count = value;
7327
+ containerBox.appendChild( counterText );
7328
+
7329
+ if( options.label )
7330
+ {
7331
+ const counterLabel = document.createElement( 'span' );
7332
+ counterLabel.className = "lexcounterlabel";
7333
+ counterLabel.innerHTML = options.label;
7334
+ containerBox.appendChild( counterLabel );
7335
+ }
7336
+
7337
+ this.queue( container );
7338
+
7339
+ this.addButton(null, "<a style='margin-top: 0px;' class='fa-solid fa-plus'></a>", (value, e) => {
7340
+ let mult = step ?? 1;
7341
+ if( e.shiftKey ) mult *= 10;
7342
+ _onChange( counterText.count + mult, false, e );
7343
+ }, { className: "micro", skipInlineCount: true, title: "Plus" });
7344
+
7345
+ this.clearQueue();
7346
+
7347
+ return widget;
7348
+ }
6935
7349
  }
6936
7350
 
6937
7351
  LX.Panel = Panel;
@@ -7167,6 +7581,104 @@ class Branch {
7167
7581
 
7168
7582
  LX.Branch = Branch;
7169
7583
 
7584
+ /**
7585
+ * @class Footer
7586
+ */
7587
+
7588
+ class Footer {
7589
+ /**
7590
+ * @param {*} options:
7591
+ * columns: Array with data per column { title, items: [ { title, link } ] }
7592
+ * credits: html string
7593
+ * socials: Array with data per item { title, link, iconHtml }
7594
+ */
7595
+ constructor( options = {} ) {
7596
+
7597
+ const root = document.createElement( "footer" );
7598
+ root.className = "lexfooter";
7599
+
7600
+ const wrapper = document.createElement( "div" );
7601
+ wrapper.className = "wrapper";
7602
+ root.appendChild( wrapper );
7603
+
7604
+ if( options.columns && options.columns.constructor == Array )
7605
+ {
7606
+ const cols = document.createElement( "div" );
7607
+ cols.className = "columns";
7608
+ cols.style.gridTemplateColumns = "1fr ".repeat( options.columns.length );
7609
+ wrapper.appendChild( cols );
7610
+
7611
+ for( let col of options.columns )
7612
+ {
7613
+ const colDom = document.createElement( "div" );
7614
+ colDom.className = "col";
7615
+ cols.appendChild( colDom );
7616
+
7617
+ const colTitle = document.createElement( "h2" );
7618
+ colTitle.innerHTML = col.title;
7619
+ colDom.appendChild( colTitle );
7620
+
7621
+ if( !col.items || !col.items.length )
7622
+ {
7623
+ continue;
7624
+ }
7625
+
7626
+ const itemListDom = document.createElement( "ul" );
7627
+ colDom.appendChild( itemListDom );
7628
+
7629
+ for( let item of col.items )
7630
+ {
7631
+ const itemDom = document.createElement( "li" );
7632
+ itemDom.innerHTML = `<a class="" href="${ item.link }">${ item.title }</a>`;
7633
+ itemListDom.appendChild( itemDom );
7634
+ }
7635
+ }
7636
+ }
7637
+
7638
+ if( options.credits || options.socials )
7639
+ {
7640
+ const hr = document.createElement( "hr" );
7641
+ wrapper.appendChild( hr );
7642
+
7643
+ const creditsSocials = document.createElement( "div" );
7644
+ creditsSocials.className = "credits-and-socials";
7645
+ wrapper.appendChild( creditsSocials );
7646
+
7647
+ if( options.credits )
7648
+ {
7649
+ const credits = document.createElement( "p" );
7650
+ credits.innerHTML = options.credits;
7651
+ creditsSocials.appendChild( credits );
7652
+ }
7653
+
7654
+ if( options.socials )
7655
+ {
7656
+ const socials = document.createElement( "div" );
7657
+ socials.className = "social";
7658
+
7659
+ for( let social of options.socials )
7660
+ {
7661
+ const itemDom = document.createElement( "a" );
7662
+ itemDom.title = social.title;
7663
+ itemDom.innerHTML = social.icon;
7664
+ itemDom.href = social.link;
7665
+ itemDom.target = "_blank";
7666
+ socials.appendChild( itemDom );
7667
+ }
7668
+
7669
+ creditsSocials.appendChild( socials );
7670
+ }
7671
+ }
7672
+
7673
+ // Append directly to body
7674
+ const parent = options.parent ?? document.body;
7675
+ parent.appendChild( root );
7676
+ }
7677
+
7678
+ }
7679
+
7680
+ LX.Footer = Footer;
7681
+
7170
7682
  /**
7171
7683
  * @class Dialog
7172
7684
  */
@@ -9312,7 +9824,7 @@ Object.assign(LX, {
9312
9824
  //request.mimeType = "text/plain; charset=x-user-defined";
9313
9825
  dataType = "arraybuffer";
9314
9826
  request.mimeType = "application/octet-stream";
9315
- }
9827
+ }
9316
9828
 
9317
9829
  //regular case, use AJAX call
9318
9830
  var xhr = new XMLHttpRequest();
@@ -9553,6 +10065,14 @@ Element.prototype.getComputedSize = function() {
9553
10065
  }
9554
10066
  }
9555
10067
 
10068
+ Element.prototype.getParentArea = function() {
10069
+ let parent = this.parentElement;
10070
+ while( parent ) {
10071
+ if( parent.classList.contains( "lexarea" ) ) { return parent; }
10072
+ parent = parent.parentElement;
10073
+ }
10074
+ }
10075
+
9556
10076
  LX.UTILS = {
9557
10077
  getTime() { return new Date().getTime() },
9558
10078
  compareThreshold( v, p, n, t ) { return Math.abs(v - p) >= t || Math.abs(v - n) >= t },