UNPKG

36.7 kBJavaScriptView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3import { Button } from '@jupyter/react-components';
4import { addJupyterLabThemeChangeListener, jpButton, jpToolbar, provideJupyterDesignSystem } from '@jupyter/web-components';
5import { nullTranslator } from '@jupyterlab/translation';
6import { find, map, some } from '@lumino/algorithm';
7import { CommandRegistry } from '@lumino/commands';
8import { MessageLoop } from '@lumino/messaging';
9import { AttachedProperty } from '@lumino/properties';
10import { PanelLayout, Widget } from '@lumino/widgets';
11import { Throttler } from '@lumino/polling';
12import * as React from 'react';
13import { ellipsesIcon, LabIcon } from '../icon';
14import { classes } from '../utils';
15import { ReactWidget, UseSignal } from './vdom';
16provideJupyterDesignSystem().register([jpButton(), jpToolbar()]);
17addJupyterLabThemeChangeListener();
18/**
19 * The class name added to toolbars.
20 */
21const TOOLBAR_CLASS = 'jp-Toolbar';
22/**
23 * The class name added to toolbar items.
24 */
25const TOOLBAR_ITEM_CLASS = 'jp-Toolbar-item';
26/**
27 * Toolbar pop-up opener button name
28 */
29const TOOLBAR_OPENER_NAME = 'toolbar-popup-opener';
30/**
31 * The class name added to toolbar spacer.
32 */
33const TOOLBAR_SPACER_CLASS = 'jp-Toolbar-spacer';
34/**
35 * A layout for toolbars.
36 *
37 * #### Notes
38 * This layout automatically collapses its height if there are no visible
39 * toolbar widgets, and expands to the standard toolbar height if there are
40 * visible toolbar widgets.
41 */
42class ToolbarLayout extends PanelLayout {
43 constructor() {
44 super(...arguments);
45 this._dirty = false;
46 }
47 /**
48 * A message handler invoked on a `'fit-request'` message.
49 *
50 * If any child widget is visible, expand the toolbar height to the normal
51 * toolbar height.
52 */
53 onFitRequest(msg) {
54 super.onFitRequest(msg);
55 if (this.parent.isAttached) {
56 // If there are any widgets not explicitly hidden, expand the toolbar to
57 // accommodate them.
58 if (some(this.widgets, w => !w.isHidden)) {
59 this.parent.node.style.minHeight = 'var(--jp-private-toolbar-height)';
60 this.parent.removeClass('jp-Toolbar-micro');
61 }
62 else {
63 this.parent.node.style.minHeight = '';
64 this.parent.addClass('jp-Toolbar-micro');
65 }
66 }
67 // Set the dirty flag to ensure only a single update occurs.
68 this._dirty = true;
69 // Notify the ancestor that it should fit immediately. This may
70 // cause a resize of the parent, fulfilling the required update.
71 if (this.parent.parent) {
72 MessageLoop.sendMessage(this.parent.parent, Widget.Msg.FitRequest);
73 }
74 // If the dirty flag is still set, the parent was not resized.
75 // Trigger the required update on the parent widget immediately.
76 if (this._dirty) {
77 MessageLoop.sendMessage(this.parent, Widget.Msg.UpdateRequest);
78 }
79 }
80 /**
81 * A message handler invoked on an `'update-request'` message.
82 */
83 onUpdateRequest(msg) {
84 super.onUpdateRequest(msg);
85 if (this.parent.isVisible) {
86 this._dirty = false;
87 }
88 }
89 /**
90 * A message handler invoked on a `'child-shown'` message.
91 */
92 onChildShown(msg) {
93 super.onChildShown(msg);
94 // Post a fit request for the parent widget.
95 this.parent.fit();
96 }
97 /**
98 * A message handler invoked on a `'child-hidden'` message.
99 */
100 onChildHidden(msg) {
101 super.onChildHidden(msg);
102 // Post a fit request for the parent widget.
103 this.parent.fit();
104 }
105 /**
106 * A message handler invoked on a `'before-attach'` message.
107 */
108 onBeforeAttach(msg) {
109 super.onBeforeAttach(msg);
110 // Post a fit request for the parent widget.
111 this.parent.fit();
112 }
113 /**
114 * Attach a widget to the parent's DOM node.
115 *
116 * @param index - The current index of the widget in the layout.
117 *
118 * @param widget - The widget to attach to the parent.
119 *
120 * #### Notes
121 * This is a reimplementation of the superclass method.
122 */
123 attachWidget(index, widget) {
124 super.attachWidget(index, widget);
125 // Post a fit request for the parent widget.
126 this.parent.fit();
127 }
128 /**
129 * Detach a widget from the parent's DOM node.
130 *
131 * @param index - The previous index of the widget in the layout.
132 *
133 * @param widget - The widget to detach from the parent.
134 *
135 * #### Notes
136 * This is a reimplementation of the superclass method.
137 */
138 detachWidget(index, widget) {
139 super.detachWidget(index, widget);
140 // Post a fit request for the parent widget.
141 this.parent.fit();
142 }
143}
144/**
145 * A class which provides a toolbar widget.
146 */
147export class Toolbar extends Widget {
148 /**
149 * Construct a new toolbar widget.
150 */
151 constructor(options = {}) {
152 var _a;
153 super({ node: document.createElement('jp-toolbar') });
154 this.addClass(TOOLBAR_CLASS);
155 this.layout = (_a = options.layout) !== null && _a !== void 0 ? _a : new ToolbarLayout();
156 }
157 /**
158 * Get an iterator over the ordered toolbar item names.
159 *
160 * @returns An iterator over the toolbar item names.
161 */
162 names() {
163 const layout = this.layout;
164 return map(layout.widgets, widget => {
165 return Private.nameProperty.get(widget);
166 });
167 }
168 /**
169 * Add an item to the end of the toolbar.
170 *
171 * @param name - The name of the widget to add to the toolbar.
172 *
173 * @param widget - The widget to add to the toolbar.
174 *
175 * @param index - The optional name of the item to insert after.
176 *
177 * @returns Whether the item was added to toolbar. Returns false if
178 * an item of the same name is already in the toolbar.
179 *
180 * #### Notes
181 * The item can be removed from the toolbar by setting its parent to `null`.
182 */
183 addItem(name, widget) {
184 const layout = this.layout;
185 return this.insertItem(layout.widgets.length, name, widget);
186 }
187 /**
188 * Insert an item into the toolbar at the specified index.
189 *
190 * @param index - The index at which to insert the item.
191 *
192 * @param name - The name of the item.
193 *
194 * @param widget - The widget to add.
195 *
196 * @returns Whether the item was added to the toolbar. Returns false if
197 * an item of the same name is already in the toolbar.
198 *
199 * #### Notes
200 * The index will be clamped to the bounds of the items.
201 * The item can be removed from the toolbar by setting its parent to `null`.
202 */
203 insertItem(index, name, widget) {
204 const existing = find(this.names(), value => value === name);
205 if (existing) {
206 return false;
207 }
208 widget.addClass(TOOLBAR_ITEM_CLASS);
209 const layout = this.layout;
210 const j = Math.max(0, Math.min(index, layout.widgets.length));
211 layout.insertWidget(j, widget);
212 Private.nameProperty.set(widget, name);
213 return true;
214 }
215 /**
216 * Insert an item into the toolbar at the after a target item.
217 *
218 * @param at - The target item to insert after.
219 *
220 * @param name - The name of the item.
221 *
222 * @param widget - The widget to add.
223 *
224 * @returns Whether the item was added to the toolbar. Returns false if
225 * an item of the same name is already in the toolbar.
226 *
227 * #### Notes
228 * The index will be clamped to the bounds of the items.
229 * The item can be removed from the toolbar by setting its parent to `null`.
230 */
231 insertAfter(at, name, widget) {
232 return this.insertRelative(at, 1, name, widget);
233 }
234 /**
235 * Insert an item into the toolbar at the before a target item.
236 *
237 * @param at - The target item to insert before.
238 *
239 * @param name - The name of the item.
240 *
241 * @param widget - The widget to add.
242 *
243 * @returns Whether the item was added to the toolbar. Returns false if
244 * an item of the same name is already in the toolbar.
245 *
246 * #### Notes
247 * The index will be clamped to the bounds of the items.
248 * The item can be removed from the toolbar by setting its parent to `null`.
249 */
250 insertBefore(at, name, widget) {
251 return this.insertRelative(at, 0, name, widget);
252 }
253 /**
254 * Insert an item relatively to an other item.
255 */
256 insertRelative(at, offset, name, widget) {
257 const nameWithIndex = map(this.names(), (name, i) => {
258 return { name: name, index: i };
259 });
260 const target = find(nameWithIndex, x => x.name === at);
261 if (target) {
262 return this.insertItem(target.index + offset, name, widget);
263 }
264 return false;
265 }
266 /**
267 * Handle the DOM events for the widget.
268 *
269 * @param event - The DOM event sent to the widget.
270 *
271 * #### Notes
272 * This method implements the DOM `EventListener` interface and is
273 * called in response to events on the dock panel's node. It should
274 * not be called directly by user code.
275 */
276 handleEvent(event) {
277 switch (event.type) {
278 case 'click':
279 this.handleClick(event);
280 break;
281 default:
282 break;
283 }
284 }
285 /**
286 * Handle a DOM click event.
287 */
288 handleClick(event) {
289 // Stop propagating the click outside the toolbar
290 event.stopPropagation();
291 // Clicking a label focuses the corresponding control
292 // that is linked with `for` attribute, so let it be.
293 if (event.target instanceof HTMLLabelElement) {
294 const forId = event.target.getAttribute('for');
295 if (forId && this.node.querySelector(`#${forId}`)) {
296 return;
297 }
298 }
299 // If this click already focused a control, let it be.
300 if (this.node.contains(document.activeElement)) {
301 return;
302 }
303 // Otherwise, activate the parent widget, which may take focus if desired.
304 if (this.parent) {
305 this.parent.activate();
306 }
307 }
308 /**
309 * Handle `after-attach` messages for the widget.
310 */
311 onAfterAttach(msg) {
312 this.node.addEventListener('click', this);
313 }
314 /**
315 * Handle `before-detach` messages for the widget.
316 */
317 onBeforeDetach(msg) {
318 this.node.removeEventListener('click', this);
319 }
320}
321/**
322 * A class which provides a toolbar widget.
323 */
324export class ReactiveToolbar extends Toolbar {
325 /**
326 * Construct a new toolbar widget.
327 */
328 constructor() {
329 super();
330 this.popupOpener = new ToolbarPopupOpener();
331 this._widgetWidths = new Map();
332 this._widgetPositions = new Map();
333 this._zoomChanged = true;
334 this.insertItem(0, TOOLBAR_OPENER_NAME, this.popupOpener);
335 this.popupOpener.hide();
336 this._resizer = new Throttler(async (callTwice = false) => {
337 await this._onResize(callTwice);
338 }, 500);
339 }
340 /**
341 * Dispose of the widget and its descendant widgets.
342 */
343 dispose() {
344 if (this.isDisposed) {
345 return;
346 }
347 if (this._resizer) {
348 this._resizer.dispose();
349 }
350 super.dispose();
351 }
352 /**
353 * Insert an item into the toolbar at the after a target item.
354 *
355 * @param at - The target item to insert after.
356 *
357 * @param name - The name of the item.
358 *
359 * @param widget - The widget to add.
360 *
361 * @returns Whether the item was added to the toolbar. Returns false if
362 * an item of the same name is already in the toolbar or if the target
363 * is the toolbar pop-up opener.
364 *
365 * #### Notes
366 * The index will be clamped to the bounds of the items.
367 * The item can be removed from the toolbar by setting its parent to `null`.
368 */
369 insertAfter(at, name, widget) {
370 if (at === TOOLBAR_OPENER_NAME) {
371 return false;
372 }
373 return super.insertAfter(at, name, widget);
374 }
375 /**
376 * Insert an item relatively to an other item.
377 */
378 insertRelative(at, offset, name, widget) {
379 const targetPosition = this._widgetPositions.get(at);
380 const position = (targetPosition !== null && targetPosition !== void 0 ? targetPosition : 0) + offset;
381 return this.insertItem(position, name, widget);
382 }
383 /**
384 * Insert an item into the toolbar at the specified index.
385 *
386 * @param index - The index at which to insert the item.
387 *
388 * @param name - The name of the item.
389 *
390 * @param widget - The widget to add.
391 *
392 * @returns Whether the item was added to the toolbar. Returns false if
393 * an item of the same name is already in the toolbar.
394 *
395 * #### Notes
396 * The index will be clamped to the bounds of the items.
397 * The item can be removed from the toolbar by setting its parent to `null`.
398 */
399 insertItem(index, name, widget) {
400 var _a;
401 let status;
402 if (widget instanceof ToolbarPopupOpener) {
403 status = super.insertItem(index, name, widget);
404 }
405 else {
406 const j = Math.max(0, Math.min(index, this.layout.widgets.length - 1));
407 status = super.insertItem(j, name, widget);
408 }
409 // Save the widgets position when a widget is inserted or moved.
410 if (name !== TOOLBAR_OPENER_NAME &&
411 this._widgetPositions.get(name) !== index) {
412 // If the widget is inserted, set its current position as last.
413 const currentPosition = (_a = this._widgetPositions.get(name)) !== null && _a !== void 0 ? _a : this._widgetPositions.size;
414 // Change the position of moved widgets.
415 this._widgetPositions.forEach((value, key) => {
416 if (key !== TOOLBAR_OPENER_NAME) {
417 if (value >= index && value < currentPosition) {
418 this._widgetPositions.set(key, value + 1);
419 }
420 else if (value <= index && value > currentPosition) {
421 this._widgetPositions.set(key, value - 1);
422 }
423 }
424 });
425 // Save the new position of the widget.
426 this._widgetPositions.set(name, index);
427 // Invokes resizing to ensure correct display of items after an addition, only
428 // if the toolbar is rendered.
429 if (this.isVisible) {
430 void this._resizer.invoke();
431 }
432 }
433 return status;
434 }
435 /**
436 * A message handler invoked on an `'after-show'` message.
437 *
438 * Invokes resizing to ensure correct display of items.
439 */
440 onAfterShow(msg) {
441 void this._resizer.invoke(true);
442 }
443 /**
444 * A message handler invoked on a `'before-hide'` message.
445 *
446 * It will hide the pop-up panel
447 */
448 onBeforeHide(msg) {
449 this.popupOpener.hidePopup();
450 super.onBeforeHide(msg);
451 }
452 onResize(msg) {
453 super.onResize(msg);
454 // Check if the resize event is due to a zoom change.
455 const zoom = Math.round((window.outerWidth / window.innerWidth) * 100);
456 if (zoom !== this._zoom) {
457 this._zoomChanged = true;
458 this._zoom = zoom;
459 }
460 if (msg.width > 0 && this._resizer) {
461 void this._resizer.invoke();
462 }
463 }
464 /**
465 * Move the toolbar items between the reactive toolbar and the popup toolbar,
466 * depending on the width of the toolbar and the width of each item.
467 *
468 * @param callTwice - whether to call the function twice.
469 *
470 * **NOTES**
471 * The `callTwice` parameter is useful when the toolbar is displayed the first time,
472 * because the size of the items is unknown before their first rendering. The first
473 * call will usually add all the items in the main toolbar, and the second call will
474 * reorganize the items between the main toolbar and the popup toolbar.
475 */
476 async _onResize(callTwice = false) {
477 if (!(this.parent && this.parent.isAttached)) {
478 return;
479 }
480 const toolbarWidth = this.node.clientWidth;
481 const opener = this.popupOpener;
482 const openerWidth = 32;
483 // left and right padding.
484 const toolbarPadding = 2 + 5;
485 let width = opener.isHidden ? toolbarPadding : toolbarPadding + openerWidth;
486 this._getWidgetsToRemove(width, toolbarWidth, openerWidth)
487 .then(values => {
488 var _a, _b;
489 let { width, widgetsToRemove } = values;
490 while (widgetsToRemove.length > 0) {
491 // Insert the widget at the right position in the opener popup, relatively
492 // to the saved position of the first item of the popup toolbar.
493 // Get the saved position of the widget to insert.
494 const widget = widgetsToRemove.pop();
495 const name = Private.nameProperty.get(widget);
496 width -= this._widgetWidths.get(name) || 0;
497 const position = (_a = this._widgetPositions.get(name)) !== null && _a !== void 0 ? _a : 0;
498 // Get the saved position of the first item in the popup toolbar.
499 // If there is no widget, set the value at last item.
500 let openerFirstIndex = this._widgetPositions.size;
501 const openerFirst = opener.widgetAt(0);
502 if (openerFirst) {
503 const openerFirstName = Private.nameProperty.get(openerFirst);
504 openerFirstIndex =
505 (_b = this._widgetPositions.get(openerFirstName)) !== null && _b !== void 0 ? _b : openerFirstIndex;
506 }
507 // Insert the widget in the popup toolbar.
508 const index = position - openerFirstIndex;
509 opener.insertWidget(index, widget);
510 }
511 if (opener.widgetCount() > 0) {
512 const widgetsToAdd = [];
513 let index = 0;
514 const widgetCount = opener.widgetCount();
515 while (index < widgetCount) {
516 let widget = opener.widgetAt(index);
517 if (widget) {
518 width += this._getWidgetWidth(widget);
519 if (widgetCount - widgetsToAdd.length === 1) {
520 width -= openerWidth;
521 }
522 }
523 else {
524 break;
525 }
526 if (width < toolbarWidth) {
527 widgetsToAdd.push(widget);
528 }
529 else {
530 break;
531 }
532 index++;
533 }
534 while (widgetsToAdd.length > 0) {
535 // Insert the widget in the right position in the toolbar.
536 const widget = widgetsToAdd.shift();
537 const name = Private.nameProperty.get(widget);
538 if (this._widgetPositions.has(name)) {
539 this.insertItem(this._widgetPositions.get(name), name, widget);
540 }
541 else {
542 this.addItem(name, widget);
543 }
544 }
545 }
546 if (opener.widgetCount() > 0) {
547 opener.updatePopup();
548 opener.show();
549 }
550 else {
551 opener.hide();
552 }
553 if (callTwice) {
554 void this._onResize();
555 }
556 })
557 .catch(msg => {
558 console.error('Error while computing the ReactiveToolbar', msg);
559 });
560 }
561 async _getWidgetsToRemove(width, toolbarWidth, openerWidth) {
562 var _a;
563 const opener = this.popupOpener;
564 const layout = this.layout;
565 const toIndex = layout.widgets.length - 1;
566 const widgetsToRemove = [];
567 let index = 0;
568 while (index < toIndex) {
569 const widget = layout.widgets[index];
570 const name = Private.nameProperty.get(widget);
571 // Compute the widget size only if
572 // - the zoom has changed.
573 // - the widget size has not been computed yet.
574 let widgetWidth;
575 if (this._zoomChanged) {
576 widgetWidth = await this._saveWidgetWidth(name, widget);
577 }
578 else {
579 // The widget widths can be 0px if it has been added to the toolbar but
580 // not rendered, this is why we must use '||' instead of '??'.
581 widgetWidth =
582 this._getWidgetWidth(widget) ||
583 (await this._saveWidgetWidth(name, widget));
584 }
585 width += widgetWidth;
586 if (widgetsToRemove.length === 0 &&
587 opener.isHidden &&
588 width + openerWidth > toolbarWidth) {
589 width += openerWidth;
590 }
591 // Remove the widget if it is out of the toolbar or incorrectly positioned.
592 // Incorrect positioning can occur when the widget is added after the toolbar
593 // has been rendered and should be in the popup. E.g. debugger icon with a
594 // narrow notebook toolbar.
595 if (width > toolbarWidth ||
596 ((_a = this._widgetPositions.get(name)) !== null && _a !== void 0 ? _a : 0) > index) {
597 widgetsToRemove.push(widget);
598 }
599 index++;
600 }
601 this._zoomChanged = false;
602 return {
603 width: width,
604 widgetsToRemove: widgetsToRemove
605 };
606 }
607 async _saveWidgetWidth(name, widget) {
608 if (widget instanceof ReactWidget) {
609 await widget.renderPromise;
610 }
611 const widgetWidth = widget.hasClass(TOOLBAR_SPACER_CLASS)
612 ? 2
613 : widget.node.clientWidth;
614 this._widgetWidths.set(name, widgetWidth);
615 return widgetWidth;
616 }
617 _getWidgetWidth(widget) {
618 const widgetName = Private.nameProperty.get(widget);
619 return this._widgetWidths.get(widgetName) || 0;
620 }
621}
622/**
623 * The namespace for Toolbar class statics.
624 */
625(function (Toolbar) {
626 /**
627 * Create a toolbar spacer item.
628 *
629 * #### Notes
630 * It is a flex spacer that separates the left toolbar items
631 * from the right toolbar items.
632 */
633 function createSpacerItem() {
634 return new Private.Spacer();
635 }
636 Toolbar.createSpacerItem = createSpacerItem;
637})(Toolbar || (Toolbar = {}));
638/**
639 * React component for a toolbar button.
640 *
641 * @param props - The props for ToolbarButtonComponent.
642 */
643export function ToolbarButtonComponent(props) {
644 var _a, _b;
645 const actualOnClick = (_a = props.actualOnClick) !== null && _a !== void 0 ? _a : false;
646 // In some browsers, a button click event moves the focus from the main
647 // content to the button (see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus).
648 // We avoid a click event by calling preventDefault in mousedown, and
649 // we bind the button action to `mousedown`.
650 const handleMouseDown = actualOnClick
651 ? undefined
652 : (event) => {
653 var _a;
654 // Fire action only when left button is pressed.
655 if (event.button === 0) {
656 event.preventDefault();
657 (_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props);
658 }
659 };
660 const handleClick = actualOnClick
661 ? (event) => {
662 var _a;
663 if (event.button === 0) {
664 (_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props);
665 }
666 }
667 : undefined;
668 const handleKeyDown = (event) => {
669 var _a;
670 const { key } = event;
671 if (key === 'Enter' || key === ' ') {
672 (_a = props.onClick) === null || _a === void 0 ? void 0 : _a.call(props);
673 }
674 };
675 const getTooltip = () => {
676 if (props.enabled === false && props.disabledTooltip) {
677 return props.disabledTooltip;
678 }
679 else if (props.pressed && props.pressedTooltip) {
680 return props.pressedTooltip;
681 }
682 else {
683 return props.tooltip || props.iconLabel;
684 }
685 };
686 return (React.createElement(Button, { appearance: "stealth", className: props.className
687 ? props.className + ' jp-ToolbarButtonComponent'
688 : 'jp-ToolbarButtonComponent', "aria-pressed": props.pressed, "aria-disabled": props.enabled === false, ...props.dataset, disabled: props.enabled === false, onClick: handleClick, onMouseDown: handleMouseDown, onKeyDown: handleKeyDown, title: getTooltip(), minimal: true },
689 (props.icon || props.iconClass) && (React.createElement(LabIcon.resolveReact, { icon: props.pressed ? (_b = props.pressedIcon) !== null && _b !== void 0 ? _b : props.icon : props.icon, iconClass:
690 // add some extra classes for proper support of icons-as-css-background
691 classes(props.iconClass, 'jp-Icon'), tag: null })),
692 props.label && (React.createElement("span", { className: "jp-ToolbarButtonComponent-label" }, props.label))));
693}
694/**
695 * Adds the toolbar button class to the toolbar widget.
696 * @param w Toolbar button widget.
697 */
698export function addToolbarButtonClass(w) {
699 w.addClass('jp-ToolbarButton');
700 return w;
701}
702/**
703 * Lumino Widget version of static ToolbarButtonComponent.
704 */
705export class ToolbarButton extends ReactWidget {
706 /**
707 * Creates a toolbar button
708 * @param props props for underlying `ToolbarButton` component
709 */
710 constructor(props = {}) {
711 var _a, _b;
712 super();
713 this.props = props;
714 addToolbarButtonClass(this);
715 this._enabled = (_a = props.enabled) !== null && _a !== void 0 ? _a : true;
716 this._pressed = this._enabled && ((_b = props.pressed) !== null && _b !== void 0 ? _b : false);
717 this._onClick = props.onClick;
718 }
719 /**
720 * Sets the pressed state for the button
721 * @param value true if button is pressed, false otherwise
722 */
723 set pressed(value) {
724 if (this.enabled && value !== this._pressed) {
725 this._pressed = value;
726 this.update();
727 }
728 }
729 /**
730 * Returns true if button is pressed, false otherwise
731 */
732 get pressed() {
733 return this._pressed;
734 }
735 /**
736 * Sets the enabled state for the button
737 * @param value true to enable the button, false otherwise
738 */
739 set enabled(value) {
740 if (value != this._enabled) {
741 this._enabled = value;
742 if (!this._enabled) {
743 this._pressed = false;
744 }
745 this.update();
746 }
747 }
748 /**
749 * Returns true if button is enabled, false otherwise
750 */
751 get enabled() {
752 return this._enabled;
753 }
754 /**
755 * Sets the click handler for the button
756 * @param value click handler
757 */
758 set onClick(value) {
759 if (value !== this._onClick) {
760 this._onClick = value;
761 this.update();
762 }
763 }
764 /**
765 * Returns the click handler for the button
766 */
767 get onClick() {
768 return this._onClick;
769 }
770 render() {
771 return (React.createElement(ToolbarButtonComponent, { ...this.props, pressed: this.pressed, enabled: this.enabled, onClick: this.onClick }));
772 }
773}
774/**
775 * React component for a toolbar button that wraps a command.
776 *
777 * This wraps the ToolbarButtonComponent and watches the command registry
778 * for changes to the command.
779 */
780export function CommandToolbarButtonComponent(props) {
781 return (React.createElement(UseSignal, { signal: props.commands.commandChanged, shouldUpdate: (sender, args) => (args.id === props.id && args.type === 'changed') ||
782 args.type === 'many-changed' }, () => props.commands.listCommands().includes(props.id) ? (React.createElement(ToolbarButtonComponent, { ...Private.propsFromCommand(props) })) : null));
783}
784/*
785 * Adds the command toolbar button class to the command toolbar widget.
786 * @param w Command toolbar button widget.
787 */
788export function addCommandToolbarButtonClass(w) {
789 w.addClass('jp-CommandToolbarButton');
790 return w;
791}
792/**
793 * Phosphor Widget version of CommandToolbarButtonComponent.
794 */
795export class CommandToolbarButton extends ReactWidget {
796 /**
797 * Creates a command toolbar button
798 * @param props props for underlying `CommandToolbarButtonComponent` component
799 */
800 constructor(props) {
801 super();
802 this.props = props;
803 const { commands, id, args } = props;
804 addCommandToolbarButtonClass(this);
805 this.setCommandAttributes(commands, id, args);
806 commands.commandChanged.connect((_, change) => {
807 if (change.id === props.id) {
808 this.setCommandAttributes(commands, id, args);
809 }
810 }, this);
811 }
812 setCommandAttributes(commands, id, args) {
813 if (commands.isToggled(id, args)) {
814 this.addClass('lm-mod-toggled');
815 }
816 else {
817 this.removeClass('lm-mod-toggled');
818 }
819 if (commands.isVisible(id, args)) {
820 this.removeClass('lm-mod-hidden');
821 }
822 else {
823 this.addClass('lm-mod-hidden');
824 }
825 if (commands.isEnabled(id, args)) {
826 if ('disabled' in this.node) {
827 this.node.disabled = false;
828 }
829 }
830 else {
831 if ('disabled' in this.node) {
832 this.node.disabled = true;
833 }
834 }
835 }
836 render() {
837 return React.createElement(CommandToolbarButtonComponent, { ...this.props });
838 }
839}
840/**
841 * A class which provides a toolbar popup
842 * used to store widgets that don't fit
843 * in the toolbar when it is resized
844 */
845class ToolbarPopup extends Widget {
846 /**
847 * Construct a new ToolbarPopup
848 */
849 constructor() {
850 super({ node: document.createElement('jp-toolbar') });
851 this.width = 0;
852 this.addClass('jp-Toolbar');
853 this.addClass('jp-Toolbar-responsive-popup');
854 this.layout = new PanelLayout();
855 Widget.attach(this, document.body);
856 this.hide();
857 }
858 /**
859 * Updates the width of the popup, this
860 * should match with the toolbar width
861 *
862 * @param width - The width to resize to
863 * @protected
864 */
865 updateWidth(width) {
866 if (width > 0) {
867 this.width = width;
868 this.node.style.width = `${width}px`;
869 }
870 }
871 /**
872 * Aligns the popup to left bottom of widget
873 *
874 * @param widget the widget to align to
875 * @private
876 */
877 alignTo(widget) {
878 const { height: widgetHeight, width: widgetWidth, x: widgetX, y: widgetY } = widget.node.getBoundingClientRect();
879 const width = this.width;
880 this.node.style.left = `${widgetX + widgetWidth - width + 1}px`;
881 this.node.style.top = `${widgetY + widgetHeight + 1}px`;
882 }
883 /**
884 * Inserts the widget at specified index
885 * @param index the index
886 * @param widget widget to add
887 */
888 insertWidget(index, widget) {
889 this.layout.insertWidget(index, widget);
890 }
891 /**
892 * Total number of widgets in the popup
893 */
894 widgetCount() {
895 return this.layout.widgets.length;
896 }
897 /**
898 * Returns the widget at index
899 * @param index the index
900 */
901 widgetAt(index) {
902 return this.layout.widgets[index];
903 }
904}
905/**
906 * A class that provides a ToolbarPopupOpener,
907 * which is a button added to toolbar when
908 * the toolbar items overflow toolbar width
909 */
910class ToolbarPopupOpener extends ToolbarButton {
911 /**
912 * Create a new popup opener
913 */
914 constructor(props = {}) {
915 const trans = (props.translator || nullTranslator).load('jupyterlab');
916 super({
917 icon: ellipsesIcon,
918 onClick: () => {
919 this.handleClick();
920 },
921 tooltip: trans.__('More commands')
922 });
923 this.addClass('jp-Toolbar-responsive-opener');
924 this.popup = new ToolbarPopup();
925 }
926 /**
927 * Add widget to the popup, prepends widgets
928 * @param widget the widget to add
929 */
930 addWidget(widget) {
931 this.popup.insertWidget(0, widget);
932 }
933 /**
934 * Insert widget to the popup.
935 * @param widget the widget to add
936 */
937 insertWidget(index, widget) {
938 this.popup.insertWidget(index, widget);
939 }
940 /**
941 * Dispose of the widget and its descendant widgets.
942 *
943 * #### Notes
944 * It is unsafe to use the widget after it has been disposed.
945 *
946 * All calls made to this method after the first are a no-op.
947 */
948 dispose() {
949 if (this.isDisposed) {
950 return;
951 }
952 this.popup.dispose();
953 super.dispose();
954 }
955 /**
956 * Hides the opener and the popup
957 */
958 hide() {
959 super.hide();
960 this.hidePopup();
961 }
962 /**
963 * Hides the popup
964 */
965 hidePopup() {
966 this.popup.hide();
967 }
968 /**
969 * Updates width and position of the popup
970 * to align with the toolbar
971 */
972 updatePopup() {
973 this.popup.updateWidth(this.parent.node.clientWidth);
974 this.popup.alignTo(this.parent);
975 }
976 /**
977 * Returns widget at index in the popup
978 * @param index
979 */
980 widgetAt(index) {
981 return this.popup.widgetAt(index);
982 }
983 /**
984 * Returns total number of widgets in the popup
985 *
986 * @returns Number of widgets
987 */
988 widgetCount() {
989 return this.popup.widgetCount();
990 }
991 handleClick() {
992 this.updatePopup();
993 this.popup.setHidden(!this.popup.isHidden);
994 }
995}
996/**
997 * A namespace for private data.
998 */
999var Private;
1000(function (Private) {
1001 function propsFromCommand(options) {
1002 var _a, _b;
1003 const { commands, id, args } = options;
1004 const iconClass = commands.iconClass(id, args);
1005 const iconLabel = commands.iconLabel(id, args);
1006 const icon = (_a = options.icon) !== null && _a !== void 0 ? _a : commands.icon(id, args);
1007 const label = commands.label(id, args);
1008 let className = commands.className(id, args);
1009 // Add the boolean state classes and aria attributes.
1010 let pressed;
1011 if (commands.isToggleable(id, args)) {
1012 pressed = commands.isToggled(id, args);
1013 if (pressed) {
1014 className += ' lm-mod-toggled';
1015 }
1016 }
1017 if (!commands.isVisible(id, args)) {
1018 className += ' lm-mod-hidden';
1019 }
1020 const labelOverride = typeof options.label === 'function'
1021 ? options.label(args !== null && args !== void 0 ? args : {})
1022 : options.label;
1023 let tooltip = commands.caption(id, args) || labelOverride || label || iconLabel;
1024 // Shows hot keys in tooltips
1025 const binding = commands.keyBindings.find(b => b.command === id);
1026 if (binding) {
1027 const ks = binding.keys.map(CommandRegistry.formatKeystroke).join(', ');
1028 tooltip = `${tooltip} (${ks})`;
1029 }
1030 const onClick = () => {
1031 void commands.execute(id, args);
1032 };
1033 const enabled = commands.isEnabled(id, args);
1034 return {
1035 className,
1036 dataset: { 'data-command': options.id },
1037 icon,
1038 iconClass,
1039 tooltip: (_b = options.caption) !== null && _b !== void 0 ? _b : tooltip,
1040 onClick,
1041 enabled,
1042 label: labelOverride !== null && labelOverride !== void 0 ? labelOverride : label,
1043 pressed
1044 };
1045 }
1046 Private.propsFromCommand = propsFromCommand;
1047 /**
1048 * An attached property for the name of a toolbar item.
1049 */
1050 Private.nameProperty = new AttachedProperty({
1051 name: 'name',
1052 create: () => ''
1053 });
1054 /**
1055 * A spacer widget.
1056 */
1057 class Spacer extends Widget {
1058 /**
1059 * Construct a new spacer widget.
1060 */
1061 constructor() {
1062 super();
1063 this.addClass(TOOLBAR_SPACER_CLASS);
1064 }
1065 }
1066 Private.Spacer = Spacer;
1067})(Private || (Private = {}));
1068//# sourceMappingURL=toolbar.js.map
\No newline at end of file