x4js 2.2.45 → 2.2.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "x4js",
3
- "version": "2.2.45",
3
+ "version": "2.2.47",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -33,9 +33,7 @@
33
33
  "application"
34
34
  ],
35
35
  "devDependencies": {
36
- "typescript": "^5.8.3"
37
- },
38
- "dependencies": {
36
+ "typescript": "^5.8.3",
39
37
  "typedoc": "^0.28.19",
40
38
  "typedoc-plugin-markdown": "^4.12.0"
41
39
  }
@@ -14,9 +14,10 @@
14
14
  * that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
15
15
  **/
16
16
 
17
+ import { Menu, MenuElement, MenuProps } from 'x4js';
17
18
  import { Component, ComponentEvents, ComponentProps, EvClick } from "../../core/component"
18
19
  import { EventCallback } from '../../core/core_events';
19
- import { class_ns, UnsafeHtml } from '../../core/core_tools';
20
+ import { class_ns, isFunction, UnsafeHtml } from '../../core/core_tools';
20
21
 
21
22
  import { Icon } from "../icon/icon"
22
23
 
@@ -68,6 +69,8 @@ export interface ButtonProps extends ComponentProps {
68
69
  * cf. ButtonEvents
69
70
  */
70
71
  click?: EventCallback<EvClick>;
72
+
73
+ menu?: MenuElement[] | (( ) => MenuElement[]);
71
74
  }
72
75
 
73
76
  /**
@@ -131,17 +134,17 @@ export class Button extends Component<ButtonProps,ButtonEvents> {
131
134
 
132
135
  protected _on_click( ev: MouseEvent ) {
133
136
 
134
- //if (this.m_props.menu) {
135
- // let menu = new Menu({
136
- // items: isFunction(this.m_props.menu) ? this.m_props.menu() : this.m_props.menu
137
- // });
138
- //
139
- // let rc = this.getBoundingRect();
140
- // menu.displayAt(rc.left, rc.bottom, 'tl');
141
- //}
142
- //else {
137
+ if (this.props.menu) {
138
+ let menu = new Menu({
139
+ items: isFunction(this.props.menu) ? this.props.menu() : this.props.menu
140
+ });
141
+
142
+ let rc = this.getBoundingRect();
143
+ menu.displayNear(rc, 'top-left', 'bottom-left' );
144
+ }
145
+ else {
143
146
  this.fire('click', {} );
144
- //}
147
+ }
145
148
 
146
149
  ev.preventDefault();
147
150
  ev.stopPropagation();
@@ -17,16 +17,17 @@
17
17
  @use "../shared.scss";
18
18
 
19
19
  :root {
20
- --listbox-item-background-sel: var( --accent-background );
20
+ --listbox-item-color: var( --text-primary );
21
+ --listbox-icon-color: var( --text-ternary );
22
+ --listbox-item-color-disabled: var( --disabled-color-dark );
23
+
21
24
  --listbox-item-color-sel: var( --accent-color );
25
+ --listbox-item-background-sel: var( --accent-background );
22
26
  --listbox-item-color-sel-disabled: var( --disabled-background );
23
27
 
24
- --listbox-item-color: var( --text-primary );
25
- --listbox-item-color-disabled: var( --disabled-color-dark );
28
+ --listbox-item-color-hover: var( --text-primary );;
26
29
  --listbox-item-background-hover: var( --background-secondary );
27
30
 
28
- --listbox-icon-color: var( --text-ternary );
29
-
30
31
  --listbox-background: white;
31
32
  --listbox-border: var( --border );
32
33
  --listbox-border-focus: var( --accent-background-focus );
@@ -79,6 +80,7 @@
79
80
  }
80
81
  }
81
82
  }
83
+ }
82
84
 
83
85
  .x4item {
84
86
  @extend %flex;
@@ -93,9 +95,10 @@
93
95
  padding: 4px 0;
94
96
  }
95
97
 
98
+ --label-color: var( --listbox-item-color );
99
+
96
100
  .x4label,
97
101
  .x4simpletext {
98
- color: inherit;
99
102
  padding: 2px 6px;
100
103
 
101
104
  #icon {
@@ -106,12 +109,15 @@
106
109
 
107
110
  &:hover {
108
111
  background-color: var( --listbox-item-background-hover );
112
+ --label-color: var( --listbox-item-color-hover );
109
113
  }
110
114
 
111
115
  &.selected {
112
116
  background-color: var( --listbox-item-background-sel );
113
117
  color: var( --listbox-item-color-sel );
114
118
 
119
+ --label-color: var( --listbox-item-color-sel );
120
+
115
121
  &> .x4label {
116
122
  #icon {
117
123
  color: var( --listbox-item-color-sel );
@@ -133,7 +139,10 @@
133
139
  //color: var(--color-0);
134
140
  //}
135
141
 
136
- }
142
+ }
143
+
144
+ &.empty .body .x4viewport {
145
+ height: 100%;
137
146
  }
138
147
 
139
148
  &> #footer {
@@ -125,9 +125,7 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
125
125
  contextmenu: (e) => this._on_ctx_menu(e),
126
126
  } );
127
127
 
128
- asap( ( ) => {
129
128
  this.setItems( props.items, false );
130
- } );
131
129
  }
132
130
 
133
131
  /**
@@ -446,6 +444,7 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
446
444
  let update_sel = false;
447
445
 
448
446
  if( this._items.length ) {
447
+ this.removeClass( "empty" );
449
448
  const content = items.map( x => this.renderItem(x) );
450
449
  this._view.setContent( content );
451
450
 
@@ -454,6 +453,7 @@ export class Listbox extends Component<ListboxProps,ListboxEvents> {
454
453
  }
455
454
  }
456
455
  else {
456
+ this.addClass( "empty" );
457
457
  update_sel = oldSel.length>0;
458
458
  this._view.setContent( new Label( { cls: "empty vertical", icon: icons.empty, text: this.props.emptyMsg ?? _tr.global.empty_list } ) );
459
459
  }
@@ -16,11 +16,12 @@
16
16
 
17
17
  import { Component } from "../../core/component"
18
18
  import { DOMEventHandler } from '../../core/core_dom';
19
- import { Timer, UnsafeHtml, class_ns, isString } from '../../core/core_tools';
19
+ import { Timer, UnsafeHtml, class_ns, isFunction, isString } from '../../core/core_tools';
20
20
 
21
21
  import { Popup, PopupProps } from '../popup/popup';
22
22
  import { Icon } from '../icon/icon';
23
23
 
24
+ import icons from "../assets/icons";
24
25
  import "./menu.module.scss"
25
26
 
26
27
  const OPEN_DELAY = 400;
@@ -35,6 +36,7 @@ export interface MenuItem {
35
36
  text: string | UnsafeHtml;
36
37
  menu?: Menu;
37
38
  disabled?: boolean;
39
+ checked?: boolean | ( () => boolean);
38
40
  click?: DOMEventHandler;
39
41
  }
40
42
 
@@ -74,8 +76,16 @@ class CMenuItem extends Component {
74
76
  this.addClass( "popup" );
75
77
  }
76
78
 
79
+ let iconId = itm.icon;
80
+ if( !iconId && itm.checked ) {
81
+ const chk = isFunction(itm.checked) ? itm.checked() : itm.checked;
82
+ if( chk ) {
83
+ iconId = icons.check;
84
+ }
85
+ }
86
+
77
87
  this.setContent( [
78
- new Icon({ id:"icon",iconId:itm.icon}),
88
+ new Icon({ id:"icon", iconId }),
79
89
  new Component( { id: "text", content: itm.text } )
80
90
  ]);
81
91
 
@@ -28,10 +28,17 @@
28
28
  .x4notification {
29
29
  @extend %shadow-xl;
30
30
 
31
- padding: 8px;
31
+ padding: 4px 8px;
32
32
  border: 1px solid var(--notification-border);
33
33
  border-left: 3px solid var( --accent-background );
34
34
  background-color: var( --notification-background );
35
+ min-width: 250px;
36
+ max-width: 30vw;
37
+ position: absolute;
38
+
39
+ &.smooth {
40
+ transition: top .4s ease;
41
+ }
35
42
 
36
43
  &> .x4hbox {
37
44
  gap: 8px;
@@ -77,8 +84,8 @@
77
84
  color: var( --color-gray-7 );
78
85
  border: none;
79
86
 
80
- &:hover {
87
+ //&:hover {
81
88
  //background-color: var( -- );;
82
- }
89
+ //}
83
90
  }
84
91
  }
@@ -15,7 +15,7 @@
15
15
  **/
16
16
 
17
17
  import { ComponentProps } from '../../core/component';
18
- import { class_ns, Rect, UnsafeHtml } from '../../core/core_tools';
18
+ import { asap, class_ns, Rect, UnsafeHtml } from '../../core/core_tools';
19
19
 
20
20
  import { Popup } from '../popup/popup';
21
21
  import { HBox, VBox } from '../boxes/boxes';
@@ -47,6 +47,8 @@ interface NotificationProps extends ComponentProps {
47
47
 
48
48
  @class_ns( "x4" )
49
49
  export class Notification extends Popup {
50
+ private static list = new Set<Notification>( );
51
+
50
52
  constructor( props: NotificationProps ) {
51
53
  super( { } );
52
54
 
@@ -85,19 +87,39 @@ export class Notification extends Popup {
85
87
  }
86
88
 
87
89
  close( ) {
90
+ Notification.list.delete( this );
91
+ asap( ( ) => Notification.updatePositions( ) )
92
+
88
93
  this.clearTimeout( "close" );
89
94
  super.close( );
90
95
  }
91
96
 
92
97
  display( time_in_s = 0 ) {
93
- const r = new Rect( 0, 0, window.innerWidth, window.innerHeight );
94
- this.displayNear( r, "bottom right", "bottom right", { x: -20, y: -10 } );
95
-
98
+ Notification.list.add( this );
96
99
  if( time_in_s ) {
97
100
  this.setTimeout( "close", time_in_s*1000, ( ) => {
98
101
  this.close()
99
102
  } );
100
103
  }
104
+
105
+ //this.setStyleValue( "top", "9999px" );
106
+
107
+ asap( ( ) => Notification.updatePositions( ) )
108
+ }
109
+
110
+ static updatePositions( ) {
111
+ const r = new Rect( 0, 0, window.innerWidth, window.innerHeight );
112
+
113
+ for( const n of this.list ) {
114
+ n.addClass( "smooth" );
115
+ n.displayNear( r, "bottom right", "bottom right", { x: -20, y: -10 }, true );
116
+ const nr = n.getBoundingRect( );
117
+ r.height -= nr.height+10;
118
+ if( r.height<10 ) {
119
+ r.height = 10;
120
+ }
121
+ }
122
+
101
123
  }
102
124
  }
103
125
 
@@ -79,9 +79,12 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
79
79
  *
80
80
  */
81
81
 
82
- displayNear( rc: Rect, dst = "top left", src = "top left", offset = {x:0,y:0} ) {
82
+ displayNear( rc: Rect, dst = "top left", src = "top left", offset = {x:0,y:0}, keep_pos = false ) {
83
83
 
84
+ if( !keep_pos ) {
84
85
  this.setStyle( { left: "0px", top: "0px" } ); // avoid scrollbar
86
+ }
87
+
85
88
  this._do_show( ); // to compute size
86
89
 
87
90
  let rm = this.getBoundingRect();
@@ -4,27 +4,37 @@
4
4
  * @copyright (c) 2026 R-libre ingenierie, all rights reserved.
5
5
  **/
6
6
 
7
+ :root {
8
+ --tag-border: var( --border );
9
+ --tag-border-focus: var( --border-focus );
10
+ --tag-selection: var( --accent-background );
11
+ --tag-color: black;
12
+ --tag-icon-color: var( --tag-color );
13
+ }
14
+
7
15
  .x4tag {
8
- color: black;
9
16
  border-radius: 16px;
10
- border: 1px solid var( --border );
17
+ border: 1px solid var( --tag-border );
11
18
  padding: 2px 8px;
12
19
  gap: 4px;
13
20
  font-weight: normal;
21
+ --label-color: var( --tag-color );
14
22
 
15
23
  .x4icon:empty {
16
24
  display: none;
17
25
  }
18
26
 
19
27
  .x4icon {
28
+ height: 1em;
29
+ color: var( --tag-icon-color );
20
30
  }
21
31
 
22
- .x4simpletext {
23
- color: inherit;
32
+ &> .x4simpletext {
33
+ padding: 0;
24
34
  }
25
35
 
26
36
  &.alert {
27
- color: var( --alert-color );
37
+ --tag-color: var( --alert-color );
28
38
  background-color: var( --alert-background );
29
39
  }
30
40
  }
@@ -22,24 +22,7 @@
22
22
  --textarea-selection: var( --accent-background );
23
23
  }
24
24
 
25
- .x4textarea {
26
- margin: 5px;
27
-
28
- .x4label{
29
- gap: 0;
30
- padding: 4px 0;
31
-
32
- &::after {
33
- content: ":"
34
- }
35
- }
36
-
37
- .x4label:has(.empty) {
38
- display: none;
39
- }
40
-
41
- textarea {
42
- @extend %flex;
25
+ .x4simpletextarea {
43
26
 
44
27
  padding: 4px;
45
28
  outline: none;
@@ -60,4 +43,25 @@
60
43
  resize: none;
61
44
  }
62
45
  }
46
+
47
+ .x4textarea {
48
+ margin: 5px;
49
+
50
+ .x4label{
51
+ gap: 0;
52
+ padding: 4px 0;
53
+
54
+ &::after {
55
+ content: ":"
56
+ }
57
+ }
58
+
59
+ .x4label:has(.empty) {
60
+ display: none;
61
+ }
62
+
63
+ .x4simpletextarea {
64
+ @extend %flex;
65
+
66
+ }
63
67
  }
@@ -42,7 +42,8 @@ interface TextAreaProps extends BaseProps {
42
42
  *
43
43
  */
44
44
 
45
- class SimpleTextArea extends Component<TextAreaProps> {
45
+ @class_ns( "x4" )
46
+ export class SimpleTextArea extends Component<TextAreaProps> {
46
47
 
47
48
  constructor( props: TextAreaProps ) {
48
49
  super( { ...props, tag: "textarea" } );
@@ -72,6 +73,10 @@ class SimpleTextArea extends Component<TextAreaProps> {
72
73
  return text;
73
74
  }
74
75
 
76
+ scrollToBottom( ) {
77
+ this.dom.scrollTop = this.dom.scrollHeight;
78
+ }
79
+
75
80
  queryInterface<T>(name: string): T {
76
81
  if( name=="form-element" ) {
77
82
  const i: IFormElement = {
@@ -115,6 +120,10 @@ export class TextArea extends VBox {
115
120
  return this._input.getText( );
116
121
  }
117
122
 
123
+ scrollToBottom( ) {
124
+ this._input.scrollToBottom( );
125
+ }
126
+
118
127
  queryInterface<T>(name: string): T {
119
128
  if( name=="form-element" ) {
120
129
  const i: IFormElement = {
@@ -206,9 +206,7 @@ export class Treeview extends Component<TreeviewProps,TreeviewEvents> {
206
206
  keydown: ( ev ) => this._onkey( ev ),
207
207
  });
208
208
 
209
- asap( ( ) => {
210
209
  this.setItems( props.items );
211
- } );
212
210
  }
213
211
 
214
212
  /**
@@ -478,8 +476,8 @@ export class Treeview extends Component<TreeviewProps,TreeviewEvents> {
478
476
  this._selitem = undefined;
479
477
  }
480
478
 
481
- this._selection = undefined;
482
- this.fire( "selectionChange", { selection: [], empty: true } );
479
+ this._selection = undefined;
480
+ this.fire( "selectionChange", { selection: [], empty: true } );
483
481
  }
484
482
 
485
483
  /**
@@ -5,7 +5,7 @@
5
5
  **/
6
6
 
7
7
  import { CoreEvent, EventMap, EventSource } from './core_events.js';
8
- import { isPlainObject } from './core_tools.js';
8
+ import { getMemberValue, isPlainObject } from './core_tools.js';
9
9
 
10
10
  type StateData = boolean | number | string | Date | unknown;
11
11
  type State = Record<string, StateData>;
@@ -210,6 +210,11 @@ export class StateManager<T extends State> extends EventSource<StateEvents> {
210
210
  if( _path_matches( e.path, path ) ) {
211
211
  cb( e );
212
212
  }
213
+ else if( _path_matches( path, e.path ) ) {
214
+ const ee = { ...e };
215
+ ee.value = getMemberValue(this._proxy,path );
216
+ cb( ee );
217
+ }
213
218
  });
214
219
  }
215
220
  }
@@ -224,7 +229,7 @@ export class StateManager<T extends State> extends EventSource<StateEvents> {
224
229
  * state.items.push( 4 ); // "items.3 = 4"
225
230
  */
226
231
 
227
- export function makeState<T extends State>( initialState: T ): StateProxy<T> {
232
+ export function makeState<T extends Record<keyof T, StateData>>( initialState: T ): StateProxy<T> {
228
233
  return new StateManager( initialState ).proxify( );
229
234
  }
230
235