@jupyterlab/notebook 4.6.0-alpha.4 → 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.
- package/lib/actions.d.ts +3 -1
- package/lib/actions.js +25 -19
- package/lib/actions.js.map +1 -1
- package/lib/cellcounterstatus.d.ts +78 -0
- package/lib/cellcounterstatus.js +281 -0
- package/lib/cellcounterstatus.js.map +1 -0
- package/lib/default-toolbar.js +1 -1
- package/lib/default-toolbar.js.map +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/index.js.map +1 -1
- package/lib/widget.d.ts +3 -0
- package/lib/widget.js +78 -9
- package/lib/widget.js.map +1 -1
- package/package.json +20 -20
- package/src/actions.tsx +34 -27
- package/src/cellcounterstatus.tsx +523 -0
- package/src/default-toolbar.tsx +1 -1
- package/src/index.ts +1 -0
- package/src/widget.ts +86 -9
- package/style/base.css +1 -79
- package/style/executionindicator.css +0 -6
|
@@ -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
|
+
}
|
package/src/default-toolbar.tsx
CHANGED
|
@@ -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.
|
|
366
|
+
if (event.key === 'Enter') {
|
|
367
367
|
this._notebook.activate();
|
|
368
368
|
}
|
|
369
369
|
};
|
package/src/index.ts
CHANGED
package/src/widget.ts
CHANGED
|
@@ -614,6 +614,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
614
614
|
for (const cell of cells) {
|
|
615
615
|
this._insertCell(++index, cell);
|
|
616
616
|
}
|
|
617
|
+
this._syncMarkdownCellTrust();
|
|
617
618
|
newValue.cells.changed.connect(this._onCellsChanged, this);
|
|
618
619
|
newValue.metadataChanged.connect(this.onMetadataChanged, this);
|
|
619
620
|
newValue.contentChanged.connect(this.onModelContentChanged, this);
|
|
@@ -675,6 +676,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
675
676
|
this.addHeader();
|
|
676
677
|
}
|
|
677
678
|
|
|
679
|
+
this._syncMarkdownCellTrust();
|
|
678
680
|
this.update();
|
|
679
681
|
}
|
|
680
682
|
|
|
@@ -697,10 +699,12 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
697
699
|
default:
|
|
698
700
|
widget = this._createRawCell(cell as IRawCellModel);
|
|
699
701
|
}
|
|
702
|
+
cell.stateChanged.connect(this._onCellStateChanged, this);
|
|
700
703
|
widget.inViewportChanged.connect(this._onCellInViewportChanged, this);
|
|
701
704
|
widget.addClass(NB_CELL_CLASS);
|
|
702
705
|
|
|
703
706
|
ArrayExt.insert(this.cellsArray, index, widget);
|
|
707
|
+
this._syncMarkdownCellTrust(widget);
|
|
704
708
|
this.onCellInserted(index, widget);
|
|
705
709
|
|
|
706
710
|
this._scheduleCellRenderOnIdle();
|
|
@@ -725,9 +729,15 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
725
729
|
translator: this.translator
|
|
726
730
|
};
|
|
727
731
|
const cell = this.contentFactory.createCodeCell(options);
|
|
728
|
-
cell.syncCollapse
|
|
729
|
-
|
|
730
|
-
|
|
732
|
+
if (cell.syncCollapse === undefined) {
|
|
733
|
+
cell.syncCollapse = true;
|
|
734
|
+
}
|
|
735
|
+
if (cell.syncEditable === undefined) {
|
|
736
|
+
cell.syncEditable = true;
|
|
737
|
+
}
|
|
738
|
+
if (cell.syncScrolled === undefined) {
|
|
739
|
+
cell.syncScrolled = true;
|
|
740
|
+
}
|
|
731
741
|
cell.outputArea.inputRequested.connect((_, stdin) => {
|
|
732
742
|
this._onInputRequested(cell).catch(reason => {
|
|
733
743
|
console.error('Failed to scroll to cell requesting input.', reason);
|
|
@@ -759,8 +769,12 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
759
769
|
this._notebookConfig.showEditorForReadOnlyMarkdown
|
|
760
770
|
};
|
|
761
771
|
const cell = this.contentFactory.createMarkdownCell(options);
|
|
762
|
-
cell.syncCollapse
|
|
763
|
-
|
|
772
|
+
if (cell.syncCollapse === undefined) {
|
|
773
|
+
cell.syncCollapse = true;
|
|
774
|
+
}
|
|
775
|
+
if (cell.syncEditable === undefined) {
|
|
776
|
+
cell.syncEditable = true;
|
|
777
|
+
}
|
|
764
778
|
// Connect collapsed signal for each markdown cell widget
|
|
765
779
|
cell.headingCollapsedChanged.connect(this._onCellCollapsed, this);
|
|
766
780
|
return cell;
|
|
@@ -779,8 +793,12 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
779
793
|
placeholder: this._notebookConfig.windowingMode !== 'none'
|
|
780
794
|
};
|
|
781
795
|
const cell = this.contentFactory.createRawCell(options);
|
|
782
|
-
cell.syncCollapse
|
|
783
|
-
|
|
796
|
+
if (cell.syncCollapse === undefined) {
|
|
797
|
+
cell.syncCollapse = true;
|
|
798
|
+
}
|
|
799
|
+
if (cell.syncEditable === undefined) {
|
|
800
|
+
cell.syncEditable = true;
|
|
801
|
+
}
|
|
784
802
|
return cell;
|
|
785
803
|
}
|
|
786
804
|
|
|
@@ -789,12 +807,60 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
789
807
|
*/
|
|
790
808
|
private _removeCell(index: number): void {
|
|
791
809
|
const widget = this.cellsArray[index];
|
|
810
|
+
widget.model.stateChanged.disconnect(this._onCellStateChanged, this);
|
|
792
811
|
widget.parent = null;
|
|
793
812
|
ArrayExt.removeAt(this.cellsArray, index);
|
|
794
813
|
this.onCellRemoved(index, widget);
|
|
795
814
|
widget.dispose();
|
|
796
815
|
}
|
|
797
816
|
|
|
817
|
+
private _shouldTrustMarkdown(): boolean {
|
|
818
|
+
// Note: this returns false in a notebook without trsuted code cells;
|
|
819
|
+
// This is intended since only Code cells carry trust status on disk.
|
|
820
|
+
if (!this._notebookModel) {
|
|
821
|
+
return false;
|
|
822
|
+
}
|
|
823
|
+
let hasCodeCell = false;
|
|
824
|
+
for (const cell of this._notebookModel.cells) {
|
|
825
|
+
if (cell.type === 'code') {
|
|
826
|
+
hasCodeCell = true;
|
|
827
|
+
if (!cell.trusted) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return hasCodeCell;
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private _syncMarkdownCellTrust(cell?: Cell): void {
|
|
836
|
+
const trusted = this._shouldTrustMarkdown();
|
|
837
|
+
const trustHandler = this.rendermime.trustHandler;
|
|
838
|
+
if (!trustHandler) {
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
const cells = cell ? [cell] : this.widgets;
|
|
843
|
+
for (const widget of cells) {
|
|
844
|
+
if (!(widget instanceof MarkdownCell)) {
|
|
845
|
+
continue;
|
|
846
|
+
}
|
|
847
|
+
if (trusted) {
|
|
848
|
+
trustHandler.markTrusted(widget.node);
|
|
849
|
+
} else {
|
|
850
|
+
trustHandler.unmarkTrusted(widget.node);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
private _onCellStateChanged(
|
|
856
|
+
model: ICellModel,
|
|
857
|
+
args: IChangedArgs<any>
|
|
858
|
+
): void {
|
|
859
|
+
if (args.name === 'trusted') {
|
|
860
|
+
this._syncMarkdownCellTrust();
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
798
864
|
/**
|
|
799
865
|
* Update the mimetype of the notebook.
|
|
800
866
|
*/
|
|
@@ -821,6 +887,14 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
821
887
|
cell
|
|
822
888
|
.getHeadings()
|
|
823
889
|
.then(() => {
|
|
890
|
+
// Heading parsing is async; ignore stale callbacks that no longer match
|
|
891
|
+
// the current collapsed state.
|
|
892
|
+
if (
|
|
893
|
+
cell.isDisposed ||
|
|
894
|
+
(cell instanceof MarkdownCell && cell.headingCollapsed !== collapsed)
|
|
895
|
+
) {
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
824
898
|
NotebookActions.setHeadingCollapse(cell, collapsed, this);
|
|
825
899
|
this._cellCollapsed.emit(cell);
|
|
826
900
|
})
|
|
@@ -1635,6 +1709,7 @@ export class Notebook extends StaticNotebook {
|
|
|
1635
1709
|
* Construct a notebook widget.
|
|
1636
1710
|
*/
|
|
1637
1711
|
constructor(options: Notebook.IOptions) {
|
|
1712
|
+
const trans = (options.translator || nullTranslator).load('jupyterlab');
|
|
1638
1713
|
super({
|
|
1639
1714
|
renderer: {
|
|
1640
1715
|
createOuter(): HTMLElement {
|
|
@@ -1644,7 +1719,7 @@ export class Notebook extends StaticNotebook {
|
|
|
1644
1719
|
createViewport(): HTMLElement {
|
|
1645
1720
|
const el = document.createElement('div');
|
|
1646
1721
|
el.setAttribute('role', 'feed');
|
|
1647
|
-
el.setAttribute('aria-label', 'Cells');
|
|
1722
|
+
el.setAttribute('aria-label', trans.__('Cells'));
|
|
1648
1723
|
return el;
|
|
1649
1724
|
},
|
|
1650
1725
|
|
|
@@ -2824,7 +2899,9 @@ export class Notebook extends StaticNotebook {
|
|
|
2824
2899
|
if (widget && widget.editorWidget?.node.contains(target)) {
|
|
2825
2900
|
// Prevent CodeMirror from focusing the editor.
|
|
2826
2901
|
// TODO: find an editor-agnostic solution.
|
|
2827
|
-
|
|
2902
|
+
if (!target.closest('[data-jp-suppress-context-menu]')) {
|
|
2903
|
+
event.preventDefault();
|
|
2904
|
+
}
|
|
2828
2905
|
}
|
|
2829
2906
|
}
|
|
2830
2907
|
|