UNPKG

29.9 kBPlain TextView Raw
1/* eslint-disable @typescript-eslint/no-empty-function */
2// Copyright (c) Jupyter Development Team.
3// Distributed under the terms of the Modified BSD License.
4/*-----------------------------------------------------------------------------
5| Copyright (c) 2014-2017, PhosphorJS Contributors
6|
7| Distributed under the terms of the BSD 3-Clause License.
8|
9| The full license is in the file LICENSE, distributed with this software.
10|----------------------------------------------------------------------------*/
11import { empty, IIterator } from '@lumino/algorithm';
12
13import { IObservableDisposable } from '@lumino/disposable';
14
15import {
16 ConflatableMessage,
17 IMessageHandler,
18 Message,
19 MessageLoop
20} from '@lumino/messaging';
21
22import { AttachedProperty } from '@lumino/properties';
23
24import { ISignal, Signal } from '@lumino/signaling';
25
26import { Layout } from './layout';
27
28import { Title } from './title';
29
30/**
31 * The base class of the lumino widget hierarchy.
32 *
33 * #### Notes
34 * This class will typically be subclassed in order to create a useful
35 * widget. However, it can be used directly to host externally created
36 * content.
37 */
38export class Widget implements IMessageHandler, IObservableDisposable {
39 /**
40 * Construct a new widget.
41 *
42 * @param options - The options for initializing the widget.
43 */
44 constructor(options: Widget.IOptions = {}) {
45 this.node = Private.createNode(options);
46 this.addClass('lm-Widget');
47 /* <DEPRECATED> */
48 this.addClass('p-Widget');
49 /* </DEPRECATED> */
50 }
51
52 /**
53 * Dispose of the widget and its descendant widgets.
54 *
55 * #### Notes
56 * It is unsafe to use the widget after it has been disposed.
57 *
58 * All calls made to this method after the first are a no-op.
59 */
60 dispose(): void {
61 // Do nothing if the widget is already disposed.
62 if (this.isDisposed) {
63 return;
64 }
65
66 // Set the disposed flag and emit the disposed signal.
67 this.setFlag(Widget.Flag.IsDisposed);
68 this._disposed.emit(undefined);
69
70 // Remove or detach the widget if necessary.
71 if (this.parent) {
72 this.parent = null;
73 } else if (this.isAttached) {
74 Widget.detach(this);
75 }
76
77 // Dispose of the widget layout.
78 if (this._layout) {
79 this._layout.dispose();
80 this._layout = null;
81 }
82
83 // Dispose the title
84 this.title.dispose();
85
86 // Clear the extra data associated with the widget.
87 Signal.clearData(this);
88 MessageLoop.clearData(this);
89 AttachedProperty.clearData(this);
90 }
91
92 /**
93 * A signal emitted when the widget is disposed.
94 */
95 get disposed(): ISignal<this, void> {
96 return this._disposed;
97 }
98
99 /**
100 * Get the DOM node owned by the widget.
101 */
102 readonly node: HTMLElement;
103
104 /**
105 * Test whether the widget has been disposed.
106 */
107 get isDisposed(): boolean {
108 return this.testFlag(Widget.Flag.IsDisposed);
109 }
110
111 /**
112 * Test whether the widget's node is attached to the DOM.
113 */
114 get isAttached(): boolean {
115 return this.testFlag(Widget.Flag.IsAttached);
116 }
117
118 /**
119 * Test whether the widget is explicitly hidden.
120 */
121 get isHidden(): boolean {
122 return this.testFlag(Widget.Flag.IsHidden);
123 }
124
125 /**
126 * Test whether the widget is visible.
127 *
128 * #### Notes
129 * A widget is visible when it is attached to the DOM, is not
130 * explicitly hidden, and has no explicitly hidden ancestors.
131 */
132 get isVisible(): boolean {
133 return this.testFlag(Widget.Flag.IsVisible);
134 }
135
136 /**
137 * The title object for the widget.
138 *
139 * #### Notes
140 * The title object is used by some container widgets when displaying
141 * the widget alongside some title, such as a tab panel or side bar.
142 *
143 * Since not all widgets will use the title, it is created on demand.
144 *
145 * The `owner` property of the title is set to this widget.
146 */
147 get title(): Title<Widget> {
148 return Private.titleProperty.get(this);
149 }
150
151 /**
152 * Get the id of the widget's DOM node.
153 */
154 get id(): string {
155 return this.node.id;
156 }
157
158 /**
159 * Set the id of the widget's DOM node.
160 */
161 set id(value: string) {
162 this.node.id = value;
163 }
164
165 /**
166 * The dataset for the widget's DOM node.
167 */
168 get dataset(): DOMStringMap {
169 return this.node.dataset;
170 }
171
172 /**
173 * Get the method for hiding the widget.
174 */
175 get hiddenMode(): Widget.HiddenMode {
176 return this._hiddenMode;
177 }
178
179 /**
180 * Set the method for hiding the widget.
181 */
182 set hiddenMode(value: Widget.HiddenMode) {
183 if (this._hiddenMode === value) {
184 return;
185 }
186 this._hiddenMode = value;
187 switch (value) {
188 case Widget.HiddenMode.Display:
189 this.node.style.willChange = 'auto';
190 break;
191 case Widget.HiddenMode.Scale:
192 this.node.style.willChange = 'transform';
193 break;
194 }
195
196 if (this.isHidden) {
197 if (value === Widget.HiddenMode.Display) {
198 this.addClass('lm-mod-hidden');
199 /* <DEPRECATED> */
200 this.addClass('p-mod-hidden');
201 /* </DEPRECATED> */
202 this.node.style.transform = '';
203 } else {
204 this.node.style.transform = 'scale(0)';
205 this.removeClass('lm-mod-hidden');
206 /* <DEPRECATED> */
207 this.removeClass('p-mod-hidden');
208 /* </DEPRECATED> */
209 }
210 }
211 }
212
213 /**
214 * Get the parent of the widget.
215 */
216 get parent(): Widget | null {
217 return this._parent;
218 }
219
220 /**
221 * Set the parent of the widget.
222 *
223 * #### Notes
224 * Children are typically added to a widget by using a layout, which
225 * means user code will not normally set the parent widget directly.
226 *
227 * The widget will be automatically removed from its old parent.
228 *
229 * This is a no-op if there is no effective parent change.
230 */
231 set parent(value: Widget | null) {
232 if (this._parent === value) {
233 return;
234 }
235 if (value && this.contains(value)) {
236 throw new Error('Invalid parent widget.');
237 }
238 if (this._parent && !this._parent.isDisposed) {
239 let msg = new Widget.ChildMessage('child-removed', this);
240 MessageLoop.sendMessage(this._parent, msg);
241 }
242 this._parent = value;
243 if (this._parent && !this._parent.isDisposed) {
244 let msg = new Widget.ChildMessage('child-added', this);
245 MessageLoop.sendMessage(this._parent, msg);
246 }
247 if (!this.isDisposed) {
248 MessageLoop.sendMessage(this, Widget.Msg.ParentChanged);
249 }
250 }
251
252 /**
253 * Get the layout for the widget.
254 */
255 get layout(): Layout | null {
256 return this._layout;
257 }
258
259 /**
260 * Set the layout for the widget.
261 *
262 * #### Notes
263 * The layout is single-use only. It cannot be changed after the
264 * first assignment.
265 *
266 * The layout is disposed automatically when the widget is disposed.
267 */
268 set layout(value: Layout | null) {
269 if (this._layout === value) {
270 return;
271 }
272 if (this.testFlag(Widget.Flag.DisallowLayout)) {
273 throw new Error('Cannot set widget layout.');
274 }
275 if (this._layout) {
276 throw new Error('Cannot change widget layout.');
277 }
278 if (value!.parent) {
279 throw new Error('Cannot change layout parent.');
280 }
281 this._layout = value;
282 value!.parent = this;
283 }
284
285 /**
286 * Create an iterator over the widget's children.
287 *
288 * @returns A new iterator over the children of the widget.
289 *
290 * #### Notes
291 * The widget must have a populated layout in order to have children.
292 *
293 * If a layout is not installed, the returned iterator will be empty.
294 */
295 children(): IIterator<Widget> {
296 return this._layout ? this._layout.iter() : empty<Widget>();
297 }
298
299 /**
300 * Test whether a widget is a descendant of this widget.
301 *
302 * @param widget - The descendant widget of interest.
303 *
304 * @returns `true` if the widget is a descendant, `false` otherwise.
305 */
306 contains(widget: Widget): boolean {
307 for (let value: Widget | null = widget; value; value = value._parent) {
308 if (value === this) {
309 return true;
310 }
311 }
312 return false;
313 }
314
315 /**
316 * Test whether the widget's DOM node has the given class name.
317 *
318 * @param name - The class name of interest.
319 *
320 * @returns `true` if the node has the class, `false` otherwise.
321 */
322 hasClass(name: string): boolean {
323 return this.node.classList.contains(name);
324 }
325
326 /**
327 * Add a class name to the widget's DOM node.
328 *
329 * @param name - The class name to add to the node.
330 *
331 * #### Notes
332 * If the class name is already added to the node, this is a no-op.
333 *
334 * The class name must not contain whitespace.
335 */
336 addClass(name: string): void {
337 this.node.classList.add(name);
338 }
339
340 /**
341 * Remove a class name from the widget's DOM node.
342 *
343 * @param name - The class name to remove from the node.
344 *
345 * #### Notes
346 * If the class name is not yet added to the node, this is a no-op.
347 *
348 * The class name must not contain whitespace.
349 */
350 removeClass(name: string): void {
351 this.node.classList.remove(name);
352 }
353
354 /**
355 * Toggle a class name on the widget's DOM node.
356 *
357 * @param name - The class name to toggle on the node.
358 *
359 * @param force - Whether to force add the class (`true`) or force
360 * remove the class (`false`). If not provided, the presence of
361 * the class will be toggled from its current state.
362 *
363 * @returns `true` if the class is now present, `false` otherwise.
364 *
365 * #### Notes
366 * The class name must not contain whitespace.
367 */
368 toggleClass(name: string, force?: boolean): boolean {
369 if (force === true) {
370 this.node.classList.add(name);
371 return true;
372 }
373 if (force === false) {
374 this.node.classList.remove(name);
375 return false;
376 }
377 return this.node.classList.toggle(name);
378 }
379
380 /**
381 * Post an `'update-request'` message to the widget.
382 *
383 * #### Notes
384 * This is a simple convenience method for posting the message.
385 */
386 update(): void {
387 MessageLoop.postMessage(this, Widget.Msg.UpdateRequest);
388 }
389
390 /**
391 * Post a `'fit-request'` message to the widget.
392 *
393 * #### Notes
394 * This is a simple convenience method for posting the message.
395 */
396 fit(): void {
397 MessageLoop.postMessage(this, Widget.Msg.FitRequest);
398 }
399
400 /**
401 * Post an `'activate-request'` message to the widget.
402 *
403 * #### Notes
404 * This is a simple convenience method for posting the message.
405 */
406 activate(): void {
407 MessageLoop.postMessage(this, Widget.Msg.ActivateRequest);
408 }
409
410 /**
411 * Send a `'close-request'` message to the widget.
412 *
413 * #### Notes
414 * This is a simple convenience method for sending the message.
415 */
416 close(): void {
417 MessageLoop.sendMessage(this, Widget.Msg.CloseRequest);
418 }
419
420 /**
421 * Show the widget and make it visible to its parent widget.
422 *
423 * #### Notes
424 * This causes the [[isHidden]] property to be `false`.
425 *
426 * If the widget is not explicitly hidden, this is a no-op.
427 */
428 show(): void {
429 if (!this.testFlag(Widget.Flag.IsHidden)) {
430 return;
431 }
432 if (this.isAttached && (!this.parent || this.parent.isVisible)) {
433 MessageLoop.sendMessage(this, Widget.Msg.BeforeShow);
434 }
435 this.clearFlag(Widget.Flag.IsHidden);
436 this.node.removeAttribute('aria-hidden');
437 if (this.hiddenMode === Widget.HiddenMode.Display) {
438 this.removeClass('lm-mod-hidden');
439 /* <DEPRECATED> */
440 this.removeClass('p-mod-hidden');
441 /* </DEPRECATED> */
442 } else {
443 this.node.style.transform = '';
444 }
445
446 if (this.isAttached && (!this.parent || this.parent.isVisible)) {
447 MessageLoop.sendMessage(this, Widget.Msg.AfterShow);
448 }
449 if (this.parent) {
450 let msg = new Widget.ChildMessage('child-shown', this);
451 MessageLoop.sendMessage(this.parent, msg);
452 }
453 }
454
455 /**
456 * Hide the widget and make it hidden to its parent widget.
457 *
458 * #### Notes
459 * This causes the [[isHidden]] property to be `true`.
460 *
461 * If the widget is explicitly hidden, this is a no-op.
462 */
463 hide(): void {
464 if (this.testFlag(Widget.Flag.IsHidden)) {
465 return;
466 }
467 if (this.isAttached && (!this.parent || this.parent.isVisible)) {
468 MessageLoop.sendMessage(this, Widget.Msg.BeforeHide);
469 }
470 this.setFlag(Widget.Flag.IsHidden);
471 this.node.setAttribute('aria-hidden', 'true');
472 if (this.hiddenMode === Widget.HiddenMode.Display) {
473 this.addClass('lm-mod-hidden');
474 /* <DEPRECATED> */
475 this.addClass('p-mod-hidden');
476 /* </DEPRECATED> */
477 } else {
478 this.node.style.transform = 'scale(0)';
479 }
480
481 if (this.isAttached && (!this.parent || this.parent.isVisible)) {
482 MessageLoop.sendMessage(this, Widget.Msg.AfterHide);
483 }
484 if (this.parent) {
485 let msg = new Widget.ChildMessage('child-hidden', this);
486 MessageLoop.sendMessage(this.parent, msg);
487 }
488 }
489
490 /**
491 * Show or hide the widget according to a boolean value.
492 *
493 * @param hidden - `true` to hide the widget, or `false` to show it.
494 *
495 * #### Notes
496 * This is a convenience method for `hide()` and `show()`.
497 */
498 setHidden(hidden: boolean): void {
499 if (hidden) {
500 this.hide();
501 } else {
502 this.show();
503 }
504 }
505
506 /**
507 * Test whether the given widget flag is set.
508 *
509 * #### Notes
510 * This will not typically be called directly by user code.
511 */
512 testFlag(flag: Widget.Flag): boolean {
513 return (this._flags & flag) !== 0;
514 }
515
516 /**
517 * Set the given widget flag.
518 *
519 * #### Notes
520 * This will not typically be called directly by user code.
521 */
522 setFlag(flag: Widget.Flag): void {
523 this._flags |= flag;
524 }
525
526 /**
527 * Clear the given widget flag.
528 *
529 * #### Notes
530 * This will not typically be called directly by user code.
531 */
532 clearFlag(flag: Widget.Flag): void {
533 this._flags &= ~flag;
534 }
535
536 /**
537 * Process a message sent to the widget.
538 *
539 * @param msg - The message sent to the widget.
540 *
541 * #### Notes
542 * Subclasses may reimplement this method as needed.
543 */
544 processMessage(msg: Message): void {
545 switch (msg.type) {
546 case 'resize':
547 this.notifyLayout(msg);
548 this.onResize(msg as Widget.ResizeMessage);
549 break;
550 case 'update-request':
551 this.notifyLayout(msg);
552 this.onUpdateRequest(msg);
553 break;
554 case 'fit-request':
555 this.notifyLayout(msg);
556 this.onFitRequest(msg);
557 break;
558 case 'before-show':
559 this.notifyLayout(msg);
560 this.onBeforeShow(msg);
561 break;
562 case 'after-show':
563 this.setFlag(Widget.Flag.IsVisible);
564 this.notifyLayout(msg);
565 this.onAfterShow(msg);
566 break;
567 case 'before-hide':
568 this.notifyLayout(msg);
569 this.onBeforeHide(msg);
570 break;
571 case 'after-hide':
572 this.clearFlag(Widget.Flag.IsVisible);
573 this.notifyLayout(msg);
574 this.onAfterHide(msg);
575 break;
576 case 'before-attach':
577 this.notifyLayout(msg);
578 this.onBeforeAttach(msg);
579 break;
580 case 'after-attach':
581 if (!this.isHidden && (!this.parent || this.parent.isVisible)) {
582 this.setFlag(Widget.Flag.IsVisible);
583 }
584 this.setFlag(Widget.Flag.IsAttached);
585 this.notifyLayout(msg);
586 this.onAfterAttach(msg);
587 break;
588 case 'before-detach':
589 this.notifyLayout(msg);
590 this.onBeforeDetach(msg);
591 break;
592 case 'after-detach':
593 this.clearFlag(Widget.Flag.IsVisible);
594 this.clearFlag(Widget.Flag.IsAttached);
595 this.notifyLayout(msg);
596 this.onAfterDetach(msg);
597 break;
598 case 'activate-request':
599 this.notifyLayout(msg);
600 this.onActivateRequest(msg);
601 break;
602 case 'close-request':
603 this.notifyLayout(msg);
604 this.onCloseRequest(msg);
605 break;
606 case 'child-added':
607 this.notifyLayout(msg);
608 this.onChildAdded(msg as Widget.ChildMessage);
609 break;
610 case 'child-removed':
611 this.notifyLayout(msg);
612 this.onChildRemoved(msg as Widget.ChildMessage);
613 break;
614 default:
615 this.notifyLayout(msg);
616 break;
617 }
618 }
619
620 /**
621 * Invoke the message processing routine of the widget's layout.
622 *
623 * @param msg - The message to dispatch to the layout.
624 *
625 * #### Notes
626 * This is a no-op if the widget does not have a layout.
627 *
628 * This will not typically be called directly by user code.
629 */
630 protected notifyLayout(msg: Message): void {
631 if (this._layout) {
632 this._layout.processParentMessage(msg);
633 }
634 }
635
636 /**
637 * A message handler invoked on a `'close-request'` message.
638 *
639 * #### Notes
640 * The default implementation unparents or detaches the widget.
641 */
642 protected onCloseRequest(msg: Message): void {
643 if (this.parent) {
644 this.parent = null;
645 } else if (this.isAttached) {
646 Widget.detach(this);
647 }
648 }
649
650 /**
651 * A message handler invoked on a `'resize'` message.
652 *
653 * #### Notes
654 * The default implementation of this handler is a no-op.
655 */
656 protected onResize(msg: Widget.ResizeMessage): void {}
657
658 /**
659 * A message handler invoked on an `'update-request'` message.
660 *
661 * #### Notes
662 * The default implementation of this handler is a no-op.
663 */
664 protected onUpdateRequest(msg: Message): void {}
665
666 /**
667 * A message handler invoked on a `'fit-request'` message.
668 *
669 * #### Notes
670 * The default implementation of this handler is a no-op.
671 */
672 protected onFitRequest(msg: Message): void {}
673
674 /**
675 * A message handler invoked on an `'activate-request'` message.
676 *
677 * #### Notes
678 * The default implementation of this handler is a no-op.
679 */
680 protected onActivateRequest(msg: Message): void {}
681
682 /**
683 * A message handler invoked on a `'before-show'` message.
684 *
685 * #### Notes
686 * The default implementation of this handler is a no-op.
687 */
688 protected onBeforeShow(msg: Message): void {}
689
690 /**
691 * A message handler invoked on an `'after-show'` message.
692 *
693 * #### Notes
694 * The default implementation of this handler is a no-op.
695 */
696 protected onAfterShow(msg: Message): void {}
697
698 /**
699 * A message handler invoked on a `'before-hide'` message.
700 *
701 * #### Notes
702 * The default implementation of this handler is a no-op.
703 */
704 protected onBeforeHide(msg: Message): void {}
705
706 /**
707 * A message handler invoked on an `'after-hide'` message.
708 *
709 * #### Notes
710 * The default implementation of this handler is a no-op.
711 */
712 protected onAfterHide(msg: Message): void {}
713
714 /**
715 * A message handler invoked on a `'before-attach'` message.
716 *
717 * #### Notes
718 * The default implementation of this handler is a no-op.
719 */
720 protected onBeforeAttach(msg: Message): void {}
721
722 /**
723 * A message handler invoked on an `'after-attach'` message.
724 *
725 * #### Notes
726 * The default implementation of this handler is a no-op.
727 */
728 protected onAfterAttach(msg: Message): void {}
729
730 /**
731 * A message handler invoked on a `'before-detach'` message.
732 *
733 * #### Notes
734 * The default implementation of this handler is a no-op.
735 */
736 protected onBeforeDetach(msg: Message): void {}
737
738 /**
739 * A message handler invoked on an `'after-detach'` message.
740 *
741 * #### Notes
742 * The default implementation of this handler is a no-op.
743 */
744 protected onAfterDetach(msg: Message): void {}
745
746 /**
747 * A message handler invoked on a `'child-added'` message.
748 *
749 * #### Notes
750 * The default implementation of this handler is a no-op.
751 */
752 protected onChildAdded(msg: Widget.ChildMessage): void {}
753
754 /**
755 * A message handler invoked on a `'child-removed'` message.
756 *
757 * #### Notes
758 * The default implementation of this handler is a no-op.
759 */
760 protected onChildRemoved(msg: Widget.ChildMessage): void {}
761
762 private _flags = 0;
763 private _layout: Layout | null = null;
764 private _parent: Widget | null = null;
765 private _disposed = new Signal<this, void>(this);
766 private _hiddenMode: Widget.HiddenMode = Widget.HiddenMode.Display;
767}
768
769/**
770 * The namespace for the `Widget` class statics.
771 */
772export namespace Widget {
773 /**
774 * An options object for initializing a widget.
775 */
776 export interface IOptions {
777 /**
778 * The optional node to use for the widget.
779 *
780 * If a node is provided, the widget will assume full ownership
781 * and control of the node, as if it had created the node itself.
782 *
783 * The default is a new `<div>`.
784 */
785 node?: HTMLElement;
786
787 /**
788 * The optional element tag, used for constructing the widget's node.
789 *
790 * If a pre-constructed node is provided via the `node` arg, this
791 * value is ignored.
792 */
793 tag?: keyof HTMLElementTagNameMap;
794 }
795
796 /**
797 * The method for hiding the widget.
798 *
799 * The default is Display.
800 *
801 * Using `Scale` will often increase performance as most browsers will not
802 * trigger style computation for the `transform` action. This should be used
803 * sparingly and tested, since increasing the number of composition layers
804 * may slow things down.
805 *
806 * To ensure the transformation does not trigger style recomputation, you
807 * may need to set the widget CSS style `will-change: transform`. This
808 * should be used only when needed as it may overwhelm the browser with a
809 * high number of layers. See
810 * https://developer.mozilla.org/en-US/docs/Web/CSS/will-change
811 */
812 export enum HiddenMode {
813 /**
814 * Set a `lm-mod-hidden` CSS class to hide the widget using `display:none`
815 * CSS from the standard Lumino CSS.
816 */
817 Display = 0,
818
819 /**
820 * Hide the widget by setting the `transform` to `'scale(0)'`.
821 */
822 Scale
823 }
824
825 /**
826 * An enum of widget bit flags.
827 */
828 export enum Flag {
829 /**
830 * The widget has been disposed.
831 */
832 IsDisposed = 0x1,
833
834 /**
835 * The widget is attached to the DOM.
836 */
837 IsAttached = 0x2,
838
839 /**
840 * The widget is hidden.
841 */
842 IsHidden = 0x4,
843
844 /**
845 * The widget is visible.
846 */
847 IsVisible = 0x8,
848
849 /**
850 * A layout cannot be set on the widget.
851 */
852 DisallowLayout = 0x10
853 }
854
855 /**
856 * A collection of stateless messages related to widgets.
857 */
858 export namespace Msg {
859 /**
860 * A singleton `'before-show'` message.
861 *
862 * #### Notes
863 * This message is sent to a widget before it becomes visible.
864 *
865 * This message is **not** sent when the widget is being attached.
866 */
867 export const BeforeShow = new Message('before-show');
868
869 /**
870 * A singleton `'after-show'` message.
871 *
872 * #### Notes
873 * This message is sent to a widget after it becomes visible.
874 *
875 * This message is **not** sent when the widget is being attached.
876 */
877 export const AfterShow = new Message('after-show');
878
879 /**
880 * A singleton `'before-hide'` message.
881 *
882 * #### Notes
883 * This message is sent to a widget before it becomes not-visible.
884 *
885 * This message is **not** sent when the widget is being detached.
886 */
887 export const BeforeHide = new Message('before-hide');
888
889 /**
890 * A singleton `'after-hide'` message.
891 *
892 * #### Notes
893 * This message is sent to a widget after it becomes not-visible.
894 *
895 * This message is **not** sent when the widget is being detached.
896 */
897 export const AfterHide = new Message('after-hide');
898
899 /**
900 * A singleton `'before-attach'` message.
901 *
902 * #### Notes
903 * This message is sent to a widget before it is attached.
904 */
905 export const BeforeAttach = new Message('before-attach');
906
907 /**
908 * A singleton `'after-attach'` message.
909 *
910 * #### Notes
911 * This message is sent to a widget after it is attached.
912 */
913 export const AfterAttach = new Message('after-attach');
914
915 /**
916 * A singleton `'before-detach'` message.
917 *
918 * #### Notes
919 * This message is sent to a widget before it is detached.
920 */
921 export const BeforeDetach = new Message('before-detach');
922
923 /**
924 * A singleton `'after-detach'` message.
925 *
926 * #### Notes
927 * This message is sent to a widget after it is detached.
928 */
929 export const AfterDetach = new Message('after-detach');
930
931 /**
932 * A singleton `'parent-changed'` message.
933 *
934 * #### Notes
935 * This message is sent to a widget when its parent has changed.
936 */
937 export const ParentChanged = new Message('parent-changed');
938
939 /**
940 * A singleton conflatable `'update-request'` message.
941 *
942 * #### Notes
943 * This message can be dispatched to supporting widgets in order to
944 * update their content based on the current widget state. Not all
945 * widgets will respond to messages of this type.
946 *
947 * For widgets with a layout, this message will inform the layout to
948 * update the position and size of its child widgets.
949 */
950 export const UpdateRequest = new ConflatableMessage('update-request');
951
952 /**
953 * A singleton conflatable `'fit-request'` message.
954 *
955 * #### Notes
956 * For widgets with a layout, this message will inform the layout to
957 * recalculate its size constraints to fit the space requirements of
958 * its child widgets, and to update their position and size. Not all
959 * layouts will respond to messages of this type.
960 */
961 export const FitRequest = new ConflatableMessage('fit-request');
962
963 /**
964 * A singleton conflatable `'activate-request'` message.
965 *
966 * #### Notes
967 * This message should be dispatched to a widget when it should
968 * perform the actions necessary to activate the widget, which
969 * may include focusing its node or descendant node.
970 */
971 export const ActivateRequest = new ConflatableMessage('activate-request');
972
973 /**
974 * A singleton conflatable `'close-request'` message.
975 *
976 * #### Notes
977 * This message should be dispatched to a widget when it should close
978 * and remove itself from the widget hierarchy.
979 */
980 export const CloseRequest = new ConflatableMessage('close-request');
981 }
982
983 /**
984 * A message class for child related messages.
985 */
986 export class ChildMessage extends Message {
987 /**
988 * Construct a new child message.
989 *
990 * @param type - The message type.
991 *
992 * @param child - The child widget for the message.
993 */
994 constructor(type: string, child: Widget) {
995 super(type);
996 this.child = child;
997 }
998
999 /**
1000 * The child widget for the message.
1001 */
1002 readonly child: Widget;
1003 }
1004
1005 /**
1006 * A message class for `'resize'` messages.
1007 */
1008 export class ResizeMessage extends Message {
1009 /**
1010 * Construct a new resize message.
1011 *
1012 * @param width - The **offset width** of the widget, or `-1` if
1013 * the width is not known.
1014 *
1015 * @param height - The **offset height** of the widget, or `-1` if
1016 * the height is not known.
1017 */
1018 constructor(width: number, height: number) {
1019 super('resize');
1020 this.width = width;
1021 this.height = height;
1022 }
1023
1024 /**
1025 * The offset width of the widget.
1026 *
1027 * #### Notes
1028 * This will be `-1` if the width is unknown.
1029 */
1030 readonly width: number;
1031
1032 /**
1033 * The offset height of the widget.
1034 *
1035 * #### Notes
1036 * This will be `-1` if the height is unknown.
1037 */
1038 readonly height: number;
1039 }
1040
1041 /**
1042 * The namespace for the `ResizeMessage` class statics.
1043 */
1044 export namespace ResizeMessage {
1045 /**
1046 * A singleton `'resize'` message with an unknown size.
1047 */
1048 export const UnknownSize = new ResizeMessage(-1, -1);
1049 }
1050
1051 /**
1052 * Attach a widget to a host DOM node.
1053 *
1054 * @param widget - The widget of interest.
1055 *
1056 * @param host - The DOM node to use as the widget's host.
1057 *
1058 * @param ref - The child of `host` to use as the reference element.
1059 * If this is provided, the widget will be inserted before this
1060 * node in the host. The default is `null`, which will cause the
1061 * widget to be added as the last child of the host.
1062 *
1063 * #### Notes
1064 * This will throw an error if the widget is not a root widget, if
1065 * the widget is already attached, or if the host is not attached
1066 * to the DOM.
1067 */
1068 export function attach(
1069 widget: Widget,
1070 host: HTMLElement,
1071 ref: HTMLElement | null = null
1072 ): void {
1073 if (widget.parent) {
1074 throw new Error('Cannot attach a child widget.');
1075 }
1076 if (widget.isAttached || widget.node.isConnected) {
1077 throw new Error('Widget is already attached.');
1078 }
1079 if (!host.isConnected) {
1080 throw new Error('Host is not attached.');
1081 }
1082 MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
1083 host.insertBefore(widget.node, ref);
1084 MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
1085 }
1086
1087 /**
1088 * Detach the widget from its host DOM node.
1089 *
1090 * @param widget - The widget of interest.
1091 *
1092 * #### Notes
1093 * This will throw an error if the widget is not a root widget,
1094 * or if the widget is not attached to the DOM.
1095 */
1096 export function detach(widget: Widget): void {
1097 if (widget.parent) {
1098 throw new Error('Cannot detach a child widget.');
1099 }
1100 if (!widget.isAttached || !widget.node.isConnected) {
1101 throw new Error('Widget is not attached.');
1102 }
1103 MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
1104 widget.node.parentNode!.removeChild(widget.node);
1105 MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
1106 }
1107}
1108
1109/**
1110 * The namespace for the module implementation details.
1111 */
1112namespace Private {
1113 /**
1114 * An attached property for the widget title object.
1115 */
1116 export const titleProperty = new AttachedProperty<Widget, Title<Widget>>({
1117 name: 'title',
1118 create: owner => new Title<Widget>({ owner })
1119 });
1120
1121 /**
1122 * Create a DOM node for the given widget options.
1123 */
1124 export function createNode(options: Widget.IOptions): HTMLElement {
1125 return options.node || document.createElement(options.tag || 'div');
1126 }
1127}