@jupyterlab/notebook 4.6.2 → 4.6.4
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.js +37 -5
- package/lib/actions.js.map +1 -1
- package/lib/executionindicator.js +1 -1
- package/lib/executionindicator.js.map +1 -1
- package/lib/panel.js +5 -2
- package/lib/panel.js.map +1 -1
- package/lib/searchprovider.d.ts +1 -0
- package/lib/searchprovider.js +17 -11
- package/lib/searchprovider.js.map +1 -1
- package/lib/widget.d.ts +34 -2
- package/lib/widget.js +138 -14
- package/lib/widget.js.map +1 -1
- package/package.json +20 -20
- package/src/actions.tsx +45 -5
- package/src/executionindicator.tsx +3 -1
- package/src/panel.ts +8 -2
- package/src/searchprovider.ts +19 -12
- package/src/widget.ts +150 -16
package/src/searchprovider.ts
CHANGED
|
@@ -26,6 +26,7 @@ import type { IObservableList, IObservableMap } from '@jupyterlab/observables';
|
|
|
26
26
|
import type { ITranslator } from '@jupyterlab/translation';
|
|
27
27
|
import { nullTranslator } from '@jupyterlab/translation';
|
|
28
28
|
import { ArrayExt } from '@lumino/algorithm';
|
|
29
|
+
import { Debouncer } from '@lumino/polling';
|
|
29
30
|
import type { Widget } from '@lumino/widgets';
|
|
30
31
|
import type { CellList } from './celllist';
|
|
31
32
|
import { NotebookPanel } from './panel';
|
|
@@ -68,18 +69,9 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
68
69
|
|
|
69
70
|
private _onNotebookStateChanged(_: Notebook, args: IChangedArgs<any>) {
|
|
70
71
|
if (args.name === 'mode') {
|
|
71
|
-
//
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
args.newValue === 'command' &&
|
|
75
|
-
document.activeElement?.closest('.jp-DocumentSearch-overlay')
|
|
76
|
-
) {
|
|
77
|
-
// Do not request updating mode when user switched focus to search overlay.
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
this._updateSelectionMode();
|
|
81
|
-
this._filtersChanged.emit();
|
|
82
|
-
}, 0);
|
|
72
|
+
// Debounce to ensure that `document.activeElement` settled, and to keep
|
|
73
|
+
// only the most recent mode transition.
|
|
74
|
+
void this._modeChangeDebouncer.invoke();
|
|
83
75
|
}
|
|
84
76
|
}
|
|
85
77
|
|
|
@@ -208,6 +200,8 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
208
200
|
);
|
|
209
201
|
this._stopObservingLastCell();
|
|
210
202
|
|
|
203
|
+
this._modeChangeDebouncer.dispose();
|
|
204
|
+
|
|
211
205
|
super.dispose();
|
|
212
206
|
|
|
213
207
|
const index = this.widget.content.activeCellIndex;
|
|
@@ -932,6 +926,19 @@ export class NotebookSearchProvider extends SearchProvider<NotebookPanel> {
|
|
|
932
926
|
protected delayedActiveCellChangeHandlerReady: Promise<void>;
|
|
933
927
|
private _currentProviderIndex: number | null = null;
|
|
934
928
|
private _delayedActiveCellChangeHandler: number | null = null;
|
|
929
|
+
private _modeChangeDebouncer = new Debouncer(() => {
|
|
930
|
+
// The mode can change again before this handler runs; check the
|
|
931
|
+
// current mode because this is what `_updateSelectionMode()` acts on.
|
|
932
|
+
if (
|
|
933
|
+
this.widget.content.mode === 'command' &&
|
|
934
|
+
document.activeElement?.closest('.jp-DocumentSearch-overlay')
|
|
935
|
+
) {
|
|
936
|
+
// Do not request updating mode when user switched focus to search overlay.
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
this._updateSelectionMode();
|
|
940
|
+
this._filtersChanged.emit();
|
|
941
|
+
}, 0);
|
|
935
942
|
private _filters: IFilters | undefined;
|
|
936
943
|
private _onSelection = false;
|
|
937
944
|
private _selectedCells: number = 1;
|
package/src/widget.ts
CHANGED
|
@@ -618,7 +618,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
618
618
|
for (const cell of cells) {
|
|
619
619
|
this._insertCell(++index, cell);
|
|
620
620
|
}
|
|
621
|
-
this.
|
|
621
|
+
this._syncCellTrust();
|
|
622
622
|
newValue.cells.changed.connect(this._onCellsChanged, this);
|
|
623
623
|
newValue.metadataChanged.connect(this.onMetadataChanged, this);
|
|
624
624
|
newValue.contentChanged.connect(this.onModelContentChanged, this);
|
|
@@ -680,7 +680,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
680
680
|
this.addHeader();
|
|
681
681
|
}
|
|
682
682
|
|
|
683
|
-
this.
|
|
683
|
+
this._syncCellTrust();
|
|
684
684
|
this.update();
|
|
685
685
|
}
|
|
686
686
|
|
|
@@ -708,7 +708,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
708
708
|
widget.addClass(NB_CELL_CLASS);
|
|
709
709
|
|
|
710
710
|
ArrayExt.insert(this.cellsArray, index, widget);
|
|
711
|
-
this.
|
|
711
|
+
this._syncCellTrust(widget);
|
|
712
712
|
this.onCellInserted(index, widget);
|
|
713
713
|
|
|
714
714
|
this._scheduleCellRenderOnIdle();
|
|
@@ -818,8 +818,8 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
818
818
|
widget.dispose();
|
|
819
819
|
}
|
|
820
820
|
|
|
821
|
-
private
|
|
822
|
-
// Note: this returns false in a notebook without
|
|
821
|
+
private _shouldTrustCell(): boolean {
|
|
822
|
+
// Note: this returns false in a notebook without trusted code cells;
|
|
823
823
|
// This is intended since only Code cells carry trust status on disk.
|
|
824
824
|
if (!this._notebookModel) {
|
|
825
825
|
return false;
|
|
@@ -836,13 +836,19 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
836
836
|
return hasCodeCell;
|
|
837
837
|
}
|
|
838
838
|
|
|
839
|
-
private
|
|
840
|
-
const trusted = this.
|
|
839
|
+
private _syncCellTrust(cell?: Cell): void {
|
|
840
|
+
const trusted = this._shouldTrustCell();
|
|
841
841
|
const trustHandler = this.rendermime.trustHandler;
|
|
842
842
|
if (!trustHandler) {
|
|
843
843
|
return;
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
+
if (trusted) {
|
|
847
|
+
trustHandler.markTrusted(this.node);
|
|
848
|
+
} else {
|
|
849
|
+
trustHandler.unmarkTrusted(this.node);
|
|
850
|
+
}
|
|
851
|
+
|
|
846
852
|
const cells = cell ? [cell] : this.widgets;
|
|
847
853
|
for (const widget of cells) {
|
|
848
854
|
if (!(widget instanceof MarkdownCell)) {
|
|
@@ -861,7 +867,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
861
867
|
args: IChangedArgs<any>
|
|
862
868
|
): void {
|
|
863
869
|
if (args.name === 'trusted') {
|
|
864
|
-
this.
|
|
870
|
+
this._syncCellTrust();
|
|
865
871
|
}
|
|
866
872
|
}
|
|
867
873
|
|
|
@@ -1203,7 +1209,7 @@ export class StaticNotebook extends WindowedList<NotebookViewModel> {
|
|
|
1203
1209
|
this._contentVisibilityObserver!.observe(cell.node);
|
|
1204
1210
|
});
|
|
1205
1211
|
});
|
|
1206
|
-
});
|
|
1212
|
+
}, this);
|
|
1207
1213
|
}
|
|
1208
1214
|
|
|
1209
1215
|
protected cellsArray: Array<Cell>;
|
|
@@ -2873,16 +2879,84 @@ export class Notebook extends StaticNotebook {
|
|
|
2873
2879
|
}
|
|
2874
2880
|
}
|
|
2875
2881
|
|
|
2882
|
+
/**
|
|
2883
|
+
* Find the cell containing a node, traversing open shadow roots.
|
|
2884
|
+
*
|
|
2885
|
+
* Returned cell is `null` if the node is not in a cell of this notebook.
|
|
2886
|
+
*/
|
|
2887
|
+
private _cellContaining(node: Node | null): Cell | null {
|
|
2888
|
+
let current = node;
|
|
2889
|
+
while (current) {
|
|
2890
|
+
const element =
|
|
2891
|
+
current instanceof Element ? current : current.parentElement;
|
|
2892
|
+
if (element) {
|
|
2893
|
+
const index = this._findCell(element);
|
|
2894
|
+
if (index !== -1) {
|
|
2895
|
+
return this.widgets[index];
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
const root = (element ?? current).getRootNode();
|
|
2899
|
+
current = root instanceof ShadowRoot ? root.host : null;
|
|
2900
|
+
}
|
|
2901
|
+
return null;
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
/**
|
|
2905
|
+
* Whether a shift-click on `target` should be left to the browser to handle
|
|
2906
|
+
* as an extension of the current text selection, rather than be turned into
|
|
2907
|
+
* a cell range selection.
|
|
2908
|
+
*
|
|
2909
|
+
* This is the case when the click lands in a region of a cell whose text the
|
|
2910
|
+
* browser selects - the output area of a code cell, or the rendered input of
|
|
2911
|
+
* a rendered markdown cell - and the current selection starts in such a
|
|
2912
|
+
* region too; see https://github.com/jupyterlab/jupyterlab/issues/4800. A
|
|
2913
|
+
* click on a cell prompt, on cell chrome, or on a cell without such a region
|
|
2914
|
+
* (an unrendered markdown cell, a code cell without outputs) always selects
|
|
2915
|
+
* cells.
|
|
2916
|
+
*
|
|
2917
|
+
* #### Notes
|
|
2918
|
+
* Only the anchor of the selection is considered, because a shift-click
|
|
2919
|
+
* moves the focus and leaves the anchor in place. The anchor may sit in a
|
|
2920
|
+
* different cell than the one clicked, which is what allows a selection to
|
|
2921
|
+
* be extended across the outputs of several cells; and disregarding the
|
|
2922
|
+
* focus keeps this working when a drag overshot the edge of an output and
|
|
2923
|
+
* left the focus outside of it.
|
|
2924
|
+
*
|
|
2925
|
+
* This trade-off between extending the text selection and extending the cell
|
|
2926
|
+
* selection can be adjusted in the future. For now the text selection wins to
|
|
2927
|
+
* preserve the behaviour from before refactor that users may be accustomed to.
|
|
2928
|
+
*/
|
|
2929
|
+
private _isExtendingTextSelection(
|
|
2930
|
+
selection: Selection | null,
|
|
2931
|
+
targetCell: Cell,
|
|
2932
|
+
target: Node
|
|
2933
|
+
): boolean {
|
|
2934
|
+
if (!selection || selection.isCollapsed) {
|
|
2935
|
+
return false;
|
|
2936
|
+
}
|
|
2937
|
+
if (!Private.isInTextRegion(targetCell, target)) {
|
|
2938
|
+
return false;
|
|
2939
|
+
}
|
|
2940
|
+
const { anchorNode } = selection;
|
|
2941
|
+
// The selection usually starts in the very cell which was clicked, which
|
|
2942
|
+
// can be checked without having to locate the cell of the anchor.
|
|
2943
|
+
if (Private.isInTextRegion(targetCell, anchorNode)) {
|
|
2944
|
+
return true;
|
|
2945
|
+
}
|
|
2946
|
+
const anchorCell = this._cellContaining(anchorNode);
|
|
2947
|
+
return !!anchorCell && Private.isInTextRegion(anchorCell, anchorNode);
|
|
2948
|
+
}
|
|
2949
|
+
|
|
2876
2950
|
/**
|
|
2877
2951
|
* Find the cell index containing the target html element.
|
|
2878
2952
|
*
|
|
2879
2953
|
* #### Notes
|
|
2880
2954
|
* Returns -1 if the cell is not found.
|
|
2881
2955
|
*/
|
|
2882
|
-
private _findCell(node:
|
|
2956
|
+
private _findCell(node: Element): number {
|
|
2883
2957
|
// Trace up the DOM hierarchy to find the root cell node.
|
|
2884
2958
|
// Then find the corresponding child and select it.
|
|
2885
|
-
let n:
|
|
2959
|
+
let n: Element | null = node;
|
|
2886
2960
|
while (n && n !== this.node) {
|
|
2887
2961
|
if (n.classList.contains(NB_CELL_CLASS)) {
|
|
2888
2962
|
const i = ArrayExt.findFirstIndex(
|
|
@@ -3116,18 +3190,22 @@ export class Notebook extends StaticNotebook {
|
|
|
3116
3190
|
if (targetArea === 'notebook') {
|
|
3117
3191
|
this.deselectAll();
|
|
3118
3192
|
} else if (targetArea === 'prompt' || targetArea === 'cell') {
|
|
3119
|
-
// We don't want to prevent the default selection behavior
|
|
3120
|
-
// if there is currently text selected in an output.
|
|
3121
|
-
const hasSelection = (window.getSelection() ?? '').toString() !== '';
|
|
3122
3193
|
if (
|
|
3123
3194
|
button === 0 &&
|
|
3124
3195
|
shiftKey &&
|
|
3125
|
-
!
|
|
3126
|
-
|
|
3196
|
+
!['INPUT', 'OPTION'].includes(target.tagName) &&
|
|
3197
|
+
// We don't want to prevent the default selection behavior when the
|
|
3198
|
+
// user is extending a text selection into the clicked region.
|
|
3199
|
+
!this._isExtendingTextSelection(window.getSelection(), widget, target)
|
|
3127
3200
|
) {
|
|
3128
3201
|
// Prevent browser selecting text in prompt or output
|
|
3129
3202
|
event.preventDefault();
|
|
3130
3203
|
|
|
3204
|
+
// Preventing the default also stops the browser from collapsing any
|
|
3205
|
+
// text selection which is already there; drop it explicitly so that
|
|
3206
|
+
// stale highlighted text is not left over the new cell selection.
|
|
3207
|
+
window.getSelection()?.removeAllRanges();
|
|
3208
|
+
|
|
3131
3209
|
// Shift-click - extend selection
|
|
3132
3210
|
try {
|
|
3133
3211
|
this.extendContiguousSelectionTo(index);
|
|
@@ -3780,6 +3858,62 @@ namespace Private {
|
|
|
3780
3858
|
}
|
|
3781
3859
|
}
|
|
3782
3860
|
|
|
3861
|
+
/**
|
|
3862
|
+
* Check whether `node` is `parent` or a descendant of it.
|
|
3863
|
+
*
|
|
3864
|
+
* Unlike `Node.contains()` this traverses out of open shadow roots, so a
|
|
3865
|
+
* node rendered by a widget library which attaches a shadow root (such as
|
|
3866
|
+
* Panel or Bokeh) is still recognised as belonging to its host subtree.
|
|
3867
|
+
*/
|
|
3868
|
+
function containsDeep(parent: Node, node: Node | null): boolean {
|
|
3869
|
+
let current = node;
|
|
3870
|
+
while (current) {
|
|
3871
|
+
if (parent.contains(current)) {
|
|
3872
|
+
return true;
|
|
3873
|
+
}
|
|
3874
|
+
const root = current.getRootNode();
|
|
3875
|
+
current = root instanceof ShadowRoot ? root.host : null;
|
|
3876
|
+
}
|
|
3877
|
+
return false;
|
|
3878
|
+
}
|
|
3879
|
+
|
|
3880
|
+
/**
|
|
3881
|
+
* Whether a cell exposes an output area.
|
|
3882
|
+
*
|
|
3883
|
+
* #### Notes
|
|
3884
|
+
* This is a structural check rather than `instanceof CodeCell` so that it
|
|
3885
|
+
* still holds when a federated extension loads its own copy of
|
|
3886
|
+
* `@jupyterlab/cells`.
|
|
3887
|
+
*/
|
|
3888
|
+
function hasOutputArea(cell: Cell): cell is CodeCell {
|
|
3889
|
+
return 'outputArea' in cell;
|
|
3890
|
+
}
|
|
3891
|
+
|
|
3892
|
+
/**
|
|
3893
|
+
* The regions of a cell whose text the browser, rather than the notebook,
|
|
3894
|
+
* is responsible for selecting: the output area of a code cell and the
|
|
3895
|
+
* rendered input of a cell which renders it, such as a rendered markdown
|
|
3896
|
+
* cell.
|
|
3897
|
+
*/
|
|
3898
|
+
function textRegionsOf(cell: Cell): Node[] {
|
|
3899
|
+
const regions: Node[] = [];
|
|
3900
|
+
if (hasOutputArea(cell)) {
|
|
3901
|
+
regions.push(cell.outputArea.node);
|
|
3902
|
+
}
|
|
3903
|
+
const rendered = cell.inputArea?.renderedInput;
|
|
3904
|
+
if (rendered) {
|
|
3905
|
+
regions.push(rendered.node);
|
|
3906
|
+
}
|
|
3907
|
+
return regions;
|
|
3908
|
+
}
|
|
3909
|
+
|
|
3910
|
+
/**
|
|
3911
|
+
* Whether a node is in a region of a cell whose text the browser selects.
|
|
3912
|
+
*/
|
|
3913
|
+
export function isInTextRegion(cell: Cell, node: Node | null): boolean {
|
|
3914
|
+
return textRegionsOf(cell).some(region => containsDeep(region, node));
|
|
3915
|
+
}
|
|
3916
|
+
|
|
3783
3917
|
/**
|
|
3784
3918
|
* Create a cell drag image.
|
|
3785
3919
|
*/
|