@jupyterlab/notebook 4.6.0-alpha.3 → 4.6.0-alpha.5

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.
@@ -0,0 +1,523 @@
1
+ /*
2
+ * Copyright (c) Jupyter Development Team.
3
+ * Distributed under the terms of the Modified BSD License.
4
+ */
5
+
6
+ import { DOMUtils } from '@jupyterlab/apputils';
7
+ import type { Popup } from '@jupyterlab/statusbar';
8
+ import { showPopup, TextItem } from '@jupyterlab/statusbar';
9
+ import type { ITranslator, TranslationBundle } from '@jupyterlab/translation';
10
+ import { nullTranslator } from '@jupyterlab/translation';
11
+ import {
12
+ classes,
13
+ lineFormIcon,
14
+ ReactWidget,
15
+ VDomModel,
16
+ VDomRenderer
17
+ } from '@jupyterlab/ui-components';
18
+ import React from 'react';
19
+ import type { Notebook } from '.';
20
+
21
+ /**
22
+ * A namespace for CellNumberFormComponent statics.
23
+ */
24
+ namespace CellNumberFormComponent {
25
+ /**
26
+ * Props for the form component.
27
+ */
28
+ export interface IProps {
29
+ /**
30
+ * A callback for when the form is submitted.
31
+ */
32
+ handleSubmit: (value: number) => void;
33
+
34
+ /**
35
+ * The maximum cell number the form can take.
36
+ */
37
+ maxCell: number;
38
+
39
+ /**
40
+ * The application language translator.
41
+ */
42
+ translator?: ITranslator;
43
+ }
44
+
45
+ /**
46
+ * State for the form component.
47
+ */
48
+ export interface IState {
49
+ /**
50
+ * The current value of the form.
51
+ */
52
+ value: string;
53
+
54
+ /**
55
+ * Whether the form has focus.
56
+ */
57
+ hasFocus: boolean;
58
+
59
+ /**
60
+ * A generated ID for the input field.
61
+ */
62
+ textInputId: string;
63
+ }
64
+ }
65
+
66
+ /**
67
+ * A component for rendering a "go-to-cell" form.
68
+ */
69
+ class CellNumberFormComponent extends React.Component<
70
+ CellNumberFormComponent.IProps,
71
+ CellNumberFormComponent.IState
72
+ > {
73
+ /**
74
+ * Construct a new CellNumberFormComponent.
75
+ */
76
+ constructor(props: CellNumberFormComponent.IProps) {
77
+ super(props);
78
+ const translator = props.translator || nullTranslator;
79
+ this._trans = translator.load('jupyterlab');
80
+ this.state = {
81
+ value: '',
82
+ hasFocus: false,
83
+ textInputId: DOMUtils.createDomID() + '-cell-number-input'
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Focus the element on mount.
89
+ */
90
+ componentDidMount() {
91
+ this._textInput?.focus();
92
+ }
93
+
94
+ /**
95
+ * Render the CellNumberFormComponent.
96
+ */
97
+ render() {
98
+ return (
99
+ <div className="jp-lineFormSearch">
100
+ <form name="cellNumberForm" onSubmit={this._handleSubmit} noValidate>
101
+ <div
102
+ className={classes(
103
+ 'jp-lineFormWrapper',
104
+ 'lm-lineForm-wrapper',
105
+ this.state.hasFocus ? 'jp-lineFormWrapperFocusWithin' : undefined
106
+ )}
107
+ >
108
+ <input
109
+ type="number"
110
+ id={this.state.textInputId}
111
+ className="jp-lineFormInput"
112
+ min={1}
113
+ max={this.props.maxCell}
114
+ onChange={this._handleChange}
115
+ onFocus={this._handleFocus}
116
+ onBlur={this._handleBlur}
117
+ value={this.state.value}
118
+ ref={input => {
119
+ this._textInput = input;
120
+ }}
121
+ />
122
+ <div className="jp-baseLineForm jp-lineFormButtonContainer">
123
+ <lineFormIcon.react
124
+ className="jp-baseLineForm jp-lineFormButtonIcon"
125
+ elementPosition="center"
126
+ />
127
+ <input
128
+ type="submit"
129
+ className="jp-baseLineForm jp-lineFormButton"
130
+ value=""
131
+ />
132
+ </div>
133
+ </div>
134
+ <label
135
+ className="jp-lineFormCaption"
136
+ htmlFor={this.state.textInputId}
137
+ >
138
+ {this._trans.__(
139
+ 'Go to cell number between 1 and %1',
140
+ this.props.maxCell
141
+ )}
142
+ </label>
143
+ </form>
144
+ </div>
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Handle a change to the value in the input field.
150
+ */
151
+ private _handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
152
+ this.setState({ value: event.currentTarget.value });
153
+ };
154
+
155
+ /**
156
+ * Handle submission of the input field.
157
+ */
158
+ private _handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
159
+ event.preventDefault();
160
+
161
+ const value = parseInt(this._textInput?.value ?? '', 10);
162
+ if (
163
+ !isNaN(value) &&
164
+ isFinite(value) &&
165
+ 1 <= value &&
166
+ value <= this.props.maxCell
167
+ ) {
168
+ this.props.handleSubmit(value);
169
+ }
170
+
171
+ return false;
172
+ };
173
+
174
+ /**
175
+ * Handle focusing of the input field.
176
+ */
177
+ private _handleFocus = () => {
178
+ this.setState({ hasFocus: true });
179
+ };
180
+
181
+ /**
182
+ * Handle blurring of the input field.
183
+ */
184
+ private _handleBlur = () => {
185
+ this.setState({ hasFocus: false });
186
+ };
187
+
188
+ private _trans: TranslationBundle;
189
+ private _textInput: HTMLInputElement | null = null;
190
+ }
191
+
192
+ /**
193
+ * Props for CellCounterComponent.
194
+ */
195
+ namespace CellCounterComponent {
196
+ export interface IProps {
197
+ /**
198
+ * Current active cell number (1-based).
199
+ */
200
+ activeCell: number;
201
+
202
+ /**
203
+ * First selected cell number (1-based).
204
+ */
205
+ selectionStart: number;
206
+
207
+ /**
208
+ * Last selected cell number (1-based).
209
+ */
210
+ selectionEnd: number;
211
+
212
+ /**
213
+ * Total number of notebook cells.
214
+ */
215
+ totalCells: number;
216
+
217
+ /**
218
+ * The application language translator.
219
+ */
220
+ translator?: ITranslator;
221
+
222
+ /**
223
+ * Click handler used to launch the go-to-cell form.
224
+ */
225
+ handleClick: () => void;
226
+ }
227
+ }
228
+
229
+ /**
230
+ * A pure functional component for rendering a notebook cell counter.
231
+ */
232
+ function CellCounterComponent(
233
+ props: CellCounterComponent.IProps
234
+ ): React.ReactElement<CellCounterComponent.IProps> {
235
+ const translator = props.translator || nullTranslator;
236
+ const trans = translator.load('jupyterlab');
237
+ const source =
238
+ props.selectionStart > 0 && props.selectionStart !== props.selectionEnd
239
+ ? trans.__(
240
+ '%1:%2/%3',
241
+ props.selectionStart,
242
+ props.selectionEnd,
243
+ props.totalCells
244
+ )
245
+ : trans.__('Cell %1/%2', props.activeCell, props.totalCells);
246
+ const keydownHandler = (event: React.KeyboardEvent<HTMLImageElement>) => {
247
+ if (
248
+ event.key === 'Enter' ||
249
+ event.key === 'Spacebar' ||
250
+ event.key === ' '
251
+ ) {
252
+ event.preventDefault();
253
+ event.stopPropagation();
254
+ props.handleClick();
255
+ }
256
+ };
257
+
258
+ return (
259
+ <TextItem
260
+ role="button"
261
+ aria-haspopup
262
+ onClick={props.handleClick}
263
+ source={source}
264
+ title={trans.__('Go to cell…')}
265
+ tabIndex={0}
266
+ onKeyDown={keydownHandler}
267
+ />
268
+ );
269
+ }
270
+
271
+ /**
272
+ * A widget implementing a notebook cell counter status item.
273
+ */
274
+ export class CellCounterStatus extends VDomRenderer<CellCounterStatus.Model> {
275
+ /**
276
+ * Construct a new CellCounterStatus status item.
277
+ */
278
+ constructor(options: CellCounterStatus.IOptions = {}) {
279
+ super(new CellCounterStatus.Model());
280
+ this.addClass('jp-mod-highlighted');
281
+ this._translator = options.translator || nullTranslator;
282
+ }
283
+
284
+ /**
285
+ * Render the status item.
286
+ */
287
+ render(): JSX.Element | null {
288
+ if (this.model === null) {
289
+ return null;
290
+ }
291
+
292
+ return (
293
+ <CellCounterComponent
294
+ activeCell={this.model.activeCell}
295
+ selectionStart={this.model.selectionStart}
296
+ selectionEnd={this.model.selectionEnd}
297
+ totalCells={this.model.totalCells}
298
+ translator={this._translator}
299
+ handleClick={() => this._handleClick()}
300
+ />
301
+ );
302
+ }
303
+
304
+ /**
305
+ * A click handler for the widget.
306
+ */
307
+ private _handleClick(): void {
308
+ if (this.model!.totalCells < 1) {
309
+ return;
310
+ }
311
+
312
+ if (this._popup) {
313
+ this._popup.dispose();
314
+ }
315
+
316
+ const body = ReactWidget.create(
317
+ <CellNumberFormComponent
318
+ handleSubmit={value => this._handleSubmit(value)}
319
+ maxCell={this.model!.totalCells}
320
+ translator={this._translator}
321
+ />
322
+ );
323
+
324
+ this._popup = showPopup({
325
+ body,
326
+ anchor: this,
327
+ align: 'right'
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Handle submission for the widget.
333
+ */
334
+ private _handleSubmit(value: number): void {
335
+ const notebook = this.model!.notebook;
336
+ if (!notebook) {
337
+ return;
338
+ }
339
+
340
+ const cellIndex = value - 1;
341
+ notebook.activeCellIndex = cellIndex;
342
+ notebook.deselectAll();
343
+ void notebook.scrollToItem(cellIndex).catch(reason => {
344
+ console.error('Go to cell', reason);
345
+ });
346
+
347
+ this._popup?.dispose();
348
+ notebook.activate();
349
+ }
350
+
351
+ private _translator: ITranslator;
352
+ private _popup: Popup | null = null;
353
+ }
354
+
355
+ /**
356
+ * A namespace for CellCounterStatus statics.
357
+ */
358
+ export namespace CellCounterStatus {
359
+ /**
360
+ * Options for creating a CellCounterStatus item.
361
+ */
362
+ export interface IOptions {
363
+ /**
364
+ * The application language translator.
365
+ */
366
+ translator?: ITranslator;
367
+ }
368
+
369
+ /**
370
+ * Snapshot of the model state used for change detection.
371
+ */
372
+ interface IState {
373
+ /**
374
+ * Current active cell number (1-based).
375
+ */
376
+ activeCell: number;
377
+
378
+ /**
379
+ * First selected cell number (1-based).
380
+ */
381
+ selectionStart: number;
382
+
383
+ /**
384
+ * Last selected cell number (1-based).
385
+ */
386
+ selectionEnd: number;
387
+
388
+ /**
389
+ * Total number of cells.
390
+ */
391
+ totalCells: number;
392
+ }
393
+
394
+ /**
395
+ * A VDom model for a status item tracking active and total notebook cells.
396
+ */
397
+ export class Model extends VDomModel {
398
+ /**
399
+ * The notebook tracked by this model.
400
+ */
401
+ get notebook(): Notebook | null {
402
+ return this._notebook;
403
+ }
404
+
405
+ set notebook(notebook: Notebook | null) {
406
+ const oldNotebook = this._notebook;
407
+ if (oldNotebook) {
408
+ oldNotebook.activeCellChanged.disconnect(this._onChanged, this);
409
+ oldNotebook.modelContentChanged.disconnect(this._onChanged, this);
410
+ oldNotebook.selectionChanged.disconnect(this._onChanged, this);
411
+ }
412
+
413
+ const oldState = this._getAllState();
414
+ this._notebook = notebook;
415
+
416
+ if (!this._notebook) {
417
+ this._activeCell = 0;
418
+ this._selectionStart = 0;
419
+ this._selectionEnd = 0;
420
+ this._totalCells = 0;
421
+ } else {
422
+ this._notebook.activeCellChanged.connect(this._onChanged, this);
423
+ this._notebook.modelContentChanged.connect(this._onChanged, this);
424
+ this._notebook.selectionChanged.connect(this._onChanged, this);
425
+ this._updateStateFromNotebook(this._notebook);
426
+ }
427
+
428
+ this._triggerChange(oldState, this._getAllState());
429
+ }
430
+
431
+ /**
432
+ * The current active cell index shown to users (1-based).
433
+ */
434
+ get activeCell(): number {
435
+ return this._activeCell;
436
+ }
437
+
438
+ /**
439
+ * The first selected cell index shown to users (1-based).
440
+ */
441
+ get selectionStart(): number {
442
+ return this._selectionStart;
443
+ }
444
+
445
+ /**
446
+ * The last selected cell index shown to users (1-based).
447
+ */
448
+ get selectionEnd(): number {
449
+ return this._selectionEnd;
450
+ }
451
+
452
+ /**
453
+ * The total number of cells.
454
+ */
455
+ get totalCells(): number {
456
+ return this._totalCells;
457
+ }
458
+
459
+ /**
460
+ * React to notebook changes by refreshing the tracked state.
461
+ */
462
+ private _onChanged(notebook: Notebook): void {
463
+ const oldState = this._getAllState();
464
+ this._updateStateFromNotebook(notebook);
465
+ this._triggerChange(oldState, this._getAllState());
466
+ }
467
+
468
+ private _updateStateFromNotebook(notebook: Notebook): void {
469
+ const activeCellIndex = notebook.activeCellIndex;
470
+ this._activeCell = activeCellIndex >= 0 ? activeCellIndex + 1 : 0;
471
+ this._totalCells = notebook.widgets.length;
472
+
473
+ let selectionStart = this._activeCell;
474
+ let selectionEnd = this._activeCell;
475
+ let seenSelection = false;
476
+
477
+ notebook.widgets.forEach((cell, index) => {
478
+ if (!notebook.isSelectedOrActive(cell)) {
479
+ return;
480
+ }
481
+
482
+ const oneBasedIndex = index + 1;
483
+ if (!seenSelection) {
484
+ selectionStart = oneBasedIndex;
485
+ selectionEnd = oneBasedIndex;
486
+ seenSelection = true;
487
+ return;
488
+ }
489
+
490
+ selectionEnd = oneBasedIndex;
491
+ });
492
+
493
+ this._selectionStart = seenSelection ? selectionStart : 0;
494
+ this._selectionEnd = seenSelection ? selectionEnd : 0;
495
+ }
496
+
497
+ private _getAllState(): IState {
498
+ return {
499
+ activeCell: this._activeCell,
500
+ selectionStart: this._selectionStart,
501
+ selectionEnd: this._selectionEnd,
502
+ totalCells: this._totalCells
503
+ };
504
+ }
505
+
506
+ private _triggerChange(oldState: IState, newState: IState) {
507
+ if (
508
+ oldState.activeCell !== newState.activeCell ||
509
+ oldState.selectionStart !== newState.selectionStart ||
510
+ oldState.selectionEnd !== newState.selectionEnd ||
511
+ oldState.totalCells !== newState.totalCells
512
+ ) {
513
+ this.stateChanged.emit(void 0);
514
+ }
515
+ }
516
+
517
+ private _activeCell = 0;
518
+ private _selectionStart = 0;
519
+ private _selectionEnd = 0;
520
+ private _totalCells = 0;
521
+ private _notebook: Notebook | null = null;
522
+ }
523
+ }
@@ -363,7 +363,7 @@ export class CellTypeSwitcher extends ReactWidget {
363
363
  * Handle `keydown` events for the HTMLSelect component.
364
364
  */
365
365
  handleKeyDown = (event: React.KeyboardEvent): void => {
366
- if (event.keyCode === 13) {
366
+ if (event.key === 'Enter') {
367
367
  this._notebook.activate();
368
368
  }
369
369
  };
package/src/index.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  export * from './actions';
9
+ export * from './cellcounterstatus';
9
10
  export * from './cellexecutor';
10
11
  export * from './celllist';
11
12
  export * from './default-toolbar';
package/src/model.ts CHANGED
@@ -395,6 +395,7 @@ close the notebook without saving it.`,
395
395
  list: CellList,
396
396
  change: IObservableList.IChangedArgs<ICellModel>
397
397
  ): void {
398
+ // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check
398
399
  switch (change.type) {
399
400
  case 'add':
400
401
  change.newValues.forEach(cell => {