Creating a simple JupyterLab plugin having BookMark menu

# abstract

in this document, we are going to discuss how to create a simple jupyterlab plugin having a custom menu 'BookMark'.

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/8b372343-5fb6-bf0f-1290-b0218491c3ae.png)


# Let's start

let's start with a simple plugin project created from [extension-cookiecutter-ts](https://github.com/jupyterlab/extension-cookiecutter-ts)

if you are a newbie to cookiecutter, this [post](https://qiita.com/crowdy/items/167a65cee4a857184ac7) would be a help.

# Basic Skelecton

this is a basic skelecton of a jupyterlab plugin.
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/3f0db7a9-2a11-d2b6-9ac5-f61d88491175.png)


## import part
```typescript
import {IMainMenu} from '@jupyterlab/mainmenu'

import {IFrame, ICommandPalette} from '@jupyterlab/apputils'

import {Menu} from '@phosphor/widgets'
```

to use `@jupyterlab/mainmenu` package, you might need to describe the package name in `package.json` file.

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/caced09a-804e-6051-9944-5cf24731c8aa.png)

if you use pycharm, alt+enter will do that for you.

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/0e60d492-0740-8040-4105-f68e84fa3aa4.png)

not installed? don't be worry.  `jlpm install` comannd will make you have the package under `node_modules` directory.

## extension part
```typescript
/**
 * Initialization data for the jupyterlab_myext extension.
 */
const extension: JupyterLabPlugin<void> = {
  id: 'jupyterlab_myext',
  autoStart: true,
  requires: [IMainMenu, ICommandPalette],
  activate: activate_custom_menu
};
```

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/152a4973-d643-b3ff-eeef-cabd1a6cc94b.png)

in webstorm, do Ctrl+click the `requires` and you can navigate to the definition.

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/5f283cb0-5bff-04a8-751f-95856dd9dbba.png)

## bookmark data part
```typescript
export const BookMarks = [
    {
        name: 'gmo',
        url: 'https://www.gmo.jp/en/',
        description: 'Creating a simple JupyterLab plugin adding BookMark menu',
        target: 'widget'
    }
];
```

this is a hard-coded bookmark data. of course, you can change it to load from file, url(ajax) or to be delivered by javascript from index.html, which is  rendered by jupyterlab_launcher/handlers.py

## activation definition part
```typescript
export function activate_custom_menu(app: JupyterLab, mainMenu: IMainMenu, palette:
    ICommandPalette): Promise<void>{
    // todo
    return Promise.resolve(void 0);
}
```
by the declaration `requires: [IMainMenu, ICommandPalette]` at extension definition,
those parameters are dynamically delivered when calling `activate_custom_menu`

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/3393c56c-7028-5372-143a-0829635a7972.png)

if you want to provide a service other widgets can refer, use `provides` property.

for details, refer
jupyterlab_myext\node_modules\@phosphor\application\lib\index.d.ts


# Register Commands
phosphorjs uses command|event / listener model like VSCode does.

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/49b13922-077f-c426-b36d-6a1de394993f.png)

I believe you can understand what this typescript does.

```typescript
    function appendNewCommand(item: any) {
        let iframe: IFrame = null;
        let command = `BookMark-${item.name}:show`;
        app.commands.addCommand(command, {

            label: item.name,
            execute: () => {
                if (item.target == '_blank') {
                    let win = window.open(item.url, '_blank');
                    win.focus();
                } else if (item.target == 'widget') {
                    if (!iframe) {
                        iframe = new IFrame();
                        iframe.url = item.url;
                        iframe.id = item.name;
                        iframe.title.label = item.name;
                        iframe.title.closable = true;
                        iframe.node.style.overflowY = 'auto';
                    }

                    if (iframe == null || !iframe.isAttached) {
                        app.shell.addToMainArea(iframe);
                        app.shell.activateById(iframe.id);
                    } else {
                        app.shell.activateById(iframe.id);
                    }
                }
            }
        });
    }

    BookMarks.forEach(item => appendNewCommand(item));
```

`IFrame` came from @jupyterlab/apputils

```typescript
import {IFrame, ICommandPalette} from '@jupyterlab/apputils'
```

# UI rednering
and make menu show actually.  
we usually describe rendering part on Private namespace like below.
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/be97d05a-b99a-6aa7-fd82-2e1d731bb6f6.png)

# build

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/7a674e40-782f-6017-79ec-90842656cc18.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/0f15f987-2356-8164-d97a-288ea66ddfce.png)
what? no `Set`? then, make target and lib use `ES6` on `tsconfig.json`
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/5bae443c-4615-e335-8948-392670453ab2.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/3be7f9f3-5e35-a344-ec22-a468fe3dee32.png)
Okay.

```
jupyter labextension list
jupyter lab build
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/bf1d5da3-730d-db33-bfbc-1790c4ae6d0d.png)
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/0fc46d15-b346-17a9-f552-d78612ab3b21.png)


# run
```
jupyter lab
```
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/8b372343-5fb6-bf0f-1290-b0218491c3ae.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/7b792416-4e46-5ae9-d8af-49af8cfa1852.png)

# the whole code
```typescript
import {
  JupyterLab, JupyterLabPlugin
} from '@jupyterlab/application';

import {IMainMenu} from '@jupyterlab/mainmenu'

import {IFrame, ICommandPalette} from '@jupyterlab/apputils'

import {Menu} from '@phosphor/widgets'

import '../style/index.css';


/**
 * Initialization data for the jupyterlab_myext extension.
 */
const extension: JupyterLabPlugin<void> = {
  id: 'jupyterlab_myext',
  autoStart: true,
  requires: [IMainMenu, ICommandPalette],
  activate: activate_custom_menu
};


export default extension;

export const BookMarks = [
    {
        name: 'gmo',
        url: 'https://www.gmo.jp/en/',
        description: 'Creating a simple JupyterLab plugin adding BookMark menu',
        target: 'widget'
    }
];

export function activate_custom_menu(app: JupyterLab, mainMenu: IMainMenu, palette:
    ICommandPalette): Promise<void>{

    // create new commands and add them to app.commands

    function appendNewCommand(item: any) {
        let iframe: IFrame = null;
        let command = `BookMark-${item.name}:show`;
        app.commands.addCommand(command, {

            label: item.name,
            execute: () => {
                if (item.target == '_blank') {
                    let win = window.open(item.url, '_blank');
                    win.focus();
                } else if (item.target == 'widget') {
                    if (!iframe) {
                        iframe = new IFrame();
                        iframe.url = item.url;
                        iframe.id = item.name;
                        iframe.title.label = item.name;
                        iframe.title.closable = true;
                        iframe.node.style.overflowY = 'auto';
                    }

                    if (iframe == null || !iframe.isAttached) {
                        app.shell.addToMainArea(iframe);
                        app.shell.activateById(iframe.id);
                    } else {
                        app.shell.activateById(iframe.id);
                    }
                }
            }
        });
    }

    BookMarks.forEach(item => appendNewCommand(item));

    // add to mainMenu
    let menu = Private.createMenu(app);
    mainMenu.addMenu(menu, {rank: 80});
    return Promise.resolve(void 0);
}

/**
 * A namespace for help plugin private functions.
 */

namespace Private {

    /**
     * Creates a menu for the help plugin.
     */
    export function createMenu(app: JupyterLab): Menu {

        const {commands} = app;
        let menu:Menu = new Menu({commands});
        menu.title.label = 'BookMark';
        BookMarks.forEach(item => menu.addItem({command: `BookMark-${item.name}:show`}));

        return menu;
    }
}
```

# ref
jupyterlabのexternal extensionを作ってみた。
https://qiita.com/crowdy/items/167a65cee4a857184ac7