UNPKG

10.7 kBPlain TextView Raw
1// Copyright (c) Jupyter Development Team.
2// Distributed under the terms of the Modified BSD License.
3/*-----------------------------------------------------------------------------
4| Copyright (c) 2014-2017, PhosphorJS Contributors
5|
6| Distributed under the terms of the BSD 3-Clause License.
7|
8| The full license is in the file LICENSE, distributed with this software.
9|----------------------------------------------------------------------------*/
10import { ArrayExt, each } from '@lumino/algorithm';
11
12import { ElementExt } from '@lumino/domutils';
13
14import { Message, MessageLoop } from '@lumino/messaging';
15
16import { Layout, LayoutItem } from './layout';
17
18import { PanelLayout } from './panellayout';
19
20import { Widget } from './widget';
21
22/**
23 * A layout where visible widgets are stacked atop one another.
24 *
25 * #### Notes
26 * The Z-order of the visible widgets follows their layout order.
27 */
28export class StackedLayout extends PanelLayout {
29 constructor(options: StackedLayout.IOptions = {}) {
30 super(options);
31 this._hiddenMode =
32 options.hiddenMode !== undefined
33 ? options.hiddenMode
34 : Widget.HiddenMode.Display;
35 }
36
37 /**
38 * The method for hiding widgets.
39 *
40 * #### Notes
41 * If there is only one child widget, `Display` hiding mode will be used
42 * regardless of this setting.
43 */
44 get hiddenMode(): Widget.HiddenMode {
45 return this._hiddenMode;
46 }
47
48 /**
49 * Set the method for hiding widgets.
50 *
51 * #### Notes
52 * If there is only one child widget, `Display` hiding mode will be used
53 * regardless of this setting.
54 */
55 set hiddenMode(v: Widget.HiddenMode) {
56 if (this._hiddenMode === v) {
57 return;
58 }
59 this._hiddenMode = v;
60 if (this.widgets.length > 1) {
61 this.widgets.forEach(w => {
62 w.hiddenMode = this._hiddenMode;
63 });
64 }
65 }
66
67 /**
68 * Dispose of the resources held by the layout.
69 */
70 dispose(): void {
71 // Dispose of the layout items.
72 each(this._items, item => {
73 item.dispose();
74 });
75
76 // Clear the layout state.
77 this._box = null;
78 this._items.length = 0;
79
80 // Dispose of the rest of the layout.
81 super.dispose();
82 }
83
84 /**
85 * Attach a widget to the parent's DOM node.
86 *
87 * @param index - The current index of the widget in the layout.
88 *
89 * @param widget - The widget to attach to the parent.
90 *
91 * #### Notes
92 * This is a reimplementation of the superclass method.
93 */
94 protected attachWidget(index: number, widget: Widget): void {
95 // Using transform create an additional layer in the pixel pipeline
96 // to limit the number of layer, it is set only if there is more than one widget.
97 if (
98 this._hiddenMode === Widget.HiddenMode.Scale &&
99 this._items.length > 0
100 ) {
101 if (this._items.length === 1) {
102 this.widgets[0].hiddenMode = Widget.HiddenMode.Scale;
103 }
104 widget.hiddenMode = Widget.HiddenMode.Scale;
105 } else {
106 widget.hiddenMode = Widget.HiddenMode.Display;
107 }
108
109 // Create and add a new layout item for the widget.
110 ArrayExt.insert(this._items, index, new LayoutItem(widget));
111
112 // Send a `'before-attach'` message if the parent is attached.
113 if (this.parent!.isAttached) {
114 MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
115 }
116
117 // Add the widget's node to the parent.
118 this.parent!.node.appendChild(widget.node);
119
120 // Send an `'after-attach'` message if the parent is attached.
121 if (this.parent!.isAttached) {
122 MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
123 }
124
125 // Post a fit request for the parent widget.
126 this.parent!.fit();
127 }
128
129 /**
130 * Move a widget in the parent's DOM node.
131 *
132 * @param fromIndex - The previous index of the widget in the layout.
133 *
134 * @param toIndex - The current index of the widget in the layout.
135 *
136 * @param widget - The widget to move in the parent.
137 *
138 * #### Notes
139 * This is a reimplementation of the superclass method.
140 */
141 protected moveWidget(
142 fromIndex: number,
143 toIndex: number,
144 widget: Widget
145 ): void {
146 // Move the layout item for the widget.
147 ArrayExt.move(this._items, fromIndex, toIndex);
148
149 // Post an update request for the parent widget.
150 this.parent!.update();
151 }
152
153 /**
154 * Detach a widget from the parent's DOM node.
155 *
156 * @param index - The previous index of the widget in the layout.
157 *
158 * @param widget - The widget to detach from the parent.
159 *
160 * #### Notes
161 * This is a reimplementation of the superclass method.
162 */
163 protected detachWidget(index: number, widget: Widget): void {
164 // Remove the layout item for the widget.
165 let item = ArrayExt.removeAt(this._items, index);
166
167 // Send a `'before-detach'` message if the parent is attached.
168 if (this.parent!.isAttached) {
169 MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
170 }
171
172 // Remove the widget's node from the parent.
173 this.parent!.node.removeChild(widget.node);
174
175 // Send an `'after-detach'` message if the parent is attached.
176 if (this.parent!.isAttached) {
177 MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
178 }
179
180 // Reset the z-index for the widget.
181 item!.widget.node.style.zIndex = '';
182
183 // Reset the hidden mode for the widget.
184 if (this._hiddenMode === Widget.HiddenMode.Scale) {
185 widget.hiddenMode = Widget.HiddenMode.Display;
186
187 // Reset the hidden mode for the first widget if necessary.
188 if (this._items.length === 1) {
189 this._items[0].widget.hiddenMode = Widget.HiddenMode.Display;
190 }
191 }
192
193 // Dispose of the layout item.
194 item!.dispose();
195
196 // Post a fit request for the parent widget.
197 this.parent!.fit();
198 }
199
200 /**
201 * A message handler invoked on a `'before-show'` message.
202 */
203 protected onBeforeShow(msg: Message): void {
204 super.onBeforeShow(msg);
205 this.parent!.update();
206 }
207
208 /**
209 * A message handler invoked on a `'before-attach'` message.
210 */
211 protected onBeforeAttach(msg: Message): void {
212 super.onBeforeAttach(msg);
213 this.parent!.fit();
214 }
215
216 /**
217 * A message handler invoked on a `'child-shown'` message.
218 */
219 protected onChildShown(msg: Widget.ChildMessage): void {
220 this.parent!.fit();
221 }
222
223 /**
224 * A message handler invoked on a `'child-hidden'` message.
225 */
226 protected onChildHidden(msg: Widget.ChildMessage): void {
227 this.parent!.fit();
228 }
229
230 /**
231 * A message handler invoked on a `'resize'` message.
232 */
233 protected onResize(msg: Widget.ResizeMessage): void {
234 if (this.parent!.isVisible) {
235 this._update(msg.width, msg.height);
236 }
237 }
238
239 /**
240 * A message handler invoked on an `'update-request'` message.
241 */
242 protected onUpdateRequest(msg: Message): void {
243 if (this.parent!.isVisible) {
244 this._update(-1, -1);
245 }
246 }
247
248 /**
249 * A message handler invoked on a `'fit-request'` message.
250 */
251 protected onFitRequest(msg: Message): void {
252 if (this.parent!.isAttached) {
253 this._fit();
254 }
255 }
256
257 /**
258 * Fit the layout to the total size required by the widgets.
259 */
260 private _fit(): void {
261 // Set up the computed minimum size.
262 let minW = 0;
263 let minH = 0;
264
265 // Update the computed minimum size.
266 for (let i = 0, n = this._items.length; i < n; ++i) {
267 // Fetch the item.
268 let item = this._items[i];
269
270 // Ignore hidden items.
271 if (item.isHidden) {
272 continue;
273 }
274
275 // Update the size limits for the item.
276 item.fit();
277
278 // Update the computed minimum size.
279 minW = Math.max(minW, item.minWidth);
280 minH = Math.max(minH, item.minHeight);
281 }
282
283 // Update the box sizing and add it to the computed min size.
284 let box = (this._box = ElementExt.boxSizing(this.parent!.node));
285 minW += box.horizontalSum;
286 minH += box.verticalSum;
287
288 // Update the parent's min size constraints.
289 let style = this.parent!.node.style;
290 style.minWidth = `${minW}px`;
291 style.minHeight = `${minH}px`;
292
293 // Set the dirty flag to ensure only a single update occurs.
294 this._dirty = true;
295
296 // Notify the ancestor that it should fit immediately. This may
297 // cause a resize of the parent, fulfilling the required update.
298 if (this.parent!.parent) {
299 MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
300 }
301
302 // If the dirty flag is still set, the parent was not resized.
303 // Trigger the required update on the parent widget immediately.
304 if (this._dirty) {
305 MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
306 }
307 }
308
309 /**
310 * Update the layout position and size of the widgets.
311 *
312 * The parent offset dimensions should be `-1` if unknown.
313 */
314 private _update(offsetWidth: number, offsetHeight: number): void {
315 // Clear the dirty flag to indicate the update occurred.
316 this._dirty = false;
317
318 // Compute the visible item count.
319 let nVisible = 0;
320 for (let i = 0, n = this._items.length; i < n; ++i) {
321 nVisible += +!this._items[i].isHidden;
322 }
323
324 // Bail early if there are no visible items to layout.
325 if (nVisible === 0) {
326 return;
327 }
328
329 // Measure the parent if the offset dimensions are unknown.
330 if (offsetWidth < 0) {
331 offsetWidth = this.parent!.node.offsetWidth;
332 }
333 if (offsetHeight < 0) {
334 offsetHeight = this.parent!.node.offsetHeight;
335 }
336
337 // Ensure the parent box sizing data is computed.
338 if (!this._box) {
339 this._box = ElementExt.boxSizing(this.parent!.node);
340 }
341
342 // Compute the actual layout bounds adjusted for border and padding.
343 let top = this._box.paddingTop;
344 let left = this._box.paddingLeft;
345 let width = offsetWidth - this._box.horizontalSum;
346 let height = offsetHeight - this._box.verticalSum;
347
348 // Update the widget stacking order and layout geometry.
349 for (let i = 0, n = this._items.length; i < n; ++i) {
350 // Fetch the item.
351 let item = this._items[i];
352
353 // Ignore hidden items.
354 if (item.isHidden) {
355 continue;
356 }
357
358 // Set the z-index for the widget.
359 item.widget.node.style.zIndex = `${i}`;
360
361 // Update the item geometry.
362 item.update(left, top, width, height);
363 }
364 }
365
366 private _dirty = false;
367 private _items: LayoutItem[] = [];
368 private _box: ElementExt.IBoxSizing | null = null;
369 private _hiddenMode: Widget.HiddenMode;
370}
371
372/**
373 * The namespace for the `StackedLayout` class statics.
374 */
375export namespace StackedLayout {
376 /**
377 * An options object for initializing a stacked layout.
378 */
379 export interface IOptions extends Layout.IOptions {
380 /**
381 * The method for hiding widgets.
382 *
383 * The default is `Widget.HiddenMode.Display`.
384 */
385 hiddenMode?: Widget.HiddenMode;
386 }
387}