UNPKG

27.4 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3/*-----------------------------------------------------------------------------
4| Copyright (c) 2014-2017, PhosphorJS Contributors
5|
6| Distributed under the terms of the BSD 3-Clause License.
7|
8| The full license is in the file LICENSE, distributed with this software.
9|----------------------------------------------------------------------------*/
10import { ArrayExt } from '@lumino/algorithm';
11
12import { ElementExt } from '@lumino/domutils';
13
14import { getKeyboardLayout } from '@lumino/keyboard';
15
16import { Message, MessageLoop } from '@lumino/messaging';
17
18import {
19 ElementARIAAttrs,
20 ElementDataset,
21 h,
22 VirtualDOM,
23 VirtualElement
24} from '@lumino/virtualdom';
25
26import { Menu } from './menu';
27
28import { Title } from './title';
29
30import { Widget } from './widget';
31
32/**
33 * A widget which displays menus as a canonical menu bar.
34 */
35export class MenuBar extends Widget {
36 /**
37 * Construct a new menu bar.
38 *
39 * @param options - The options for initializing the menu bar.
40 */
41 constructor(options: MenuBar.IOptions = {}) {
42 super({ node: Private.createNode() });
43 this.addClass('lm-MenuBar');
44 /* <DEPRECATED> */
45 this.addClass('p-MenuBar');
46 /* </DEPRECATED> */
47 this.setFlag(Widget.Flag.DisallowLayout);
48 this.renderer = options.renderer || MenuBar.defaultRenderer;
49 this._forceItemsPosition = options.forceItemsPosition || {
50 forceX: true,
51 forceY: true
52 };
53 }
54
55 /**
56 * Dispose of the resources held by the widget.
57 */
58 dispose(): void {
59 this._closeChildMenu();
60 this._menus.length = 0;
61 super.dispose();
62 }
63
64 /**
65 * The renderer used by the menu bar.
66 */
67 readonly renderer: MenuBar.IRenderer;
68
69 /**
70 * The child menu of the menu bar.
71 *
72 * #### Notes
73 * This will be `null` if the menu bar does not have an open menu.
74 */
75 get childMenu(): Menu | null {
76 return this._childMenu;
77 }
78
79 /**
80 * Get the menu bar content node.
81 *
82 * #### Notes
83 * This is the node which holds the menu title nodes.
84 *
85 * Modifying this node directly can lead to undefined behavior.
86 */
87 get contentNode(): HTMLUListElement {
88 return this.node.getElementsByClassName(
89 'lm-MenuBar-content'
90 )[0] as HTMLUListElement;
91 }
92
93 /**
94 * Get the currently active menu.
95 */
96 get activeMenu(): Menu | null {
97 return this._menus[this._activeIndex] || null;
98 }
99
100 /**
101 * Set the currently active menu.
102 *
103 * #### Notes
104 * If the menu does not exist, the menu will be set to `null`.
105 */
106 set activeMenu(value: Menu | null) {
107 this.activeIndex = value ? this._menus.indexOf(value) : -1;
108 }
109
110 /**
111 * Get the index of the currently active menu.
112 *
113 * #### Notes
114 * This will be `-1` if no menu is active.
115 */
116 get activeIndex(): number {
117 return this._activeIndex;
118 }
119
120 /**
121 * Set the index of the currently active menu.
122 *
123 * #### Notes
124 * If the menu cannot be activated, the index will be set to `-1`.
125 */
126 set activeIndex(value: number) {
127 // Adjust the value for an out of range index.
128 if (value < 0 || value >= this._menus.length) {
129 value = -1;
130 }
131
132 // Bail early if the index will not change.
133 if (this._activeIndex === value) {
134 return;
135 }
136
137 // Update the active index.
138 this._activeIndex = value;
139
140 // Update focus to new active index
141 if (
142 this._activeIndex >= 0 &&
143 this.contentNode.childNodes[this._activeIndex]
144 ) {
145 (this.contentNode.childNodes[this._activeIndex] as HTMLElement).focus();
146 }
147
148 // Schedule an update of the items.
149 this.update();
150 }
151
152 /**
153 * A read-only array of the menus in the menu bar.
154 */
155 get menus(): ReadonlyArray<Menu> {
156 return this._menus;
157 }
158
159 /**
160 * Open the active menu and activate its first menu item.
161 *
162 * #### Notes
163 * If there is no active menu, this is a no-op.
164 */
165 openActiveMenu(): void {
166 // Bail early if there is no active item.
167 if (this._activeIndex === -1) {
168 return;
169 }
170
171 // Open the child menu.
172 this._openChildMenu();
173
174 // Activate the first item in the child menu.
175 if (this._childMenu) {
176 this._childMenu.activeIndex = -1;
177 this._childMenu.activateNextItem();
178 }
179 }
180
181 /**
182 * Add a menu to the end of the menu bar.
183 *
184 * @param menu - The menu to add to the menu bar.
185 *
186 * #### Notes
187 * If the menu is already added to the menu bar, it will be moved.
188 */
189 addMenu(menu: Menu): void {
190 this.insertMenu(this._menus.length, menu);
191 }
192
193 /**
194 * Insert a menu into the menu bar at the specified index.
195 *
196 * @param index - The index at which to insert the menu.
197 *
198 * @param menu - The menu to insert into the menu bar.
199 *
200 * #### Notes
201 * The index will be clamped to the bounds of the menus.
202 *
203 * If the menu is already added to the menu bar, it will be moved.
204 */
205 insertMenu(index: number, menu: Menu): void {
206 // Close the child menu before making changes.
207 this._closeChildMenu();
208
209 // Look up the index of the menu.
210 let i = this._menus.indexOf(menu);
211
212 // Clamp the insert index to the array bounds.
213 let j = Math.max(0, Math.min(index, this._menus.length));
214
215 // If the menu is not in the array, insert it.
216 if (i === -1) {
217 // Insert the menu into the array.
218 ArrayExt.insert(this._menus, j, menu);
219
220 // Add the styling class to the menu.
221 menu.addClass('lm-MenuBar-menu');
222 /* <DEPRECATED> */
223 menu.addClass('p-MenuBar-menu');
224 /* </DEPRECATED> */
225
226 // Connect to the menu signals.
227 menu.aboutToClose.connect(this._onMenuAboutToClose, this);
228 menu.menuRequested.connect(this._onMenuMenuRequested, this);
229 menu.title.changed.connect(this._onTitleChanged, this);
230
231 // Schedule an update of the items.
232 this.update();
233
234 // There is nothing more to do.
235 return;
236 }
237
238 // Otherwise, the menu exists in the array and should be moved.
239
240 // Adjust the index if the location is at the end of the array.
241 if (j === this._menus.length) {
242 j--;
243 }
244
245 // Bail if there is no effective move.
246 if (i === j) {
247 return;
248 }
249
250 // Move the menu to the new locations.
251 ArrayExt.move(this._menus, i, j);
252
253 // Schedule an update of the items.
254 this.update();
255 }
256
257 /**
258 * Remove a menu from the menu bar.
259 *
260 * @param menu - The menu to remove from the menu bar.
261 *
262 * #### Notes
263 * This is a no-op if the menu is not in the menu bar.
264 */
265 removeMenu(menu: Menu): void {
266 this.removeMenuAt(this._menus.indexOf(menu));
267 }
268
269 /**
270 * Remove the menu at a given index from the menu bar.
271 *
272 * @param index - The index of the menu to remove.
273 *
274 * #### Notes
275 * This is a no-op if the index is out of range.
276 */
277 removeMenuAt(index: number): void {
278 // Close the child menu before making changes.
279 this._closeChildMenu();
280
281 // Remove the menu from the array.
282 let menu = ArrayExt.removeAt(this._menus, index);
283
284 // Bail if the index is out of range.
285 if (!menu) {
286 return;
287 }
288
289 // Disconnect from the menu signals.
290 menu.aboutToClose.disconnect(this._onMenuAboutToClose, this);
291 menu.menuRequested.disconnect(this._onMenuMenuRequested, this);
292 menu.title.changed.disconnect(this._onTitleChanged, this);
293
294 // Remove the styling class from the menu.
295 menu.removeClass('lm-MenuBar-menu');
296 /* <DEPRECATED> */
297 menu.removeClass('p-MenuBar-menu');
298 /* </DEPRECATED> */
299
300 // Schedule an update of the items.
301 this.update();
302 }
303
304 /**
305 * Remove all menus from the menu bar.
306 */
307 clearMenus(): void {
308 // Bail if there is nothing to remove.
309 if (this._menus.length === 0) {
310 return;
311 }
312
313 // Close the child menu before making changes.
314 this._closeChildMenu();
315
316 // Disconnect from the menu signals and remove the styling class.
317 for (let menu of this._menus) {
318 menu.aboutToClose.disconnect(this._onMenuAboutToClose, this);
319 menu.menuRequested.disconnect(this._onMenuMenuRequested, this);
320 menu.title.changed.disconnect(this._onTitleChanged, this);
321 menu.removeClass('lm-MenuBar-menu');
322 /* <DEPRECATED> */
323 menu.removeClass('p-MenuBar-menu');
324 /* </DEPRECATED> */
325 }
326
327 // Clear the menus array.
328 this._menus.length = 0;
329
330 // Schedule an update of the items.
331 this.update();
332 }
333
334 /**
335 * Handle the DOM events for the menu bar.
336 *
337 * @param event - The DOM event sent to the menu bar.
338 *
339 * #### Notes
340 * This method implements the DOM `EventListener` interface and is
341 * called in response to events on the menu bar's DOM nodes. It
342 * should not be called directly by user code.
343 */
344 handleEvent(event: Event): void {
345 switch (event.type) {
346 case 'keydown':
347 this._evtKeyDown(event as KeyboardEvent);
348 break;
349 case 'mousedown':
350 this._evtMouseDown(event as MouseEvent);
351 break;
352 case 'mousemove':
353 this._evtMouseMove(event as MouseEvent);
354 break;
355 case 'mouseleave':
356 this._evtMouseLeave(event as MouseEvent);
357 break;
358 case 'contextmenu':
359 event.preventDefault();
360 event.stopPropagation();
361 break;
362 }
363 }
364
365 /**
366 * A message handler invoked on a `'before-attach'` message.
367 */
368 protected onBeforeAttach(msg: Message): void {
369 this.node.addEventListener('keydown', this);
370 this.node.addEventListener('mousedown', this);
371 this.node.addEventListener('mousemove', this);
372 this.node.addEventListener('mouseleave', this);
373 this.node.addEventListener('contextmenu', this);
374 }
375
376 /**
377 * A message handler invoked on an `'after-detach'` message.
378 */
379 protected onAfterDetach(msg: Message): void {
380 this.node.removeEventListener('keydown', this);
381 this.node.removeEventListener('mousedown', this);
382 this.node.removeEventListener('mousemove', this);
383 this.node.removeEventListener('mouseleave', this);
384 this.node.removeEventListener('contextmenu', this);
385 this._closeChildMenu();
386 }
387
388 /**
389 * A message handler invoked on an `'activate-request'` message.
390 */
391 protected onActivateRequest(msg: Message): void {
392 if (this.isAttached) {
393 this.node.focus();
394 }
395 }
396
397 /**
398 * A message handler invoked on an `'update-request'` message.
399 */
400 protected onUpdateRequest(msg: Message): void {
401 let menus = this._menus;
402 let renderer = this.renderer;
403 let activeIndex = this._activeIndex;
404 let content = new Array<VirtualElement>(menus.length);
405 for (let i = 0, n = menus.length; i < n; ++i) {
406 let title = menus[i].title;
407 let active = i === activeIndex;
408 if (active && menus[i].items.length == 0) {
409 active = false;
410 }
411 content[i] = renderer.renderItem({
412 title,
413 active,
414 onfocus: () => {
415 this.activeIndex = i;
416 }
417 });
418 }
419 VirtualDOM.render(content, this.contentNode);
420 }
421
422 /**
423 * Handle the `'keydown'` event for the menu bar.
424 */
425 private _evtKeyDown(event: KeyboardEvent): void {
426 // A menu bar handles all keydown events.
427 event.preventDefault();
428 event.stopPropagation();
429
430 // Fetch the key code for the event.
431 let kc = event.keyCode;
432
433 // Enter, Up Arrow, Down Arrow
434 if (kc === 13 || kc === 38 || kc === 40) {
435 this.openActiveMenu();
436 return;
437 }
438
439 // Escape
440 if (kc === 27) {
441 this._closeChildMenu();
442 this.activeIndex = -1;
443 this.node.blur();
444 return;
445 }
446
447 // Left Arrow
448 if (kc === 37) {
449 let i = this._activeIndex;
450 let n = this._menus.length;
451 this.activeIndex = i === 0 ? n - 1 : i - 1;
452 return;
453 }
454
455 // Right Arrow
456 if (kc === 39) {
457 let i = this._activeIndex;
458 let n = this._menus.length;
459 this.activeIndex = i === n - 1 ? 0 : i + 1;
460 return;
461 }
462
463 // Get the pressed key character.
464 let key = getKeyboardLayout().keyForKeydownEvent(event);
465
466 // Bail if the key is not valid.
467 if (!key) {
468 return;
469 }
470
471 // Search for the next best matching mnemonic item.
472 let start = this._activeIndex + 1;
473 let result = Private.findMnemonic(this._menus, key, start);
474
475 // Handle the requested mnemonic based on the search results.
476 // If exactly one mnemonic is matched, that menu is opened.
477 // Otherwise, the next mnemonic is activated if available,
478 // followed by the auto mnemonic if available.
479 if (result.index !== -1 && !result.multiple) {
480 this.activeIndex = result.index;
481 this.openActiveMenu();
482 } else if (result.index !== -1) {
483 this.activeIndex = result.index;
484 } else if (result.auto !== -1) {
485 this.activeIndex = result.auto;
486 }
487 }
488
489 /**
490 * Handle the `'mousedown'` event for the menu bar.
491 */
492 private _evtMouseDown(event: MouseEvent): void {
493 // Bail if the mouse press was not on the menu bar. This can occur
494 // when the document listener is installed for an active menu bar.
495 if (!ElementExt.hitTest(this.node, event.clientX, event.clientY)) {
496 return;
497 }
498
499 // Stop the propagation of the event. Immediate propagation is
500 // also stopped so that an open menu does not handle the event.
501 event.preventDefault();
502 event.stopPropagation();
503 event.stopImmediatePropagation();
504
505 // Check if the mouse is over one of the menu items.
506 let index = ArrayExt.findFirstIndex(this.contentNode.children, node => {
507 return ElementExt.hitTest(node, event.clientX, event.clientY);
508 });
509
510 // If the press was not on an item, close the child menu.
511 if (index === -1) {
512 this._closeChildMenu();
513 return;
514 }
515
516 // If the press was not the left mouse button, do nothing further.
517 if (event.button !== 0) {
518 return;
519 }
520
521 // Otherwise, toggle the open state of the child menu.
522 if (this._childMenu) {
523 this._closeChildMenu();
524 this.activeIndex = index;
525 } else {
526 this.activeIndex = index;
527 this._openChildMenu();
528 }
529 }
530
531 /**
532 * Handle the `'mousemove'` event for the menu bar.
533 */
534 private _evtMouseMove(event: MouseEvent): void {
535 // Check if the mouse is over one of the menu items.
536 let index = ArrayExt.findFirstIndex(this.contentNode.children, node => {
537 return ElementExt.hitTest(node, event.clientX, event.clientY);
538 });
539
540 // Bail early if the active index will not change.
541 if (index === this._activeIndex) {
542 return;
543 }
544
545 // Bail early if a child menu is open and the mouse is not over
546 // an item. This allows the child menu to be kept open when the
547 // mouse is over the empty part of the menu bar.
548 if (index === -1 && this._childMenu) {
549 return;
550 }
551
552 // Update the active index to the hovered item.
553 this.activeIndex = index;
554
555 // Open the new menu if a menu is already open.
556 if (this._childMenu) {
557 this._openChildMenu();
558 }
559 }
560
561 /**
562 * Handle the `'mouseleave'` event for the menu bar.
563 */
564 private _evtMouseLeave(event: MouseEvent): void {
565 // Reset the active index if there is no open menu.
566 if (!this._childMenu) {
567 this.activeIndex = -1;
568 }
569 }
570
571 /**
572 * Open the child menu at the active index immediately.
573 *
574 * If a different child menu is already open, it will be closed,
575 * even if there is no active menu.
576 */
577 private _openChildMenu(): void {
578 // If there is no active menu, close the current menu.
579 let newMenu = this.activeMenu;
580 if (!newMenu) {
581 this._closeChildMenu();
582 return;
583 }
584
585 // Bail if there is no effective menu change.
586 let oldMenu = this._childMenu;
587 if (oldMenu === newMenu) {
588 return;
589 }
590
591 // Swap the internal menu reference.
592 this._childMenu = newMenu;
593
594 // Close the current menu, or setup for the new menu.
595 if (oldMenu) {
596 oldMenu.close();
597 } else {
598 this.addClass('lm-mod-active');
599 /* <DEPRECATED> */
600 this.addClass('p-mod-active');
601 /* </DEPRECATED> */
602 document.addEventListener('mousedown', this, true);
603 }
604
605 // Ensure the menu bar is updated and look up the item node.
606 MessageLoop.sendMessage(this, Widget.Msg.UpdateRequest);
607 let itemNode = this.contentNode.children[this._activeIndex];
608
609 // Get the positioning data for the new menu.
610 let { left, bottom } = (itemNode as HTMLElement).getBoundingClientRect();
611
612 // Open the new menu at the computed location.
613 if (newMenu.items.length > 0) {
614 newMenu.open(left, bottom, this._forceItemsPosition);
615 }
616 }
617
618 /**
619 * Close the child menu immediately.
620 *
621 * This is a no-op if a child menu is not open.
622 */
623 private _closeChildMenu(): void {
624 // Bail if no child menu is open.
625 if (!this._childMenu) {
626 return;
627 }
628
629 // Remove the active class from the menu bar.
630 this.removeClass('lm-mod-active');
631 /* <DEPRECATED> */
632 this.removeClass('p-mod-active');
633 /* </DEPRECATED> */
634
635 // Remove the document listeners.
636 document.removeEventListener('mousedown', this, true);
637
638 // Clear the internal menu reference.
639 let menu = this._childMenu;
640 this._childMenu = null;
641
642 // Close the menu.
643 menu.close();
644
645 // Reset the active index.
646 this.activeIndex = -1;
647 }
648
649 /**
650 * Handle the `aboutToClose` signal of a menu.
651 */
652 private _onMenuAboutToClose(sender: Menu): void {
653 // Bail if the sender is not the child menu.
654 if (sender !== this._childMenu) {
655 return;
656 }
657
658 // Remove the active class from the menu bar.
659 this.removeClass('lm-mod-active');
660 /* <DEPRECATED> */
661 this.removeClass('p-mod-active');
662 /* </DEPRECATED> */
663
664 // Remove the document listeners.
665 document.removeEventListener('mousedown', this, true);
666
667 // Clear the internal menu reference.
668 this._childMenu = null;
669
670 // Reset the active index.
671 this.activeIndex = -1;
672 }
673
674 /**
675 * Handle the `menuRequested` signal of a child menu.
676 */
677 private _onMenuMenuRequested(sender: Menu, args: 'next' | 'previous'): void {
678 // Bail if the sender is not the child menu.
679 if (sender !== this._childMenu) {
680 return;
681 }
682
683 // Look up the active index and menu count.
684 let i = this._activeIndex;
685 let n = this._menus.length;
686
687 // Active the next requested index.
688 switch (args) {
689 case 'next':
690 this.activeIndex = i === n - 1 ? 0 : i + 1;
691 break;
692 case 'previous':
693 this.activeIndex = i === 0 ? n - 1 : i - 1;
694 break;
695 }
696
697 // Open the active menu.
698 this.openActiveMenu();
699 }
700
701 /**
702 * Handle the `changed` signal of a title object.
703 */
704 private _onTitleChanged(): void {
705 this.update();
706 }
707
708 private _activeIndex = -1;
709 private _forceItemsPosition: Menu.IOpenOptions;
710 private _menus: Menu[] = [];
711 private _childMenu: Menu | null = null;
712}
713
714/**
715 * The namespace for the `MenuBar` class statics.
716 */
717export namespace MenuBar {
718 /**
719 * An options object for creating a menu bar.
720 */
721 export interface IOptions {
722 /**
723 * A custom renderer for creating menu bar content.
724 *
725 * The default is a shared renderer instance.
726 */
727 renderer?: IRenderer;
728 /**
729 * Whether to force the position of the menu. The MenuBar forces the
730 * coordinates of its menus by default. With this option you can disable it.
731 *
732 * Setting to `false` will enable the logic which repositions the
733 * coordinates of the menu if it will not fit entirely on screen.
734 *
735 * The default is `true`.
736 */
737 forceItemsPosition?: Menu.IOpenOptions;
738 }
739
740 /**
741 * An object which holds the data to render a menu bar item.
742 */
743 export interface IRenderData {
744 /**
745 * The title to be rendered.
746 */
747 readonly title: Title<Widget>;
748
749 /**
750 * Whether the item is the active item.
751 */
752 readonly active: boolean;
753
754 readonly onfocus?: (event: FocusEvent) => void;
755 }
756
757 /**
758 * A renderer for use with a menu bar.
759 */
760 export interface IRenderer {
761 /**
762 * Render the virtual element for a menu bar item.
763 *
764 * @param data - The data to use for rendering the item.
765 *
766 * @returns A virtual element representing the item.
767 */
768 renderItem(data: IRenderData): VirtualElement;
769 }
770
771 /**
772 * The default implementation of `IRenderer`.
773 *
774 * #### Notes
775 * Subclasses are free to reimplement rendering methods as needed.
776 */
777 export class Renderer implements IRenderer {
778 /**
779 * Render the virtual element for a menu bar item.
780 *
781 * @param data - The data to use for rendering the item.
782 *
783 * @returns A virtual element representing the item.
784 */
785 renderItem(data: IRenderData): VirtualElement {
786 let className = this.createItemClass(data);
787 let dataset = this.createItemDataset(data);
788 let aria = this.createItemARIA(data);
789 return h.li(
790 { className, dataset, tabindex: '0', onfocus: data.onfocus, ...aria },
791 this.renderIcon(data),
792 this.renderLabel(data)
793 );
794 }
795
796 /**
797 * Render the icon element for a menu bar item.
798 *
799 * @param data - The data to use for rendering the icon.
800 *
801 * @returns A virtual element representing the item icon.
802 */
803 renderIcon(data: IRenderData): VirtualElement {
804 let className = this.createIconClass(data);
805
806 /* <DEPRECATED> */
807 if (typeof data.title.icon === 'string') {
808 return h.div({ className }, data.title.iconLabel);
809 }
810 /* </DEPRECATED> */
811
812 // if data.title.icon is undefined, it will be ignored
813 return h.div({ className }, data.title.icon!, data.title.iconLabel);
814 }
815
816 /**
817 * Render the label element for a menu item.
818 *
819 * @param data - The data to use for rendering the label.
820 *
821 * @returns A virtual element representing the item label.
822 */
823 renderLabel(data: IRenderData): VirtualElement {
824 let content = this.formatLabel(data);
825 return h.div(
826 {
827 className:
828 'lm-MenuBar-itemLabel' +
829 /* <DEPRECATED> */
830 ' p-MenuBar-itemLabel'
831 /* </DEPRECATED> */
832 },
833 content
834 );
835 }
836
837 /**
838 * Create the class name for the menu bar item.
839 *
840 * @param data - The data to use for the class name.
841 *
842 * @returns The full class name for the menu item.
843 */
844 createItemClass(data: IRenderData): string {
845 let name = 'lm-MenuBar-item';
846 /* <DEPRECATED> */
847 name += ' p-MenuBar-item';
848 /* </DEPRECATED> */
849 if (data.title.className) {
850 name += ` ${data.title.className}`;
851 }
852 if (data.active) {
853 name += ' lm-mod-active';
854 /* <DEPRECATED> */
855 name += ' p-mod-active';
856 /* </DEPRECATED> */
857 }
858 return name;
859 }
860
861 /**
862 * Create the dataset for a menu bar item.
863 *
864 * @param data - The data to use for the item.
865 *
866 * @returns The dataset for the menu bar item.
867 */
868 createItemDataset(data: IRenderData): ElementDataset {
869 return data.title.dataset;
870 }
871
872 /**
873 * Create the aria attributes for menu bar item.
874 *
875 * @param data - The data to use for the aria attributes.
876 *
877 * @returns The aria attributes object for the item.
878 */
879 createItemARIA(data: IRenderData): ElementARIAAttrs {
880 return { role: 'menuitem', 'aria-haspopup': 'true' };
881 }
882
883 /**
884 * Create the class name for the menu bar item icon.
885 *
886 * @param data - The data to use for the class name.
887 *
888 * @returns The full class name for the item icon.
889 */
890 createIconClass(data: IRenderData): string {
891 let name = 'lm-MenuBar-itemIcon';
892 /* <DEPRECATED> */
893 name += ' p-MenuBar-itemIcon';
894 /* </DEPRECATED> */
895 let extra = data.title.iconClass;
896 return extra ? `${name} ${extra}` : name;
897 }
898
899 /**
900 * Create the render content for the label node.
901 *
902 * @param data - The data to use for the label content.
903 *
904 * @returns The content to add to the label node.
905 */
906 formatLabel(data: IRenderData): h.Child {
907 // Fetch the label text and mnemonic index.
908 let { label, mnemonic } = data.title;
909
910 // If the index is out of range, do not modify the label.
911 if (mnemonic < 0 || mnemonic >= label.length) {
912 return label;
913 }
914
915 // Split the label into parts.
916 let prefix = label.slice(0, mnemonic);
917 let suffix = label.slice(mnemonic + 1);
918 let char = label[mnemonic];
919
920 // Wrap the mnemonic character in a span.
921 let span = h.span(
922 {
923 className:
924 'lm-MenuBar-itemMnemonic' +
925 /* <DEPRECATED> */
926 ' p-MenuBar-itemMnemonic'
927 /* </DEPRECATED> */
928 },
929 char
930 );
931
932 // Return the content parts.
933 return [prefix, span, suffix];
934 }
935 }
936
937 /**
938 * The default `Renderer` instance.
939 */
940 export const defaultRenderer = new Renderer();
941}
942
943/**
944 * The namespace for the module implementation details.
945 */
946namespace Private {
947 /**
948 * Create the DOM node for a menu bar.
949 */
950 export function createNode(): HTMLDivElement {
951 let node = document.createElement('div');
952 let content = document.createElement('ul');
953 content.className = 'lm-MenuBar-content';
954 /* <DEPRECATED> */
955 content.classList.add('p-MenuBar-content');
956 /* </DEPRECATED> */
957 node.appendChild(content);
958 content.setAttribute('role', 'menubar');
959 node.tabIndex = 0;
960 content.tabIndex = 0;
961 return node;
962 }
963
964 /**
965 * The results of a mnemonic search.
966 */
967 export interface IMnemonicResult {
968 /**
969 * The index of the first matching mnemonic item, or `-1`.
970 */
971 index: number;
972
973 /**
974 * Whether multiple mnemonic items matched.
975 */
976 multiple: boolean;
977
978 /**
979 * The index of the first auto matched non-mnemonic item.
980 */
981 auto: number;
982 }
983
984 /**
985 * Find the best matching mnemonic item.
986 *
987 * The search starts at the given index and wraps around.
988 */
989 export function findMnemonic(
990 menus: ReadonlyArray<Menu>,
991 key: string,
992 start: number
993 ): IMnemonicResult {
994 // Setup the result variables.
995 let index = -1;
996 let auto = -1;
997 let multiple = false;
998
999 // Normalize the key to upper case.
1000 let upperKey = key.toUpperCase();
1001
1002 // Search the items from the given start index.
1003 for (let i = 0, n = menus.length; i < n; ++i) {
1004 // Compute the wrapped index.
1005 let k = (i + start) % n;
1006
1007 // Look up the menu title.
1008 let title = menus[k].title;
1009
1010 // Ignore titles with an empty label.
1011 if (title.label.length === 0) {
1012 continue;
1013 }
1014
1015 // Look up the mnemonic index for the label.
1016 let mn = title.mnemonic;
1017
1018 // Handle a valid mnemonic index.
1019 if (mn >= 0 && mn < title.label.length) {
1020 if (title.label[mn].toUpperCase() === upperKey) {
1021 if (index === -1) {
1022 index = k;
1023 } else {
1024 multiple = true;
1025 }
1026 }
1027 continue;
1028 }
1029
1030 // Finally, handle the auto index if possible.
1031 if (auto === -1 && title.label[0].toUpperCase() === upperKey) {
1032 auto = k;
1033 }
1034 }
1035
1036 // Return the search results.
1037 return { index, multiple, auto };
1038 }
1039}