lexgui 0.1.45 → 0.2.0

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,10 +8,11 @@
8
8
  */
9
9
 
10
10
  var LX = {
11
- version: "0.1.45",
11
+ version: "0.2.0",
12
12
  ready: false,
13
13
  components: [], // specific pre-build components
14
- signals: {} // events and triggers
14
+ signals: {}, // events and triggers
15
+ extraCommandbarEntries: [] // user specific entries for command bar
15
16
  };
16
17
 
17
18
  LX.MOUSE_LEFT_CLICK = 0;
@@ -32,34 +33,103 @@ LX.clamp = clamp;
32
33
  LX.round = round;
33
34
  LX.remapRange = remapRange;
34
35
 
35
- function getSupportedDOMName( string )
36
+ // Timer that works everywhere (from litegraph.js)
37
+ if ( typeof performance != "undefined" )
38
+ {
39
+ LX.getTime = performance.now.bind( performance );
40
+ }
41
+ else if( typeof Date != "undefined" && Date.now )
42
+ {
43
+ LX.getTime = Date.now.bind( Date );
44
+ }
45
+ else if ( typeof process != "undefined" )
46
+ {
47
+ LX.getTime = function() {
48
+ var t = process.hrtime();
49
+ return t[ 0 ] * 0.001 + t[ 1 ] * 1e-6;
50
+ };
51
+ }
52
+ else
53
+ {
54
+ LX.getTime = function() {
55
+ return new Date().getTime();
56
+ };
57
+ }
58
+
59
+ let ASYNC_ENABLED = true;
60
+
61
+ /**
62
+ * @method doAsync
63
+ * @description Call a function asynchronously
64
+ * @param {Function} fn Function to call
65
+ * @param {Number} ms Time to wait until calling the function (in milliseconds)
66
+ */
67
+ function doAsync( fn, ms ) {
68
+ if( ASYNC_ENABLED )
69
+ {
70
+ setTimeout( fn, ms ?? 0 );
71
+ }
72
+ else
73
+ {
74
+ fn();
75
+ }
76
+ }
77
+
78
+ LX.doAsync = doAsync;
79
+
80
+ /**
81
+ * @method getSupportedDOMName
82
+ * @description Convert a text string to a valid DOM name
83
+ * @param {String} text Original text
84
+ */
85
+ function getSupportedDOMName( text )
36
86
  {
37
- return string.replace(/\s/g, '').replaceAll('@', '_').replaceAll('+', '_plus_').replaceAll('.', '');
87
+ return text.replace(/\s/g, '').replaceAll('@', '_').replaceAll('+', '_plus_').replaceAll('.', '');
38
88
  }
39
89
 
40
90
  LX.getSupportedDOMName = getSupportedDOMName;
41
91
 
42
- function has( component_name )
92
+ /**
93
+ * @method has
94
+ * @description Ask if LexGUI is using a specific component
95
+ * @param {String} componentName Name of the LexGUI component
96
+ */
97
+ function has( componentName )
43
98
  {
44
- return (LX.components.indexOf( component_name ) > -1);
99
+ return ( LX.components.indexOf( componentName ) > -1 );
45
100
  }
46
101
 
47
102
  LX.has = has;
48
103
 
49
- function getExtension( s )
104
+ /**
105
+ * @method getExtension
106
+ * @description Get a extension from a path/url/filename
107
+ * @param {String} name
108
+ */
109
+ function getExtension( name )
50
110
  {
51
- return s.includes('.') ? s.split('.').pop() : null;
111
+ return name.includes('.') ? name.split('.').pop() : null;
52
112
  }
53
113
 
54
114
  LX.getExtension = getExtension;
55
115
 
56
- function deepCopy( o )
116
+ /**
117
+ * @method deepCopy
118
+ * @description Create a deep copy with no references from an object
119
+ * @param {Object} obj
120
+ */
121
+ function deepCopy( obj )
57
122
  {
58
- return JSON.parse(JSON.stringify(o))
123
+ return JSON.parse( JSON.stringify( obj ) )
59
124
  }
60
125
 
61
126
  LX.deepCopy = deepCopy;
62
127
 
128
+ /**
129
+ * @method setTheme
130
+ * @description Set dark or light theme
131
+ * @param {String} colorScheme Name of the scheme
132
+ */
63
133
  function setTheme( colorScheme )
64
134
  {
65
135
  colorScheme = ( colorScheme == "light" ) ? "light" : "dark";
@@ -69,6 +139,12 @@ function setTheme( colorScheme )
69
139
 
70
140
  LX.setTheme = setTheme;
71
141
 
142
+ /**
143
+ * @method setThemeColor
144
+ * @description Sets a new value for one of the main theme variables
145
+ * @param {String} colorName Name of the theme variable
146
+ * @param {String} color Color in rgba/hex
147
+ */
72
148
  function setThemeColor( colorName, color )
73
149
  {
74
150
  var r = document.querySelector( ':root' );
@@ -77,16 +153,22 @@ function setThemeColor( colorName, color )
77
153
 
78
154
  LX.setThemeColor = setThemeColor;
79
155
 
156
+ /**
157
+ * @method getThemeColor
158
+ * @description Get the value for one of the main theme variables
159
+ * @param {String} colorName Name of the theme variable
160
+ */
80
161
  function getThemeColor( colorName )
81
162
  {
82
163
  const r = getComputedStyle( document.querySelector( ':root' ) );
83
164
  const value = r.getPropertyValue( '--' + colorName );
165
+ const theme = document.documentElement.getAttribute( "data-theme" );
84
166
 
85
- if( value.includes( "light-dark" ) && window.matchMedia )
167
+ if( value.includes( "light-dark" ) )
86
168
  {
87
169
  const currentScheme = r.getPropertyValue( "color-scheme" );
88
170
 
89
- if( ( window.matchMedia( "(prefers-color-scheme: light)" ).matches ) || ( currentScheme == "light" ) )
171
+ if( currentScheme == "light" )
90
172
  {
91
173
  return value.substring( value.indexOf( '(' ) + 1, value.indexOf( ',' ) ).replace( /\s/g, '' );
92
174
  }
@@ -101,7 +183,13 @@ function getThemeColor( colorName )
101
183
 
102
184
  LX.getThemeColor = getThemeColor;
103
185
 
104
- function getBase64Image( img ) {
186
+ /**
187
+ * @method getBase64Image
188
+ * @description Convert an image to a base64 string
189
+ * @param {Image} img
190
+ */
191
+ function getBase64Image( img )
192
+ {
105
193
  var canvas = document.createElement( 'canvas' );
106
194
  canvas.width = img.width;
107
195
  canvas.height = img.height;
@@ -112,7 +200,13 @@ function getBase64Image( img ) {
112
200
 
113
201
  LX.getBase64Image = getBase64Image;
114
202
 
115
- function hexToRgb( hexStr ) {
203
+ /**
204
+ * @method hexToRgb
205
+ * @description Convert a hexadecimal string to a valid RGB color array
206
+ * @param {String} hexStr Hexadecimal color
207
+ */
208
+ function hexToRgb( hexStr )
209
+ {
116
210
  const red = parseInt( hexStr.substring( 1, 3 ), 16 ) / 255;
117
211
  const green = parseInt( hexStr.substring( 3, 5 ), 16 ) / 255;
118
212
  const blue = parseInt( hexStr.substring( 5, 7 ), 16 ) / 255;
@@ -121,7 +215,13 @@ function hexToRgb( hexStr ) {
121
215
 
122
216
  LX.hexToRgb = hexToRgb;
123
217
 
124
- function rgbToHex( rgb ) {
218
+ /**
219
+ * @method rgbToHex
220
+ * @description Convert a RGB color array to a hexadecimal string
221
+ * @param {Array} rgb Array containing R, G, B, A*
222
+ */
223
+ function rgbToHex( rgb )
224
+ {
125
225
  let hex = "#";
126
226
  for( let c of rgb ) {
127
227
  c = Math.floor( c * 255 );
@@ -132,7 +232,14 @@ function rgbToHex( rgb ) {
132
232
 
133
233
  LX.rgbToHex = rgbToHex;
134
234
 
135
- function measureRealWidth( value, paddingPlusMargin = 8 ) {
235
+ /**
236
+ * @method measureRealWidth
237
+ * @description Measure the pixel width of a text
238
+ * @param {Object} value Text to measure
239
+ * @param {Number} paddingPlusMargin Padding offset
240
+ */
241
+ function measureRealWidth( value, paddingPlusMargin = 8 )
242
+ {
136
243
  var i = document.createElement( "span" );
137
244
  i.className = "lexinputmeasure";
138
245
  i.innerHTML = value;
@@ -144,7 +251,12 @@ function measureRealWidth( value, paddingPlusMargin = 8 ) {
144
251
 
145
252
  LX.measureRealWidth = measureRealWidth;
146
253
 
147
- function simple_guidGenerator() {
254
+ /**
255
+ * @method simple_guidGenerator
256
+ * @description Get a random unique id
257
+ */
258
+ function simple_guidGenerator()
259
+ {
148
260
  var S4 = function() {
149
261
  return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
150
262
  };
@@ -153,7 +265,21 @@ function simple_guidGenerator() {
153
265
 
154
266
  LX.guidGenerator = simple_guidGenerator;
155
267
 
156
- function buildTextPattern( options = {} ) {
268
+ /**
269
+ * @method buildTextPattern
270
+ * @description Create a validation pattern using specific options
271
+ * @param {Object} options
272
+ * lowercase (Boolean): Text must contain a lowercase char
273
+ * uppercase (Boolean): Text must contain an uppercase char
274
+ * digit (Boolean): Text must contain a digit
275
+ * specialChar (Boolean): Text must contain a special char
276
+ * noSpaces (Boolean): Do not allow spaces in text
277
+ * minLength (Number): Text minimum length
278
+ * maxLength (Number): Text maximum length
279
+ * asRegExp (Boolean): Return pattern as Regular Expression instance
280
+ */
281
+ function buildTextPattern( options = {} )
282
+ {
157
283
  let patterns = [];
158
284
  if ( options.lowercase ) patterns.push("(?=.*[a-z])");
159
285
  if ( options.uppercase ) patterns.push("(?=.*[A-Z])");
@@ -170,67 +296,9 @@ function buildTextPattern( options = {} ) {
170
296
 
171
297
  LX.buildTextPattern = buildTextPattern;
172
298
 
173
- // Timer that works everywhere (from litegraph.js)
174
- if (typeof performance != "undefined") {
175
- LX.getTime = performance.now.bind(performance);
176
- } else if (typeof Date != "undefined" && Date.now) {
177
- LX.getTime = Date.now.bind(Date);
178
- } else if (typeof process != "undefined") {
179
- LX.getTime = function() {
180
- var t = process.hrtime();
181
- return t[0] * 0.001 + t[1] * 1e-6;
182
- };
183
- } else {
184
- LX.getTime = function getTime() {
185
- return new Date().getTime();
186
- };
187
- }
188
-
189
- let ASYNC_ENABLED = true;
190
-
191
- function doAsync( fn, ms ) {
192
- if( ASYNC_ENABLED )
193
- {
194
- setTimeout( fn, ms ?? 0 );
195
- }
196
- else
197
- {
198
- fn();
199
- }
200
- }
201
-
202
- // Math classes
203
-
204
- class vec2 {
205
-
206
- constructor( x, y ) {
207
- this.x = x ?? 0;
208
- this.y = y ?? ( x ?? 0 );
209
- }
210
-
211
- get xy() { return [ this.x, this.y ]; }
212
- get yx() { return [ this.y, this.x ]; }
213
-
214
- set ( x, y ) { this.x = x; this.y = y; }
215
- add ( v, v0 = new vec2() ) { v0.set( this.x + v.x, this.y + v.y ); return v0; }
216
- sub ( v, v0 = new vec2() ) { v0.set( this.x - v.x, this.y - v.y ); return v0; }
217
- 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; }
218
- 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; }
219
- abs ( v0 = new vec2() ) { v0.set( Math.abs( this.x ), Math.abs( this.y ) ); return v0; }
220
- dot ( v ) { return this.x * v.x + this.y * v.y; }
221
- len2 () { return this.dot( this ) }
222
- len () { return Math.sqrt( this.len2() ); }
223
- nrm ( v0 = new vec2() ) { v0.set( this.x, this.y ); return v0.mul( 1.0 / this.len(), v0 ); }
224
- dst ( v ) { return v.sub( this ).len(); }
225
- clp ( min, max, v0 = new vec2() ) { v0.set( clamp( this.x, min, max ), clamp( this.y, min, max ) ); return v0; }
226
- };
227
-
228
- LX.vec2 = vec2;
229
-
230
- // Other utils
231
-
232
299
  /**
233
300
  * @method makeDraggable
301
+ * @description Allow an element to be dragged
234
302
  * @param {Element} domEl
235
303
  * @param {Object} options
236
304
  * autoAdjust (Bool): Sets in a correct position at the beggining
@@ -238,8 +306,8 @@ LX.vec2 = vec2;
238
306
  * onMove (Function): Called each move event
239
307
  * onDragStart (Function): Called when drag event starts
240
308
  */
241
- function makeDraggable( domEl, options = { } ) {
242
-
309
+ function makeDraggable( domEl, options = { } )
310
+ {
243
311
  let offsetX = 0;
244
312
  let offsetY = 0;
245
313
  let currentTarget = null;
@@ -329,24 +397,166 @@ function makeDraggable( domEl, options = { } ) {
329
397
 
330
398
  LX.makeDraggable = makeDraggable;
331
399
 
332
- function create_global_searchbar( root ) {
400
+ /**
401
+ * @method makeCodeSnippet
402
+ * @description Create a code snippet in a specific language
403
+ * @param {String} code
404
+ * @param {Array} size
405
+ * @param {Object} options
406
+ * language (String):
407
+ * windowMode (Boolean):
408
+ * lineNumbers (Boolean):
409
+ * firstLine (Number): TODO
410
+ * linesAdded (Array):
411
+ * linesRemoved (Array):
412
+ * tabName (String):
413
+ */
414
+ function makeCodeSnippet( code, size, options = { } )
415
+ {
416
+ if( !LX.has('CodeEditor') )
417
+ {
418
+ console.error( "Import the CodeEditor component to create snippets!" );
419
+ return;
420
+ }
421
+
422
+ const snippet = document.createElement( "div" );
423
+ snippet.className = "lexcodesnippet";
424
+ snippet.style.width = size ? size[ 0 ] : "auto";
425
+ snippet.style.height = size ? size[ 1 ] : "auto";
426
+ const area = new Area( { noAppend: true } );
427
+ let editor = new LX.CodeEditor( area, {
428
+ skipInfo: true,
429
+ disableEdition: true,
430
+ allowAddScripts: false,
431
+ name: options.tabName,
432
+ // showTab: options.showTab ?? true
433
+ } );
434
+ editor.setText( code, options.language ?? "Plain Text" );
435
+
436
+ if( options.linesAdded )
437
+ {
438
+ const code = editor.root.querySelector( ".code" );
439
+ for( let l of options.linesAdded )
440
+ {
441
+ if( l.constructor == Number )
442
+ {
443
+ code.childNodes[ l - 1 ].classList.add( "added" );
444
+ }
445
+ else if( l.constructor == Array ) // It's a range
446
+ {
447
+ for( let i = ( l[0] - 1 ); i <= ( l[1] - 1 ); i++ )
448
+ {
449
+ code.childNodes[ i ].classList.add( "added" );
450
+ }
451
+ }
452
+ }
453
+ }
454
+
455
+ if( options.linesRemoved )
456
+ {
457
+ const code = editor.root.querySelector( ".code" );
458
+ for( let l of options.linesRemoved )
459
+ {
460
+ if( l.constructor == Number )
461
+ {
462
+ code.childNodes[ l - 1 ].classList.add( "removed" );
463
+ }
464
+ else if( l.constructor == Array ) // It's a range
465
+ {
466
+ for( let i = ( l[0] - 1 ); i <= ( l[1] - 1 ); i++ )
467
+ {
468
+ code.childNodes[ i ].classList.add( "removed" );
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ if( options.windowMode )
475
+ {
476
+ const windowActionButtons = document.createElement( "div" );
477
+ windowActionButtons.className = "lexwindowbuttons";
478
+ const aButton = document.createElement( "span" );
479
+ aButton.style.background = "#ee4f50";
480
+ const bButton = document.createElement( "span" );
481
+ bButton.style.background = "#f5b720";
482
+ const cButton = document.createElement( "span" );
483
+ cButton.style.background = "#53ca29";
484
+ windowActionButtons.appendChild( aButton );
485
+ windowActionButtons.appendChild( bButton );
486
+ windowActionButtons.appendChild( cButton );
487
+ const tabs = editor.root.querySelector( ".lexareatabs" );
488
+ tabs.prepend( windowActionButtons );
489
+ }
490
+
491
+ if( !( options.lineNumbers ?? true ) )
492
+ {
493
+ editor.root.classList.add( "no-gutter" );
494
+ }
495
+
496
+ snippet.appendChild( area.root );
497
+ return snippet;
498
+ }
499
+
500
+ LX.makeCodeSnippet = makeCodeSnippet;
501
+
502
+ /**
503
+ * @method registerCommandbarEntry
504
+ * @description Adds an extra command bar entry
505
+ * @param {String} name
506
+ * @param {Function} callback
507
+ */
508
+ function registerCommandbarEntry( name, callback )
509
+ {
510
+ LX.extraCommandbarEntries.push( { name, callback } );
511
+ }
512
+
513
+ LX.registerCommandbarEntry = registerCommandbarEntry;
514
+
515
+ // Math classes
516
+
517
+ class vec2 {
518
+
519
+ constructor( x, y ) {
520
+ this.x = x ?? 0;
521
+ this.y = y ?? ( x ?? 0 );
522
+ }
333
523
 
334
- let globalSearch = document.createElement("div");
335
- globalSearch.id = "global-search";
336
- globalSearch.className = "hidden";
337
- globalSearch.tabIndex = -1;
338
- root.appendChild( globalSearch );
524
+ get xy() { return [ this.x, this.y ]; }
525
+ get yx() { return [ this.y, this.x ]; }
526
+
527
+ set ( x, y ) { this.x = x; this.y = y; }
528
+ add ( v, v0 = new vec2() ) { v0.set( this.x + v.x, this.y + v.y ); return v0; }
529
+ sub ( v, v0 = new vec2() ) { v0.set( this.x - v.x, this.y - v.y ); return v0; }
530
+ 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; }
531
+ 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; }
532
+ abs ( v0 = new vec2() ) { v0.set( Math.abs( this.x ), Math.abs( this.y ) ); return v0; }
533
+ dot ( v ) { return this.x * v.x + this.y * v.y; }
534
+ len2 () { return this.dot( this ) }
535
+ len () { return Math.sqrt( this.len2() ); }
536
+ nrm ( v0 = new vec2() ) { v0.set( this.x, this.y ); return v0.mul( 1.0 / this.len(), v0 ); }
537
+ dst ( v ) { return v.sub( this ).len(); }
538
+ clp ( min, max, v0 = new vec2() ) { v0.set( clamp( this.x, min, max ), clamp( this.y, min, max ) ); return v0; }
539
+ };
540
+
541
+ LX.vec2 = vec2;
542
+
543
+ function _createCommandbar( root )
544
+ {
545
+ let commandbar = document.createElement( "dialog" );
546
+ commandbar.className = "commandbar";
547
+ commandbar.tabIndex = -1;
548
+ root.appendChild( commandbar );
339
549
 
340
550
  let allItems = [];
341
551
  let hoverElId = null;
342
552
 
343
- globalSearch.addEventListener('keydown', function( e ) {
553
+ commandbar.addEventListener('keydown', function( e ) {
344
554
  e.stopPropagation();
345
555
  e.stopImmediatePropagation();
346
556
  hoverElId = hoverElId ?? -1;
347
557
  if( e.key == 'Escape' )
348
558
  {
349
- this.classList.add("hidden");
559
+ this.close();
350
560
  _resetBar( true );
351
561
  }
352
562
  else if( e.key == 'Enter' )
@@ -355,7 +565,7 @@ function create_global_searchbar( root ) {
355
565
  if( el )
356
566
  {
357
567
  const isCheckbox = (el.item.type && el.item.type === 'checkbox');
358
- this.classList.toggle('hidden');
568
+ this.close();
359
569
  if( isCheckbox )
360
570
  {
361
571
  el.item.checked = !el.item.checked;
@@ -370,7 +580,7 @@ function create_global_searchbar( root ) {
370
580
  else if ( e.key == 'ArrowDown' && hoverElId < (allItems.length - 1) )
371
581
  {
372
582
  hoverElId++;
373
- globalSearch.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
583
+ commandbar.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
374
584
  allItems[ hoverElId ].classList.add('hovered');
375
585
 
376
586
  let dt = allItems[ hoverElId ].offsetHeight * (hoverElId + 1) - itemContainer.offsetHeight;
@@ -385,19 +595,19 @@ function create_global_searchbar( root ) {
385
595
  } else if ( e.key == 'ArrowUp' && hoverElId > 0 )
386
596
  {
387
597
  hoverElId--;
388
- globalSearch.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
598
+ commandbar.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
389
599
  allItems[ hoverElId ].classList.add('hovered');
390
600
  }
391
601
  });
392
602
 
393
- globalSearch.addEventListener('focusout', function( e ) {
603
+ commandbar.addEventListener('focusout', function( e ) {
394
604
  if( e.relatedTarget == e.currentTarget )
395
605
  {
396
606
  return;
397
607
  }
398
608
  e.stopPropagation();
399
609
  e.stopImmediatePropagation();
400
- this.classList.add( "hidden" );
610
+ this.close();
401
611
  _resetBar( true );
402
612
  });
403
613
 
@@ -406,9 +616,7 @@ function create_global_searchbar( root ) {
406
616
  {
407
617
  e.stopImmediatePropagation();
408
618
  e.stopPropagation();
409
- globalSearch.classList.toggle('hidden');
410
- globalSearch.querySelector('input').focus();
411
- _addElements( undefined );
619
+ LX.setCommandbarState( true );
412
620
  }
413
621
  else
414
622
  {
@@ -486,22 +694,22 @@ function create_global_searchbar( root ) {
486
694
  const isCheckbox = (i && i.type && i.type === 'checkbox');
487
695
  if( isCheckbox )
488
696
  {
489
- searchItem.innerHTML = "<a class='fa fa-check'></a><span>" + p + t + "</span>"
697
+ searchItem.innerHTML = "<a class='fa fa-check'></a><span>" + ( p + t ) + "</span>"
490
698
  }
491
699
  else
492
700
  {
493
- searchItem.innerHTML = p + t;
701
+ searchItem.innerHTML = ( p + t );
494
702
  }
495
703
  searchItem.entry_name = t;
496
704
  searchItem.callback = c;
497
705
  searchItem.item = i;
498
706
  searchItem.addEventListener('click', function(e) {
499
- this.callback.call(window, this.entry_name);
500
- globalSearch.classList.toggle('hidden');
707
+ this.callback.call( window, this.entry_name );
708
+ LX.setCommandbarState( false );
501
709
  _resetBar( true );
502
710
  });
503
711
  searchItem.addEventListener('mouseenter', function(e) {
504
- globalSearch.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
712
+ commandbar.querySelectorAll(".hovered").forEach(e => e.classList.remove('hovered'));
505
713
  this.classList.add('hovered');
506
714
  hoverElId = allItems.indexOf( this );
507
715
  });
@@ -535,7 +743,7 @@ function create_global_searchbar( root ) {
535
743
  _propagateAdd( c, filter, path );
536
744
  };
537
745
 
538
- const _addElements = filter => {
746
+ commandbar._addElements = filter => {
539
747
 
540
748
  _resetBar();
541
749
 
@@ -547,6 +755,16 @@ function create_global_searchbar( root ) {
547
755
  }
548
756
  }
549
757
 
758
+ for( let entry of LX.extraCommandbarEntries )
759
+ {
760
+ const name = entry.name;
761
+ if( !name.toLowerCase().includes( filter ) )
762
+ {
763
+ continue;
764
+ }
765
+ _addElement( name, entry.callback, "", {} );
766
+ }
767
+
550
768
  if( LX.has('CodeEditor') )
551
769
  {
552
770
  const instances = LX.CodeEditor.getInstances();
@@ -564,7 +782,7 @@ function create_global_searchbar( root ) {
564
782
 
565
783
  value += key + " <span class='lang-ext'>(" + languages[ l ].ext + ")</span>";
566
784
  if( key.toLowerCase().includes( filter ) ) {
567
- add_element( value, () => {
785
+ _addElement( value, () => {
568
786
  for( let i of instances ) {
569
787
  i._changeLanguage( l );
570
788
  }
@@ -575,14 +793,14 @@ function create_global_searchbar( root ) {
575
793
  }
576
794
 
577
795
  input.addEventListener('input', function(e) {
578
- _addElements( this.value.toLowerCase() );
796
+ commandbar._addElements( this.value.toLowerCase() );
579
797
  });
580
798
 
581
- globalSearch.appendChild( header );
582
- globalSearch.appendChild( tabArea.root );
583
- globalSearch.appendChild( itemContainer );
799
+ commandbar.appendChild( header );
800
+ commandbar.appendChild( tabArea.root );
801
+ commandbar.appendChild( itemContainer );
584
802
 
585
- return globalSearch;
803
+ return commandbar;
586
804
  }
587
805
 
588
806
  /**
@@ -592,6 +810,7 @@ function create_global_searchbar( root ) {
592
810
  * id: Id of the main area
593
811
  * skipRoot: Skip adding LX root container
594
812
  * skipDefaultArea: Skip creation of main area
813
+ * strictViewport: Use only window area
595
814
  */
596
815
 
597
816
  function init( options = { } )
@@ -614,16 +833,17 @@ function init( options = { } )
614
833
  this.root = root;
615
834
  this.container = document.body;
616
835
 
617
- // this.modal.toggleAttribute( 'hidden', true );
618
- // this.modal.toggle = function( force ) { this.toggleAttribute( 'hidden', force ); };
619
-
620
836
  this.modal.classList.add( 'hiddenOpacity' );
621
837
  this.modal.toggle = function( force ) { this.classList.toggle( 'hiddenOpacity', force ); };
622
838
 
623
839
  if( options.container )
840
+ {
624
841
  this.container = document.getElementById( options.container );
842
+ }
843
+
844
+ document.documentElement.setAttribute( "data-strictVP", ( options.strictViewport ?? true ) ? "true" : "false" );
625
845
 
626
- this.globalSearch = create_global_searchbar( this.container );
846
+ this.commandbar = _createCommandbar( this.container );
627
847
 
628
848
  this.container.appendChild( modal );
629
849
 
@@ -667,16 +887,48 @@ function init( options = { } )
667
887
  this.main_area = new Area( { id: options.id ?? 'mainarea' } );
668
888
  }
669
889
 
670
- window.matchMedia( "(prefers-color-scheme: dark)" ).addEventListener( "change", event => {
671
- const newColorScheme = event.matches ? "dark" : "light";
672
- LX.emit( "@on_new_color_scheme", newColorScheme );
673
- });
890
+ if( ( options.autoTheme ?? true ) && window.matchMedia && window.matchMedia( "(prefers-color-scheme: light)" ).matches )
891
+ {
892
+ LX.setTheme( "light" );
893
+
894
+ window.matchMedia( "(prefers-color-scheme: dark)" ).addEventListener( "change", event => {
895
+ LX.setTheme( event.matches ? "dark" : "light" );
896
+ });
897
+ }
674
898
 
675
899
  return this.main_area;
676
900
  }
677
901
 
678
902
  LX.init = init;
679
903
 
904
+ /**
905
+ * @method setCommandbarState
906
+ * @param {Boolean} value
907
+ * @param {Boolean} resetEntries
908
+ */
909
+
910
+ function setCommandbarState( value, resetEntries = true )
911
+ {
912
+ const cb = this.commandbar;
913
+
914
+ if( value )
915
+ {
916
+ cb.show();
917
+ cb.querySelector('input').focus();
918
+
919
+ if( resetEntries )
920
+ {
921
+ cb._addElements( undefined );
922
+ }
923
+ }
924
+ else
925
+ {
926
+ cb.close();
927
+ }
928
+ }
929
+
930
+ LX.setCommandbarState = setCommandbarState;
931
+
680
932
  /**
681
933
  * @method message
682
934
  * @param {String} text
@@ -709,9 +961,9 @@ LX.message = message;
709
961
  * @param {String} title (Optional)
710
962
  * @param {*} options
711
963
  * id: Id of the message dialog
712
- * time: (Number) Delay time before close automatically (ms). Defalut: [3000]
713
- * position: (Array) [x,y] Dialog position in screen. Default: [screen centered]
714
- * size: (Array) [width, height]
964
+ * timeout (Number): Delay time before it closes automatically (ms). Default: [3000]
965
+ * position (Array): [x,y] Dialog position in screen. Default: [screen centered]
966
+ * size (Array): [width, height]
715
967
  */
716
968
 
717
969
  function popup( text, title, options = {} )
@@ -721,7 +973,7 @@ function popup( text, title, options = {} )
721
973
  throw("No message to show");
722
974
  }
723
975
 
724
- options.size = options.size ?? [ "auto", "auto" ];
976
+ options.size = options.size ?? [ "max-content", "auto" ];
725
977
  options.class = "lexpopup";
726
978
 
727
979
  const time = options.timeout || 3000;
@@ -729,13 +981,9 @@ function popup( text, title, options = {} )
729
981
  p.addTextArea( null, text, null, { disabled: true, fitHeight: true } );
730
982
  }, options );
731
983
 
732
- dialog.root.classList.add( 'fadein' );
733
- setTimeout(() => {
734
- dialog.root.classList.remove( 'fadein' );
735
- dialog.root.classList.add( 'fadeout' );
736
- }, time - 1000 );
737
-
738
- setTimeout( dialog.close, time );
984
+ setTimeout( () => {
985
+ dialog.close();
986
+ }, Math.max( time, 150 ) );
739
987
 
740
988
  return dialog;
741
989
  }
@@ -820,6 +1068,25 @@ function badge( text, className, options = {} )
820
1068
 
821
1069
  LX.badge = badge;
822
1070
 
1071
+ /**
1072
+ * @method makeContainer
1073
+ * @param {Array} size
1074
+ * @param {String} className
1075
+ * @param {Object} overrideStyle
1076
+ */
1077
+
1078
+ function makeContainer( size, className, overrideStyle = {} )
1079
+ {
1080
+ const container = document.createElement( "div" );
1081
+ container.className = "lexcontainer " + ( className ?? "" );
1082
+ container.style.width = size && size[ 0 ] ? size[ 0 ] : "100%";
1083
+ container.style.height = size && size[ 1 ] ? size[ 1 ] : "100%";
1084
+ Object.assign( container.style, overrideStyle );
1085
+ return container;
1086
+ }
1087
+
1088
+ LX.makeContainer = makeContainer;
1089
+
823
1090
  /*
824
1091
  * Events and Signals
825
1092
  */
@@ -1302,8 +1569,8 @@ class Area {
1302
1569
  }
1303
1570
 
1304
1571
  area1.root.style.width = "100%";
1305
- area1.root.style.height = "calc( " + height1 + " - " + data + " )";
1306
- area2.root.style.height = "calc( " + height2 + " - " + data + " )";
1572
+ area1.root.style.height = ( height1 == "auto" ? height1 : "calc( " + height1 + " - " + data + " )");
1573
+ area2.root.style.height = ( height2 == "auto" ? height2 : "calc( " + height2 + " - " + data + " )");
1307
1574
  }
1308
1575
  }
1309
1576
 
@@ -1903,7 +2170,7 @@ class Tabs {
1903
2170
 
1904
2171
  area.root.classList.add( "lexareatabscontainer" );
1905
2172
 
1906
- area.split({type: 'vertical', sizes: "auto", resize: false, top: 6});
2173
+ area.split({type: 'vertical', sizes: options.sizes ?? "auto", resize: false, top: 6});
1907
2174
  area.sections[0].attach( container );
1908
2175
 
1909
2176
  this.area = area.sections[1];
@@ -2590,6 +2857,17 @@ class Menubar {
2590
2857
  const swapIcon = document.createElement( "a" );
2591
2858
  swapIcon.className = data.swap + " swap-on lexicon";
2592
2859
  button.appendChild( swapIcon );
2860
+
2861
+ button.swap = function() {
2862
+ const swapInput = this.querySelector( "input" );
2863
+ swapInput.checked = !swapInput.checked;
2864
+ };
2865
+
2866
+ // Set if swap has to be performed
2867
+ button.setState = function( v ) {
2868
+ const swapInput = this.querySelector( "input" );
2869
+ swapInput.checked = v;
2870
+ };
2593
2871
  }
2594
2872
 
2595
2873
  trigger.addEventListener("click", e => {
@@ -2758,6 +3036,7 @@ class Widget {
2758
3036
  static FORM = 27;
2759
3037
  static DIAL = 28;
2760
3038
  static COUNTER = 29;
3039
+ static TABLE = 30;
2761
3040
 
2762
3041
  static NO_CONTEXT_TYPES = [
2763
3042
  Widget.BUTTON,
@@ -2848,6 +3127,7 @@ class Widget {
2848
3127
  case Widget.FORM: return "Form";
2849
3128
  case Widget.DIAL: return "Dial";
2850
3129
  case Widget.COUNTER: return "Counter";
3130
+ case Widget.TABLE: return "Table";
2851
3131
  case Widget.CUSTOM: return this.customName;
2852
3132
  }
2853
3133
 
@@ -3870,8 +4150,8 @@ class Panel {
3870
4150
 
3871
4151
  let searchIcon = document.createElement('a');
3872
4152
  searchIcon.className = "fa-solid fa-magnifying-glass";
3873
- element.appendChild(input);
3874
4153
  element.appendChild(searchIcon);
4154
+ element.appendChild(input);
3875
4155
 
3876
4156
  input.addEventListener("input", (e) => {
3877
4157
  if(options.callback)
@@ -3923,26 +4203,28 @@ class Panel {
3923
4203
  }
3924
4204
  }
3925
4205
 
3926
- _search_options(options, value) {
3927
- // push to right container
4206
+ _filterOptions( options, value ) {
4207
+
4208
+ // Push to right container
3928
4209
  const emptyFilter = !value.length;
3929
4210
  let filteredOptions = [];
3930
- // add widgets
3931
- for( let i = 0; i < options.length; i++) {
3932
- let o = options[i];
3933
- if(!emptyFilter)
4211
+
4212
+ // Add widgets
4213
+ for( let i = 0; i < options.length; i++ )
4214
+ {
4215
+ let o = options[ i ];
4216
+ if( !emptyFilter )
3934
4217
  {
3935
- let toCompare = (typeof o == 'string') ? o : o.value;
3936
- ;
4218
+ let toCompare = ( typeof o == 'string' ) ? o : o.value;
3937
4219
  const filterWord = value.toLowerCase();
3938
4220
  const name = toCompare.toLowerCase();
3939
- if(!name.includes(filterWord)) continue;
4221
+ if( !name.includes( filterWord ) ) continue;
3940
4222
  }
3941
- // insert filtered widget
3942
- filteredOptions.push(o);
4223
+
4224
+ filteredOptions.push( o );
3943
4225
  }
3944
4226
 
3945
- this.refresh(filteredOptions);
4227
+ this.refresh( filteredOptions );
3946
4228
  }
3947
4229
 
3948
4230
  _trigger( event, callback ) {
@@ -4429,66 +4711,82 @@ class Panel {
4429
4711
  * @param {Array} values Each of the {value, callback} items
4430
4712
  * @param {*} options:
4431
4713
  * float: Justify content (left, center, right) [center]
4714
+ * selected: Selected item by default by value
4432
4715
  * noSelection: Buttons can be clicked, but they are not selectable
4433
4716
  */
4434
4717
 
4435
4718
  addComboButtons( name, values, options = {} ) {
4436
4719
 
4437
- let widget = this.create_widget(name, Widget.BUTTON, options);
4720
+ let widget = this.create_widget( name, Widget.BUTTON, options );
4438
4721
  let element = widget.domEl;
4439
4722
 
4440
4723
  let that = this;
4441
4724
  let container = document.createElement('div');
4442
4725
  container.className = "lexcombobuttons ";
4443
- if( options.float ) container.className += options.float;
4726
+
4727
+ if( options.float )
4728
+ {
4729
+ container.className += options.float;
4730
+ }
4731
+
4444
4732
  container.style.width = "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
4445
4733
 
4446
- let should_select = !(options.noSelection ?? false);
4734
+ let buttonsBox = document.createElement('div');
4735
+ buttonsBox.className = "lexcombobuttonsbox ";
4736
+
4737
+ let shouldSelect = !( options.noSelection ?? false );
4738
+
4447
4739
  for( let b of values )
4448
4740
  {
4449
- if( !b.value ) throw("Set 'value' for each button!");
4741
+ if( !b.value )
4742
+ {
4743
+ throw( "Set 'value' for each button!" );
4744
+ }
4450
4745
 
4451
4746
  let buttonEl = document.createElement('button');
4452
4747
  buttonEl.className = "lexbutton combo";
4453
4748
  buttonEl.title = b.icon ? b.value : "";
4454
- if(options.buttonClass)
4455
- buttonEl.classList.add(options.buttonClass);
4749
+ buttonEl.id = b.id ?? "";
4456
4750
 
4457
- if(options.selected == b.value)
4458
- buttonEl.classList.add("selected");
4751
+ if( options.buttonClass )
4752
+ {
4753
+ buttonEl.classList.add( options.buttonClass );
4754
+ }
4459
4755
 
4460
- if(b.id)
4461
- buttonEl.id = b.id;
4756
+ if( shouldSelect && options.selected == b.value )
4757
+ {
4758
+ buttonEl.classList.add("selected");
4759
+ }
4462
4760
 
4463
- buttonEl.innerHTML = (b.icon ? "<a class='" + b.icon +"'></a>" : "") + "<span>" + (b.icon ? "" : b.value) + "</span>";
4761
+ buttonEl.innerHTML = ( b.icon ? "<a class='" + b.icon +"'></a>" : "" ) + "<span>" + ( b.icon ? "" : b.value ) + "</span>";
4464
4762
 
4465
- if(options.disabled)
4466
- buttonEl.setAttribute("disabled", true);
4763
+ if( options.disabled )
4764
+ {
4765
+ buttonEl.setAttribute( "disabled", true );
4766
+ }
4467
4767
 
4468
- buttonEl.addEventListener("click", function(e) {
4469
- if(should_select) {
4768
+ buttonEl.addEventListener("click", function( e ) {
4769
+ if( shouldSelect )
4770
+ {
4470
4771
  container.querySelectorAll('button').forEach( s => s.classList.remove('selected'));
4471
4772
  this.classList.add('selected');
4472
4773
  }
4473
- that._trigger( new IEvent(name, b.value, e), b.callback );
4474
- });
4475
4774
 
4476
- container.appendChild(buttonEl);
4775
+ that._trigger( new IEvent( name, b.value, e ), b.callback );
4776
+ });
4477
4777
 
4478
- // Remove branch padding and margins
4479
- if(widget.name === undefined) {
4480
- buttonEl.className += " noname";
4481
- buttonEl.style.width = "100%";
4482
- }
4778
+ buttonsBox.appendChild( buttonEl );
4483
4779
  }
4484
4780
 
4485
4781
  // Remove branch padding and margins
4486
- if(widget.name !== undefined) {
4782
+ if( !widget.name)
4783
+ {
4487
4784
  element.className += " noname";
4488
4785
  container.style.width = "100%";
4489
4786
  }
4490
4787
 
4491
- element.appendChild(container);
4788
+ container.appendChild( buttonsBox );
4789
+ element.appendChild( container );
4492
4790
 
4493
4791
  return widget;
4494
4792
  }
@@ -4740,6 +5038,8 @@ class Panel {
4740
5038
  * filter: Add a search bar to the widget [false]
4741
5039
  * disabled: Make the widget disabled [false]
4742
5040
  * skipReset: Don't add the reset value button when value changes
5041
+ * placeholder: Placeholder for the filter input
5042
+ * emptyMsg: Custom message to show when no filtered results
4743
5043
  */
4744
5044
 
4745
5045
  addDropdown( name, values, value, callback, options = {} ) {
@@ -4785,17 +5085,74 @@ class Panel {
4785
5085
  let buttonName = value;
4786
5086
  buttonName += "<a class='fa-solid fa-angle-down' style='float:right; margin-right: 3px;'></a>";
4787
5087
 
4788
- this.queue(container);
5088
+ this.queue( container );
5089
+
5090
+ const _placeOptions = ( parent ) => {
4789
5091
 
4790
- const _getMaxListWidth = () => {
5092
+ console.log("Replacing container");
4791
5093
 
4792
- let maxWidth = 0;
4793
- for( let i of values )
5094
+ const overflowContainer = parent.getParentArea();
5095
+ const rect = selectedOption.getBoundingClientRect();
5096
+ const nestedDialog = parent.parentElement.closest( "dialog" );
5097
+
5098
+ // Manage vertical aspect
5099
+ {
5100
+ const listHeight = parent.offsetHeight;
5101
+ let topPosition = rect.y;
5102
+
5103
+ let maxY = window.innerHeight;
5104
+
5105
+ if( overflowContainer )
5106
+ {
5107
+ const parentRect = overflowContainer.getBoundingClientRect();
5108
+ maxY = parentRect.y + parentRect.height;
5109
+ }
5110
+
5111
+ if( nestedDialog )
5112
+ {
5113
+ const rect = nestedDialog.getBoundingClientRect();
5114
+ topPosition -= rect.y;
5115
+ }
5116
+
5117
+ parent.style.top = ( topPosition + selectedOption.offsetHeight ) + 'px';
5118
+
5119
+ const showAbove = ( topPosition + listHeight ) > maxY;
5120
+ if( showAbove )
5121
+ {
5122
+ parent.style.top = ( topPosition - listHeight ) + 'px';
5123
+ parent.classList.add( "place-above" );
5124
+ }
5125
+ }
5126
+
5127
+ // Manage horizontal aspect
4794
5128
  {
4795
- const iString = String( i );
4796
- maxWidth = Math.max( iString.length, maxWidth );
5129
+ const listWidth = parent.offsetWidth;
5130
+ let leftPosition = rect.x;
5131
+
5132
+ parent.style.minWidth = ( rect.width ) + 'px';
5133
+
5134
+ if( nestedDialog )
5135
+ {
5136
+ const rect = nestedDialog.getBoundingClientRect();
5137
+ leftPosition -= rect.x;
5138
+ }
5139
+
5140
+ parent.style.left = ( leftPosition ) + 'px';
5141
+
5142
+ let maxX = window.innerWidth;
5143
+
5144
+ if( overflowContainer )
5145
+ {
5146
+ const parentRect = overflowContainer.getBoundingClientRect();
5147
+ maxX = parentRect.x + parentRect.width;
5148
+ }
5149
+
5150
+ const showLeft = ( leftPosition + listWidth ) > maxX;
5151
+ if( showLeft )
5152
+ {
5153
+ parent.style.left = ( leftPosition - ( listWidth - rect.width ) ) + 'px';
5154
+ }
4797
5155
  }
4798
- return Math.max( maxWidth * 10, 80 );
4799
5156
  };
4800
5157
 
4801
5158
  let selectedOption = this.addButton( null, buttonName, ( value, event ) => {
@@ -4805,51 +5162,50 @@ class Panel {
4805
5162
  return;
4806
5163
  }
4807
5164
 
4808
- list.toggleAttribute( "hidden" );
4809
- list.classList.remove( "place-above" );
4810
-
4811
- const listHeight = 26 * values.length;
4812
- const rect = selectedOption.getBoundingClientRect();
4813
- const topPosition = rect.y;
5165
+ listDialog.classList.remove( "place-above" );
5166
+ const opened = listDialog.hasAttribute( "open" );
4814
5167
 
4815
- let maxY = window.innerHeight;
4816
-
4817
- if( this.mainContainer )
5168
+ if( !opened )
4818
5169
  {
4819
- const parentRect = this.mainContainer.getBoundingClientRect();
4820
- maxY = parentRect.y + parentRect.height;
5170
+ listDialog.show();
5171
+ _placeOptions( listDialog );
5172
+ }
5173
+ else
5174
+ {
5175
+ listDialog.close();
4821
5176
  }
4822
5177
 
4823
- list.style.top = ( topPosition + selectedOption.offsetHeight ) + 'px';
4824
-
4825
- const showAbove = ( topPosition + listHeight ) > maxY;
4826
- if( showAbove )
5178
+ if( filter )
4827
5179
  {
4828
- list.style.top = ( topPosition - listHeight ) + 'px';
4829
- list.classList.add( "place-above" );
5180
+ filter.querySelector( "input" ).focus();
4830
5181
  }
4831
5182
 
4832
- list.style.width = (event.currentTarget.clientWidth) + 'px';
4833
- list.style.minWidth = (_getMaxListWidth()) + 'px';
4834
- list.focus();
4835
- }, { buttonClass: "array", skipInlineCount: true });
5183
+ }, { buttonClass: "array", skipInlineCount: true, disabled: options.disabled });
4836
5184
 
4837
5185
  this.clearQueue();
4838
5186
 
4839
5187
  selectedOption.style.width = "100%";
4840
5188
 
4841
5189
  selectedOption.refresh = (v) => {
4842
- if(selectedOption.querySelector("span").innerText == "")
5190
+ if( selectedOption.querySelector("span").innerText == "" )
5191
+ {
4843
5192
  selectedOption.querySelector("span").innerText = v;
5193
+ }
4844
5194
  else
5195
+ {
4845
5196
  selectedOption.querySelector("span").innerHTML = selectedOption.querySelector("span").innerHTML.replaceAll(selectedOption.querySelector("span").innerText, v);
5197
+ }
4846
5198
  }
4847
5199
 
4848
5200
  // Add dropdown options container
5201
+
5202
+ const listDialog = document.createElement( 'dialog' );
5203
+ listDialog.className = "lexdropdownoptions";
5204
+
4849
5205
  let list = document.createElement( 'ul' );
4850
5206
  list.tabIndex = -1;
4851
5207
  list.className = "lexoptions";
4852
- list.hidden = true;
5208
+ listDialog.appendChild( list )
4853
5209
 
4854
5210
  list.addEventListener( 'focusout', function( e ) {
4855
5211
  e.stopPropagation();
@@ -4867,97 +5223,118 @@ class Panel {
4867
5223
  {
4868
5224
  return;
4869
5225
  }
4870
- this.toggleAttribute( 'hidden', true );
5226
+ listDialog.close();
4871
5227
  });
4872
5228
 
4873
5229
  // Add filter options
4874
5230
  let filter = null;
4875
- if(options.filter ?? false)
5231
+ if( options.filter ?? false )
4876
5232
  {
4877
- filter = this._addFilter("Search option", {container: list, callback: this._search_options.bind(list, values)});
4878
- }
4879
-
4880
- // Create option list to empty it easily..
4881
- const listOptions = document.createElement('span');
4882
- list.appendChild( listOptions );
5233
+ filter = this._addFilter( options.placeholder ?? "Search...", { container: list, callback: this._filterOptions.bind( list, values )} );
4883
5234
 
4884
- if( filter )
4885
- {
4886
- list.prepend( filter );
4887
- listOptions.style.height = "calc(100% - 25px)";
5235
+ list.appendChild( filter );
4888
5236
 
4889
5237
  filter.addEventListener('focusout', function( e ) {
4890
5238
  if (e.relatedTarget && e.relatedTarget.tagName == "UL" && e.relatedTarget.classList.contains("lexoptions"))
4891
5239
  {
4892
5240
  return;
4893
5241
  }
4894
- list.toggleAttribute( 'hidden', true );
5242
+ listDialog.close();
4895
5243
  });
4896
5244
  }
4897
5245
 
5246
+ // Create option list to empty it easily..
5247
+ const listOptions = document.createElement('span');
5248
+ listOptions.style.height = "calc(100% - 25px)";
5249
+ list.appendChild( listOptions );
5250
+
4898
5251
  // Add dropdown options list
4899
- list.refresh = options => {
5252
+ list.refresh = ( options ) => {
4900
5253
 
4901
5254
  // Empty list
4902
5255
  listOptions.innerHTML = "";
4903
5256
 
4904
- for(let i = 0; i < options.length; i++)
5257
+ if( !options.length )
5258
+ {
5259
+ let iValue = options.emptyMsg ?? "No options found.";
5260
+
5261
+ let option = document.createElement( "div" );
5262
+ option.className = "option";
5263
+ option.style.flexDirection = "unset";
5264
+ option.innerHTML = iValue;
5265
+
5266
+ let li = document.createElement( "li" );
5267
+ li.className = "lexdropdownitem empty";
5268
+ li.appendChild( option );
5269
+
5270
+ listOptions.appendChild( li );
5271
+ return;
5272
+ }
5273
+
5274
+ for( let i = 0; i < options.length; i++ )
4905
5275
  {
4906
- let iValue = options[i];
4907
- let li = document.createElement('li');
4908
- let option = document.createElement('div');
5276
+ let iValue = options[ i ];
5277
+ let li = document.createElement( "li" );
5278
+ let option = document.createElement( "div" );
4909
5279
  option.className = "option";
4910
- li.appendChild(option);
4911
- li.addEventListener("click", (e) => {
4912
- element.querySelector(".lexoptions").toggleAttribute('hidden', true);
4913
- const currentSelected = element.querySelector(".lexoptions .selected");
4914
- if(currentSelected) currentSelected.classList.remove("selected");
4915
- value = e.currentTarget.getAttribute("value");
4916
- e.currentTarget.toggleAttribute('hidden', false);
4917
- e.currentTarget.classList.add("selected");
5280
+ li.appendChild( option );
5281
+
5282
+ li.addEventListener( "click", e => {
5283
+ listDialog.close();
5284
+ const currentSelected = element.querySelector( ".lexoptions .selected" );
5285
+ if(currentSelected) currentSelected.classList.remove( "selected" );
5286
+ value = e.currentTarget.getAttribute( "value" );
5287
+ e.currentTarget.toggleAttribute( "hidden", false );
5288
+ e.currentTarget.classList.add( "selected" );
4918
5289
  selectedOption.refresh(value);
4919
5290
 
4920
- let btn = element.querySelector(".lexwidgetname .lexicon");
4921
- if(btn) btn.style.display = (value != wValue.iValue ? "block" : "none");
4922
- that._trigger( new IEvent(name, value, null), callback );
5291
+ let btn = element.querySelector( ".lexwidgetname .lexicon" );
5292
+ if( btn ) btn.style.display = (value != wValue.iValue ? "block" : "none");
5293
+ that._trigger( new IEvent( name, value, null ), callback );
4923
5294
 
4924
5295
  // Reset filter
4925
- if(filter)
5296
+ if( filter )
4926
5297
  {
4927
- filter.querySelector('input').value = "";
4928
- this._search_options.bind(list, values, "")();
5298
+ filter.querySelector( "input" ).value = "";
5299
+ this._filterOptions.bind( list, values, "" )();
4929
5300
  }
4930
5301
  });
4931
5302
 
4932
5303
  // Add string option
4933
- if( iValue.constructor != Object ) {
4934
- option.style.flexDirection = 'unset';
5304
+ if( iValue.constructor != Object )
5305
+ {
5306
+ option.style.flexDirection = "unset";
4935
5307
  option.innerHTML = "</a><span>" + iValue + "</span><a class='fa-solid fa-check'>";
4936
5308
  option.value = iValue;
4937
- li.setAttribute("value", iValue);
5309
+ li.setAttribute( "value", iValue );
4938
5310
  li.className = "lexdropdownitem";
4939
- if( i == (options.length - 1) ) li.className += " last";
4940
- if(iValue == value) {
4941
- li.classList.add("selected");
5311
+
5312
+ if( iValue == value )
5313
+ {
5314
+ li.classList.add( "selected" );
4942
5315
  wValue.innerHTML = iValue;
4943
5316
  }
4944
5317
  }
4945
- else {
5318
+ else
5319
+ {
4946
5320
  // Add image option
4947
- let img = document.createElement("img");
5321
+ let img = document.createElement( "img" );
4948
5322
  img.src = iValue.src;
4949
- li.setAttribute("value", iValue.value);
5323
+ li.setAttribute( "value", iValue.value );
4950
5324
  li.className = "lexlistitem";
4951
5325
  option.innerText = iValue.value;
4952
5326
  option.className += " media";
4953
- option.prepend(img);
5327
+ option.prepend( img );
4954
5328
 
4955
- option.setAttribute("value", iValue.value);
4956
- option.setAttribute("data-index", i);
4957
- option.setAttribute("data-src", iValue.src);
4958
- option.setAttribute("title", iValue.value);
4959
- if(value == iValue.value)
4960
- li.classList.add("selected");
5329
+ option.setAttribute( "value", iValue.value );
5330
+ option.setAttribute( "data-index", i );
5331
+ option.setAttribute( "data-src", iValue.src );
5332
+ option.setAttribute( "title", iValue.value );
5333
+
5334
+ if( value == iValue.value )
5335
+ {
5336
+ li.classList.add( "selected" );
5337
+ }
4961
5338
  }
4962
5339
 
4963
5340
  listOptions.appendChild( li );
@@ -4966,7 +5343,7 @@ class Panel {
4966
5343
 
4967
5344
  list.refresh( values );
4968
5345
 
4969
- container.appendChild( list );
5346
+ container.appendChild( listDialog );
4970
5347
  element.appendChild( container );
4971
5348
 
4972
5349
  // Remove branch padding and margins
@@ -7145,6 +7522,228 @@ class Panel {
7145
7522
 
7146
7523
  return widget;
7147
7524
  }
7525
+
7526
+ /**
7527
+ * @method addTable
7528
+ * @param {String} name Widget name
7529
+ * @param {Number} data Table data
7530
+ * @param {*} options:
7531
+ * head: Table headers (each of the headers per column)
7532
+ * body: Table body (data per row for each column)
7533
+ * rowActions: Allow to add actions per row
7534
+ * onMenuAction: Function callback to fill the "menu" context
7535
+ * selectable: Each row can be selected
7536
+ */
7537
+
7538
+ addTable( name, data, options = { } ) {
7539
+
7540
+ if( !data )
7541
+ {
7542
+ throw( "Data is needed to create a table!" );
7543
+ }
7544
+
7545
+ let widget = this.create_widget( name, Widget.TABLE, options );
7546
+
7547
+ widget.onGetValue = () => {
7548
+
7549
+ };
7550
+
7551
+ widget.onSetValue = ( newValue, skipCallback ) => {
7552
+
7553
+ };
7554
+
7555
+ let element = widget.domEl;
7556
+
7557
+ const container = document.createElement('div');
7558
+ container.className = "lextable";
7559
+ container.style.width = "calc( 100% - " + LX.DEFAULT_NAME_WIDTH + ")";
7560
+
7561
+ const table = document.createElement( 'table' );
7562
+ container.appendChild( table );
7563
+
7564
+ data.head = data.head ?? [];
7565
+ data.body = data.body ?? [];
7566
+ data.orderMap = { };
7567
+ data.checkMap = { };
7568
+
7569
+ function compareFn( idx, order, a, b) {
7570
+ if (a[idx] < b[idx]) return -order;
7571
+ else if (a[idx] > b[idx]) return order;
7572
+ return 0;
7573
+ }
7574
+
7575
+ widget.refreshTable = () => {
7576
+
7577
+ table.innerHTML = "";
7578
+
7579
+ // Head
7580
+ {
7581
+ const head = document.createElement( 'thead' );
7582
+ head.className = "lextablehead";
7583
+ table.appendChild( head );
7584
+
7585
+ const hrow = document.createElement( 'tr' );
7586
+
7587
+ if( options.selectable )
7588
+ {
7589
+ const th = document.createElement( 'th' );
7590
+ const input = document.createElement( 'input' );
7591
+ input.type = "checkbox";
7592
+ input.className = "lexcheckbox";
7593
+ input.checked = data.checkMap[ ":root" ] ?? false;
7594
+ th.appendChild( input );
7595
+
7596
+ input.addEventListener( 'change', function() {
7597
+
7598
+ data.checkMap[ ":root" ] = this.checked;
7599
+
7600
+ const body = table.querySelector( "tbody" );
7601
+ for( const el of body.childNodes )
7602
+ {
7603
+ data.checkMap[ el.getAttribute( "rowId" ) ] = this.checked;
7604
+ el.querySelector( "input" ).checked = this.checked;
7605
+ }
7606
+ });
7607
+
7608
+ hrow.appendChild( th );
7609
+ }
7610
+
7611
+ for( const headData of data.head )
7612
+ {
7613
+ const th = document.createElement( 'th' );
7614
+ th.innerHTML = `${ headData } <a class="fa-solid fa-sort"></a>`;
7615
+
7616
+ th.querySelector( 'a' ).addEventListener( 'click', () => {
7617
+
7618
+ if( !data.orderMap[ headData ] )
7619
+ {
7620
+ data.orderMap[ headData ] = 1;
7621
+ }
7622
+
7623
+ const idx = data.head.indexOf(headData);
7624
+ data.body = data.body.sort( compareFn.bind( this, idx,data.orderMap[ headData ] ) );
7625
+ data.orderMap[ headData ] = -data.orderMap[ headData ];
7626
+
7627
+ widget.refreshTable();
7628
+
7629
+ });
7630
+
7631
+ hrow.appendChild( th );
7632
+ }
7633
+
7634
+ // Add empty header column
7635
+ if( options.rowActions )
7636
+ {
7637
+ const th = document.createElement( 'th' );
7638
+ th.className = "sm";
7639
+ hrow.appendChild( th );
7640
+ }
7641
+
7642
+ head.appendChild( hrow );
7643
+ }
7644
+
7645
+ // Body
7646
+ {
7647
+ const body = document.createElement( 'tbody' );
7648
+ body.className = "lextablebody";
7649
+ table.appendChild( body );
7650
+
7651
+ for( let r = 0; r < data.body.length; ++r )
7652
+ {
7653
+ const bodyData = data.body[ r ];
7654
+ const row = document.createElement( 'tr' );
7655
+ const rowId = LX.getSupportedDOMName( bodyData.join( '-' ) );
7656
+ row.setAttribute( "rowId", rowId );
7657
+
7658
+ if( options.selectable )
7659
+ {
7660
+ const td = document.createElement( 'td' );
7661
+ const input = document.createElement( 'input' );
7662
+ input.type = "checkbox";
7663
+ input.className = "lexcheckbox";
7664
+ input.checked = data.checkMap[ rowId ];
7665
+ td.appendChild( input );
7666
+
7667
+ input.addEventListener( 'change', function() {
7668
+ data.checkMap[ rowId ] = this.checked;
7669
+ });
7670
+
7671
+ row.appendChild( td );
7672
+ }
7673
+
7674
+ for( const rowData of bodyData )
7675
+ {
7676
+ const td = document.createElement( 'td' );
7677
+ td.innerHTML = `${ rowData }`;
7678
+ row.appendChild( td );
7679
+ }
7680
+
7681
+ if( options.rowActions )
7682
+ {
7683
+ const td = document.createElement( 'td' );
7684
+ td.className = "sm";
7685
+
7686
+ const buttons = document.createElement( 'div' );
7687
+ buttons.className = "lextablebuttons";
7688
+ td.appendChild( buttons );
7689
+
7690
+ for( const action of options.rowActions )
7691
+ {
7692
+ const button = document.createElement( 'a' );
7693
+ button.className = "lexicon";
7694
+
7695
+ if( action == "delete" )
7696
+ {
7697
+ button.className += " fa-solid fa-trash-can";
7698
+ button.addEventListener( 'click', function() {
7699
+ // Don't need to refresh table..
7700
+ data.body.splice( r, 1 );
7701
+ row.remove();
7702
+ });
7703
+ }
7704
+ else if( action == "menu" )
7705
+ {
7706
+ button.className += " fa-solid fa-ellipsis";
7707
+ button.addEventListener( 'click', function( event ) {
7708
+ addContextMenu( null, event, c => {
7709
+ if( options.onMenuAction )
7710
+ {
7711
+ options.onMenuAction( c );
7712
+ return;
7713
+ }
7714
+ console.warn( "Using <Menu action> without action callbacks." );
7715
+ } );
7716
+ });
7717
+ }
7718
+ else // custom actions
7719
+ {
7720
+ console.assert( action.constructor == Object );
7721
+ button.className += ` ${ action.icon }`;
7722
+ }
7723
+
7724
+ buttons.appendChild( button );
7725
+ }
7726
+
7727
+ row.appendChild( td );
7728
+ }
7729
+
7730
+ body.appendChild( row );
7731
+ }
7732
+ }
7733
+ }
7734
+
7735
+ widget.refreshTable();
7736
+
7737
+ if( !widget.name )
7738
+ {
7739
+ element.className += " noname";
7740
+ container.style.width = "100%";
7741
+ }
7742
+
7743
+ element.appendChild( container );
7744
+
7745
+ return widget;
7746
+ }
7148
7747
  }
7149
7748
 
7150
7749
  LX.Panel = Panel;
@@ -7501,16 +8100,15 @@ class Dialog {
7501
8100
  draggable = options.draggable ?? true,
7502
8101
  modal = options.modal ?? false;
7503
8102
 
7504
- if( modal )
7505
- {
7506
- LX.modal.toggle( false );
7507
- }
7508
-
7509
- var root = document.createElement('div');
8103
+ var root = document.createElement('dialog');
7510
8104
  root.className = "lexdialog " + (options.class ?? "");
7511
8105
  root.id = options.id ?? "dialog" + Dialog._last_id++;
7512
8106
  LX.root.appendChild( root );
7513
8107
 
8108
+ doAsync( () => {
8109
+ modal ? root.showModal() : root.show();
8110
+ }, 10 );
8111
+
7514
8112
  let that = this;
7515
8113
 
7516
8114
  var titleDiv = document.createElement('div');
@@ -7592,18 +8190,17 @@ class Dialog {
7592
8190
 
7593
8191
  if( !options.onclose )
7594
8192
  {
7595
- that.panel.clear();
7596
- root.remove();
8193
+ root.close();
8194
+
8195
+ doAsync( () => {
8196
+ that.panel.clear();
8197
+ root.remove();
8198
+ }, 150 );
7597
8199
  }
7598
8200
  else
7599
8201
  {
7600
8202
  options.onclose( this.root );
7601
8203
  }
7602
-
7603
- if( modal )
7604
- {
7605
- LX.modal.toggle( true );
7606
- }
7607
8204
  };
7608
8205
 
7609
8206
  var closeButton = document.createElement( 'a' );
@@ -7718,21 +8315,29 @@ class PocketDialog extends Dialog {
7718
8315
  options.draggable = options.draggable ?? false;
7719
8316
  options.closable = options.closable ?? false;
7720
8317
 
8318
+ const dragMargin = 3;
8319
+
7721
8320
  super( title, callback, options );
7722
8321
 
7723
8322
  let that = this;
7724
8323
  // Update margins on branch title closes/opens
7725
8324
  LX.addSignal("@on_branch_closed", this.panel, closed => {
7726
8325
  if( this.dock_pos == PocketDialog.BOTTOM )
7727
- this.root.style.top = "calc(100% - " + (this.root.offsetHeight + 6) + "px)";
8326
+ {
8327
+ this.root.style.top = "calc(100% - " + (this.root.offsetHeight + dragMargin) + "px)";
8328
+ }
7728
8329
  });
7729
8330
 
7730
8331
  // Custom
7731
8332
  this.root.classList.add( "pocket" );
7732
- if( !options.position ) {
7733
- this.root.style.left = "calc(100% - " + (this.root.offsetWidth + 6) + "px)";
7734
- this.root.style.top = "0px";
8333
+ this.root.style.left = "unset";
8334
+
8335
+ if( !options.position )
8336
+ {
8337
+ this.root.style.right = dragMargin + "px";
8338
+ this.root.style.top = dragMargin + "px";
7735
8339
  }
8340
+
7736
8341
  this.panel.root.style.width = "calc( 100% - 12px )";
7737
8342
  this.panel.root.style.height = "calc( 100% - 40px )";
7738
8343
  this.dock_pos = PocketDialog.TOP;
@@ -7753,7 +8358,7 @@ class PocketDialog extends Dialog {
7753
8358
 
7754
8359
  if( this.dock_pos == PocketDialog.BOTTOM )
7755
8360
  that.root.style.top = this.root.classList.contains("minimized") ?
7756
- "calc(100% - " + (that.title.offsetHeight + 6) + "px)" : "calc(100% - " + (that.root.offsetHeight + 6) + "px)";
8361
+ "calc(100% - " + (that.title.offsetHeight + 6) + "px)" : "calc(100% - " + (that.root.offsetHeight + dragMargin) + "px)";
7757
8362
  });
7758
8363
 
7759
8364
  if( !options.draggable )
@@ -7768,26 +8373,42 @@ class PocketDialog extends Dialog {
7768
8373
  switch( t )
7769
8374
  {
7770
8375
  case 'b':
7771
- this.root.style.top = "calc(100% - " + (this.root.offsetHeight + 6) + "px)";
8376
+ this.root.style.top = "calc(100% - " + (this.root.offsetHeight + dragMargin) + "px)";
7772
8377
  break;
7773
8378
  case 'l':
7774
- this.root.style.left = options.position ? options.position[ 1 ] : "0px";
8379
+ this.root.style.right = "unset";
8380
+ this.root.style.left = options.position ? options.position[ 1 ] : ( dragMargin + "px" );
7775
8381
  break;
7776
8382
  }
7777
8383
  }
7778
8384
  }
7779
8385
 
7780
8386
  this.root.classList.add('dockable');
7781
- this.title.addEventListener("keydown", function(e) {
7782
- if( e.ctrlKey && e.key == 'ArrowLeft' ) {
8387
+
8388
+ this.title.addEventListener("keydown", function( e ) {
8389
+ if( !e.ctrlKey )
8390
+ {
8391
+ return;
8392
+ }
8393
+
8394
+ that.root.style.right = "unset";
8395
+
8396
+ if( e.key == 'ArrowLeft' )
8397
+ {
7783
8398
  that.root.style.left = '0px';
7784
- } else if( e.ctrlKey && e.key == 'ArrowRight' ) {
7785
- that.root.style.left = "calc(100% - " + (that.root.offsetWidth + 6) + "px)";
7786
- }else if( e.ctrlKey && e.key == 'ArrowUp' ) {
8399
+ }
8400
+ else if( e.key == 'ArrowRight' )
8401
+ {
8402
+ that.root.style.left = "calc(100% - " + (that.root.offsetWidth + dragMargin) + "px)";
8403
+ }
8404
+ else if( e.key == 'ArrowUp' )
8405
+ {
7787
8406
  that.root.style.top = "0px";
7788
8407
  that.dock_pos = PocketDialog.TOP;
7789
- }else if( e.ctrlKey && e.key == 'ArrowDown' ) {
7790
- that.root.style.top = "calc(100% - " + (that.root.offsetHeight + 6) + "px)";
8408
+ }
8409
+ else if( e.key == 'ArrowDown' )
8410
+ {
8411
+ that.root.style.top = "calc(100% - " + (that.root.offsetHeight + dragMargin) + "px)";
7791
8412
  that.dock_pos = PocketDialog.BOTTOM;
7792
8413
  }
7793
8414
  });
@@ -9086,7 +9707,7 @@ class AssetView {
9086
9707
  }
9087
9708
 
9088
9709
  this.rightPanel.sameLine();
9089
- this.rightPanel.addDropdown( "Filter", this.allowedTypes, this.allowedTypes[ 0 ], v => this._refreshContent.call(this, null, v), { width: "20%", minWidth: "128px" } );
9710
+ this.rightPanel.addDropdown( "Filter", this.allowedTypes, this.allowedTypes[ 0 ], v => this._refreshContent.call(this, null, v), { width: "30%", minWidth: "128px" } );
9090
9711
  this.rightPanel.addText( null, this.searchValue ?? "", v => this._refreshContent.call(this, v, null), { placeholder: "Search assets.." } );
9091
9712
  this.rightPanel.addButton( null, "<a class='fa fa-arrow-up-short-wide'></a>", on_sort.bind(this), { className: "micro", title: "Sort" } );
9092
9713
  this.rightPanel.addButton( null, "<a class='fa-solid fa-grip'></a>", on_change_view.bind(this), { className: "micro", title: "View" } );
@@ -9864,6 +10485,14 @@ Element.prototype.getComputedSize = function() {
9864
10485
  }
9865
10486
  }
9866
10487
 
10488
+ Element.prototype.getParentArea = function() {
10489
+ let parent = this.parentElement;
10490
+ while( parent ) {
10491
+ if( parent.classList.contains( "lexarea" ) ) { return parent; }
10492
+ parent = parent.parentElement;
10493
+ }
10494
+ }
10495
+
9867
10496
  LX.UTILS = {
9868
10497
  getTime() { return new Date().getTime() },
9869
10498
  compareThreshold( v, p, n, t ) { return Math.abs(v - p) >= t || Math.abs(v - n) >= t },