Skip to content
This repository was archived by the owner on Feb 16, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"@lumino/coreutils": "^1.4.2",
"@lumino/messaging": "^1.3.3",
"@lumino/widgets": "^1.10.2",
"@material-ui/core": "^4.10.2",
"react": "^16.8.6",
"react-dom": "^16.8.6"
},
Expand Down
19 changes: 9 additions & 10 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ async function activateTOC(
rendermime: IRenderMimeRegistry,
settingRegistry: ISettingRegistry
): Promise<ITableOfContentsRegistry> {
// Attempt to load plugin settings:
let settings: ISettingRegistry.ISettings | undefined;
try {
settings = await settingRegistry.load('@jupyterlab/toc:plugin');
} catch (error) {
console.error(
`Failed to load settings for the Table of Contents extension.\n\n${error}`
);
}
// Create the ToC widget:
const toc = new TableOfContents({ docmanager, rendermime });

Expand All @@ -67,16 +76,6 @@ async function activateTOC(
// Add the ToC widget to the application restorer:
restorer.add(toc, '@jupyterlab/toc:plugin');

// Attempt to load plugin settings:
let settings: ISettingRegistry.ISettings | undefined;
try {
settings = await settingRegistry.load('@jupyterlab/toc:plugin');
} catch (error) {
console.error(
`Failed to load settings for the Table of Contents extension.\n\n${error}`
);
}

// Create a notebook generator:
const notebookGenerator = createNotebookGenerator(
notebookTracker,
Expand Down
245 changes: 241 additions & 4 deletions src/toc_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,75 @@
// Distributed under the terms of the Modified BSD License.

import * as React from 'react';
import { IHeading } from './utils/headings';
import ListItem from '@material-ui/core/ListItem';
import Menu from '@material-ui/core/Menu';
import MenuItem from '@material-ui/core/MenuItem';
import { CodeCell } from '@jupyterlab/cells';
import { NotebookPanel } from '@jupyterlab/notebook';
import { IHeading, INotebookHeading } from './utils/headings';

/**
* Tests whether a heading is a notebook heading.
*
* @private
* @param heading - heading to test
* @returns boolean indicating whether a heading is a notebook heading
*/
function isNotebookHeading(heading: any): heading is INotebookHeading {
return heading.type !== undefined && heading.cellRef !== undefined;
}

/**
* Checks whether a heading has runnable code cells.
*
* @private
* @param headings - list of headings
* @param heading - heading
* @returns boolean indicating whether a heading has runnable code cells
*/
function hasCodeCells(headings: IHeading[], heading: IHeading): boolean {
let h: INotebookHeading;
let i: number;

if (!isNotebookHeading(heading)) {
return false;
}
// Find the heading in the list of headings...
for (i = 0; i < headings.length; i++) {
if (heading === headings[i]) {
break;
}
}
// Check if the current heading is a "code" heading...
h = heading as INotebookHeading;
if (h.type === 'code') {
return true;
}
// Check for nested code headings...
const level = heading.level;
for (i = i + 1; i < headings.length; i++) {
h = headings[i] as INotebookHeading;
if (h.level <= level) {
return false;
}
if (h.type === 'code') {
return true;
}
}
return false;
}

/**
* Interface describing component properties.
*
* @private
*/
interface IProperties extends React.Props<TOCItem> {
/**
* List of all headings.
*/
headings: IHeading[];

/**
* Heading to render.
*/
Expand All @@ -29,14 +90,38 @@ interface IProperties extends React.Props<TOCItem> {
*
* @private
*/
interface IState {}
interface IState {
/**
* Mouse x-position.
*/
mouseX: number | null;

/**
* Mouse y-position.
*/
mouseY: number | null;
}

/**
* React component for a table of contents entry.
*
* @private
*/
class TOCItem extends React.Component<IProperties, IState> {
/**
* Returns a component which renders a table of contents entry.
*
* @param props - component properties
* @returns component
*/
constructor(props: IProperties) {
super(props);
this.state = {
mouseX: null,
mouseY: null
};
}

/**
* Renders a table of contents entry.
*
Expand All @@ -52,9 +137,161 @@ class TOCItem extends React.Component<IProperties, IState> {
event.stopPropagation();
heading.onClick();
};
const content = this.props.itemRenderer(heading);
if (!content) {
return null;
}
const FLG = hasCodeCells(this.props.headings, heading);
return (
<ListItem
onClick={onClick}
onContextMenu={
FLG
? this._onContextMenuFactory(heading as INotebookHeading)
: undefined
}
>
{content}
{FLG ? (
<Menu
keepMounted
classes={{
list: 'jp-TableOfContents-item-contextmenu'
}}
open={this.state.mouseY !== null}
onClose={this._onContextMenuClose}
anchorReference="anchorPosition"
anchorPosition={
this.state.mouseY !== null && this.state.mouseX !== null
? { top: this.state.mouseY, left: this.state.mouseX }
: void 0
}
>
<MenuItem
className="jp-TableOfContents-item-contextmenu-item"
onClick={this._onRunFactory(heading as INotebookHeading)}
>
Run Cell(s)
</MenuItem>
</Menu>
) : null}
</ListItem>
);
}

/**
* Returns a callback which is invoked upon opening a context menu.
*
* @param heading - heading
* @returns callback
*/
private _onContextMenuFactory(heading: INotebookHeading) {
const self = this;
return onContextMenu;

/**
* Callback invoked upon opening a job's context menu.
*
* @private
* @param event - event object
*/
function onContextMenu(event: any): void {
event.preventDefault();
event.stopPropagation();

self.setState({
mouseX: event.clientX - 2,
mouseY: event.clientY - 4
});
}
}

/**
* Returns a callback which is invoked upon clicking a menu item to run code cells.
*
* @param heading - heading
* @returns callback
*/
private _onRunFactory(heading: INotebookHeading) {
const self = this;
return onClick;

let content = this.props.itemRenderer(heading);
return content && <li onClick={onClick}>{content}</li>;
/**
* Callback invoked upon clicking a menu item to run code cells.
*
* @private
* @param event - event object
*/
async function onClick(event: any): Promise<void> {
let code: INotebookHeading[];
let h: INotebookHeading;
let i: number;

event.preventDefault();
event.stopPropagation();

self._closeContextMenu();

// Find the heading in the list of ToC headings...
const headings = self.props.headings;
for (i = 0; i < headings.length; i++) {
if (heading === headings[i]) {
break;
}
}
code = [];

// Check if the current heading is a "code" heading...
h = heading as INotebookHeading;
if (h.type === 'code') {
code.push(h);
}
// Find all nested code headings...
else {
const level = heading.level;
for (i = i + 1; i < headings.length; i++) {
h = headings[i] as INotebookHeading;
if (h.level <= level) {
break;
}
if (h.type === 'code') {
code.push(h);
}
}
}
// Run each of the associated code cells...
for (i = 0; i < code.length; i++) {
if (code[i].cellRef) {
const cell = code[i].cellRef as CodeCell;
const panel = cell.parent?.parent as NotebookPanel;
if (panel) {
await CodeCell.execute(cell, panel.sessionContext);
}
}
}
}
}

/**
* Callback invoked upon closing a context menu.
*
* @param event - event object
*/
private _onContextMenuClose = (event: any): void => {
event.preventDefault();
event.stopPropagation();

this._closeContextMenu();
};

/**
* Closes a context menu.
*/
private _closeContextMenu(): void {
this.setState({
mouseX: null,
mouseY: null
});
}
}

Expand Down
14 changes: 13 additions & 1 deletion src/toc_tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Distributed under the terms of the Modified BSD License.

import * as React from 'react';
import List from '@material-ui/core/List';
import { Widget } from '@lumino/widgets';
import { IHeading } from './utils/headings';
import { TableOfContentsRegistry as Registry } from './registry';
Expand Down Expand Up @@ -55,6 +56,16 @@ interface IState {}
* @private
*/
class TOCTree extends React.Component<IProperties, IState> {
/**
* Returns a component which renders a table of contents tree.
*
* @param props - component properties
* @returns component
*/
constructor(props: IProperties) {
super(props);
}

Comment on lines +59 to +68
Copy link
Member

Choose a reason for hiding this comment

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

Does this need to be here?

Copy link
Member Author

Choose a reason for hiding this comment

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

No, it is not necessary. I usually include it as a matter of course so that the constructor is documented and setup in the event that component is necessary (e.g., as in toc.tsx). I can remove, if this is a blocker.

/**
* Renders a table of contents tree.
*/
Expand All @@ -66,6 +77,7 @@ class TOCTree extends React.Component<IProperties, IState> {
let list: JSX.Element[] = this.props.toc.map(el => {
return (
<TOCItem
headings={this.props.toc}
heading={el}
itemRenderer={this.props.itemRenderer}
key={`${el.text}-${el.level}-${i++}`}
Expand All @@ -76,7 +88,7 @@ class TOCTree extends React.Component<IProperties, IState> {
<div className="jp-TableOfContents">
<header>{this.props.title}</header>
{Toolbar && <Toolbar />}
<ul className="jp-TableOfContents-content">{list}</ul>
<List className="jp-TableOfContents-content">{list}</List>
</div>
);
}
Expand Down
15 changes: 15 additions & 0 deletions style/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,18 @@
.toc-level-size-default {
font-size: 9px;
}

/**
* TOC item context menu.
*/

.jp-TableOfContents-item-contextmenu {
padding-top: 0 !important;
padding-bottom: 0 !important;

background-color: var(--jp-layout-color2);
}

.jp-TableOfContents-item-contextmenu-item {
color: var(--jp-ui-font-color0) !important;
}
Loading