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