x4js 2.3.14 → 2.3.15
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/aicontext.md +28 -9
- package/package.json +1 -1
- package/src/components/colorinput/colorinput.ts +5 -2
- package/src/components/popup/popup.ts +7 -6
- package/src/core/component.ts +38 -1
- package/src/core/core_application.ts +33 -0
- package/src/core/core_shortcuts.ts +181 -0
- package/src/core/core_state.ts +7 -3
- package/src/x4.d.ts +1 -0
- package/src/x4.ts +1 -0
package/aicontext.md
CHANGED
|
@@ -46,10 +46,11 @@ CoreElement<E> named timers + event surface (no DOM)
|
|
|
46
46
|
|
|
47
47
|
EventSource<E> standalone event registry
|
|
48
48
|
├── DataStore<T>
|
|
49
|
-
|
|
49
|
+
├── Router
|
|
50
|
+
└── StateManager<T>
|
|
50
51
|
|
|
51
52
|
CoreElement-based (non-DOM): DataProxy, DataView, Store
|
|
52
|
-
Standalone: Rect, Timer, Color, DataModel,
|
|
53
|
+
Standalone: Rect, Timer, Color, DataModel, Stylesheet, ComputedStyle,
|
|
53
54
|
SvgBuilder, X4PDFBuilder, x4_react, CMover, UnsafeHtml (extends String)
|
|
54
55
|
```
|
|
55
56
|
|
|
@@ -1277,17 +1278,36 @@ initTooltips(): void // initialize the global
|
|
|
1277
1278
|
|
|
1278
1279
|
---
|
|
1279
1280
|
|
|
1280
|
-
## State (`core_state`)
|
|
1281
|
+
## State (`core_state`)
|
|
1281
1282
|
|
|
1282
1283
|
```ts
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1284
|
+
makeState<T>(initialState: T): StateProxy<T> // the data itself + on / off / once / watch
|
|
1285
|
+
|
|
1286
|
+
const state = makeState({ count: 0, user: { name: '' }, items: [1, 2, 3] })
|
|
1287
|
+
state.on('change', e => ...) // EvStateChange { path, value }
|
|
1288
|
+
state.watch('user', e => ...) // 'user', anything below it, or a parent replaced; returns { off() }
|
|
1289
|
+
state.count++ // path "count"
|
|
1290
|
+
state.user.name = 'x' // path "user.name"
|
|
1291
|
+
state.items.push(4) // path "items[3]"
|
|
1292
|
+
|
|
1293
|
+
class StateManager<T> extends EventSource<StateEvents> {
|
|
1294
|
+
constructor(initialState: T)
|
|
1295
|
+
proxify(): StateProxy<T>
|
|
1296
|
+
on(name, listener): { off(): void }
|
|
1297
|
+
off(name, listener): void
|
|
1298
|
+
once(name, listener): { off(): void }
|
|
1299
|
+
watch(path: string, cb: (ev: EvStateChange) => void): { off(): void }
|
|
1287
1300
|
}
|
|
1288
|
-
|
|
1301
|
+
|
|
1302
|
+
// Component (protected): a state kept in localStorage under "x4@persist:<name>",
|
|
1303
|
+
// saved 500 ms after a change
|
|
1304
|
+
loadPState(name: string, defaults: Record<string, any>): any
|
|
1289
1305
|
```
|
|
1290
1306
|
|
|
1307
|
+
- Plain objects and arrays are proxied when read; Date, Map, Set and class instances are not.
|
|
1308
|
+
- Setting a property to the value it already has fires nothing.
|
|
1309
|
+
- Reading a property that does not exist logs an error and returns `undefined`.
|
|
1310
|
+
|
|
1291
1311
|
---
|
|
1292
1312
|
|
|
1293
1313
|
## Common patterns
|
|
@@ -1487,7 +1507,6 @@ Optional keys: `external`, `define`, `esbuild`, `dev` (`host`, `port`, `https`,
|
|
|
1487
1507
|
- The default language is `fr`. Call `selectLanguage('en')` to switch to English.
|
|
1488
1508
|
- `Gridview` is backed by a `DataStore`; `Spreadsheet` is backed by a `Store` (different, cell-based).
|
|
1489
1509
|
- `DataRecordID` is currently typed `any`; `strictNullChecks` is intentionally off in the recommended `tsconfig.json`.
|
|
1490
|
-
- `StateManager` (`core_state`) is marked **Experimental** — verify before relying on it.
|
|
1491
1510
|
- `MonacoEditor` requires `MonacoEditor.start()` (async) before use; register extra typings with `MonacoEditor.addTypelib(name, code)`.
|
|
1492
1511
|
- `x4_react.create_element` is the JSX pragma;
|
|
1493
1512
|
- `unbubbleEvents` is the set of DOM events that do not bubble through the x4 component tree.
|
package/package.json
CHANGED
|
@@ -86,6 +86,7 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
86
86
|
private _edit: Input;
|
|
87
87
|
private _swatches: Swatches | ( ( ) => Swatches );
|
|
88
88
|
private _popup: Popup; // the color picker, once it was opened
|
|
89
|
+
private _closedAt = -1; // when it closed for the last time (performance.now)
|
|
89
90
|
|
|
90
91
|
constructor( props: ColorInputProps ) {
|
|
91
92
|
super( props );
|
|
@@ -128,11 +129,12 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
128
129
|
if( props.picker ) {
|
|
129
130
|
// A click on the swatch opens the picker, a second one closes it.
|
|
130
131
|
// The picker closes by itself as soon as the button goes down
|
|
131
|
-
// outside of it
|
|
132
|
+
// outside of it, before the swatch hears of it: it was open if
|
|
133
|
+
// it closed since this very press began.
|
|
132
134
|
let wasOpen = false;
|
|
133
135
|
|
|
134
136
|
this._swatch.addClass( "picker" );
|
|
135
|
-
this._swatch.addDOMEvent( "pointerdown",
|
|
137
|
+
this._swatch.addDOMEvent( "pointerdown", ev => { wasOpen = this._closedAt >= ev.timeStamp; } );
|
|
136
138
|
this._swatch.addDOMEvent( "click", ( ) => {
|
|
137
139
|
if( !wasOpen ) {
|
|
138
140
|
this._openPicker( );
|
|
@@ -202,6 +204,7 @@ export class ColorInput extends HBox<ColorInputProps,ColorInputEvents> {
|
|
|
202
204
|
} );
|
|
203
205
|
|
|
204
206
|
const popup = this._popup = new Popup( { cls: "x4colorinput-popup", autoClose: true, content: picker } );
|
|
207
|
+
popup.on( "closed", ( ) => { this._closedAt = performance.now( ); } );
|
|
205
208
|
|
|
206
209
|
// after the construction: the picker fires a first change when it is built
|
|
207
210
|
picker.on( "change", ev => {
|
|
@@ -233,7 +233,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
233
233
|
if( idx>=0 ) {
|
|
234
234
|
autoclose_list.splice( idx, 1 );
|
|
235
235
|
if( autoclose_list.length==0 ) {
|
|
236
|
-
document.removeEventListener( "pointerdown", this._dismiss );
|
|
236
|
+
document.removeEventListener( "pointerdown", this._dismiss, true );
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
239
|
}
|
|
@@ -263,7 +263,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
263
263
|
|
|
264
264
|
if( this.props.autoClose ) {
|
|
265
265
|
if( autoclose_list.length==0 ) {
|
|
266
|
-
document.addEventListener( "pointerdown", this._dismiss );
|
|
266
|
+
document.addEventListener( "pointerdown", this._dismiss, true );
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
autoclose_list.push( this );
|
|
@@ -322,6 +322,10 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
322
322
|
|
|
323
323
|
/**
|
|
324
324
|
* binded
|
|
325
|
+
*
|
|
326
|
+
* listened on the document while the event goes down (capture): a press
|
|
327
|
+
* closes the popups even when what is pressed stops its propagation.
|
|
328
|
+
* the event is left as it is, what is pressed handles it as usual.
|
|
325
329
|
*/
|
|
326
330
|
|
|
327
331
|
private _dismiss = ( e: UIEvent ) => {
|
|
@@ -330,9 +334,6 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
330
334
|
return;
|
|
331
335
|
}
|
|
332
336
|
|
|
333
|
-
e.preventDefault( );
|
|
334
|
-
e.stopPropagation( );
|
|
335
|
-
|
|
336
337
|
this.dismiss( );
|
|
337
338
|
}
|
|
338
339
|
|
|
@@ -368,7 +369,7 @@ export class Popup<P extends PopupProps = PopupProps, E extends PopupEvents = Po
|
|
|
368
369
|
const list = inc_group.reverse( );
|
|
369
370
|
autoclose_list = excl_group;
|
|
370
371
|
if( autoclose_list.length==0 ) {
|
|
371
|
-
document.removeEventListener( "pointerdown", this._dismiss );
|
|
372
|
+
document.removeEventListener( "pointerdown", this._dismiss, true );
|
|
372
373
|
}
|
|
373
374
|
|
|
374
375
|
list.forEach( x => x.close() );
|
package/src/core/component.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { CoreEvent, EventMap } from './core_events';
|
|
|
21
21
|
import { addEvent, DOMEventHandler, GlobalDOMEvents, COMPONENT } from './core_dom';
|
|
22
22
|
import { Application, EvMessage } from './core_application';
|
|
23
23
|
import { makeState } from './core_state.js';
|
|
24
|
+
import { Shortcuts, ShortcutCallback, ShortcutOptions } from './core_shortcuts';
|
|
24
25
|
|
|
25
26
|
type ComponentAttributes = Record<string,string|number|boolean>;
|
|
26
27
|
type CreateComponentCallBack = ( attrs: Record<string,string> ) => ComponentContent;
|
|
@@ -169,6 +170,7 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
169
170
|
|
|
170
171
|
#store: Map<string|symbol,any>;
|
|
171
172
|
#pstate: any;
|
|
173
|
+
#shortcuts: Shortcuts;
|
|
172
174
|
|
|
173
175
|
constructor(props: P) {
|
|
174
176
|
super( );
|
|
@@ -1169,6 +1171,40 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
1169
1171
|
return null;
|
|
1170
1172
|
}
|
|
1171
1173
|
|
|
1174
|
+
// :: SHORTCUTS ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
1175
|
+
|
|
1176
|
+
/**
|
|
1177
|
+
* Adds a keyboard shortcut to the component. It works while the focus
|
|
1178
|
+
* is in the component, and while the focus is nowhere if this component
|
|
1179
|
+
* is the last one with shortcuts that had it: of two views that are shown
|
|
1180
|
+
* together, the one the user works in answers.
|
|
1181
|
+
*
|
|
1182
|
+
* A key that a child has used (it prevented its default, or stopped its
|
|
1183
|
+
* propagation) is not a shortcut. Nor is a key typed in a field, unless
|
|
1184
|
+
* `editable` is set.
|
|
1185
|
+
*
|
|
1186
|
+
* @param keys - "Mod+C", "Shift+Mod+Z", "Delete"... or several of them. Mod is Ctrl, or Cmd on a Mac.
|
|
1187
|
+
* @see Application.addShortcut for the shortcuts of the whole application
|
|
1188
|
+
*
|
|
1189
|
+
* @example
|
|
1190
|
+
* this.addShortcut( "Mod+C", ( ) => this.copy( ) );
|
|
1191
|
+
* this.addShortcut( ["Mod+Y", "Shift+Mod+Z"], ( ) => this.redo( ) );
|
|
1192
|
+
*/
|
|
1193
|
+
|
|
1194
|
+
addShortcut( keys: string | string[], callback: ShortcutCallback, options?: ShortcutOptions ): this {
|
|
1195
|
+
if( !this.#shortcuts ) {
|
|
1196
|
+
const shortcuts = this.#shortcuts = new Shortcuts( );
|
|
1197
|
+
|
|
1198
|
+
// straight on the element, not with addDOMEvent: the handlers of the
|
|
1199
|
+
// children are called exactly as before
|
|
1200
|
+
this.dom.addEventListener( "keydown", ( ev: KeyboardEvent ) => shortcuts.handle( ev ) );
|
|
1201
|
+
this.dom.addEventListener( "focusin", ( ) => Application.instance( ).setActiveShortcuts( this, shortcuts ) );
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
this.#shortcuts.add( keys, callback, options );
|
|
1205
|
+
return this;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1172
1208
|
// :: PERSISTENCE ::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
|
|
1173
1209
|
|
|
1174
1210
|
protected loadPState( name: string, defaults: Record<string, any> ): any {
|
|
@@ -1194,9 +1230,10 @@ export class Component<P extends ComponentProps = ComponentProps, E extends Comp
|
|
|
1194
1230
|
|
|
1195
1231
|
const state = makeState( raw );
|
|
1196
1232
|
|
|
1233
|
+
// the state is what is saved, not raw: makeState works on a copy of it
|
|
1197
1234
|
state.on( "change", ( ) => {
|
|
1198
1235
|
this.setTimeout( key, 500, ( ) => {
|
|
1199
|
-
Application.instance().setStorage( key, JSON.stringify(
|
|
1236
|
+
Application.instance().setStorage( key, JSON.stringify( state ) );
|
|
1200
1237
|
} );
|
|
1201
1238
|
});
|
|
1202
1239
|
|
|
@@ -18,6 +18,7 @@ import { Component, componentFromDOM } from './component';
|
|
|
18
18
|
import { CoreElement } from './core_element';
|
|
19
19
|
import { CoreEvent, EventMap } from './core_events';
|
|
20
20
|
import { asap, getFocusableElements, ITabHandler } from './core_tools';
|
|
21
|
+
import { Shortcuts, ShortcutCallback, ShortcutOptions } from './core_shortcuts';
|
|
21
22
|
|
|
22
23
|
const socket_sent = Symbol( 'socket' );
|
|
23
24
|
|
|
@@ -80,6 +81,9 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
|
|
|
80
81
|
private mainview: Component;
|
|
81
82
|
private props: AppProps;
|
|
82
83
|
private mounted = false;
|
|
84
|
+
|
|
85
|
+
private _shortcuts = new Shortcuts( );
|
|
86
|
+
private _active: { owner: Component, shortcuts: Shortcuts };
|
|
83
87
|
|
|
84
88
|
/**
|
|
85
89
|
* Provides access to process-related information, such as touch capabilities.
|
|
@@ -206,9 +210,38 @@ export class Application<E extends ApplicationEvents = ApplicationEvents> extend
|
|
|
206
210
|
ev.preventDefault( );
|
|
207
211
|
}
|
|
208
212
|
}
|
|
213
|
+
|
|
214
|
+
// The focus is nowhere: the shortcuts of the view the user worked in last.
|
|
215
|
+
// (when it is in a view, the view has seen the key before us)
|
|
216
|
+
if( ev.target===document.body && this._active?.owner.dom.isConnected && this._active.shortcuts.handle( ev ) ) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
this._shortcuts.handle( ev );
|
|
209
221
|
} );
|
|
210
222
|
}
|
|
211
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Adds a keyboard shortcut to the whole application: it works wherever
|
|
226
|
+
* the focus is, even in a dialog. A key typed in a field is not a
|
|
227
|
+
* shortcut, unless `editable` is set.
|
|
228
|
+
*
|
|
229
|
+
* @param keys - "Mod+S", "F1"... or several of them. Mod is Ctrl, or Cmd on a Mac.
|
|
230
|
+
* @see Component.addShortcut for the shortcuts of a view
|
|
231
|
+
*/
|
|
232
|
+
|
|
233
|
+
addShortcut( keys: string | string[], callback: ShortcutCallback, options?: ShortcutOptions ) {
|
|
234
|
+
this._shortcuts.add( keys, callback, options );
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* @internal the last component with shortcuts that had the focus
|
|
239
|
+
*/
|
|
240
|
+
|
|
241
|
+
setActiveShortcuts( owner: Component, shortcuts: Shortcuts ) {
|
|
242
|
+
this._active = { owner, shortcuts };
|
|
243
|
+
}
|
|
244
|
+
|
|
212
245
|
/**
|
|
213
246
|
* Moves focus to the next or previous focusable element within the application.
|
|
214
247
|
* Handles Tab and Shift+Tab key presses.
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ___ ___ __
|
|
3
|
+
* \ \/ / / _
|
|
4
|
+
* \ / /_| |_
|
|
5
|
+
* / \____ _|
|
|
6
|
+
* /__/\__\ |_|
|
|
7
|
+
*
|
|
8
|
+
* @file core_shortcuts.ts
|
|
9
|
+
* @author Etienne Cochard
|
|
10
|
+
*
|
|
11
|
+
* @copyright (c) 2026 R-libre ingenierie
|
|
12
|
+
*
|
|
13
|
+
* Use of this source code is governed by an MIT-style license
|
|
14
|
+
* that can be found in the LICENSE file or at https://opensource.org/licenses/MIT.
|
|
15
|
+
**/
|
|
16
|
+
|
|
17
|
+
import { isArray } from './core_tools';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Keyboard shortcuts.
|
|
21
|
+
*
|
|
22
|
+
* A shortcut is a sequence of keys, written as it is read: "Mod+C",
|
|
23
|
+
* "Shift+Mod+Z", "Delete", "Alt+ArrowLeft". The modifiers are Shift, Ctrl,
|
|
24
|
+
* Cmd, Alt and Mod: the key of the commands, which is Ctrl on Windows and
|
|
25
|
+
* Linux, and Cmd on a Mac.
|
|
26
|
+
*
|
|
27
|
+
* This class only holds a list of shortcuts and finds the one a keyboard
|
|
28
|
+
* event is. It is used by Component.addShortcut (the shortcuts of a view,
|
|
29
|
+
* while the focus is in it) and by Application.addShortcut (those of the
|
|
30
|
+
* whole application).
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
export type ShortcutCallback = ( ev: KeyboardEvent ) => void;
|
|
34
|
+
|
|
35
|
+
export interface ShortcutOptions {
|
|
36
|
+
/** also while the user types in a field (Mod+S, F5...) */
|
|
37
|
+
editable?: boolean;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface Shortcut {
|
|
41
|
+
sequence: string;
|
|
42
|
+
callback: ShortcutCallback;
|
|
43
|
+
editable: boolean;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface Keys {
|
|
47
|
+
shift: boolean;
|
|
48
|
+
ctrl: boolean;
|
|
49
|
+
cmd: boolean;
|
|
50
|
+
alt: boolean;
|
|
51
|
+
key: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const isMac = typeof navigator!=="undefined" && /mac|iphone|ipad/i.test( navigator.platform ?? "" );
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* "Shift+Mod+K" -> shift, ctrl (cmd on a Mac), k
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
function parse( keys: string ): Keys {
|
|
61
|
+
const parts = keys.toLowerCase( ).split( "+" ).map( p => p.trim( ) );
|
|
62
|
+
const key = parts.pop( ) || "+"; // "Ctrl++": the key is +
|
|
63
|
+
const has = ( name: string ) => parts.includes( name );
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
shift: has( "shift" ),
|
|
67
|
+
ctrl: has( "ctrl" ) || ( has( "mod" ) && !isMac ),
|
|
68
|
+
cmd: has( "cmd" ) || ( has( "mod" ) && isMac ),
|
|
69
|
+
alt: has( "alt" ),
|
|
70
|
+
key
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* always in the same order: "shift+ctrl+cmd+alt+key"
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
function join( k: Keys ): string {
|
|
79
|
+
return ( k.shift ? "shift+" : "" ) + ( k.ctrl ? "ctrl+" : "" ) + ( k.cmd ? "cmd+" : "" ) + ( k.alt ? "alt+" : "" ) + k.key;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function sequenceOf( ev: KeyboardEvent ): string {
|
|
83
|
+
// AltGr writes characters, never a command (Windows reports it as ctrl + alt)
|
|
84
|
+
if( ev.getModifierState( "AltGraph" ) ) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// a keyboard without latin letters: the key where the letter is on a latin one
|
|
89
|
+
let key = ev.key.toLowerCase( );
|
|
90
|
+
if( key.length===1 && ( key<"a" || key>"z" ) && /^Key[A-Z]$/.test( ev.code ) ) {
|
|
91
|
+
key = ev.code[3].toLowerCase( );
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return join( { shift: ev.shiftKey, ctrl: ev.ctrlKey, cmd: ev.metaKey, alt: ev.altKey, key } );
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function isEditable( target: EventTarget ): boolean {
|
|
98
|
+
const el = target as HTMLElement;
|
|
99
|
+
return el.isContentEditable || el.tagName==="INPUT" || el.tagName==="TEXTAREA" || el.tagName==="SELECT";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
*
|
|
104
|
+
*/
|
|
105
|
+
|
|
106
|
+
export class Shortcuts {
|
|
107
|
+
|
|
108
|
+
private list: Shortcut[] = [];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param keys - a sequence, or several ones for the same action
|
|
112
|
+
* @example
|
|
113
|
+
* shortcuts.add( "Mod+C", ( ) => this.copy( ) );
|
|
114
|
+
* shortcuts.add( ["Mod+Y", "Shift+Mod+Z"], ( ) => this.redo( ) );
|
|
115
|
+
*/
|
|
116
|
+
|
|
117
|
+
add( keys: string | string[], callback: ShortcutCallback, options: ShortcutOptions = {} ) {
|
|
118
|
+
for( const k of isArray( keys ) ? keys : [keys] ) {
|
|
119
|
+
this.list.push( { sequence: join( parse( k ) ), callback, editable: !!options.editable } );
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
clear( ) {
|
|
124
|
+
this.list = [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* calls the shortcut a key is, if any
|
|
129
|
+
* @returns true when it was one
|
|
130
|
+
*/
|
|
131
|
+
|
|
132
|
+
handle( ev: KeyboardEvent ): boolean {
|
|
133
|
+
// already used by what has the focus (Enter on a button...)
|
|
134
|
+
if( ev.defaultPrevented ) {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const sequence = sequenceOf( ev );
|
|
139
|
+
const found = this.list.find( s => s.sequence===sequence && ( s.editable || !isEditable( ev.target ) ) );
|
|
140
|
+
if( !found ) {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
ev.preventDefault( );
|
|
145
|
+
found.callback( ev );
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* how the keys with a name are written
|
|
152
|
+
*/
|
|
153
|
+
|
|
154
|
+
const key_names: Record<string,string> = {
|
|
155
|
+
arrowleft: "←",
|
|
156
|
+
arrowup: "↑",
|
|
157
|
+
arrowright: "→",
|
|
158
|
+
arrowdown: "↓",
|
|
159
|
+
escape: "Esc",
|
|
160
|
+
delete: "Del",
|
|
161
|
+
" ": "Space",
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* How a sequence is written for the user, in a tooltip or a menu:
|
|
166
|
+
* "Ctrl+C", "Ctrl+Shift+Z", and on a Mac the signs of its keys.
|
|
167
|
+
*
|
|
168
|
+
* @example
|
|
169
|
+
* new Button( { icon, tooltip: `Undo (${shortcutText( "Mod+Z" )})` } )
|
|
170
|
+
*/
|
|
171
|
+
|
|
172
|
+
export function shortcutText( keys: string ): string {
|
|
173
|
+
const k = parse( keys );
|
|
174
|
+
const key = key_names[k.key] ?? ( k.key[0].toUpperCase( ) + k.key.substring( 1 ) );
|
|
175
|
+
|
|
176
|
+
if( isMac ) {
|
|
177
|
+
return ( k.ctrl ? "⌃" : "" ) + ( k.alt ? "⌥" : "" ) + ( k.shift ? "⇧" : "" ) + ( k.cmd ? "⌘" : "" ) + key;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return ( k.ctrl ? "Ctrl+" : "" ) + ( k.cmd ? "Meta+" : "" ) + ( k.alt ? "Alt+" : "" ) + ( k.shift ? "Shift+" : "" ) + key;
|
|
181
|
+
}
|
package/src/core/core_state.ts
CHANGED
|
@@ -139,9 +139,13 @@ export class StateManager<T extends State> extends EventSource<StateEvents> {
|
|
|
139
139
|
return Reflect.get( target, prop, receiver );
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// genuinely missing property ('in' walks the prototype chain, so array methods, toString
|
|
142
|
+
// genuinely missing property ('in' walks the prototype chain, so array methods, toString... never trigger a false positive).
|
|
143
|
+
// toJSON is asked by JSON.stringify to every object: a plain one has none, and that is not an error
|
|
143
144
|
if( !( prop in target ) ) {
|
|
144
|
-
|
|
145
|
+
if( prop !== "toJSON" ) {
|
|
146
|
+
console.error( `state error, unable to find ${_child_path(path,prop,target)}` );
|
|
147
|
+
}
|
|
148
|
+
|
|
145
149
|
return undefined;
|
|
146
150
|
}
|
|
147
151
|
|
|
@@ -237,7 +241,7 @@ export class StateManager<T extends State> extends EventSource<StateEvents> {
|
|
|
237
241
|
* const state = makeState( { count: 0, items: [1,2,3] } );
|
|
238
242
|
* state.on( "change", e => console.log( e.path, "=", e.value ) );
|
|
239
243
|
* state.count++; // "count = 1"
|
|
240
|
-
* state.items.push( 4 ); // "items
|
|
244
|
+
* state.items.push( 4 ); // "items[3] = 4"
|
|
241
245
|
*/
|
|
242
246
|
|
|
243
247
|
export function makeState<T extends Record<keyof T, StateData>>( initialState: T ): StateProxy<T> {
|
package/src/x4.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from "./core/core_i18n";
|
|
|
25
25
|
export * from "./core/core_pdf";
|
|
26
26
|
export * from "./core/core_react";
|
|
27
27
|
export * from "./core/core_router";
|
|
28
|
+
export * from "./core/core_shortcuts";
|
|
28
29
|
export * from "./core/core_state";
|
|
29
30
|
export * from "./core/core_styles";
|
|
30
31
|
export * from "./core/core_svg";
|
package/src/x4.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from "./core/core_i18n"
|
|
|
26
26
|
export * from "./core/core_pdf"
|
|
27
27
|
export * from "./core/core_react"
|
|
28
28
|
export * from "./core/core_router"
|
|
29
|
+
export * from "./core/core_shortcuts"
|
|
29
30
|
export * from "./core/core_state"
|
|
30
31
|
export * from "./core/core_styles"
|
|
31
32
|
export * from "./core/core_svg"
|