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 | // 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 | */
|
772 | export 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 | */
|
1112 | namespace 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 | }
|