'Accessing notebook cell metadata and HTML class attributes in JupyterLab Extensions

In a minimum viable JupyterLab extension, as for example tested using the JupyterLab Plugin Playground, how can I add a toolbar button that will toggle a particular class attribute on the HTML associated with one or more selected notebook cells (either code cell or markdown cell)?

To generalise the example further:

  • how would I apply different class attributes to code cells and markdown cells?
  • how would I add a class to the HTML based on the the presence of a particular metadata attribute or metadata tag element in the notebook JSON structure?

As a starting point the following code (taken from the JupyterLab extension examples) should add a button to the toolbar:


import { IDisposable, DisposableDelegate } from '@lumino/disposable';

import {
  JupyterFrontEnd,
  JupyterFrontEndPlugin,
} from '@jupyterlab/application';

import { ToolbarButton } from '@jupyterlab/apputils';

import { DocumentRegistry } from '@jupyterlab/docregistry';

import {
  NotebookActions,
  NotebookPanel,
  INotebookModel,
} from '@jupyterlab/notebook';

/**
 * The plugin registration information.
 */
const plugin: JupyterFrontEndPlugin<void> = {
  activate,
  id: 'toolbar-button',
  autoStart: true,
};

/**
 * A notebook widget extension that adds a button to the toolbar.
 */
export class ButtonExtension
  implements DocumentRegistry.IWidgetExtension<NotebookPanel, INotebookModel>
{
  /**
   * Create a new extension for the notebook panel widget.
   *
   * @param panel Notebook panel
   * @param context Notebook context
   * @returns Disposable on the added button
   */
  createNew(
    panel: NotebookPanel,
    context: DocumentRegistry.IContext<INotebookModel>
  ): IDisposable {
    const myButtonAction = () => {
      // Perform some action
    };

    const button = new ToolbarButton({
      className: 'my-action-button',
      label: 'My Button',
      onClick: myButtonAction,
      tooltip: 'Perform My Button action',
    });

    panel.toolbar.insertItem(10, 'myNewAction', button);
    return new DisposableDelegate(() => {
      button.dispose();
    });
  }
}

/**
 * Activate the extension.
 *
 * @param app Main application object
 */
function activate(app: JupyterFrontEnd): void {
  app.docRegistry.addWidgetExtension('Notebook', new ButtonExtension());
}

/**
 * Export the plugin as default.
 */
export default plugin;



Solution 1:[1]

You shall start by getting a handle of the Notebook class instance (which is the content of the notebook panel which you already have available):

// in anonymous function assigned to myButtonAction
const notebook = panel.content;

The notebook instance provides you with the active Cell widget:

const activeCell = notebook.activeCell;

The cell widget has two attributes of interest: model which enables you to access the metadata, and node which enables manipulation of the DOM structure.

For example, you could toggle class of the node of a cell if it is a markdown cell (=ICellModel has .type (CellType) equal to 'markdown'):

if (activeCell.model.type === 'markdown') {
   activeCell.node.classList.toggle('someClass');
}

The metadata is stored in cell.model.metadata.

For selection of cells something as follows should work:

const {head, anchor} = notebook.getContiguousSelection();
if (head === null || anchor === null) {
  // no selection
  return;
}
const start = head > anchor ? anchor : head;
const end = head > anchor ? head : anchor;
for (let cellIndex = start; cellIndex <= end; cellIndex++) {
  const cell = notebook.widgets[cellIndex];
  if (cell.model.type === 'code') {
      cell.node.classList.toggle('someOtherClass');
  }
}

There is a problem with this approach however, as when the notebook gets opened in a separate view, or simply reloaded, the classes will go away (as they are only toggled on the DOM nodes). If you require persistence, I would recommend to:

  • use the button to only write to cell metadata
  • add a separate callback function which will listen to any changes to the notebook model, roughly (not tested!):
    // in createNew()
    const notebook = panel.content;
    notebook.modelChanged.connect((notebook) => {
      // iterate cells and toggle DOM classes as needed, e.g.
      for (const cell of notebook.widgets) {
        if (cell.model.metadata.get('someMetaData')) {
          cell.node.classList.toggle('someOtherClass');
        }
      }
    });
    
    which should also work (in principle) with collaborative editing.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 krassowski