UNPKG

4.82 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3
4import { Message } from '@lumino/messaging';
5import { ISignal, Signal } from '@lumino/signaling';
6import { Panel, PanelLayout, Title, Widget } from '@lumino/widgets';
7
8import { caretDownIcon } from '../icon';
9
10const COLLAPSE_CLASS = 'jp-Collapse';
11
12const CONTENTS_CLASS = 'jp-Collapse-contents';
13
14const HEADER_CLASS = 'jp-Collapse-header';
15
16const HEADER_COLLAPSED_CLASS = 'jp-Collapse-header-collapsed';
17
18const ICON_CLASS = 'jp-Collapser-icon';
19
20const TITLE_CLASS = 'jp-Collapser-title';
21
22/**
23 * A panel that supports a collapsible header made from the widget's title.
24 * Clicking on the title expands or contracts the widget.
25 */
26export class Collapser<T extends Widget = Widget> extends Widget {
27 constructor(options: Collapser.IOptions<T>) {
28 super(options);
29 const { widget, collapsed = true } = options;
30
31 this.addClass(COLLAPSE_CLASS);
32 this._header = new Widget();
33 this._header.addClass(HEADER_CLASS);
34 if (collapsed) {
35 this._header.addClass(HEADER_COLLAPSED_CLASS);
36 }
37 this._header.node.appendChild(
38 caretDownIcon.element({
39 className: ICON_CLASS
40 })
41 );
42 const titleSpan = document.createElement('span');
43 titleSpan.classList.add(TITLE_CLASS);
44 titleSpan.textContent = widget.title.label;
45 this._header.node.appendChild(titleSpan);
46
47 this._content = new Panel();
48 this._content.addClass(CONTENTS_CLASS);
49
50 const layout = new PanelLayout();
51 this.layout = layout;
52 layout.addWidget(this._header);
53 layout.addWidget(this._content);
54
55 this.widget = widget;
56 this.collapsed = collapsed;
57 }
58
59 /**
60 * The widget inside the collapse panel.
61 */
62 get widget(): T {
63 return this._widget;
64 }
65 set widget(widget: T) {
66 const oldWidget = this._widget;
67 if (oldWidget) {
68 oldWidget.title.changed.disconnect(this._onTitleChanged, this);
69 oldWidget.parent = null;
70 }
71 this._widget = widget;
72 widget.title.changed.connect(this._onTitleChanged, this);
73 this._onTitleChanged(widget.title);
74 this._content.addWidget(widget);
75 }
76
77 /**
78 * The collapsed state of the panel.
79 */
80 get collapsed(): boolean {
81 return this._collapsed;
82 }
83 set collapsed(value: boolean) {
84 if (value === this._collapsed) {
85 return;
86 }
87 if (value) {
88 this._collapse();
89 } else {
90 this._uncollapse();
91 }
92 }
93
94 /**
95 * A signal for when the widget collapse state changes.
96 */
97 get collapseChanged(): ISignal<Collapser, void> {
98 return this._collapseChanged;
99 }
100
101 /**
102 * Toggle the collapse state of the panel.
103 */
104 toggle(): void {
105 this.collapsed = !this.collapsed;
106 }
107
108 /**
109 * Dispose the widget.
110 */
111 dispose(): void {
112 if (this.isDisposed) {
113 return;
114 }
115
116 // Delete references we explicitly hold to other widgets.
117 this._header = null!;
118 this._widget = null!;
119 this._content = null!;
120
121 super.dispose();
122 }
123
124 /**
125 * Handle the DOM events for the Collapser widget.
126 *
127 * @param event - The DOM event sent to the panel.
128 *
129 * #### Notes
130 * This method implements the DOM `EventListener` interface and is
131 * called in response to events on the panel's DOM node. It should
132 * not be called directly by user code.
133 */
134 handleEvent(event: Event): void {
135 switch (event.type) {
136 case 'click':
137 this._evtClick(event as MouseEvent);
138 break;
139 default:
140 break;
141 }
142 }
143
144 protected onAfterAttach(msg: Message): void {
145 this._header.node.addEventListener('click', this);
146 }
147
148 protected onBeforeDetach(msg: Message): void {
149 this._header.node.removeEventListener('click', this);
150 }
151
152 private _collapse() {
153 this._collapsed = true;
154 if (this._content) {
155 this._content.hide();
156 }
157 this._setHeader();
158 this._collapseChanged.emit(void 0);
159 }
160
161 private _uncollapse() {
162 this._collapsed = false;
163 if (this._content) {
164 this._content.show();
165 }
166 this._setHeader();
167 this._collapseChanged.emit(void 0);
168 }
169
170 private _evtClick(event: MouseEvent) {
171 this.toggle();
172 }
173
174 /**
175 * Handle the `changed` signal of a title object.
176 */
177 private _onTitleChanged(sender: Title<Widget>): void {
178 this._setHeader();
179 }
180
181 private _setHeader(): void {
182 if (this._collapsed) {
183 this._header.addClass(HEADER_COLLAPSED_CLASS);
184 } else {
185 this._header.removeClass(HEADER_COLLAPSED_CLASS);
186 }
187 }
188
189 private _collapseChanged = new Signal<this, void>(this);
190 private _collapsed: boolean;
191 private _content: Panel;
192 private _header: Widget;
193 private _widget: T;
194}
195
196export namespace Collapser {
197 export interface IOptions<T extends Widget = Widget> extends Widget.IOptions {
198 widget: T;
199 collapsed?: boolean;
200 }
201}