x4js 2.3.23 → 2.3.25

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.
@@ -75,6 +75,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
75
75
 
76
76
  private _isshown = false;
77
77
  protected _ismodal = false;
78
+ private _prevFocus: HTMLElement; // modal: the element that had the focus when it opened
78
79
 
79
80
  constructor( props: P ) {
80
81
  super( props );
@@ -220,6 +221,11 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
220
221
  return;
221
222
  }
222
223
 
224
+ // modal: the focus goes back to the element that had it when it opened, unless the
225
+ // user put it elsewhere (read before the popup leaves the page and its focus with it)
226
+ const active = document.activeElement;
227
+ const giveBack = this._ismodal && this._prevFocus && ( !active || active===document.body || this.dom.contains( active ) );
228
+
223
229
  this.__hide( );
224
230
  this.__remove( );
225
231
 
@@ -255,6 +261,11 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
255
261
  }
256
262
  }
257
263
 
264
+ if( giveBack && this._prevFocus.isConnected ) {
265
+ this._prevFocus.focus( );
266
+ }
267
+ this._prevFocus = null;
268
+
258
269
  this._isshown = false;
259
270
  this.fire( "closed", {} );
260
271
  }
@@ -272,6 +283,10 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
272
283
  this.__append( );
273
284
 
274
285
  if( this._ismodal ) {
286
+ // given back the focus when it closes; <body>: nothing had it
287
+ const active = document.activeElement as HTMLElement;
288
+ this._prevFocus = active && active!==document.body ? active : null;
289
+
275
290
  modal_stack.push( this );
276
291
  this._showModalMask( );
277
292
  }
@@ -90,6 +90,9 @@ export class PropertyGrid extends VBox {
90
90
  private root: Component;
91
91
  private groups: PropertyGroup[];
92
92
 
93
+ // the editor of each property, by its name: no id, a name is not unique in a page
94
+ private editors = new Map<string, Component>( );
95
+
93
96
  constructor(props: PropertyProps) {
94
97
  super(props);
95
98
 
@@ -118,6 +121,8 @@ export class PropertyGrid extends VBox {
118
121
 
119
122
  setItems( _grps: PropertyGroup[] ) {
120
123
 
124
+ this.editors.clear( );
125
+
121
126
  if( !_grps || _grps.length==0 ) {
122
127
  this.root.clearContent( );
123
128
  return;
@@ -218,7 +223,6 @@ export class PropertyGrid extends VBox {
218
223
  if (item.type === 'boolean') {
219
224
  editor = new Input({
220
225
  type: "checkbox",
221
- id: item.name,
222
226
  //name: item.name,
223
227
  checked: value as boolean,
224
228
  dom_events: {
@@ -232,7 +236,6 @@ export class PropertyGrid extends VBox {
232
236
  else if (item.type === 'options') {
233
237
  editor = new Select( {
234
238
  value: value as ListboxID,
235
- id: item.name,
236
239
  items: item.options,
237
240
  //name: item.name,
238
241
  change: ( e: EvChange ) => {
@@ -243,20 +246,23 @@ export class PropertyGrid extends VBox {
243
246
  else if (item.type === 'password') {
244
247
  editor = new Input({
245
248
  type: 'password',
246
- id: item.name,
247
- //name: item.name,
248
- value: String(value),
249
+ //name: item.name,
250
+ value: String(value),
249
251
  focus: ( e: EvFocus ) => {
250
252
  if( e.focus_out ) {
251
253
  item.callback?.( item.name, (editor as Input).getValue() );
252
254
  }
255
+ },
256
+ change: ( ) => {
257
+ if( item.live ) {
258
+ item.callback?.( item.name, (editor as Input).getValue() );
259
+ }
253
260
  }
254
261
  });
255
262
  }
256
263
  else if (item.type === 'number') {
257
264
  editor = new Input({
258
265
  type: 'number',
259
- id: item.name,
260
266
  //name: item.name,
261
267
  value: String(value),
262
268
  step: item.step,
@@ -276,7 +282,6 @@ export class PropertyGrid extends VBox {
276
282
  }
277
283
  else if (item.type === 'color') {
278
284
  editor = new ColorInput({
279
- id: item.name,
280
285
  color: value as string,
281
286
  format: "hex",
282
287
  picker: true,
@@ -289,14 +294,12 @@ export class PropertyGrid extends VBox {
289
294
  }
290
295
  else if (item.type === 'label') {
291
296
  editor = new Label({
292
- id: item.name,
293
297
  text: value as string,
294
298
  });
295
299
  }
296
300
  else if (item.type === 'button') {
297
301
  use_hdr = false;
298
302
  editor = new Button({
299
- id: item.name,
300
303
  cls: "button",
301
304
  label: value as string,
302
305
  click: ( ) => {
@@ -307,7 +310,6 @@ export class PropertyGrid extends VBox {
307
310
  else {
308
311
  editor = new Input({
309
312
  type: 'text',
310
- id: item.name,
311
313
  //name: item.name,
312
314
  value: value as string,
313
315
  focus: ( e: EvFocus ) => {
@@ -315,8 +317,17 @@ export class PropertyGrid extends VBox {
315
317
  item.callback?.( item.name, (editor as Input).getValue() );
316
318
  }
317
319
  },
320
+ change: ( ) => {
321
+ if( item.live ) {
322
+ item.callback?.( item.name, (editor as Input).getValue() );
323
+ }
324
+ }
318
325
  })
319
326
  }
327
+
328
+ if( item.name ) {
329
+ this.editors.set( item.name, editor );
330
+ }
320
331
 
321
332
  let cls = 'row';
322
333
  if( item.cls ) {
@@ -340,7 +351,7 @@ export class PropertyGrid extends VBox {
340
351
  cls,
341
352
  content: [
342
353
  use_hdr ? new Component({ cls: 'cell hdr', content: item.title ?? item.name, tooltip: item.desc }) : null,
343
- new HBox({ cls: 'cell', tag: "label", attrs: { "labelFor": item.name }, content }),
354
+ new HBox({ cls: 'cell', tag: "label", content }),
344
355
  ]
345
356
  });
346
357
  }
@@ -352,45 +363,28 @@ export class PropertyGrid extends VBox {
352
363
  setPropValue( name: string, value: any ) {
353
364
  const all = this.groups.flatMap( x => [ ...x.items ] ) ;
354
365
  const item = all.find( x => x.name==name );
355
- if( !item ) {
366
+ const editor = this.editors.get( name );
367
+ if( !item || !editor ) {
356
368
  return;
357
369
  }
358
370
 
359
371
  if (item.type === 'boolean') {
360
- const editor = this.root.query<Input>( '#'+item.name );
361
- if( editor ) {
362
- editor.setCheck( value as boolean );
363
- }
372
+ (editor as Input).setCheck( value as boolean );
364
373
  }
365
374
  else if (item.type === 'options') {
366
- const editor = this.root.query<Select>( '#'+item.name );
367
- if( editor ) {
368
- editor.setValue( value as string );
369
- }
375
+ (editor as Select).setValue( value as string );
370
376
  }
371
377
  else if (item.type === 'number') {
372
- const editor = this.root.query<Input>( '#'+item.name );
373
- if( editor ) {
374
- editor.setNumValue( value as number, -2 );
375
- }
378
+ (editor as Input).setNumValue( value as number, -2 );
376
379
  }
377
380
  else if (item.type === 'color') {
378
- const editor = this.root.query<ColorInput>( '#'+item.name );
379
- if( editor ) {
380
- editor.setColor( value as string );
381
- }
381
+ (editor as ColorInput).setColor( value as string );
382
382
  }
383
383
  else if (item.type === 'label') {
384
- const label = this.root.query<Label>( '#'+item.name );
385
- if( label ) {
386
- label.setText( value );
387
- }
384
+ (editor as Label).setText( value );
388
385
  }
389
386
  else {
390
- const editor = this.root.query<Input>( '#'+item.name );
391
- if( editor ) {
392
- editor.setValue( value as string );
393
- }
387
+ (editor as Input).setValue( value as string );
394
388
  }
395
389
  }
396
390
 
@@ -36,7 +36,6 @@
36
36
  margin: 0;
37
37
  padding: 6px 10px;
38
38
  border: 1px solid var( --border );
39
- border-radius: 4px;
40
39
 
41
40
  &:focus {
42
41
  border-color: var( --settingsview-marker );
@@ -100,7 +99,6 @@
100
99
  max-width: 900px;
101
100
  padding: 0 8px 10px;
102
101
  border: 1px solid var( --border );
103
- border-radius: 8px;
104
102
  background: var( --settingsview-section-background );
105
103
  overflow: hidden;
106
104
  scroll-margin-top: 20px; // brought to the top: below the padding of the view
@@ -144,7 +142,6 @@
144
142
  gap: 4px;
145
143
  padding: 10px 8px 12px;
146
144
  border-left: 2px solid transparent;
147
- border-radius: 4px;
148
145
 
149
146
  &:hover {
150
147
  background: var( --settingsview-hover-background );
@@ -100,4 +100,58 @@ $color--interval: 8.4%;
100
100
  --success-background: var(--color-success-a50);
101
101
  --success-background-active: var(--color-success-a60);
102
102
  --success-color: #ffffff;
103
+ }
104
+
105
+ // :: dark theme :::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
106
+ // Application.setTheme( "dark" ): the class x4theme-dark on <html>, like the theme Dark
107
+ // Modern of VS Code. Its colours are taken in the sources of VS Code:
108
+ // extensions/theme-defaults/themes/dark_modern.json, and for the lists the defaults of
109
+ // src/vs/platform/theme/common/colors/listColors.ts; the values marked "chosen" are not
110
+ // in VS Code. A theme of an application is written the same way, under html.x4theme-<name>.
111
+
112
+ html.x4theme-dark {
113
+ // the scale: a0 the background, a100 the text, the surfaces between them, then the blues
114
+ --color-primary-a0: #1f1f1f; // editor.background
115
+ --color-primary-a5: #252526; // chosen
116
+ --color-primary-a10: #2a2d2e; // list.hoverBackground
117
+ --color-primary-a20: #37373d; // list.inactiveSelectionBackground
118
+ --color-primary-a30: #3c3c3c; // input.border
119
+ --color-primary-a40: #04395e; // list.activeSelectionBackground
120
+ --color-primary-a50: #0078d4; // button.background, focusBorder
121
+ --color-primary-a55: #026ec1; // button.hoverBackground
122
+ --color-primary-a60: #0078d4;
123
+ --color-primary-a70: #026ec1;
124
+ --color-primary-a80: #04395e;
125
+ --color-primary-a90: #cccccc;
126
+ --color-primary-a100: #cccccc; // foreground
127
+
128
+ // what the scale does not give
129
+ --background-ternary: #181818; // sideBar.background
130
+ --background-hover: #2b2b2b; // button.secondaryHoverBackground
131
+ --background-active: #37373d; // list.inactiveSelectionBackground
132
+ --background-input: #313131; // input.background
133
+
134
+ --text-secondary: #9d9d9d; // descriptionForeground
135
+ --text-ternary: #bbbbbb; // chosen: between the description and the text
136
+
137
+ --border: #3c3c3c; // input.border
138
+ --border-light: #2b2b2b; // panel.border
139
+ --border-hover: #616161; // chosen (badge.background)
140
+
141
+ --disabled-background: #313131; // chosen
142
+ --disabled-color: #6e6e6e; // chosen
143
+ --disabled-color-dark: #6e6e6e; // chosen
144
+ --disabled-color-light: #4f4f4f; // chosen
145
+
146
+ --accent-color: #ffffff; // button.foreground: the text on the accent
147
+ --accent-color-hover: #ffffff;
148
+ --accent-color-focus: #ffffff;
149
+ --accent-color-active: #ffffff;
150
+
151
+ // the title bar of a Dialog: the one of the window of VS Code
152
+ --color-dialog-caption: #181818; // titleBar.activeBackground
153
+ --color-dialog-caption-text: #cccccc; // titleBar.activeForeground
154
+ --color-dialog-background: #202020; // editorWidget.background
155
+
156
+ color-scheme: dark; // the native parts: select, scroll bars, number spinners
103
157
  }
@@ -18,9 +18,9 @@
18
18
 
19
19
  :root {
20
20
  --tooltip-caption-background: var( --color-primary-a70 );
21
- --tooltip-caption-color: var( --color-primary-a0 );
21
+ --tooltip-caption-color: var( --accent-color );
22
22
 
23
- --tooltip-icon-background: var( --color-primary-a0 );
23
+ --tooltip-icon-background: var( --accent-color );
24
24
  --tooltip-icon-color: var( --color-primary-a70 );
25
25
 
26
26
  --tooltip-background: var( --color-primary-a10 ); //white;
@@ -32,6 +32,15 @@ let mouse_pos: Point = {x:0, y:0 };
32
32
 
33
33
  const timer = new Timer( );
34
34
 
35
+ /**
36
+ * the tooltips are shown once this is called: the text of the attribute "tooltip" (the prop
37
+ * tooltip of a component), or the one getTip( ) of a "tip-handler" gives.
38
+ * This text is shown as html: a text that comes from a user is escaped by the application,
39
+ * with sanitizeHtml( text ).
40
+ * @example
41
+ * new Button( { label: "Open", tooltip: `Open <b>${sanitizeHtml( file.name )}</b>` } );
42
+ */
43
+
35
44
  export function initTooltips( ) {
36
45
 
37
46
  document.addEventListener( "mouseenter", ( ev: MouseEvent ) => {
@@ -42,6 +42,9 @@ export interface ApplicationEvents extends EventMap {
42
42
  // singleton
43
43
  let main_app: Application = null;
44
44
 
45
+ // the class of a theme on <html>: x4theme-<name> (see Application.setTheme)
46
+ const THEME_PREFIX = "x4theme-";
47
+
45
48
  /**
46
49
  * Provides information about the user's device and browser capabilities.
47
50
  */
@@ -201,6 +204,50 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
201
204
  Application.instance().fire( "global", { msg, params } );
202
205
  }
203
206
 
207
+ /**
208
+ * Selects the theme of the page: the class `x4theme-<name>` on `<html>`, in place
209
+ * of the one of the theme before.
210
+ * - `""`: the light theme of x4, the one by default: no class.
211
+ * - `"dark"`: the dark theme of x4, like the theme Dark Modern of VS Code
212
+ * (`html.x4theme-dark` in themes.scss).
213
+ * - any other name: a theme of the application, defined in its own css under
214
+ * `html.x4theme-<name>`, with the variables of themes.scss it changes.
215
+ * @param name - the name of the theme; "" for the light theme.
216
+ * @example
217
+ * Application.setTheme( "dark" ); // the dark theme of x4
218
+ * Application.setTheme( "pink" ); // html.x4theme-pink { --color-primary-a0: ...; } in the css of the application
219
+ * Application.setTheme( "" ); // back to the light theme
220
+ */
221
+
222
+ static setTheme( name: string ) {
223
+ const classes = document.documentElement.classList;
224
+
225
+ const current = Application.getTheme( );
226
+ if( current ) {
227
+ classes.remove( THEME_PREFIX + current );
228
+ }
229
+
230
+ if( name ) {
231
+ classes.add( THEME_PREFIX + name );
232
+ }
233
+ }
234
+
235
+ /**
236
+ * The name of the theme of the page, as setTheme got it: `""` for the light theme
237
+ * of x4, `"dark"` for its dark theme, or the name of a theme of the application.
238
+ */
239
+
240
+ static getTheme( ): string {
241
+ const classes = document.documentElement.classList;
242
+ for( let i = 0; i < classes.length; i++ ) {
243
+ if( classes[i].startsWith( THEME_PREFIX ) ) {
244
+ return classes[i].replace( THEME_PREFIX, "" );
245
+ }
246
+ }
247
+
248
+ return "";
249
+ }
250
+
204
251
  /**
205
252
  *
206
253
  */
@@ -208,7 +255,11 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
208
255
  private _setupKeyboard( ) {
209
256
 
210
257
  document.addEventListener( "keydown", (ev) => {
211
- if( ev.key=="Tab" || ev.key=="Enter" ) {
258
+ // Enter in a text of several lines is a new line, not the next field
259
+ const target = ev.target as HTMLElement;
260
+ const multiline = target.tagName==="TEXTAREA" || target.isContentEditable;
261
+
262
+ if( ev.key=="Tab" || ( ev.key=="Enter" && !multiline ) ) {
212
263
  if( this.focusNext( !ev.shiftKey ) ) {
213
264
  ev.preventDefault( );
214
265
  }
@@ -216,7 +267,8 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
216
267
 
217
268
  // The focus is nowhere: the shortcuts of the view the user worked in last.
218
269
  // (when it is in a view, the view has seen the key before us)
219
- if( ev.target===document.body && this._active?.owner.dom.isConnected && this._active.shortcuts.handle( ev ) ) {
270
+ // (a view hidden, in a tab not shown: its shortcuts sleep)
271
+ if( ev.target===document.body && this._active?.owner.isVisible( ) && this._active.shortcuts.handle( ev ) ) {
220
272
  return;
221
273
  }
222
274
 
@@ -159,6 +159,8 @@ export class EventSource<E extends EventMap = EventMap > {
159
159
 
160
160
  /**
161
161
  * Dispatches an event with a given name and payload to all registered listeners.
162
+ * A listener that throws: its error goes to console.error, the next listeners are
163
+ * called, and fire returns normally.
162
164
  * @param name - The name of the event to fire (must be a key in `E`).
163
165
  * @param evx - The event payload (event object) to pass to the listeners.
164
166
  */
@@ -192,14 +194,24 @@ export class EventSource<E extends EventMap = EventMap > {
192
194
  ev.stopPropagation = stopPropagation;
193
195
  }
194
196
 
197
+ // a listener that throws stops neither the others nor the one that fires
198
+ const call = ( listener: EventCallback ) => {
199
+ try {
200
+ listener( ev );
201
+ }
202
+ catch( e ) {
203
+ console.error( e );
204
+ }
205
+ };
206
+
195
207
  // small optimisation
196
208
  if (listeners.length == 1) {
197
- listeners[0](ev);
209
+ call( listeners[0] );
198
210
  }
199
211
  else {
200
212
  const temp = listeners.slice();
201
213
  for (let i = 0, n = temp.length; i < n; i++) {
202
- temp[i](ev);
214
+ call( temp[i] );
203
215
  if (ev.propagationStopped) {
204
216
  break;
205
217
  }
@@ -336,6 +336,13 @@ let fr = {
336
336
  search: 'Rechercher un paramètre',
337
337
  nomatch: 'Aucun paramètre trouvé',
338
338
  },
339
+
340
+ progression: {
341
+ error: '{0} erreur',
342
+ errors: '{0} erreurs',
343
+ warning: '{0} avertissement',
344
+ warnings: '{0} avertissements',
345
+ },
339
346
  }
340
347
  };
341
348
 
@@ -406,6 +413,13 @@ let en = {
406
413
  search: 'Search settings',
407
414
  nomatch: 'No setting found',
408
415
  },
416
+
417
+ progression: {
418
+ error: '{0} error',
419
+ errors: '{0} errors',
420
+ warning: '{0} warning',
421
+ warnings: '{0} warnings',
422
+ },
409
423
  }
410
424
  };
411
425
 
@@ -476,6 +490,13 @@ let es = {
476
490
  search: 'Buscar un ajuste',
477
491
  nomatch: 'Ningún ajuste encontrado',
478
492
  },
493
+
494
+ progression: {
495
+ error: '{0} error',
496
+ errors: '{0} errores',
497
+ warning: '{0} advertencia',
498
+ warnings: '{0} advertencias',
499
+ },
479
500
  }
480
501
  };
481
502
 
@@ -546,6 +567,13 @@ let de = {
546
567
  search: 'Einstellungen durchsuchen',
547
568
  nomatch: 'Keine Einstellung gefunden',
548
569
  },
570
+
571
+ progression: {
572
+ error: '{0} Fehler',
573
+ errors: '{0} Fehler',
574
+ warning: '{0} Warnung',
575
+ warnings: '{0} Warnungen',
576
+ },
549
577
  }
550
578
  };
551
579