Search code examples
javascriptjupyter-labjupyter-extensions

How to use events in jupyterlab extensions?


I would like to listen to cell events of JupyterLab notebooks (version 3.2.0) in a custom extension. How can I do so?

a) Searching the documentation on "events" did not give helpful information:

https://jupyterlab.readthedocs.io/en/latest/search.html?q=events&check_keywords=yes&area=default

b) Here is some outdated example I could find:

Jupyter.events.on('execute.CodeCell', function(evt, data) {
    // data.cell is the cell object
});

https://github.com/jupyter/notebook/issues/2321

c) Here is another outdated code snipped:

require([
    "base/js/namespace",
    "base/js/events"
], 
    function(Jupyter, events){
        console.log("hello2");  
        events.on('notebook_loaded.Notebook', function(){
            console.log("hello3");
        });
        events.on('app_initialized.NotebookApp', function(){
            console.log("hello4");
        });
});

https://github.com/jupyter/notebook/issues/2499

d) I would expect to use something like

app.events

or

var { Events } = require('@jupyterlab/events');

However, those variants did not work.

Edit

I found another code snippet:

panel.notebook.model.cells.changed.connect((list, changed) => {
            if (changed.type == 'add') {
                each(changed.newValues, cellmodel => {
                    let cell = panel.notebook.widgets.find(x => x.model.id === cellmodel.id);
                    // do something with cell widget.
                });
            }
        });

https://github.com/jupyterlab/jupyterlab/issues/4316

Maybe there is no global "event registry" any more, but the events need to be accessed via the model property?

Related:

Where is a docs for Jupyter front-end extensions JavaScript API?


Solution

  • JupyterLab Extensions should listen to application-related events using the signal pattern which is based on lumino Signal implementation.

    You can identify the signal of interest in the reference JupyterLab API Reference (which is also linked from the documentation as the last entry), for example, cell execution signal is available as NotebookActions.executed.

    Extension Example

    https://github.com/jupyterlab/extension-examples/tree/master/signals

    Signals of NotebookActions

    Signal of observable list

    Signals of individual cells

    The cell model provides the signals

    Usage example:

    function __observeNotebook(app, dependencies){  
    
        let notebook = __tryToGetNotebook(app);
        if(notebook){       
            let cellModels = notebook.model.cells
            cellModels.changed.connect(__cellsChanged, this);   
    
            for(let cellIndex=0; cellIndex < cellModels.length; cellIndex++){
                let cellModel = cellModels.get(cellIndex);          
                __observeCell(cellModel, notebook);
            }
    
            let notebookActions = dependencies["NotebookActions"];
            notebookActions.executed.connect(__cellExecuted, this);  //selectionExecuted, exutionScheduled
        }        
    
    }
    
    function __observeCell(cellModel, notebook){   
        cellModel.contentChanged.connect(cellModel => __cellContentChanged(cellModel, notebook), this);   
        cellModel.stateChanged.connect(__cellStateChanged, this);   
    }
    
    function __cellsChanged(cellModels, change){
        console.log("Cells changed:")
        console.log("type: " + change.type);
        console.log("oldIndex: " + change.oldIndex);
        console.log("newIndex: " + change.newIndex);
        console.log("oldValues: " + change.oldValues); 
        console.log("newValues: " + change.newValues); 
    
        if(change.type == "add"){
            var newCellModel = cellModels.get(change.newIndex);
            __observeCell(newCellModel);
        }
    }
    
    function __cellContentChanged(cellModel, notebook){ 
        let id = cellModel.id
        console.log("Content of cell " + id + " changed");
    
        let currentText =  cellModel.value.text;
        console.log(currentText);
       
        let cellWidget = notebook.widgets.find(widget=>{
            return widget.model.id == id;
        });
    
        let outputArea = cellWidget.outputArea;
        let children = outputArea.node.children;
        if(children.length==1){
            let output = children[0];
            console.log(output);
        }
       
    }
    
    function __cellStateChanged(cellModel, change){
        let currentText =  cellModel.value.text;
        console.log("State of cell " + cellModel.id + " changed:");
        console.log("name: " + change.name);
        console.log("old value: " + change.oldValue);
        console.log("new value: " + change.newValue);
    }
    
    function __cellExecuted(any, context){
        let {cell, notebook, success, error} = context; 
        console.log("Executed cell " + cell.model.id);
    }
    
    
    function __tryToGetNotebookCell(app){   
        var notebook = __tryToGetNotebook(app);
        return notebook
            ?notebook.activeCell
            :null;      
    }
    
    function __tryToGetNotebook(app){
        var notebookPanel = __getFirstVisibleNotebookPanel(app);
        return notebookPanel
            ?notebookPanel.content
            :null;
    }
    
    
    function __getFirstVisibleNotebookPanel(app){
        var mainWidgets = app.shell.widgets('main');
        var widget = mainWidgets.next();
        while(widget){
            var type = widget.sessionContext.type;
            if(type == 'notebook'){  //other wigets might be of type DocumentWidget
                if (widget.isVisible){
                    return widget;
                }
            }
            widget = mainWidgets.next();
        }
        return null;
    }