x4js 2.2.32 → 2.2.33

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.32",
3
+ "version": "2.2.33",
4
4
  "type": "module",
5
5
  "main": "src/x4.ts",
6
6
  "module": "src/x4.ts",
@@ -36,7 +36,6 @@
36
36
  "typescript": "^5.8.3"
37
37
  },
38
38
  "dependencies": {
39
- "tsx": "^4.22.4",
40
39
  "typedoc": "^0.28.19",
41
40
  "typedoc-plugin-markdown": "^4.12.0"
42
41
  }
@@ -1,11 +1,12 @@
1
- import { class_ns, Component, ComponentProps } from '../../x4.js';
1
+ import { class_ns } from '../../core/core_tools';
2
+ import { Component, ComponentProps } from '../../core/component';
2
3
 
3
4
  import Monaco from './bin/monaco';
4
5
  import "./monaco.module.scss"
5
6
 
6
7
 
7
8
  interface MonacoEditorProps extends ComponentProps {
8
- language: "typescript" | "javascript" | "json" | "css" | "html";
9
+ language: "typescript" | "javascript" | "json" | "css" | "html" | string;
9
10
  theme?: string;
10
11
  content?: string;
11
12
  options?: Monaco.editor.IEditorOptions & Monaco.editor.IGlobalEditorOptions;
@@ -14,24 +15,22 @@ interface MonacoEditorProps extends ComponentProps {
14
15
  @class_ns( "x4" )
15
16
  export class MonacoEditor extends Component<MonacoEditorProps> {
16
17
 
17
- static initCount = 0;
18
- static basePath: string = "monaco/";
19
- static monaco: typeof Monaco;
20
- static initCbs: Function[] = [];
18
+ private static _initCount = 0;
19
+ private static _basePath: string = "monaco/";
20
+ private static _monaco: typeof Monaco;
21
+ private static _initCbs: Function[] = [];
21
22
 
22
23
  private _editor: Monaco.editor.IStandaloneCodeEditor;
23
24
 
24
- static async start( ) {
25
- if( this.initCount ) {
25
+ static async _start( ) {
26
+ if( this._initCount ) {
26
27
  return;
27
28
  }
28
29
 
29
- // path must be hard coded for esbuild to work
30
+ // dynamic import (without esbuild intervention)
30
31
  const dynamicImport = new Function("path", "return import('./'+path)");
31
- this.monaco = (await dynamicImport(this.basePath + "monaco.js")).default;
32
- //this.monaco = (await import(`${this.basePath}monaco.js`)).default;
33
- //this.monaco = (await import( "./bin/monaco.js" )).default;
34
- this.initCount++;
32
+ this._monaco = (await dynamicImport(this._basePath + "monaco.js")).default;
33
+ this._initCount++;
35
34
 
36
35
  globalThis.MonacoEnvironment = {
37
36
  getWorkerUrl: function (_moduleId, label) {
@@ -53,8 +52,8 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
53
52
  workerPath = "editor.worker.js";
54
53
  }
55
54
 
56
- const fullpath = MonacoEditor.basePath+'workers/'+workerPath;
57
- console.log( `getting "${label} path: ${fullpath}` );
55
+ const fullpath = MonacoEditor._basePath+'workers/'+workerPath;
56
+ //console.log( `getting "${label} path: ${fullpath}` );
58
57
  return fullpath;
59
58
  }
60
59
  };
@@ -62,7 +61,7 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
62
61
  // custom append css
63
62
  const link = document.createElement('link');
64
63
  link.rel = 'stylesheet';
65
- link.href = MonacoEditor.basePath+'monaco.css';
64
+ link.href = MonacoEditor._basePath+'monaco.css';
66
65
  link.type = 'text/css';
67
66
  document.head.appendChild(link);
68
67
  }
@@ -70,16 +69,16 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
70
69
  static addTypelib( name: string, code: string ) {
71
70
  const register = ( ) => {
72
71
  ///@ts-ignore
73
- const ts = MonacoEditor.monaco.languages.typescript;
72
+ const ts = MonacoEditor._monaco.languages.typescript;
74
73
  ///@ts-ignore
75
74
  ts.typescriptDefaults.addExtraLib( code, `ts:filename/${name}`);
76
75
  }
77
76
 
78
- if( MonacoEditor.monaco ) {
77
+ if( MonacoEditor._monaco ) {
79
78
  register( );
80
79
  }
81
80
  else {
82
- this.initCbs.push( register );
81
+ this._initCbs.push( register );
83
82
  }
84
83
  }
85
84
 
@@ -89,11 +88,11 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
89
88
 
90
89
  super( props );
91
90
 
92
- MonacoEditor.start( )
91
+ MonacoEditor._start( )
93
92
  .then( ( ) => {
94
- MonacoEditor.initCbs.forEach( x => x() );
93
+ MonacoEditor._initCbs.forEach( x => x() );
95
94
 
96
- this._editor = MonacoEditor.monaco.editor.create( this.dom as HTMLElement, {
95
+ this._editor = MonacoEditor._monaco.editor.create( this.dom as HTMLElement, {
97
96
  value: content,
98
97
  language: props.language,
99
98
  theme: props.theme,
@@ -112,5 +111,25 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
112
111
  }
113
112
  })
114
113
  }
114
+
115
+ setTheme( name: string ) {
116
+ MonacoEditor._monaco.editor.setTheme( name );
117
+ }
118
+
119
+ queryInterface<T>(name: string): T {
120
+ if( name=='tab-handler' ) {
121
+ const rc = {
122
+ focusNext: ( ) : boolean => { return false; }
123
+ }
124
+ return rc as T;
125
+ }
126
+
127
+ return super.queryInterface( name );
128
+ }
129
+
130
+ static async monaco( ) {
131
+ await this._start( );
132
+ return this._monaco;
133
+ }
115
134
  }
116
135
 
@@ -20,6 +20,7 @@ import { AriaAttributes, unitless } from './core_styles';
20
20
  import { CoreEvent, EventMap } from './core_events';
21
21
  import { addEvent, DOMEventHandler, GlobalDOMEvents } from './core_dom';
22
22
  import { Application, EvMessage } from './core_application';
23
+ import { makeState } from './core_state.js';
23
24
 
24
25
  interface RefType<T extends Component> {
25
26
  dom: T;
@@ -164,7 +165,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
164
165
  protected readonly clsprefix: string; // internal class name prefix (x4 internal)
165
166
 
166
167
  #store: Map<string|symbol,any>;
167
-
168
+ #pstate: any;
168
169
 
169
170
  constructor( props: P ) {
170
171
  super( );
@@ -1156,6 +1157,41 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
1156
1157
  queryInterface<T>( name: string ): T {
1157
1158
  return null;
1158
1159
  }
1160
+
1161
+ // :: PERSISTENCE ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
1162
+
1163
+ protected loadPState( name: string, defaults: Record<string, any> ): any {
1164
+
1165
+ if( !this.#pstate ) {
1166
+ this.#pstate = {};
1167
+ }
1168
+
1169
+ if( this.#pstate[name] ) {
1170
+ return this.#pstate[name];
1171
+ }
1172
+
1173
+ const key = `x4@persist:${name}`;
1174
+
1175
+ let raw: Record<string, any>;
1176
+ try {
1177
+ const stored = Application.instance().getStorage( key );
1178
+ raw = stored ? { ...defaults, ...JSON.parse( stored ) } : { ...defaults };
1179
+ }
1180
+ catch {
1181
+ raw = { ...defaults };
1182
+ }
1183
+
1184
+ const state = makeState( raw );
1185
+
1186
+ state.on( "change", ( ) => {
1187
+ this.setTimeout( key, 500, ( ) => {
1188
+ Application.instance().setStorage( key, JSON.stringify( raw ) );
1189
+ });
1190
+ });
1191
+
1192
+ this.#pstate[name] = state;
1193
+ return state;
1194
+ }
1159
1195
  }
1160
1196
 
1161
1197
 
@@ -155,7 +155,7 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
155
155
  * @param value - The value to store for the environment variable.
156
156
  */
157
157
 
158
- setEnv( name: string, value: any ) {
158
+ setEnv<T = any>( name: string, value: T ) {
159
159
  this.env.set( name, value );
160
160
  }
161
161
 
@@ -166,7 +166,7 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
166
166
  * @returns The value of the environment variable, or `def_value` if not found.
167
167
  */
168
168
 
169
- getEnv( name: string, def_value?: any ) {
169
+ getEnv<T = any>( name: string, def_value?: T ) : T {
170
170
  return this.env.get( name ) ?? def_value;
171
171
  }
172
172
 
@@ -4,64 +4,227 @@
4
4
  * @copyright (c) 2025 R-libre ingenierie, all rights reserved.
5
5
  **/
6
6
 
7
+ import { CoreEvent, EventMap, EventSource } from './core_events.js';
8
+ import { isPlainObject } from './core_tools.js';
9
+
10
+ type StateData = boolean | number | string | Date | unknown;
11
+ type State = Record<string, StateData>;
12
+
13
+ export interface EvStateChange extends CoreEvent {
14
+ readonly path: string;
15
+ readonly value: any;
16
+ }
17
+
18
+ export interface StateEvents extends EventMap {
19
+ change: EvStateChange;
20
+ }
21
+
22
+ /**
23
+ * type of the proxified state: the data itself + the manager's event API.
24
+ * Pick<> avoids re-declaring the signatures — if on/off/once evolve
25
+ * in StateManager, this type follows automatically.
26
+ */
27
+
28
+ export type StateProxy<T extends State> = T & Pick<StateManager<T>, "on" | "off" | "once" | "watch">;
7
29
 
8
- type StateData = boolean | number | string | Date | any;
9
- type State = Record<string,StateData>;
10
30
 
11
31
  /**
12
- * @experimental
32
+ * true for values that must be wrapped in a proxy when accessed
33
+ * (plain objects & arrays — excludes null, Date, RegExp, Map, Set,
34
+ * class instances, ...)
13
35
  */
14
36
 
15
- export class StateManager {
16
-
17
- private _state: StateData;
18
- private _subscribers: Map<string,any>;
19
- private _currentTracking: Set<string>;
37
+ function _is_proxyable( value: any ): boolean {
38
+ return value !== null && typeof value === "object" && ( Array.isArray( value ) || isPlainObject( value ) );
39
+ }
20
40
 
21
- constructor(initialState: StateData ) {
22
- this._state = initialState ? { ...initialState } : {};
23
- this._subscribers = new Map();
24
- this._currentTracking = new Set( );
41
+ /**
42
+ * append a segment to a dotted path, using bracket notation
43
+ * for array indices: "items[3].name" instead of "items.3.name"
44
+ */
45
+ function _child_path( path: string, prop: string, target: any ): string {
46
+ if( Array.isArray( target ) && /^\d+$/.test( prop ) ) {
47
+ return `${path}[${prop}]`;
25
48
  }
49
+ return path ? `${path}.${prop}` : prop;
50
+ }
26
51
 
27
- getState( path: string, defaultValue: StateData = null) {
28
- // Optional tracking for reactivity
29
- if (this._currentTracking) {
30
- this._currentTracking.add(path);
31
- }
32
-
33
- // Fast path-based access
34
- const parts = path.split('.');
35
- let current = this._state;
36
-
37
- for (const part of parts) {
38
- if (current?.[part] === undefined) {
39
- return defaultValue;
40
- }
52
+ /**
53
+ * true when 'path' is 'watched' itself or one of its descendants:
54
+ * _path_matches( "user", "user" ) true (the value itself)
55
+ * _path_matches( "user.name", "user" ) → true (descendant)
56
+ * _path_matches( "user.tags[2]", "user.tags") → true (array element)
57
+ * _path_matches( "username", "user" ) → false (boundary check)
58
+ */
59
+ function _path_matches( path: string, watched: string ): boolean {
60
+ if( !path.startsWith(watched) ) return false;
61
+ if( path.length===watched.length ) return true;
62
+
63
+ const c = path[watched.length];
64
+ return c==='.' || c==='[';
65
+ }
41
66
 
42
- current = current[part];
67
+ /**
68
+ *
69
+ */
70
+
71
+ export class StateManager<T extends State> extends EventSource<StateEvents> {
72
+
73
+ private _state: T;
74
+ private _proxy: StateProxy<T> | undefined;
75
+
76
+ /**
77
+ * raw object → proxy cache, scoped to THIS manager
78
+ * - avoids re-creating a proxy on every get
79
+ * - guarantees referential identity: state.user === state.user
80
+ * - WeakMap: entries are collected with their objects, and the whole cache is collected with the manager
81
+ * - instance-scoped (not module-level) so the same raw object referenced by two different managers never gets a proxy bound to the wrong manager/path
82
+ */
83
+
84
+ private _cache = new WeakMap<object, any>( );
85
+
86
+ constructor( initialState: T ) {
87
+ super( );
88
+ this._state = { ...initialState };
89
+ }
90
+
91
+ proxify( ): StateProxy<T> {
92
+
93
+ if( !this._proxy ) {
94
+ // event API attached as non-enumerable properties on the raw root object: invisible to for...in / Object.keys
95
+ // OR JSON.stringify, and served by the get trap with no special handling
96
+
97
+ Object.defineProperties( this._state, {
98
+ on: { value: this.on.bind( this ), enumerable: false },
99
+ off: { value: this.off.bind( this ), enumerable: false },
100
+ once: { value: this.once.bind( this ), enumerable: false },
101
+ watch: { value: this.watch.bind( this ), enumerable: false },
102
+ });
103
+
104
+ this._proxy = this._mk_proxy( this._state, "" );
43
105
  }
44
106
 
45
- return current;
107
+ return this._proxy;
46
108
  }
47
109
 
48
- setState(path: string, value: StateData, context: any = {} ) {
49
- // Update state
50
- const parts = path.split('.');
51
- let current = this._state;
110
+ /**
111
+ * create a lazy proxy: sub-objects and arrays are only wrapped
112
+ * when actually accessed, and are never copied — the original
113
+ * object remains the single source of truth.
114
+ */
115
+
116
+ private _mk_proxy( obj: any, path: string ): any {
117
+
118
+ const cached = this._cache.get( obj );
119
+ if( cached ) return cached;
120
+
121
+ const proxy = new Proxy( obj, {
52
122
 
53
- for (let i = 0; i < parts.length - 1; i++) {
54
- const part = parts[i];
55
- if (!current[part] || typeof current[part] !== 'object') {
56
- current[part] = {};
123
+ get: ( target, prop, receiver ) => {
124
+
125
+ // symbols (Symbol.iterator, Symbol.toPrimitive, devtools inspection...):
126
+ // pass-through, never an error nor a wrap
127
+ if( typeof prop === "symbol" ) {
128
+ return Reflect.get( target, prop, receiver );
129
+ }
130
+
131
+ // genuinely missing property ('in' walks the prototype chain, so array methods, toString, toJSON... never trigger a false positive)
132
+ if( !( prop in target ) ) {
133
+ console.error( `state error, unable to find ${_child_path(path,prop,target)}` );
134
+ return undefined;
135
+ }
136
+
137
+ const value = Reflect.get( target, prop, receiver );
138
+
139
+ // wrap on demand, only what is actually read
140
+ if( _is_proxyable( value ) ) {
141
+ return this._mk_proxy( value, _child_path( path, prop, target ) );
142
+ }
143
+
144
+ return value;
145
+ },
146
+
147
+ set: ( target, prop, value, receiver ) => {
148
+
149
+ // no notification when the value doesn't change
150
+ // (avoids noise and listener → set → listener loops)
151
+ if( Reflect.get( target, prop, receiver ) === value ) {
152
+ return true;
153
+ }
154
+
155
+ Reflect.set( target, prop, value, receiver );
156
+
157
+ // no readable path for a symbol: assign without firing
158
+ if( typeof prop === "string" ) {
159
+ this.fire( "change", { path: _child_path(path,prop,target), value } );
160
+ }
161
+
162
+ return true;
163
+ },
164
+
165
+ deleteProperty: ( target, prop ) => {
166
+
167
+ if( !( prop in target ) ) return true;
168
+
169
+ delete target[prop];
170
+
171
+ if( typeof prop === "string" ) {
172
+ this.fire( "change", { path: _child_path(path,prop,target), value: undefined } );
173
+ }
174
+
175
+ return true;
57
176
  }
58
-
59
- current = current[part];
177
+ });
178
+
179
+ this._cache.set( obj, proxy );
180
+ return proxy;
181
+ }
182
+
183
+ on<K extends keyof StateEvents>( name: K, listener: ( ev: StateEvents[K] ) => void ) {
184
+ this.addListener( name, listener );
185
+ return {
186
+ off: ( ) => this.removeListener( name, listener )
60
187
  }
188
+ }
61
189
 
62
- current[parts[parts.length - 1]] = value;
190
+ off<K extends keyof StateEvents>( name: K, listener: ( ev: StateEvents[K] ) => void ) {
191
+ this.removeListener( name, listener );
192
+ }
193
+
194
+ once<K extends keyof StateEvents>( name: K, listener: ( ev: StateEvents[K] ) => void ) {
195
+ const handle = this.on( name, ( e ) => {
196
+ handle.off( );
197
+ listener( e );
198
+ });
199
+ return handle;
200
+ }
63
201
 
64
- // Notify subscribers
65
- //this._notifySubscribers(path, value);
202
+ /**
203
+ * observe changes on a specific path and everything below it.
204
+ * fires for the path itself and for any descendant:
205
+ * watch( "user", cb ) → fires on user, user.name, user.tags[0], ...
206
+ * returns a handle with off() to unsubscribe.
207
+ */
208
+ watch( path: string, cb: ( ev: EvStateChange ) => void ) {
209
+ return this.on( "change", e => {
210
+ if( _path_matches( e.path, path ) ) {
211
+ cb( e );
212
+ }
213
+ });
66
214
  }
67
- }
215
+ }
216
+
217
+
218
+ /**
219
+ * create a ready-to-use reactive state
220
+ *
221
+ * const state = makeState( { count: 0, items: [1,2,3] } );
222
+ * state.on( "change", e => console.log( e.path, "=", e.value ) );
223
+ * state.count++; // "count = 1"
224
+ * state.items.push( 4 ); // "items.3 = 4"
225
+ */
226
+
227
+ export function makeState<T extends State>( initialState: T ): StateProxy<T> {
228
+ return new StateManager( initialState ).proxify( );
229
+ }
230
+
@@ -328,9 +328,12 @@ export class Timer {
328
328
  this.clearTimeout(name);
329
329
  }
330
330
 
331
- const tm = setTimeout(callback, time);
332
- this._timers.set(name, tm);
331
+ const tm = setTimeout( ( ) => {
332
+ this._timers?.delete( name );
333
+ callback( );
334
+ }, time);
333
335
 
336
+ this._timers.set(name, tm);
334
337
  return tm;
335
338
  }
336
339
 
@@ -373,6 +376,10 @@ export class Timer {
373
376
 
374
377
  this._timers = null;
375
378
  }
379
+
380
+ debounce( name: string, time: number, callback: Function ) {
381
+ this.setTimeout( name, time, callback );
382
+ }
376
383
  }
377
384
 
378
385
  /**
@@ -1018,4 +1025,98 @@ export function sanitizeHtml( input: string ): string {
1018
1025
  export function safeText( v: string | UnsafeHtml ): string {
1019
1026
  if( !v ) { return ""; }
1020
1027
  return v instanceof UnsafeHtml ? v.toString() : sanitizeHtml( v );
1021
- }
1028
+ }
1029
+
1030
+
1031
+ /**
1032
+ * split a dotted path into segments.
1033
+ * "user.name" → [ "user", "name" ]
1034
+ * "user.tags[2]" → [ "user", "tags", "2" ]
1035
+ * "items[0][1]" → [ "items", "0", "1" ]
1036
+ */
1037
+ function _parse_path( path: string ): string[] {
1038
+
1039
+ const segments: string[] = [];
1040
+
1041
+ for( const part of path.split( '.' ) ) {
1042
+ // "tags[2]" → "tags" then "2" ; "name" → "name"
1043
+ let rest = part;
1044
+
1045
+ const bracket = rest.indexOf( '[' );
1046
+ if( bracket < 0 ) {
1047
+ segments.push( rest );
1048
+ continue;
1049
+ }
1050
+
1051
+ if( bracket > 0 ) {
1052
+ segments.push( rest.substring( 0, bracket ) );
1053
+ }
1054
+
1055
+ // consume every [n] group
1056
+ const re = /\[(\d+)\]/g;
1057
+ let m;
1058
+ while( ( m = re.exec( rest ) ) !== null ) {
1059
+ segments.push( m[1] );
1060
+ }
1061
+ }
1062
+
1063
+ return segments;
1064
+ }
1065
+
1066
+
1067
+ /**
1068
+ * walk down 'obj' following 'segments', stopping before the last one.
1069
+ * returns the parent object of the final segment, or undefined if
1070
+ * the path is broken somewhere.
1071
+ */
1072
+ function _walk_to_parent( obj: any, segments: string[] ): any {
1073
+
1074
+ let current = obj;
1075
+
1076
+ for( let i = 0; i < segments.length - 1; i++ ) {
1077
+ if( current === null || current === undefined ) {
1078
+ return undefined;
1079
+ }
1080
+ current = current[segments[i]];
1081
+ }
1082
+
1083
+ return current;
1084
+ }
1085
+
1086
+
1087
+ /**
1088
+ * read the value at the given path.
1089
+ * getMemberValue( state, "user.tags[2]" )
1090
+ */
1091
+
1092
+ export function getMemberValue( obj: any, path: string ): any {
1093
+
1094
+ const segments = _parse_path( path );
1095
+ const parent = _walk_to_parent( obj, segments );
1096
+
1097
+ if( parent === undefined || parent === null ) {
1098
+ return undefined;
1099
+ }
1100
+
1101
+ return parent[ segments[segments.length - 1] ];
1102
+ }
1103
+
1104
+
1105
+ /**
1106
+ * write the value at the given path.
1107
+ * silently does nothing if an intermediate segment is missing
1108
+ */
1109
+
1110
+ export function setMemberValue( obj: any, path: string, value: any ) : boolean {
1111
+
1112
+ const segments = _parse_path( path );
1113
+ const parent = _walk_to_parent( obj, segments );
1114
+
1115
+ if( parent === undefined || parent === null ) {
1116
+ return false;
1117
+ }
1118
+
1119
+ parent[ segments[segments.length - 1] ] = value;
1120
+ return true;
1121
+ }
1122
+