@jupyterlab/ui-components
Version:
JupyterLab - UI components written in React
876 lines • 28.4 kB
JavaScript
// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
import { nullTranslator } from '@jupyterlab/translation';
import { find, map, some } from '@lumino/algorithm';
import { CommandRegistry } from '@lumino/commands';
import { MessageLoop } from '@lumino/messaging';
import { AttachedProperty } from '@lumino/properties';
import { PanelLayout, Widget } from '@lumino/widgets';
import { Throttler } from '@lumino/polling';
import * as React from 'react';
import { Button } from './button';
import { ellipsesIcon, LabIcon } from '../icon';
import { classes } from '../utils';
import { ReactWidget, UseSignal } from './vdom';
/**
* The class name added to toolbars.
*/
const TOOLBAR_CLASS = 'jp-Toolbar';
/**
* The class name added to toolbar items.
*/
const TOOLBAR_ITEM_CLASS = 'jp-Toolbar-item';
/**
* Toolbar pop-up opener button name
*/
const TOOLBAR_OPENER_NAME = 'toolbar-popup-opener';
/**
* The class name added to toolbar spacer.
*/
const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
/**
* A layout for toolbars.
*
* #### Notes
* This layout automatically collapses its height if there are no visible
* toolbar widgets, and expands to the standard toolbar height if there are
* visible toolbar widgets.
*/
class ToolbarLayout extends PanelLayout {
constructor() {
super(...arguments);
this._dirty = false;
}
/**
* A message handler invoked on a `'fit-request'` message.
*
* If any child widget is visible, expand the toolbar height to the normal
* toolbar height.
*/
onFitRequest(msg) {
super.onFitRequest(msg);
if (this.parent.isAttached) {
// If there are any widgets not explicitly hidden, expand the toolbar to
// accommodate them.
if (some(this.widgets, w => !w.isHidden)) {
this.parent.node.style.minHeight = 'var(--jp-private-toolbar-height)';
this.parent.removeClass('jp-Toolbar-micro');
}
else {
this.parent.node.style.minHeight = '';
this.parent.addClass('jp-Toolbar-micro');
}
}
// Set the dirty flag to ensure only a single update occurs.
this._dirty = true;
// Notify the ancestor that it should fit immediately. This may
// cause a resize of the parent, fulfilling the required update.
if (this.parent.parent) {
MessageLoop.sendMessage(this.parent.parent, Widget.Msg.FitRequest);
}
// If the dirty flag is still set, the parent was not resized.
// Trigger the required update on the parent widget immediately.
if (this._dirty) {
MessageLoop.sendMessage(this.parent, Widget.Msg.UpdateRequest);
}
}
/**
* A message handler invoked on an `'update-request'` message.
*/
onUpdateRequest(msg) {
super.onUpdateRequest(msg);
if (this.parent.isVisible) {
this._dirty = false;
}
}
/**
* A message handler invoked on a `'child-shown'` message.
*/
onChildShown(msg) {
super.onChildShown(msg);
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* A message handler invoked on a `'child-hidden'` message.
*/
onChildHidden(msg) {
super.onChildHidden(msg);
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* A message handler invoked on a `'before-attach'` message.
*/
onBeforeAttach(msg) {
super.onBeforeAttach(msg);
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Attach a widget to the parent's DOM node.
*
* @param index - The current index of the widget in the layout.
*
* @param widget - The widget to attach to the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
attachWidget(index, widget) {
super.attachWidget(index, widget);
// Post a fit request for the parent widget.
this.parent.fit();
}
/**
* Detach a widget from the parent's DOM node.
*
* @param index - The previous index of the widget in the layout.
*
* @param widget - The widget to detach from the parent.
*
* #### Notes
* This is a reimplementation of the superclass method.
*/
detachWidget(index, widget) {
super.detachWidget(index, widget);
// Post a fit request for the parent widget.
this.parent.fit();
}
}
/**
* A class which provides a toolbar widget.
*/
export class Toolbar extends Widget {
/**
* Construct a new toolbar widget.
*/
constructor(options = {}) {
var _a;
super();
this.addClass(TOOLBAR_CLASS);
this.layout = (_a = options.layout) !== null && _a !== void 0 ? _a : new ToolbarLayout();
}
/**
* Get an iterator over the ordered toolbar item names.
*
* @returns An iterator over the toolbar item names.
*/
names() {
const layout = this.layout;
return map(layout.widgets, widget => {
return Private.nameProperty.get(widget);
});
}
/**
* Add an item to the end of the toolbar.
*
* @param name - The name of the widget to add to the toolbar.
*
* @param widget - The widget to add to the toolbar.
*
* @param index - The optional name of the item to insert after.
*
* @returns Whether the item was added to toolbar. Returns false if
* an item of the same name is already in the toolbar.
*
* #### Notes
* The item can be removed from the toolbar by setting its parent to `null`.
*/
addItem(name, widget) {
const layout = this.layout;
return this.insertItem(layout.widgets.length, name, widget);
}
/**
* Insert an item into the toolbar at the specified index.
*
* @param index - The index at which to insert the item.
*
* @param name - The name of the item.
*
* @param widget - The widget to add.
*
* @returns Whether the item was added to the toolbar. Returns false if
* an item of the same name is already in the toolbar.
*
* #### Notes
* The index will be clamped to the bounds of the items.
* The item can be removed from the toolbar by setting its parent to `null`.
*/
insertItem(index, name, widget) {
const existing = find(this.names(), value => value === name);
if (existing) {
return false;
}
widget.addClass(TOOLBAR_ITEM_CLASS);
const layout = this.layout;
const j = Math.max(0, Math.min(index, layout.widgets.length));
layout.insertWidget(j, widget);
Private.nameProperty.set(widget, name);
return true;
}
/**
* Insert an item into the toolbar at the after a target item.
*
* @param at - The target item to insert after.
*
* @param name - The name of the item.
*
* @param widget - The widget to add.
*
* @returns Whether the item was added to the toolbar. Returns false if
* an item of the same name is already in the toolbar.
*
* #### Notes
* The index will be clamped to the bounds of the items.
* The item can be removed from the toolbar by setting its parent to `null`.
*/
insertAfter(at, name, widget) {
return this._insertRelative(at, 1, name, widget);
}
/**
* Insert an item into the toolbar at the before a target item.
*
* @param at - The target item to insert before.
*
* @param name - The name of the item.
*
* @param widget - The widget to add.
*
* @returns Whether the item was added to the toolbar. Returns false if
* an item of the same name is already in the toolbar.
*
* #### Notes
* The index will be clamped to the bounds of the items.
* The item can be removed from the toolbar by setting its parent to `null`.
*/
insertBefore(at, name, widget) {
return this._insertRelative(at, 0, name, widget);
}
_insertRelative(at, offset, name, widget) {
const nameWithIndex = map(this.names(), (name, i) => {
return { name: name, index: i };
});
const target = find(nameWithIndex, x => x.name === at);
if (target) {
return this.insertItem(target.index + offset, name, widget);
}
return false;
}
/**
* Handle the DOM events for the widget.
*
* @param event - The DOM event sent to the widget.
*
* #### Notes
* This method implements the DOM `EventListener` interface and is
* called in response to events on the dock panel's node. It should
* not be called directly by user code.
*/
handleEvent(event) {
switch (event.type) {
case 'click':
this.handleClick(event);
break;
default:
break;
}
}
/**
* Handle a DOM click event.
*/
handleClick(event) {
// Stop propagating the click outside the toolbar
event.stopPropagation();
// Clicking a label focuses the corresponding control
// that is linked with `for` attribute, so let it be.
if (event.target instanceof HTMLLabelElement) {
const forId = event.target.getAttribute('for');
if (forId && this.node.querySelector(`#${forId}`)) {
return;
}
}
// If this click already focused a control, let it be.
if (this.node.contains(document.activeElement)) {
return;
}
// Otherwise, activate the parent widget, which may take focus if desired.
if (this.parent) {
this.parent.activate();
}
}
/**
* Handle `after-attach` messages for the widget.
*/
onAfterAttach(msg) {
this.node.addEventListener('click', this);
}
/**
* Handle `before-detach` messages for the widget.
*/
onBeforeDetach(msg) {
this.node.removeEventListener('click', this);
}
}
/**
* A class which provides a toolbar widget.
*/
export class ReactiveToolbar extends Toolbar {
/**
* Construct a new toolbar widget.
*/
constructor() {
super();
this.popupOpener = new ToolbarPopupOpener();
this._widgetWidths = {};
this.insertItem(0, TOOLBAR_OPENER_NAME, this.popupOpener);
this.popupOpener.hide();
this._resizer = new Throttler(this._onResize.bind(this), 500);
}
/**
* Dispose of the widget and its descendant widgets.
*/
dispose() {
if (this.isDisposed) {
return;
}
if (this._resizer) {
this._resizer.dispose();
}
super.dispose();
}
/**
* Insert an item into the toolbar at the after a target item.
*
* @param at - The target item to insert after.
*
* @param name - The name of the item.
*
* @param widget - The widget to add.
*
* @returns Whether the item was added to the toolbar. Returns false if
* an item of the same name is already in the toolbar or if the target
* is the toolbar pop-up opener.
*
* #### Notes
* The index will be clamped to the bounds of the items.
* The item can be removed from the toolbar by setting its parent to `null`.
*/
insertAfter(at, name, widget) {
if (at === TOOLBAR_OPENER_NAME) {
return false;
}
return super.insertAfter(at, name, widget);
}
/**
* Insert an item into the toolbar at the specified index.
*
* @param index - The index at which to insert the item.
*
* @param name - The name of the item.
*
* @param widget - The widget to add.
*
* @returns Whether the item was added to the toolbar. Returns false if
* an item of the same name is already in the toolbar.
*
* #### Notes
* The index will be clamped to the bounds of the items.
* The item can be removed from the toolbar by setting its parent to `null`.
*/
insertItem(index, name, widget) {
if (widget instanceof ToolbarPopupOpener) {
return super.insertItem(index, name, widget);
}
else {
const j = Math.max(0, Math.min(index, this.layout.widgets.length - 1));
return super.insertItem(j, name, widget);
}
}
/**
* A message handler invoked on a `'before-hide'` message.
*
* It will hide the pop-up panel
*/
onBeforeHide(msg) {
this.popupOpener.hidePopup();
super.onBeforeHide(msg);
}
onResize(msg) {
super.onResize(msg);
if (msg.width > 0 && this._resizer) {
void this._resizer.invoke();
}
}
_onResize() {
if (this.parent && this.parent.isAttached) {
const toolbarWidth = this.node.clientWidth;
const opener = this.popupOpener;
const openerWidth = 30;
const toolbarPadding = 2;
const layout = this.layout;
let width = opener.isHidden
? toolbarPadding
: toolbarPadding + openerWidth;
let index = 0;
const widgetsToRemove = [];
const toIndex = layout.widgets.length - 1;
while (index < toIndex) {
const widget = layout.widgets[index];
this._saveWidgetWidth(widget);
width += this._getWidgetWidth(widget);
if (widgetsToRemove.length === 0 &&
opener.isHidden &&
width + openerWidth > toolbarWidth) {
width += openerWidth;
}
if (width > toolbarWidth) {
widgetsToRemove.push(widget);
}
index++;
}
while (widgetsToRemove.length > 0) {
const widget = widgetsToRemove.pop();
width -= this._getWidgetWidth(widget);
opener.addWidget(widget);
}
if (opener.widgetCount() > 0) {
const widgetsToAdd = [];
let index = 0;
let widget = opener.widgetAt(index);
const widgetCount = opener.widgetCount();
width += this._getWidgetWidth(widget);
if (widgetCount === 1 && width - openerWidth <= toolbarWidth) {
width -= openerWidth;
}
while (width < toolbarWidth && index < widgetCount) {
widgetsToAdd.push(widget);
index++;
widget = opener.widgetAt(index);
if (widget) {
width += this._getWidgetWidth(widget);
}
else {
break;
}
}
while (widgetsToAdd.length > 0) {
const widget = widgetsToAdd.shift();
this.addItem(Private.nameProperty.get(widget), widget);
}
}
if (opener.widgetCount() > 0) {
opener.updatePopup();
opener.show();
}
else {
opener.hide();
}
}
}
_saveWidgetWidth(widget) {
const widgetName = Private.nameProperty.get(widget);
this._widgetWidths[widgetName] = widget.hasClass(TOOLBAR_SPACER_CLASS)
? 2
: widget.node.clientWidth;
}
_getWidgetWidth(widget) {
const widgetName = Private.nameProperty.get(widget);
return this._widgetWidths[widgetName];
}
}
/**
* The namespace for Toolbar class statics.
*/
(function (Toolbar) {
/**
* Create a toolbar spacer item.
*
* #### Notes
* It is a flex spacer that separates the left toolbar items
* from the right toolbar items.
*/
function createSpacerItem() {
return new Private.Spacer();
}
Toolbar.createSpacerItem = createSpacerItem;
})(Toolbar || (Toolbar = {}));
/**
* React component for a toolbar button.
*
* @param props - The props for ToolbarButtonComponent.
*/
export function ToolbarButtonComponent(props) {
var _a, _b;
// In some browsers, a button click event moves the focus from the main
// content to the button (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus).
// We avoid a click event by calling preventDefault in mousedown, and
// we bind the button action to `mousedown`.
const handleMouseDown = (event) => {
var _a;
// Fire action only when left button is pressed.
if (event.button === 0) {
event.preventDefault();
(_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props);
}
};
const handleKeyDown = (event) => {
var _a;
const { key } = event;
if (key === 'Enter' || key === ' ') {
(_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props);
}
};
const handleClick = (event) => {
var _a;
if (event.button === 0) {
(_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props);
}
};
const getTooltip = () => {
if (props.enabled === false && props.disabledTooltip) {
return props.disabledTooltip;
}
else if (props.pressed && props.pressedTooltip) {
return props.pressedTooltip;
}
else {
return props.tooltip || props.iconLabel;
}
};
return (React.createElement(Button, { className: props.className
? props.className + ' jp-ToolbarButtonComponent'
: 'jp-ToolbarButtonComponent', "aria-pressed": props.pressed, "aria-disabled": props.enabled === false, ...props.dataset, disabled: props.enabled === false, onClick: ((_a = props.actualOnClick) !== null && _a !== void 0 ? _a : false) ? handleClick : undefined, onMouseDown: !((_b = props.actualOnClick) !== null && _b !== void 0 ? _b : false) ? handleMouseDown : undefined, onKeyDown: handleKeyDown, title: getTooltip(), minimal: true },
(props.icon || props.iconClass) && (React.createElement(LabIcon.resolveReact, { icon: props.pressed ? props.pressedIcon : props.icon, iconClass:
// add some extra classes for proper support of icons-as-css-background
classes(props.iconClass, 'jp-Icon'), className: "jp-ToolbarButtonComponent-icon", tag: "span", stylesheet: "toolbarButton" })),
props.label && (React.createElement("span", { className: "jp-ToolbarButtonComponent-label" }, props.label))));
}
/**
* Adds the toolbar button class to the toolbar widget.
* @param w Toolbar button widget.
*/
export function addToolbarButtonClass(w) {
w.addClass('jp-ToolbarButton');
return w;
}
/**
* Lumino Widget version of static ToolbarButtonComponent.
*/
export class ToolbarButton extends ReactWidget {
/**
* Creates a toolbar button
* @param props props for underlying `ToolbarButton` component
*/
constructor(props = {}) {
var _a, _b;
super();
this.props = props;
addToolbarButtonClass(this);
this._enabled = (_a = props.enabled) !== null && _a !== void 0 ? _a : true;
this._pressed = this._enabled && ((_b = props.pressed) !== null && _b !== void 0 ? _b : false);
this._onClick = props.onClick;
}
/**
* Sets the pressed state for the button
* @param value true if button is pressed, false otherwise
*/
set pressed(value) {
if (this.enabled && value !== this._pressed) {
this._pressed = value;
this.update();
}
}
/**
* Returns true if button is pressed, false otherwise
*/
get pressed() {
return this._pressed;
}
/**
* Sets the enabled state for the button
* @param value true to enable the button, false otherwise
*/
set enabled(value) {
if (value != this._enabled) {
this._enabled = value;
if (!this._enabled) {
this._pressed = false;
}
this.update();
}
}
/**
* Returns true if button is enabled, false otherwise
*/
get enabled() {
return this._enabled;
}
/**
* Sets the click handler for the button
* @param value click handler
*/
set onClick(value) {
if (value !== this._onClick) {
this._onClick = value;
this.update();
}
}
/**
* Returns the click handler for the button
*/
get onClick() {
return this._onClick;
}
render() {
return (React.createElement(ToolbarButtonComponent, { ...this.props, pressed: this.pressed, enabled: this.enabled, onClick: this.onClick }));
}
}
/**
* React component for a toolbar button that wraps a command.
*
* This wraps the ToolbarButtonComponent and watches the command registry
* for changes to the command.
*/
export function CommandToolbarButtonComponent(props) {
return (React.createElement(UseSignal, { signal: props.commands.commandChanged, shouldUpdate: (sender, args) => (args.id === props.id && args.type === 'changed') ||
args.type === 'many-changed' }, () => React.createElement(ToolbarButtonComponent, { ...Private.propsFromCommand(props) })));
}
/*
* Adds the command toolbar button class to the command toolbar widget.
* @param w Command toolbar button widget.
*/
export function addCommandToolbarButtonClass(w) {
w.addClass('jp-CommandToolbarButton');
return w;
}
/**
* Phosphor Widget version of CommandToolbarButtonComponent.
*/
export class CommandToolbarButton extends ReactWidget {
/**
* Creates a command toolbar button
* @param props props for underlying `CommandToolbarButtonComponent` component
*/
constructor(props) {
super();
this.props = props;
addCommandToolbarButtonClass(this);
}
render() {
return React.createElement(CommandToolbarButtonComponent, { ...this.props });
}
}
/**
* A class which provides a toolbar popup
* used to store widgets that don't fit
* in the toolbar when it is resized
*/
class ToolbarPopup extends Widget {
/**
* Construct a new ToolbarPopup
*/
constructor() {
super();
this.width = 0;
this.addClass('jp-Toolbar-responsive-popup');
this.layout = new PanelLayout();
Widget.attach(this, document.body);
this.hide();
}
/**
* Updates the width of the popup, this
* should match with the toolbar width
*
* @param width - The width to resize to
* @protected
*/
updateWidth(width) {
if (width > 0) {
this.width = width;
this.node.style.width = `${width}px`;
}
}
/**
* Aligns the popup to left bottom of widget
*
* @param widget the widget to align to
* @private
*/
alignTo(widget) {
const { height: widgetHeight, width: widgetWidth, x: widgetX, y: widgetY } = widget.node.getBoundingClientRect();
const width = this.width;
this.node.style.left = `${widgetX + widgetWidth - width + 1}px`;
this.node.style.top = `${widgetY + widgetHeight + 1}px`;
}
/**
* Inserts the widget at specified index
* @param index the index
* @param widget widget to add
*/
insertWidget(index, widget) {
this.layout.insertWidget(0, widget);
}
/**
* Total number of widgets in the popup
*/
widgetCount() {
return this.layout.widgets.length;
}
/**
* Returns the widget at index
* @param index the index
*/
widgetAt(index) {
return this.layout.widgets[index];
}
}
/**
* A class that provides a ToolbarPopupOpener,
* which is a button added to toolbar when
* the toolbar items overflow toolbar width
*/
class ToolbarPopupOpener extends ToolbarButton {
/**
* Create a new popup opener
*/
constructor(props = {}) {
const trans = (props.translator || nullTranslator).load('jupyterlab');
super({
icon: ellipsesIcon,
onClick: () => {
this.handleClick();
},
tooltip: trans.__('More commands')
});
this.addClass('jp-Toolbar-responsive-opener');
this.popup = new ToolbarPopup();
}
/**
* Add widget to the popup, prepends widgets
* @param widget the widget to add
*/
addWidget(widget) {
this.popup.insertWidget(0, widget);
}
/**
* Dispose of the widget and its descendant widgets.
*
* #### Notes
* It is unsafe to use the widget after it has been disposed.
*
* All calls made to this method after the first are a no-op.
*/
dispose() {
if (this.isDisposed) {
return;
}
this.popup.dispose();
super.dispose();
}
/**
* Hides the opener and the popup
*/
hide() {
super.hide();
this.hidePopup();
}
/**
* Hides the popup
*/
hidePopup() {
this.popup.hide();
}
/**
* Updates width and position of the popup
* to align with the toolbar
*/
updatePopup() {
this.popup.updateWidth(this.parent.node.clientWidth);
this.popup.alignTo(this.parent);
}
/**
* Returns widget at index in the popup
* @param index
*/
widgetAt(index) {
return this.popup.widgetAt(index);
}
/**
* Returns total number of widgets in the popup
*
* @returns Number of widgets
*/
widgetCount() {
return this.popup.widgetCount();
}
handleClick() {
this.updatePopup();
this.popup.setHidden(!this.popup.isHidden);
}
}
/**
* A namespace for private data.
*/
var Private;
(function (Private) {
function propsFromCommand(options) {
var _a, _b;
const { commands, id, args } = options;
const iconClass = commands.iconClass(id, args);
const iconLabel = commands.iconLabel(id, args);
const icon = (_a = options.icon) !== null && _a !== void 0 ? _a : commands.icon(id, args);
const label = commands.label(id, args);
let className = commands.className(id, args);
// Add the boolean state classes.
if (commands.isToggled(id, args)) {
className += ' lm-mod-toggled';
}
if (!commands.isVisible(id, args)) {
className += ' lm-mod-hidden';
}
let tooltip = commands.caption(id, args) || options.label || label || iconLabel;
// Shows hot keys in tooltips
const binding = commands.keyBindings.find(b => b.command === id);
if (binding) {
const ks = binding.keys.map(CommandRegistry.formatKeystroke).join(', ');
tooltip = `${tooltip} (${ks})`;
}
const onClick = () => {
void commands.execute(id, args);
};
const enabled = commands.isEnabled(id, args);
return {
className,
dataset: { 'data-command': options.id },
icon,
iconClass,
tooltip,
onClick,
enabled,
label: (_b = options.label) !== null && _b !== void 0 ? _b : label
};
}
Private.propsFromCommand = propsFromCommand;
/**
* An attached property for the name of a toolbar item.
*/
Private.nameProperty = new AttachedProperty({
name: 'name',
create: () => ''
});
/**
* A spacer widget.
*/
class Spacer extends Widget {
/**
* Construct a new spacer widget.
*/
constructor() {
super();
this.addClass(TOOLBAR_SPACER_CLASS);
}
}
Private.Spacer = Spacer;
})(Private || (Private = {}));
//# sourceMappingURL=toolbar.js.map