x4js 2.2.31 → 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 +3 -3
- package/src/components/monaco/monaco.ts +42 -19
- package/src/core/component.ts +37 -1
- package/src/core/core_application.ts +2 -2
- package/src/core/core_state.ts +205 -42
- package/src/core/core_tools.ts +104 -3
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "x4js",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.33",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "src/x4.ts",
|
|
6
6
|
"module": "src/x4.ts",
|
|
7
7
|
"types": "src/x4.ts",
|
|
8
8
|
"exports": {
|
|
9
|
-
".": "./src/x4.ts"
|
|
9
|
+
".": "./src/x4.ts",
|
|
10
|
+
"./monaco": "./src/components/monaco/monaco.ts"
|
|
10
11
|
},
|
|
11
12
|
"scripts": {
|
|
12
13
|
"build-publish": "deno run --allow-all ./publish.ts",
|
|
@@ -35,7 +36,6 @@
|
|
|
35
36
|
"typescript": "^5.8.3"
|
|
36
37
|
},
|
|
37
38
|
"dependencies": {
|
|
38
|
-
"tsx": "^4.22.4",
|
|
39
39
|
"typedoc": "^0.28.19",
|
|
40
40
|
"typedoc-plugin-markdown": "^4.12.0"
|
|
41
41
|
}
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { class_ns
|
|
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,20 +15,22 @@ interface MonacoEditorProps extends ComponentProps {
|
|
|
14
15
|
@class_ns( "x4" )
|
|
15
16
|
export class MonacoEditor extends Component<MonacoEditorProps> {
|
|
16
17
|
|
|
17
|
-
static
|
|
18
|
-
static
|
|
19
|
-
static
|
|
20
|
-
static
|
|
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
|
|
25
|
-
if( this.
|
|
25
|
+
static async _start( ) {
|
|
26
|
+
if( this._initCount ) {
|
|
26
27
|
return;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
// dynamic import (without esbuild intervention)
|
|
31
|
+
const dynamicImport = new Function("path", "return import('./'+path)");
|
|
32
|
+
this._monaco = (await dynamicImport(this._basePath + "monaco.js")).default;
|
|
33
|
+
this._initCount++;
|
|
31
34
|
|
|
32
35
|
globalThis.MonacoEnvironment = {
|
|
33
36
|
getWorkerUrl: function (_moduleId, label) {
|
|
@@ -49,8 +52,8 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
|
|
|
49
52
|
workerPath = "editor.worker.js";
|
|
50
53
|
}
|
|
51
54
|
|
|
52
|
-
const fullpath = MonacoEditor.
|
|
53
|
-
console.log( `getting "${label} path: ${fullpath}` );
|
|
55
|
+
const fullpath = MonacoEditor._basePath+'workers/'+workerPath;
|
|
56
|
+
//console.log( `getting "${label} path: ${fullpath}` );
|
|
54
57
|
return fullpath;
|
|
55
58
|
}
|
|
56
59
|
};
|
|
@@ -58,7 +61,7 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
|
|
|
58
61
|
// custom append css
|
|
59
62
|
const link = document.createElement('link');
|
|
60
63
|
link.rel = 'stylesheet';
|
|
61
|
-
link.href = MonacoEditor.
|
|
64
|
+
link.href = MonacoEditor._basePath+'monaco.css';
|
|
62
65
|
link.type = 'text/css';
|
|
63
66
|
document.head.appendChild(link);
|
|
64
67
|
}
|
|
@@ -66,16 +69,16 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
|
|
|
66
69
|
static addTypelib( name: string, code: string ) {
|
|
67
70
|
const register = ( ) => {
|
|
68
71
|
///@ts-ignore
|
|
69
|
-
const ts = MonacoEditor.
|
|
72
|
+
const ts = MonacoEditor._monaco.languages.typescript;
|
|
70
73
|
///@ts-ignore
|
|
71
74
|
ts.typescriptDefaults.addExtraLib( code, `ts:filename/${name}`);
|
|
72
75
|
}
|
|
73
76
|
|
|
74
|
-
if( MonacoEditor.
|
|
77
|
+
if( MonacoEditor._monaco ) {
|
|
75
78
|
register( );
|
|
76
79
|
}
|
|
77
80
|
else {
|
|
78
|
-
this.
|
|
81
|
+
this._initCbs.push( register );
|
|
79
82
|
}
|
|
80
83
|
}
|
|
81
84
|
|
|
@@ -85,11 +88,11 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
|
|
|
85
88
|
|
|
86
89
|
super( props );
|
|
87
90
|
|
|
88
|
-
MonacoEditor.
|
|
91
|
+
MonacoEditor._start( )
|
|
89
92
|
.then( ( ) => {
|
|
90
|
-
MonacoEditor.
|
|
93
|
+
MonacoEditor._initCbs.forEach( x => x() );
|
|
91
94
|
|
|
92
|
-
this._editor = MonacoEditor.
|
|
95
|
+
this._editor = MonacoEditor._monaco.editor.create( this.dom as HTMLElement, {
|
|
93
96
|
value: content,
|
|
94
97
|
language: props.language,
|
|
95
98
|
theme: props.theme,
|
|
@@ -108,5 +111,25 @@ export class MonacoEditor extends Component<MonacoEditorProps> {
|
|
|
108
111
|
}
|
|
109
112
|
})
|
|
110
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
|
+
}
|
|
111
134
|
}
|
|
112
135
|
|
package/src/core/component.ts
CHANGED
|
@@ -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:
|
|
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?:
|
|
169
|
+
getEnv<T = any>( name: string, def_value?: T ) : T {
|
|
170
170
|
return this.env.get( name ) ?? def_value;
|
|
171
171
|
}
|
|
172
172
|
|
package/src/core/core_state.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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
|
|
107
|
+
return this._proxy;
|
|
46
108
|
}
|
|
47
109
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
+
|
package/src/core/core_tools.ts
CHANGED
|
@@ -328,9 +328,12 @@ export class Timer {
|
|
|
328
328
|
this.clearTimeout(name);
|
|
329
329
|
}
|
|
330
330
|
|
|
331
|
-
const tm = setTimeout(
|
|
332
|
-
|
|
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
|
+
|