x4js 2.3.14 → 2.3.16
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 +144 -10
- package/package.json +1 -1
- package/src/components/colorinput/colorinput.ts +5 -2
- package/src/components/components.ts +1 -0
- package/src/components/dockingview/dockingview.module.scss +279 -0
- package/src/components/dockingview/dockingview.ts +899 -0
- package/src/components/dockingview/docklayout.ts +659 -0
- 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/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 ) => shortcuts.handle( ev as KeyboardEvent ) );
|
|
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"
|