@jupyterlab/shortcuts-extension 4.0.0-alpha.19 → 4.0.0-alpha.20

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": "@jupyterlab/shortcuts-extension",
3
- "version": "4.0.0-alpha.19",
3
+ "version": "4.0.0-alpha.20",
4
4
  "description": "JupyterLab - Shortcuts Extension",
5
5
  "homepage": "https://github.com/jupyterlab/jupyterlab",
6
6
  "bugs": {
@@ -26,7 +26,8 @@
26
26
  "lib/**/*.{d.ts,eot,gif,html,jpg,js,js.map,json,png,svg,woff2,ttf}",
27
27
  "style/**/*.{css,eot,gif,html,jpg,json,png,svg,woff2,ttf}",
28
28
  "style/index.js",
29
- "schema/*.json"
29
+ "schema/*.json",
30
+ "src/**/*.{ts,tsx}"
30
31
  ],
31
32
  "scripts": {
32
33
  "build": "tsc -b",
@@ -40,25 +41,25 @@
40
41
  "watch": "tsc -b --watch"
41
42
  },
42
43
  "dependencies": {
43
- "@jupyterlab/application": "^4.0.0-alpha.19",
44
- "@jupyterlab/settingregistry": "^4.0.0-alpha.19",
45
- "@jupyterlab/translation": "^4.0.0-alpha.19",
46
- "@jupyterlab/ui-components": "^4.0.0-alpha.34",
47
- "@lumino/algorithm": "^2.0.0-beta.0",
48
- "@lumino/commands": "^2.0.0-beta.1",
49
- "@lumino/coreutils": "^2.0.0-beta.0",
50
- "@lumino/disposable": "^2.0.0-beta.1",
51
- "@lumino/domutils": "^2.0.0-beta.0",
52
- "@lumino/keyboard": "^2.0.0-beta.0",
53
- "@lumino/widgets": "^2.0.0-beta.1",
44
+ "@jupyterlab/application": "^4.0.0-alpha.20",
45
+ "@jupyterlab/settingregistry": "^4.0.0-alpha.20",
46
+ "@jupyterlab/translation": "^4.0.0-alpha.20",
47
+ "@jupyterlab/ui-components": "^4.0.0-alpha.35",
48
+ "@lumino/algorithm": "^2.0.0-rc.0",
49
+ "@lumino/commands": "^2.0.0-rc.0",
50
+ "@lumino/coreutils": "^2.0.0-rc.0",
51
+ "@lumino/disposable": "^2.0.0-rc.0",
52
+ "@lumino/domutils": "^2.0.0-rc.0",
53
+ "@lumino/keyboard": "^2.0.0-rc.0",
54
+ "@lumino/widgets": "^2.0.0-rc.0",
54
55
  "react": "^18.2.0"
55
56
  },
56
57
  "devDependencies": {
57
- "@jupyterlab/testing": "^4.0.0-alpha.19",
58
+ "@jupyterlab/testing": "^4.0.0-alpha.20",
58
59
  "@types/jest": "^29.2.0",
59
60
  "rimraf": "~3.0.0",
60
- "typedoc": "~0.22.10",
61
- "typescript": "~4.7.3"
61
+ "typedoc": "~0.23.25",
62
+ "typescript": "~5.0.0-beta"
62
63
  },
63
64
  "publishConfig": {
64
65
  "access": "public"
@@ -0,0 +1,501 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import * as React from 'react';
7
+
8
+ import { ITranslator } from '@jupyterlab/translation';
9
+
10
+ import { EN_US } from '@lumino/keyboard';
11
+ import { checkIcon, errorIcon } from '@jupyterlab/ui-components';
12
+
13
+ export interface IShortcutInputProps {
14
+ handleUpdate: Function;
15
+ deleteShortcut: Function;
16
+ toggleInput: Function;
17
+ shortcut: ShortcutObject;
18
+ shortcutId: string;
19
+ toSymbols: Function;
20
+ keyBindingsUsed: { [index: string]: TakenByObject };
21
+ sortConflict: Function;
22
+ clearConflicts: Function;
23
+ displayInput: boolean;
24
+ newOrReplace: string;
25
+ placeholder: string;
26
+ translator: ITranslator;
27
+ }
28
+
29
+ export interface IShortcutInputState {
30
+ value: string;
31
+ userInput: string;
32
+ isAvailable: boolean;
33
+ isFunctional: boolean;
34
+ takenByObject: TakenByObject;
35
+ keys: Array<string>;
36
+ currentChain: string;
37
+ selected: boolean;
38
+ }
39
+
40
+ /** Object for shortcut items */
41
+ export class ShortcutObject {
42
+ commandName: string;
43
+ label: string;
44
+ keys: { [index: string]: Array<string> };
45
+ source: string;
46
+ selector: string;
47
+ category: string;
48
+ id: string;
49
+ hasConflict: boolean;
50
+ numberOfShortcuts: number;
51
+
52
+ constructor() {
53
+ this.commandName = '';
54
+ this.label = '';
55
+ this.keys = {};
56
+ this.source = '';
57
+ this.selector = '';
58
+ this.category = '';
59
+ this.id = '';
60
+ this.numberOfShortcuts = 0;
61
+ this.hasConflict = false;
62
+ }
63
+
64
+ get(sortCriteria: string): string {
65
+ if (sortCriteria === 'label') {
66
+ return this.label;
67
+ } else if (sortCriteria === 'selector') {
68
+ return this.selector;
69
+ } else if (sortCriteria === 'category') {
70
+ return this.category;
71
+ } else if (sortCriteria === 'source') {
72
+ return this.source;
73
+ } else {
74
+ return '';
75
+ }
76
+ }
77
+ }
78
+ /** Object for conflicting shortcut error messages */
79
+ export class ErrorObject extends ShortcutObject {
80
+ takenBy: TakenByObject;
81
+
82
+ constructor() {
83
+ super();
84
+ this.takenBy = new TakenByObject();
85
+ }
86
+ }
87
+
88
+ /** Object for showing which shortcut conflicts with the new one */
89
+ export class TakenByObject {
90
+ takenBy: ShortcutObject;
91
+ takenByKey: string;
92
+ takenByLabel: string;
93
+ id: string;
94
+
95
+ constructor(shortcut?: ShortcutObject) {
96
+ if (shortcut) {
97
+ this.takenBy = shortcut;
98
+ this.takenByKey = '';
99
+ this.takenByLabel = shortcut.category + ': ' + shortcut.label;
100
+ this.id = shortcut.commandName + '_' + shortcut.selector;
101
+ } else {
102
+ this.takenBy = new ShortcutObject();
103
+ this.takenByKey = '';
104
+ this.takenByLabel = '';
105
+ this.id = '';
106
+ }
107
+ }
108
+ }
109
+
110
+ export class ShortcutInput extends React.Component<
111
+ IShortcutInputProps,
112
+ IShortcutInputState
113
+ > {
114
+ constructor(props: IShortcutInputProps) {
115
+ super(props);
116
+
117
+ this.state = {
118
+ value: this.props.placeholder,
119
+ userInput: '',
120
+ isAvailable: true,
121
+ isFunctional: this.props.newOrReplace === 'replace',
122
+ takenByObject: new TakenByObject(),
123
+ keys: new Array<string>(),
124
+ currentChain: '',
125
+ selected: true
126
+ };
127
+ }
128
+
129
+ handleUpdate = () => {
130
+ let keys = this.state.keys;
131
+ keys.push(this.state.currentChain);
132
+ this.setState({ keys: keys });
133
+ this.props.handleUpdate(this.props.shortcut, this.state.keys);
134
+ };
135
+
136
+ handleOverwrite = async () => {
137
+ this.props
138
+ .deleteShortcut(
139
+ this.state.takenByObject.takenBy,
140
+ this.state.takenByObject.takenByKey
141
+ )
142
+ .then(this.handleUpdate());
143
+ };
144
+
145
+ handleReplace = async () => {
146
+ let keys = this.state.keys;
147
+ keys.push(this.state.currentChain);
148
+ this.props.toggleInput();
149
+ await this.props.deleteShortcut(this.props.shortcut, this.props.shortcutId);
150
+ this.props.handleUpdate(this.props.shortcut, keys);
151
+ };
152
+
153
+ /** Parse user input for chained shortcuts */
154
+ parseChaining = (
155
+ event: React.KeyboardEvent,
156
+ value: string,
157
+ userInput: string,
158
+ keys: Array<string>,
159
+ currentChain: string
160
+ ): Array<any> => {
161
+ let key = EN_US.keyForKeydownEvent(event.nativeEvent);
162
+
163
+ const modKeys = ['Shift', 'Control', 'Alt', 'Meta', 'Ctrl', 'Accel'];
164
+
165
+ if (event.key === 'Backspace') {
166
+ userInput = '';
167
+ value = '';
168
+ keys = [];
169
+ currentChain = '';
170
+ this.setState({
171
+ value: value,
172
+ userInput: userInput,
173
+ keys: keys,
174
+ currentChain: currentChain
175
+ });
176
+ } else if (event.key !== 'CapsLock') {
177
+ const lastKey = userInput
178
+ .substr(userInput.lastIndexOf(' ') + 1, userInput.length)
179
+ .trim();
180
+
181
+ /** if last key was not a modefier then there is a chain */
182
+ if (modKeys.lastIndexOf(lastKey) === -1 && lastKey != '') {
183
+ userInput = userInput + ',';
184
+ keys.push(currentChain);
185
+ currentChain = '';
186
+
187
+ /** check if a modefier key was held down through chain */
188
+ if (event.ctrlKey && event.key != 'Control') {
189
+ userInput = (userInput + ' Ctrl').trim();
190
+ currentChain = (currentChain + ' Ctrl').trim();
191
+ }
192
+ if (event.metaKey && event.key != 'Meta') {
193
+ userInput = (userInput + ' Accel').trim();
194
+ currentChain = (currentChain + ' Accel').trim();
195
+ }
196
+ if (event.altKey && event.key != 'Alt') {
197
+ userInput = (userInput + ' Alt').trim();
198
+ currentChain = (currentChain + ' Alt').trim();
199
+ }
200
+ if (event.shiftKey && event.key != 'Shift') {
201
+ userInput = (userInput + ' Shift').trim();
202
+ currentChain = (currentChain + ' Shift').trim();
203
+ }
204
+
205
+ /** if not a modefier key, add to user input and current chain */
206
+ if (modKeys.lastIndexOf(event.key) === -1) {
207
+ userInput = (userInput + ' ' + key).trim();
208
+ currentChain = (currentChain + ' ' + key).trim();
209
+
210
+ /** if a modefier key, add to user input and current chain */
211
+ } else {
212
+ if (event.key === 'Meta') {
213
+ userInput = (userInput + ' Accel').trim();
214
+ currentChain = (currentChain + ' Accel').trim();
215
+ } else if (event.key === 'Control') {
216
+ userInput = (userInput + ' Ctrl').trim();
217
+ currentChain = (currentChain + ' Ctrl').trim();
218
+ } else if (event.key === 'Shift') {
219
+ userInput = (userInput + ' Shift').trim();
220
+ currentChain = (currentChain + ' Shift').trim();
221
+ } else if (event.key === 'Alt') {
222
+ userInput = (userInput + ' Alt').trim();
223
+ currentChain = (currentChain + ' Alt').trim();
224
+ } else {
225
+ userInput = (userInput + ' ' + event.key).trim();
226
+ currentChain = (currentChain + ' ' + event.key).trim();
227
+ }
228
+ }
229
+
230
+ /** if not a chain, add the key to user input and current chain */
231
+ } else {
232
+ /** if modefier key, rename */
233
+ if (event.key === 'Control') {
234
+ userInput = (userInput + ' Ctrl').trim();
235
+ currentChain = (currentChain + ' Ctrl').trim();
236
+ } else if (event.key === 'Meta') {
237
+ userInput = (userInput + ' Accel').trim();
238
+ currentChain = (currentChain + ' Accel').trim();
239
+ } else if (event.key === 'Shift') {
240
+ userInput = (userInput + ' Shift').trim();
241
+ currentChain = (currentChain + ' Shift').trim();
242
+ } else if (event.key === 'Alt') {
243
+ userInput = (userInput + ' Alt').trim();
244
+ currentChain = (currentChain + ' Alt').trim();
245
+
246
+ /** if not a modefier key, add it regularly */
247
+ } else {
248
+ userInput = (userInput + ' ' + key).trim();
249
+ currentChain = (currentChain + ' ' + key).trim();
250
+ }
251
+ }
252
+ }
253
+
254
+ /** update state of keys and currentChain */
255
+ this.setState({
256
+ keys: keys,
257
+ currentChain: currentChain
258
+ });
259
+ return [userInput, keys, currentChain];
260
+ };
261
+
262
+ /**
263
+ * Check if shorcut being typed will work
264
+ * (does not end with ctrl, alt, command, or shift)
265
+ * */
266
+ checkNonFunctional = (shortcut: string): boolean => {
267
+ const dontEnd = ['Ctrl', 'Alt', 'Accel', 'Shift'];
268
+ const shortcutKeys = this.state.currentChain.split(' ');
269
+ const last = shortcutKeys[shortcutKeys.length - 1];
270
+ this.setState({
271
+ isFunctional: !(dontEnd.indexOf(last) !== -1)
272
+ });
273
+
274
+ return dontEnd.indexOf(last) !== -1;
275
+ };
276
+
277
+ /** Check if shortcut being typed is already taken */
278
+ checkShortcutAvailability = (
279
+ userInput: string,
280
+ keys: string[],
281
+ currentChain: string
282
+ ): TakenByObject => {
283
+ /** First, check whole shortcut */
284
+ let isAvailable =
285
+ Object.keys(this.props.keyBindingsUsed).indexOf(
286
+ keys.join(' ') + currentChain + '_' + this.props.shortcut.selector
287
+ ) === -1 || userInput === '';
288
+ let takenByObject: TakenByObject = new TakenByObject();
289
+ if (isAvailable) {
290
+ /** Next, check each piece of a chain */
291
+ for (let binding of keys) {
292
+ if (
293
+ Object.keys(this.props.keyBindingsUsed).indexOf(
294
+ binding + '_' + this.props.shortcut.selector
295
+ ) !== -1 &&
296
+ binding !== ''
297
+ ) {
298
+ isAvailable = false;
299
+ takenByObject =
300
+ this.props.keyBindingsUsed[
301
+ binding + '_' + this.props.shortcut.selector
302
+ ];
303
+ break;
304
+ }
305
+ }
306
+
307
+ /** Check current chain */
308
+ if (
309
+ isAvailable &&
310
+ Object.keys(this.props.keyBindingsUsed).indexOf(
311
+ currentChain + '_' + this.props.shortcut.selector
312
+ ) !== -1 &&
313
+ currentChain !== ''
314
+ ) {
315
+ isAvailable = false;
316
+ takenByObject =
317
+ this.props.keyBindingsUsed[
318
+ currentChain + '_' + this.props.shortcut.selector
319
+ ];
320
+ }
321
+
322
+ /** If unavailable set takenByObject */
323
+ } else {
324
+ takenByObject =
325
+ this.props.keyBindingsUsed[
326
+ keys.join(' ') + currentChain + '_' + this.props.shortcut.selector
327
+ ];
328
+ }
329
+
330
+ /** allow to set shortcut to what it initially was if replacing */
331
+ if (!isAvailable) {
332
+ if (
333
+ takenByObject.takenBy.id === this.props.shortcut.id &&
334
+ this.props.newOrReplace === 'replace'
335
+ ) {
336
+ isAvailable = true;
337
+ takenByObject = new TakenByObject();
338
+ }
339
+ }
340
+
341
+ this.setState({ isAvailable: isAvailable });
342
+ return takenByObject;
343
+ };
344
+
345
+ checkConflict(takenByObject: TakenByObject, keys: string): void {
346
+ if (
347
+ takenByObject.id !== '' &&
348
+ takenByObject.takenBy.id !== this.props.shortcut.id
349
+ ) {
350
+ this.props.sortConflict(
351
+ this.props.shortcut,
352
+ takenByObject,
353
+ takenByObject.takenByLabel,
354
+ ''
355
+ );
356
+ } else {
357
+ this.props.clearConflicts();
358
+ }
359
+ }
360
+
361
+ /** Parse and normalize user input */
362
+ handleInput = (event: React.KeyboardEvent): void => {
363
+ event.preventDefault();
364
+ this.setState({ selected: false });
365
+ const parsed = this.parseChaining(
366
+ event,
367
+ this.state.value,
368
+ this.state.userInput,
369
+ this.state.keys,
370
+ this.state.currentChain
371
+ );
372
+ const userInput = parsed[0];
373
+ const keys = parsed[1];
374
+ const currentChain = parsed[2];
375
+
376
+ const value = this.props.toSymbols(userInput);
377
+ let takenByObject = this.checkShortcutAvailability(
378
+ userInput,
379
+ keys,
380
+ currentChain
381
+ );
382
+ this.checkConflict(takenByObject, keys);
383
+
384
+ this.setState(
385
+ {
386
+ value: value,
387
+ userInput: userInput,
388
+ takenByObject: takenByObject,
389
+ keys: keys,
390
+ currentChain: currentChain
391
+ },
392
+ () => this.checkNonFunctional(this.state.userInput)
393
+ );
394
+ };
395
+
396
+ handleBlur = (event: React.FocusEvent<HTMLDivElement>) => {
397
+ if (
398
+ event.relatedTarget === null ||
399
+ ((event.relatedTarget as HTMLElement).id !== 'no-blur' &&
400
+ (event.relatedTarget as HTMLElement).id !== 'overwrite')
401
+ ) {
402
+ this.props.toggleInput();
403
+ this.setState({
404
+ value: '',
405
+ userInput: ''
406
+ });
407
+ this.props.clearConflicts();
408
+ }
409
+ };
410
+
411
+ render() {
412
+ const trans = this.props.translator.load('jupyterlab');
413
+ let inputClassName = 'jp-Shortcuts-Input';
414
+ if (!this.state.isAvailable) {
415
+ inputClassName += ' jp-mod-unavailable-Input';
416
+ }
417
+ return (
418
+ <div
419
+ className={
420
+ this.props.displayInput
421
+ ? this.props.newOrReplace === 'new'
422
+ ? 'jp-Shortcuts-InputBox jp-Shortcuts-InputBoxNew'
423
+ : 'jp-Shortcuts-InputBox'
424
+ : 'jp-mod-hidden'
425
+ }
426
+ onBlur={event => this.handleBlur(event)}
427
+ >
428
+ <div
429
+ tabIndex={0}
430
+ id="no-blur"
431
+ className={inputClassName}
432
+ onKeyDown={this.handleInput}
433
+ ref={input => input && input.focus()}
434
+ >
435
+ <p
436
+ className={
437
+ this.state.selected && this.props.newOrReplace === 'replace'
438
+ ? 'jp-Shortcuts-InputText jp-mod-selected-InputText'
439
+ : this.state.value === ''
440
+ ? 'jp-Shortcuts-InputText jp-mod-waiting-InputText'
441
+ : 'jp-Shortcuts-InputText'
442
+ }
443
+ >
444
+ {this.state.value === ''
445
+ ? trans.__('press keys')
446
+ : this.state.value}
447
+ </p>
448
+ </div>
449
+ <button
450
+ className={
451
+ !this.state.isFunctional
452
+ ? 'jp-Shortcuts-Submit jp-mod-defunc-Submit'
453
+ : !this.state.isAvailable
454
+ ? 'jp-Shortcuts-Submit jp-mod-conflict-Submit'
455
+ : 'jp-Shortcuts-Submit'
456
+ }
457
+ id={'no-blur'}
458
+ disabled={!this.state.isAvailable || !this.state.isFunctional}
459
+ onClick={() => {
460
+ if (this.props.newOrReplace === 'new') {
461
+ this.handleUpdate();
462
+ this.setState({
463
+ value: '',
464
+ keys: [],
465
+ currentChain: ''
466
+ });
467
+ this.props.toggleInput();
468
+ } else {
469
+ /** don't replace if field has not been edited */
470
+ if (this.state.selected) {
471
+ this.props.toggleInput();
472
+ this.setState({
473
+ value: '',
474
+ userInput: ''
475
+ });
476
+ this.props.clearConflicts();
477
+ } else {
478
+ void this.handleReplace();
479
+ }
480
+ }
481
+ }}
482
+ >
483
+ {this.state.isAvailable ? <checkIcon.react /> : <errorIcon.react />}
484
+ </button>
485
+ {!this.state.isAvailable && (
486
+ <button
487
+ hidden
488
+ id="overwrite"
489
+ onClick={() => {
490
+ void this.handleOverwrite();
491
+ this.props.clearConflicts();
492
+ this.props.toggleInput();
493
+ }}
494
+ >
495
+ {trans.__('Overwrite')}
496
+ </button>
497
+ )}
498
+ </div>
499
+ );
500
+ }
501
+ }