UNPKG

@jupyterlab/ui-components

Version:

JupyterLab - UI components written in React

1,113 lines 39 kB
// Copyright (c) Jupyter Development Team. // Distributed under the terms of the Modified BSD License. import { Button } from '@jupyter/react-components'; import { addJupyterLabThemeChangeListener, jpButton, jpToolbar, provideJupyterDesignSystem } from '@jupyter/web-components'; 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 { ellipsesIcon, LabIcon } from '../icon'; import { classes } from '../utils'; import { ReactWidget, UseSignal } from './vdom'; provideJupyterDesignSystem().register([jpButton(), jpToolbar()]); addJupyterLabThemeChangeListener(); /** * 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, _b; super({ node: document.createElement('jp-toolbar') }); this.addClass(TOOLBAR_CLASS); this.layout = (_a = options.layout) !== null && _a !== void 0 ? _a : new ToolbarLayout(); this.noFocusOnClick = (_b = options.noFocusOnClick) !== null && _b !== void 0 ? _b : false; } /** * 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. * * @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); widget.node.dataset['jpItemName'] = name; if (this.noFocusOnClick) { widget.node.dataset['noFocusOnClick'] = 'true'; } 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); } /** * Insert an item relatively to an other item. */ 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(options = {}) { super(options); this.popupOpener = new ToolbarPopupOpener(); this._widgetWidths = new Map(); this._widgetPositions = new Map(); this._zoomChanged = true; this.insertItem(0, TOOLBAR_OPENER_NAME, this.popupOpener); this.popupOpener.hide(); this._resizer = new Throttler(async (callTwice = false) => { await this._onResize(callTwice); }, 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 relatively to an other item. */ insertRelative(at, offset, name, widget) { const targetPosition = this._widgetPositions.get(at); const position = (targetPosition !== null && targetPosition !== void 0 ? targetPosition : 0) + offset; return this.insertItem(position, 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) { var _a; let status; if (widget instanceof ToolbarPopupOpener) { status = super.insertItem(index, name, widget); } else { // Insert the widget in the toolbar at expected index if possible, otherwise // before the popup opener. This position may change when invoking the resizer // at the end of this function. const j = Math.max(0, Math.min(index, this.layout.widgets.length - 1)); status = super.insertItem(j, name, widget); if (j !== index) { // This happens if the widget has been inserted at a wrong position: // - not enough widgets in the toolbar to insert it at the expected index // - the widget at the expected index should be in the popup // In the first situation, the stored index should be changed to match a // realistic index. index = Math.max(0, Math.min(index, this._widgetPositions.size)); } } // Save the widgets position when a widget is inserted or moved. if (name !== TOOLBAR_OPENER_NAME && this._widgetPositions.get(name) !== index) { // If the widget is inserted, set its current position as last. const currentPosition = (_a = this._widgetPositions.get(name)) !== null && _a !== void 0 ? _a : this._widgetPositions.size; // Change the position of moved widgets. this._widgetPositions.forEach((value, key) => { if (key !== TOOLBAR_OPENER_NAME) { if (value >= index && value < currentPosition) { this._widgetPositions.set(key, value + 1); } else if (value <= index && value > currentPosition) { this._widgetPositions.set(key, value - 1); } } }); // Save the new position of the widget. this._widgetPositions.set(name, index); // Invokes resizing to ensure correct display of items after an addition, only // if the toolbar is rendered. if (this.isVisible) { void this._resizer.invoke(); } } return status; } /** * A message handler invoked on an `'after-show'` message. * * Invokes resizing to ensure correct display of items. */ onAfterShow(msg) { void this._resizer.invoke(true); } /** * 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); // Check if the resize event is due to a zoom change. const zoom = Math.round((window.outerWidth / window.innerWidth) * 100); if (zoom !== this._zoom) { this._zoomChanged = true; this._zoom = zoom; } if (msg.width > 0 && this._resizer) { void this._resizer.invoke(); } } /** * Move the toolbar items between the reactive toolbar and the popup toolbar, * depending on the width of the toolbar and the width of each item. * * @param callTwice - whether to call the function twice. * * **NOTES** * The `callTwice` parameter is useful when the toolbar is displayed the first time, * because the size of the items is unknown before their first rendering. The first * call will usually add all the items in the main toolbar, and the second call will * reorganize the items between the main toolbar and the popup toolbar. */ async _onResize(callTwice = false) { if (!(this.parent && this.parent.isAttached)) { return; } const toolbarWidth = this.node.clientWidth; const opener = this.popupOpener; const openerWidth = 32; // left and right padding. const toolbarPadding = 2 + 5; let width = opener.isHidden ? toolbarPadding : toolbarPadding + openerWidth; return this._getWidgetsToRemove(width, toolbarWidth, openerWidth) .then(async (values) => { var _a, _b; let { width, widgetsToRemove } = values; while (widgetsToRemove.length > 0) { // Insert the widget at the right position in the opener popup, relatively // to the saved position of the first item of the popup toolbar. // Get the saved position of the widget to insert. const widget = widgetsToRemove.pop(); const name = Private.nameProperty.get(widget); width -= this._widgetWidths.get(name) || 0; const position = (_a = this._widgetPositions.get(name)) !== null && _a !== void 0 ? _a : 0; // Get the saved position of the first item in the popup toolbar. // If there is no widget, set the value at last item. let openerFirstIndex = this._widgetPositions.size; const openerFirst = opener.widgetAt(0); if (openerFirst) { const openerFirstName = Private.nameProperty.get(openerFirst); openerFirstIndex = (_b = this._widgetPositions.get(openerFirstName)) !== null && _b !== void 0 ? _b : openerFirstIndex; } // Insert the widget in the popup toolbar. const index = position - openerFirstIndex; opener.insertWidget(index, widget); } if (opener.widgetCount() > 0) { const widgetsToAdd = []; let index = 0; const widgetCount = opener.widgetCount(); while (index < widgetCount) { let widget = opener.widgetAt(index); if (widget) { width += this._getWidgetWidth(widget); if (widgetCount - widgetsToAdd.length === 1) { width -= openerWidth; } } else { break; } if (width < toolbarWidth) { widgetsToAdd.push(widget); } else { break; } index++; } while (widgetsToAdd.length > 0) { // Insert the widget in the right position in the toolbar. const widget = widgetsToAdd.shift(); const name = Private.nameProperty.get(widget); if (this._widgetPositions.has(name)) { this.insertItem(this._widgetPositions.get(name), name, widget); } else { this.addItem(name, widget); } } } if (opener.widgetCount() > 0) { opener.updatePopup(); opener.show(); } else { opener.hide(); } if (callTwice) { await this._onResize(); } }) .catch(msg => { console.error('Error while computing the ReactiveToolbar', msg); }); } async _getWidgetsToRemove(width, toolbarWidth, openerWidth) { var _a; const opener = this.popupOpener; const widgets = [...this.layout.widgets]; const toIndex = widgets.length - 1; const widgetsToRemove = []; let index = 0; while (index < toIndex) { const widget = widgets[index]; const name = Private.nameProperty.get(widget); // Compute the widget size only if // - the zoom has changed. // - the widget size has not been computed yet. let widgetWidth; if (this._zoomChanged) { widgetWidth = await this._saveWidgetWidth(name, widget); } else { // The widget widths can be 0px if it has been added to the toolbar but // not rendered, this is why we must use '||' instead of '??'. widgetWidth = this._getWidgetWidth(widget) || (await this._saveWidgetWidth(name, widget)); } width += widgetWidth; if (widgetsToRemove.length === 0 && opener.isHidden && width + openerWidth > toolbarWidth) { width += openerWidth; } // Remove the widget if it is out of the toolbar or incorrectly positioned. // Incorrect positioning can occur when the widget is added after the toolbar // has been rendered and should be in the popup. E.g. debugger icon with a // narrow notebook toolbar. if (width > toolbarWidth || ((_a = this._widgetPositions.get(name)) !== null && _a !== void 0 ? _a : 0) > index) { widgetsToRemove.push(widget); } index++; } this._zoomChanged = false; return { width: width, widgetsToRemove: widgetsToRemove }; } async _saveWidgetWidth(name, widget) { if (widget instanceof ReactWidget) { await widget.renderPromise; } const widgetWidth = widget.hasClass(TOOLBAR_SPACER_CLASS) ? 2 : widget.node.clientWidth; this._widgetWidths.set(name, widgetWidth); return widgetWidth; } _getWidgetWidth(widget) { const widgetName = Private.nameProperty.get(widget); return this._widgetWidths.get(widgetName) || 0; } } /** * 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, _c; // 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). const handleClick = ((_a = props.noFocusOnClick) !== null && _a !== void 0 ? _a : false) ? undefined : (event) => { var _a; if (event.button === 0) { (_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props); // In safari, the focus do not move to the button on click (see // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus). event.target.focus(); } }; // To avoid focusing the button, we avoid a click event by calling preventDefault in // mousedown, and we bind the button action to `mousedown`. // Currently this is mostly useful for the notebook panel, to retrieve the focused // cell before the click event. const handleMouseDown = ((_b = props.noFocusOnClick) !== null && _b !== void 0 ? _b : false) ? (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); } } : undefined; 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 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; } }; const title = getTooltip(); const disabled = props.enabled === false; return (React.createElement(Button, { appearance: "stealth", className: props.className ? props.className + ' jp-ToolbarButtonComponent' : 'jp-ToolbarButtonComponent', "aria-disabled": disabled, "aria-label": props.label || title, "aria-pressed": props.pressed, ...Private.normalizeDataset(props.dataset), disabled: disabled, onClick: handleClick, onMouseDown: handleMouseDown, onKeyDown: handleKeyDown, title: title }, (props.icon || props.iconClass) && (React.createElement(LabIcon.resolveReact, { icon: props.pressed ? (_c = props.pressedIcon) !== null && _c !== void 0 ? _c : props.icon : props.icon, iconClass: // add some extra classes for proper support of icons-as-css-background classes(props.iconClass, 'jp-Icon'), tag: null })), 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, noFocusOnClick: this.props.noFocusOnClick, 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' }, () => props.commands.listCommands().includes(props.id) ? (React.createElement(ToolbarButtonComponent, { ...Private.propsFromCommand(props) })) : null)); } /* * 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; const { commands, id, args } = props; addCommandToolbarButtonClass(this); this.setCommandAttributes(commands, id, args); commands.commandChanged.connect((_, change) => { if (change.id === props.id) { this.setCommandAttributes(commands, id, args); } }, this); } setCommandAttributes(commands, id, args) { if (commands.isToggled(id, args)) { this.addClass('lm-mod-toggled'); } else { this.removeClass('lm-mod-toggled'); } if (commands.isVisible(id, args)) { this.removeClass('lm-mod-hidden'); } else { this.addClass('lm-mod-hidden'); } if (commands.isEnabled(id, args)) { if ('disabled' in this.node) { this.node.disabled = false; } } else { if ('disabled' in this.node) { this.node.disabled = true; } } } render() { return React.createElement(CommandToolbarButtonComponent, { ...this.props }); } /** * Identifier of the underlying command. */ get commandId() { return this.props.id; } } /** * 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({ node: document.createElement('jp-toolbar') }); this.width = 0; this.node.setAttribute('aria-label', 'Responsive popup toolbar'); this.addClass('jp-Toolbar'); this.addClass('jp-Toolbar-responsive-popup'); this.addClass('jp-ThemedContainer'); 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(index, 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); } /** * Insert widget to the popup. * @param widget the widget to add */ insertWidget(index, widget) { this.popup.insertWidget(index, 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) { /** * Ensures all dataset keys have the 'data-' prefix. * @param dataset object */ function normalizeDataset(dataset) { if (!dataset) { return undefined; } const normalized = {}; for (const [key, value] of Object.entries(dataset)) { const normalizedKey = key.startsWith('data-') ? key : `data-${key}`; normalized[normalizedKey] = value; } return normalized; } Private.normalizeDataset = normalizeDataset; 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 and aria attributes. let pressed; if (commands.isToggleable(id, args)) { pressed = commands.isToggled(id, args); if (pressed) { className += ' lm-mod-toggled'; } } if (!commands.isVisible(id, args)) { className += ' lm-mod-hidden'; } const labelOverride = typeof options.label === 'function' ? options.label(args !== null && args !== void 0 ? args : {}) : options.label; let tooltip = commands.caption(id, args) || labelOverride || 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 }, noFocusOnClick: options.noFocusOnClick, icon, iconClass, tooltip: (_b = options.caption) !== null && _b !== void 0 ? _b : tooltip, onClick, enabled, label: labelOverride !== null && labelOverride !== void 0 ? labelOverride : label, pressed }; } 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