Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Backport PR #15386: Improve scrolling to heading #15565

Merged
merged 3 commits into from
Dec 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
32 changes: 27 additions & 5 deletions galata/test/jupyterlab/toc-scrolling.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,40 @@ test.describe('Table of Contents scrolling to heading', () => {
test('Notebook scrolls to heading', async ({ page }) => {
await page.notebook.selectCells(0);

await page.keyboard.press('Enter');
await page.getByText('Mode: Edit').waitFor();

await page.sidebar.getContentPanel(
await page.sidebar.getTabPosition('table-of-contents')
);

await page
.locator('.jp-TableOfContents-tree >> text="the last one"')
.click({
button: 'left'
});
.locator('.jp-TableOfContents-tree')
.getByText('the last one')
.click();
// Should switch to command mode
await expect.soft(page.getByText('Mode: Command')).toBeVisible();

const nbPanel = await page.notebook.getNotebookInPanel();
expect(await nbPanel.screenshot()).toMatchSnapshot(
expect
.soft(await nbPanel!.screenshot())
.toMatchSnapshot('scrolled-to-bottom-heading.png');

// Scroll up
const bbox = await nbPanel!.boundingBox();
await page.mouse.move(
bbox!.x + 0.5 * bbox!.width,
bbox!.y + 0.5 * bbox!.height
);
await page.mouse.wheel(0, -1200);
await page.waitForTimeout(100);

await page
.locator('.jp-TableOfContents-tree')
.getByText('the last one')
.click();

expect(await nbPanel!.screenshot()).toMatchSnapshot(
'scrolled-to-bottom-heading.png'
);
});
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 4 additions & 2 deletions packages/codemirror/src/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,15 +395,17 @@ export class CodeMirrorEditor implements CodeEditor.IEditor {
*
* #### Notes
* This will remove any secondary cursors.
*
* @deprecated options bias and origin are not used
*/
setCursorPosition(
position: CodeEditor.IPosition,
options?: { bias?: number; origin?: string; scroll?: boolean }
options: { bias?: number; origin?: string; scroll?: boolean } = {}
): void {
const offset = this.getOffsetAt(position);
this.editor.dispatch({
selection: { anchor: offset },
scrollIntoView: options?.scroll === false ? false : true
scrollIntoView: options.scroll === false ? false : true
});
// If the editor does not have focus, this cursor change
// will get screened out in _onCursorsChanged(). Make an
Expand Down
6 changes: 6 additions & 0 deletions packages/notebook-extension/schema/tracker.json
Original file line number Diff line number Diff line change
Expand Up @@ -739,6 +739,12 @@
"type": "number",
"default": 50
},
"scrollHeadingToTop": {
"title": "Scroll heading to top",
"description": "Whether to scroll heading to the document top when selecting it in the table of contents. Starting with JupyterLab 4.1 this setting will be enabled by default.",
"type": "boolean",
"default": false
},
"showEditorForReadOnlyMarkdown": {
"title": "Show editor for read-only Markdown cells",
"description": "Should an editor be shown for read-only markdown",
Expand Down
25 changes: 22 additions & 3 deletions packages/notebook-extension/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -871,16 +871,35 @@ const tocPlugin: JupyterFrontEndPlugin<void> = {
id: '@jupyterlab/notebook-extension:toc',
description: 'Adds table of content capability to the notebooks',
requires: [INotebookTracker, ITableOfContentsRegistry, ISanitizer],
optional: [IMarkdownParser],
optional: [IMarkdownParser, ISettingRegistry],
autoStart: true,
activate: (
app: JupyterFrontEnd,
tracker: INotebookTracker,
tocRegistry: ITableOfContentsRegistry,
sanitizer: IRenderMime.ISanitizer,
mdParser: IMarkdownParser | null
mdParser: IMarkdownParser | null,
settingRegistry: ISettingRegistry | null
): void => {
tocRegistry.add(new NotebookToCFactory(tracker, mdParser, sanitizer));
const nbTocFactory = new NotebookToCFactory(tracker, mdParser, sanitizer);
tocRegistry.add(nbTocFactory);
if (settingRegistry) {
Promise.all([app.restored, settingRegistry.load(trackerPlugin.id)])
.then(([_, setting]) => {
const onSettingsUpdate = () => {
nbTocFactory.scrollToTop =
(setting.composite['scrollHeadingToTop'] as boolean) ?? true;
};
onSettingsUpdate();
setting.changed.connect(onSettingsUpdate);
})
.catch(error => {
console.error(
'Failed to load notebook table of content settings.',
error
);
});
}
}
};

Expand Down
46 changes: 36 additions & 10 deletions packages/notebook/src/toc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,17 @@ export class NotebookToCFactory extends TableOfContentsFactory<NotebookPanel> {
super(tracker);
}

/**
* Whether to scroll the active heading to the top
* of the document or not.
*/
get scrollToTop(): boolean {
return this._scrollToTop;
}
set scrollToTop(v: boolean) {
this._scrollToTop = v;
}

/**
* Create a new table of contents model for the widget
*
Expand Down Expand Up @@ -521,24 +532,37 @@ export class NotebookToCFactory extends TableOfContentsFactory<NotebookPanel> {
const el = headingToElement.get(heading);

if (el) {
const widgetBox = widget.content.node.getBoundingClientRect();
const elementBox = el.getBoundingClientRect();

if (
elementBox.top > widgetBox.bottom ||
elementBox.bottom < widgetBox.top
) {
el.scrollIntoView({ block: 'center' });
if (this.scrollToTop) {
el.scrollIntoView({ block: 'start' });
} else {
const widgetBox = widget.content.node.getBoundingClientRect();
const elementBox = el.getBoundingClientRect();

if (
elementBox.top > widgetBox.bottom ||
elementBox.bottom < widgetBox.top
) {
el.scrollIntoView({ block: 'center' });
}
}
} else {
console.debug('scrolling to heading: using fallback strategy');
await widget.content.scrollToItem(widget.content.activeCellIndex);
await widget.content.scrollToItem(
widget.content.activeCellIndex,
this.scrollToTop ? 'start' : undefined
);
}
};

const cell = heading.cellRef;
const cells = widget.content.widgets;
const idx = cells.indexOf(cell);
// Switch to command mode to avoid entering Markdown cell in edit mode
// if the document was in edit mode
if (cell.model.type == 'markdown' && widget.content.mode != 'command') {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need ===?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is needed here; the only case these would return different results would be if cell.model.type were new String('markdown') but it cannot be that since it is typed to be literal rather than object string.

widget.content.mode = 'command';
}

widget.content.activeCellIndex = idx;

if (cell.inViewport) {
Expand All @@ -549,7 +573,7 @@ export class NotebookToCFactory extends TableOfContentsFactory<NotebookPanel> {
});
} else {
widget.content
.scrollToItem(idx)
.scrollToItem(idx, this.scrollToTop ? 'start' : undefined)
.then(() => {
return onCellInViewport(cell);
})
Expand Down Expand Up @@ -674,6 +698,8 @@ export class NotebookToCFactory extends TableOfContentsFactory<NotebookPanel> {

return model;
}

private _scrollToTop: boolean = true;
}

/**
Expand Down
8 changes: 5 additions & 3 deletions packages/toc/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,9 +200,11 @@ export abstract class TableOfContentsModel<
if (this._activeHeading !== heading) {
this._activeHeading = heading;
this.stateChanged.emit();
if (emitSignal) {
this._activeHeadingChanged.emit(heading);
}
}

if (emitSignal) {
// Always emit the signal to trigger a scroll even if the value did not change
this._activeHeadingChanged.emit(this._activeHeading);
}
}

Expand Down