Accessing notebook cell metadata and HTML class attributes in JupyterLab Extensions - jupyter-lab

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;

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.

Related

How to determine if "click" or "box-select" was used with Streamlit/Plotly to return data from chart to Streamlit

I'm not a Javascript/Typescript/React dev. I'm hacking my way through this for a work project.
I'm using Streamlit, with plotly.
I'm hacking the basic code from streamlit-plotly-events.
I was trying to have the click or box-select information passed back with the data selected via the plotlyEventHandler() (see code below.) However, both this.props.args["click_event"] and this.props.args["select_event"] are true, regardless of whether you use box-select in the plotly chart, or click a single data point in the chart.
I thought of assuming if there is only one data point, then it was a click - but you can box select only one data point.
// import React, {useState,useEffect} from "react"
import React, { ReactNode } from "react"
//import React from "react"
import {
StreamlitComponentBase,
withStreamlitConnection,
Streamlit,
// ComponentProps,
} from "streamlit-component-lib"
import Plot from "react-plotly.js"
class StreamlitPlotlyEventsCapture extends StreamlitComponentBase {
public render = (): ReactNode => {
// Pull Plotly object from args and parse
const plot_obj = JSON.parse(this.props.args["plot_obj"]);
const override_height = this.props.args["override_height"];
const override_width = this.props.args["override_width"];
// Event booleans
const click_event = this.props.args["click_event"];
const select_event = this.props.args["select_event"];
const hover_event = this.props.args["hover_event"];
Streamlit.setFrameHeight(override_height);
return (
<Plot
data={plot_obj.data}
layout={plot_obj.layout}
config={plot_obj.config}
frames={plot_obj.frames}
onClick={click_event ? this.plotlyEventHandler : function(){}}
onSelected={select_event ? this.plotlyEventHandler : function(){}}
onHover={hover_event ? this.plotlyEventHandler : function(){}}
style={{width: override_width, height: override_height}}
className="stPlotlyChart"
/>
)
}
/** Click handler for plot. */
private plotlyEventHandler = (data: any) => {
// Build array of points to return
var clickedPoints: Array<any> = [];
//const util = require('util')//#33333 used with util.inspect(arrayItem) below
// I dont know why we can't directly use "this.variables" in the clickedPoints.push
// but we can't, so we create the variables here.
var wasClicked = this.props.args["click_event"];
var wasSelected = this.props.args["select_event"];
var wasHovered = this.props.args["hover_event"];
data.points.forEach(function (arrayItem: any) {
// console.log(util.inspect(arrayItem, {maxArrayLength: null, depth:null }))
clickedPoints.push({
// I dont know why we can't directly use "this.variables" here, but we can't
// so we use the variables created above.
clicked:wasClicked,
selected:wasSelected,
hovered:wasHovered,
x: arrayItem.x,
y: arrayItem.y,
curveNumber: arrayItem.curveNumber,
pointNumber: arrayItem.pointNumber,
pointIndex: arrayItem.pointIndex
})
});
// Return array as JSON to Streamlit
Streamlit.setComponentValue(JSON.stringify(clickedPoints))
}
}
export default withStreamlitConnection(StreamlitPlotlyEventsCapture)

How to remove view sub-menu in electron app for main window

Hi i want to remove 3 options from electron title menu they are
reload
Force reload
Toggle developer tools
as shown below
Here is what i have tried
background.js
import { app, session, protocol, BrowserWindow , dialog, ipcMain, Menu} from 'electron'
const menu = Menu.getApplicationMenu(); // get default menu
console.log('menu %s',menu.items);
let menuIndex = menu.items.findIndex(o => {
return o.role == 'viewmenu'
})
//splice items based on names , i'm not able to do that because i cannot see the full json object and its structure in console.log
//Menu.setApplicationMenu(menu); //set menu
Further i don't know what to do
Please help me thanks in advance !!
You can create a new menu based on the default using Menu.buildFromTemplate and pass the default's menu items as template.
Then set that as application menu. During the construction you can cut out elements you don't want
const menu = Menu.getApplicationMenu(); // get default menu
// build a new menu based on default one
const newmenu = Menu.buildFromTemplate(
menu.items.map(i => {
// overwrite viewmenu item
if (i.role === 'viewmenu') {
// create new submenu
const newviewsub = Menu.buildFromTemplate(
i.submenu.items.slice(4) // cut first 4 item (4th is separator)
)
// replace this item's submenu with the news
return Object.assign({}, i, { submenu: newviewsub })
}
// otherwise keep
return i
})
)
Menu.setApplicationMenu(newmenu)
You should see that the 'view' menu's first 3 element is gone

Redux Toolkit - Slice utility methods

I'm building a React app with redux-toolkit and I'm splitting my store into some slices with redux-toolkit's helper function createSlice.
Here it is a simple use case:
const sidebar = createSlice({
name: "sidebar",
initialState:
{
menus: {}, // Keep track of menus states (guid <-> open/close)
visible: true
},
reducers:
{
show(state, action)
{
state.visible = action.payload.visible;
},
setMenuOpen(state, action)
{
const { id, open } = action.payload;
state.menus[id] = open;
return state;
}
}
});
export default sidebar;
Everything works fine until I "add" actions (that change the store) to the slice but consider your team looking for an utility function "getMenuOpen": this method doesn't change the store (it's not an action and cannot be addeded to reducers object). You can of course read directly the data from the store (state.menus[<your_id>]) but consider a more complex example where manipulating the data requires some library imports, more complex code, etc...I want to modularize/hide each slice as much as possible.
Actually I'm using this workaround:
const sidebar = createSlice({ /* Same previous code... */ });
sidebar.methods =
{
getMenuOpen: (state, id) => state.menus[id]
};
export default sidebar;
The above code allows importing the slice from a component, mapStateToProps to the redux store, and invoke the utilty function getMenuOpen like this:
import sidebar from "./Sidebar.slice";
// Component declaration ...
const mapStateToProps = state => ({
sidebar: state.ui.layout.sidebar,
getMenuOpen(id)
{
return sidebar.methods.getMenuOpen(this.sidebar, id);
}
});
const mapDispatchToProps = dispatch => ({
setMenuOpen: (id, open) => dispatch(sidebar.actions.setMenuOpen({id, open}))
});
The ugly part is that I need to inject the slice node (this.sidebar) as fist param of getMenuOpen because it's not mapped (as for actions with reducers/actions) automatically from redux-toolkit.
So my question is: how can I clean my workaround in order to automatically map the store for utility functions? createSlice doesn't seem to support that but maybe some internal redux's api could help me in mapping my "slice.methods" automatically to the store.
Thanks

Netsuite: how to add a custom link to the Nav Bar or Header

Is there any way to customize the Nav Bar or the Header to have a custom link?
The use-case is that I have a JIRA issue collector that is driven by javascript. I would like the user to provide feedback from the page they are having issues. However, any solution I can come up with so far takes the user away from the current page.
Example of what I have that takes the user away:
I currently have a Suitelet that is in one of the menus. That Suitelet invokes javascript but even then the user is taken away.
I have a workflow on the case record that calls some Javascript Javascript in one of the UI-based action's conditions is invoked. Similar to #1 but on the case record.
I'm thinking I'm going to need to create and public a chrome extension for my company's domain just to get a pervasive bit of javascript to run for all pages...seems like a sledgehammer.
I hope someone can prove me wrong, but as far as I am aware there is no way to natively inject Javascript or anything into the NetSuite header/navbar - they don't offer customisation to the header/navbar.
I've resorted to creating a Userscript that I load through the Violent Monkey extension for Chrome or Firefox.
Example Userscript Template
// ==UserScript==
// #name NetSuite Mods (Example)
// #namespace Violentmonkey Scripts
// #match *.netsuite.com/*
// #include *.netsuite.com/*
// #grant GM_addStyle
// #version 1.0
// #author Kane Shaw - https://stackoverflow.com/users/4561907/kane-shaw
// #description 6/11/2020, 6:25:20 PM
// ==/UserScript==
// Get access to some commonly used NLAPI functions without having to use "unsafeWindow.nlapi..." in our code
// You can add more of these if you need access to more of the functions contained on the NetSuite page
nlapiSetFieldText = unsafeWindow.nlapiSetFieldText;
nlapiSetFieldValue = unsafeWindow.nlapiSetFieldValue;
nlapiGetFieldText = unsafeWindow.nlapiGetFieldText;
nlapiGetFieldValue = unsafeWindow.nlapiGetFieldValue;
nlapiSearchRecord = unsafeWindow.nlapiSearchRecord;
nlobjSearchFilter = unsafeWindow.nlobjSearchFilter;
nlapiLookupField = unsafeWindow.nlapiLookupField;
nlapiLoadRecord = unsafeWindow.nlapiLoadRecord;
nlapiSubmitRecord = unsafeWindow.nlapiSubmitRecord;
GM_pageTransformations = {};
/**
* The entrypoint for our userscript
*/
function GM_main(jQuery) {
// We want to execute these on every NetSuite page
GM_pageTransformations.header();
GM_pageTransformations.browsertitle();
// Here we build a function name from the path (page being accessed on the NetSuite domain)
var path = location.pathname;
if(path.indexOf('.')>-1) path = path.substr(0,path.indexOf('.'));
path = toCamelCase(path,'/');
// Now we check if a page "GM_pageTransformations" function exists with a matching name
if(GM_pageTransformations[path]) {
console.log('Executing GM_pageTransformations for '+path);
GM_pageTransformations[path]();
} else {
console.log('No GM_pageTransformations for '+path);
}
}
/**
* Changes the header on all pages
*/
GM_pageTransformations['header'] = function() {
// For example, lets make the header background red
GM_addStyle('#ns_header, #ns_header * { background: red !important; }');
}
/**
* Provides useful browser/tab titles for each NetSuite page
*/
GM_pageTransformations['browsertitle'] = function() {
var title = jQuery('.uir-page-title-secondline').text().trim();
var title2 = jQuery('.uir-page-title-firstline').text().trim();
var title3 = jQuery('.ns-dashboard-detail-name').text().trim();
if(title != '') {
document.title = title+(title2 ? ': '+title2 : '')+(title3 ? ': '+title3 : '');
} else if(title2 != '') {
document.title = title2+(title3 ? ': '+title3 : '');
} else if(title3 != '') {
document.title = title3;
}
}
/**
* Changes app center card pages (dashboard pages)
*/
GM_pageTransformations['appCenterCard'] = function() {
// For example, lets make add a new heading text on all Dashboard pages
jQuery('#ns-dashboard-page').prepend('<h1>My New Dashboard Title</h1>');
}
/**
* Convert a given string into camelCase, or CamelCase
* #param {String} string - The input stirng
* #param {String} delimter - The delimiter that seperates the words in the input string (default " ")
* #param {Boolean} capitalizeFirstWord - Wheater or not to capitalize the first word (default false)
*/
function toCamelCase(string, delimiter, capitalizeFirstWord) {
if(!delimiter) delimiter = ' ';
var pieces = string.split(delimiter);
string = '';
for (var i=0; i<pieces.length; i++) {
if(pieces[i].length == 0) continue;
string += pieces[i].charAt(0).toUpperCase() + pieces[i].slice(1);
}
if(!capitalizeFirstWord) string= string.charAt(0).toLowerCase()+string.slice(1);
return string;
}
// ===============
// CREDIT FOR JQUERY INCLUSION CODE: Brock Adams # https://stackoverflow.com/a/12751531/4561907
/**
* Check if we already have a local copy of jQuery, or if we need to fetch it from a 3rd-party server
*/
if (typeof GM_info !== "undefined") {
console.log("Running with local copy of jQuery!");
GM_main(jQuery);
}
else {
console.log ("fetching jQuery from some 3rd-party server.");
add_jQuery(GM_main, "1.9.0");
}
/**
* Add the jQuery into our page for our userscript to use
*/
function add_jQuery(callbackFn, jqVersion) {
var jqVersion = jqVersion || "1.9.0";
var D = document;
var targ = D.getElementsByTagName ('head')[0] || D.body || D.documentElement;
var scriptNode = D.createElement ('script');
scriptNode.src = 'https://ajax.googleapis.com/ajax/libs/jquery/'
+ jqVersion
+ '/jquery.min.js'
;
scriptNode.addEventListener ("load", function () {
var scriptNode = D.createElement ("script");
scriptNode.textContent =
'var gm_jQuery = jQuery.noConflict (true);\n'
+ '(' + callbackFn.toString () + ')(gm_jQuery);'
;
targ.appendChild (scriptNode);
}, false);
targ.appendChild (scriptNode);
}
You can copy and paste that code as-is into a new Userscript and it will do the following:
Make Browser tabs/windows have useful titles (shows order numbers, customer names, vendor names etc - not just "Sales Order")
Change the header background to red (as an example)
Add a new heading to the top of all "Dashboard" pages that says "My New Dashboard Title" (as an example)

Bind click event to a collection of svg paths at runtime in angular 5

In my Angular 5 project I am trying to bind a click event to each of the paths of an svg object that gets dynamically loaded. I have successfully bound a click event to one path element using the following code:
constructor(private elemRef: ElementRef) { }
mapEventInit() {
this.elemRef.nativeElement.querySelector('.country')
.addEventListener('click', this.onClick.bind(this));
}
onClick(event) {
console.log(event.target.id);
}
The problem is that querySelector only lets me bind to the first element in the collection of elements with the .country class. Is there a proper 'angular' way to do this or should I just use regular typescript, as follows?
const pathCollection = document.querySelectorAll('.country');
for (let i = 0; i < pathCollection.length; i++) {
pathCollection[i].addEventListener('click', function(event) {
console.log(this.id);
});
}

Resources