x4js 2.3.13 → 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 +50 -13
- package/package.json +1 -1
- package/src/components/colorinput/colorinput.module.scss +36 -2
- package/src/components/colorinput/colorinput.ts +205 -32
- package/src/components/colorpicker/colorpicker.module.scss +23 -0
- package/src/components/colorpicker/colorpicker.ts +149 -35
- package/src/components/popup/popup.ts +7 -6
- package/src/components/propgrid/progrid.module.scss +49 -31
- package/src/components/propgrid/propgrid.ts +36 -3
- package/src/core/component.ts +52 -6
- 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/core/core_tools.ts +1 -1
- package/src/x4.d.ts +1 -0
- package/src/x4.ts +1 -0
|
@@ -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/core/core_tools.ts
CHANGED
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"
|