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 | |----------------------------------------------------------------------------*/
|
11 | import { empty, IIterator } from '@lumino/algorithm';
|
12 |
|
13 | import { IObservableDisposable } from '@lumino/disposable';
|
14 |
|
15 | import {
|
16 | ConflatableMessage,
|
17 | IMessageHandler,
|
18 | Message,
|
19 | MessageLoop
|
20 | } from '@lumino/messaging';
|
21 |
|
22 | import { AttachedProperty } from '@lumino/properties';
|
23 |
|
24 | import { ISignal, Signal } from '@lumino/signaling';
|
25 |
|
26 | import { Layout } from './layout';
|
27 |
|
28 | import { 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 | */
|
38 | export 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 | */
|
769 | export 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 | */
|
1109 | namespace 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 | }
|