UNPKG

20.8 kBJavaScriptView Raw
1"use strict";
2var __extends = (this && this.__extends) || (function () {
3 var extendStatics = function (d, b) {
4 extendStatics = Object.setPrototypeOf ||
5 ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||
6 function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };
7 return extendStatics(d, b);
8 }
9 return function (d, b) {
10 extendStatics(d, b);
11 function __() { this.constructor = d; }
12 d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
13 };
14})();
15Object.defineProperty(exports, "__esModule", { value: true });
16/*-----------------------------------------------------------------------------
17| Copyright (c) 2014-2017, PhosphorJS Contributors
18|
19| Distributed under the terms of the BSD 3-Clause License.
20|
21| The full license is in the file LICENSE, distributed with this software.
22|----------------------------------------------------------------------------*/
23var algorithm_1 = require("@phosphor/algorithm");
24var domutils_1 = require("@phosphor/domutils");
25var messaging_1 = require("@phosphor/messaging");
26var properties_1 = require("@phosphor/properties");
27var boxengine_1 = require("./boxengine");
28var layout_1 = require("./layout");
29var panellayout_1 = require("./panellayout");
30var widget_1 = require("./widget");
31/**
32 * A layout which arranges its widgets in a single row or column.
33 */
34var BoxLayout = /** @class */ (function (_super) {
35 __extends(BoxLayout, _super);
36 /**
37 * Construct a new box layout.
38 *
39 * @param options - The options for initializing the layout.
40 */
41 function BoxLayout(options) {
42 if (options === void 0) { options = {}; }
43 var _this = _super.call(this) || this;
44 _this._fixed = 0;
45 _this._spacing = 4;
46 _this._dirty = false;
47 _this._sizers = [];
48 _this._items = [];
49 _this._box = null;
50 _this._alignment = 'start';
51 _this._direction = 'top-to-bottom';
52 if (options.direction !== undefined) {
53 _this._direction = options.direction;
54 }
55 if (options.alignment !== undefined) {
56 _this._alignment = options.alignment;
57 }
58 if (options.spacing !== undefined) {
59 _this._spacing = Private.clampSpacing(options.spacing);
60 }
61 return _this;
62 }
63 /**
64 * Dispose of the resources held by the layout.
65 */
66 BoxLayout.prototype.dispose = function () {
67 // Dispose of the layout items.
68 algorithm_1.each(this._items, function (item) { item.dispose(); });
69 // Clear the layout state.
70 this._box = null;
71 this._items.length = 0;
72 this._sizers.length = 0;
73 // Dispose of the rest of the layout.
74 _super.prototype.dispose.call(this);
75 };
76 Object.defineProperty(BoxLayout.prototype, "direction", {
77 /**
78 * Get the layout direction for the box layout.
79 */
80 get: function () {
81 return this._direction;
82 },
83 /**
84 * Set the layout direction for the box layout.
85 */
86 set: function (value) {
87 if (this._direction === value) {
88 return;
89 }
90 this._direction = value;
91 if (!this.parent) {
92 return;
93 }
94 this.parent.dataset['direction'] = value;
95 this.parent.fit();
96 },
97 enumerable: true,
98 configurable: true
99 });
100 Object.defineProperty(BoxLayout.prototype, "alignment", {
101 /**
102 * Get the content alignment for the box layout.
103 *
104 * #### Notes
105 * This is the alignment of the widgets in the layout direction.
106 *
107 * The alignment has no effect if the widgets can expand to fill the
108 * entire box layout.
109 */
110 get: function () {
111 return this._alignment;
112 },
113 /**
114 * Set the content alignment for the box layout.
115 *
116 * #### Notes
117 * This is the alignment of the widgets in the layout direction.
118 *
119 * The alignment has no effect if the widgets can expand to fill the
120 * entire box layout.
121 */
122 set: function (value) {
123 if (this._alignment === value) {
124 return;
125 }
126 this._alignment = value;
127 if (!this.parent) {
128 return;
129 }
130 this.parent.dataset['alignment'] = value;
131 this.parent.update();
132 },
133 enumerable: true,
134 configurable: true
135 });
136 Object.defineProperty(BoxLayout.prototype, "spacing", {
137 /**
138 * Get the inter-element spacing for the box layout.
139 */
140 get: function () {
141 return this._spacing;
142 },
143 /**
144 * Set the inter-element spacing for the box layout.
145 */
146 set: function (value) {
147 value = Private.clampSpacing(value);
148 if (this._spacing === value) {
149 return;
150 }
151 this._spacing = value;
152 if (!this.parent) {
153 return;
154 }
155 this.parent.fit();
156 },
157 enumerable: true,
158 configurable: true
159 });
160 /**
161 * Perform layout initialization which requires the parent widget.
162 */
163 BoxLayout.prototype.init = function () {
164 this.parent.dataset['direction'] = this.direction;
165 this.parent.dataset['alignment'] = this.alignment;
166 _super.prototype.init.call(this);
167 };
168 /**
169 * Attach a widget to the parent's DOM node.
170 *
171 * @param index - The current index of the widget in the layout.
172 *
173 * @param widget - The widget to attach to the parent.
174 *
175 * #### Notes
176 * This is a reimplementation of the superclass method.
177 */
178 BoxLayout.prototype.attachWidget = function (index, widget) {
179 // Create and add a new layout item for the widget.
180 algorithm_1.ArrayExt.insert(this._items, index, new layout_1.LayoutItem(widget));
181 // Create and add a new sizer for the widget.
182 algorithm_1.ArrayExt.insert(this._sizers, index, new boxengine_1.BoxSizer());
183 // Send a `'before-attach'` message if the parent is attached.
184 if (this.parent.isAttached) {
185 messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.BeforeAttach);
186 }
187 // Add the widget's node to the parent.
188 this.parent.node.appendChild(widget.node);
189 // Send an `'after-attach'` message if the parent is attached.
190 if (this.parent.isAttached) {
191 messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.AfterAttach);
192 }
193 // Post a fit request for the parent widget.
194 this.parent.fit();
195 };
196 /**
197 * Move a widget in the parent's DOM node.
198 *
199 * @param fromIndex - The previous index of the widget in the layout.
200 *
201 * @param toIndex - The current index of the widget in the layout.
202 *
203 * @param widget - The widget to move in the parent.
204 *
205 * #### Notes
206 * This is a reimplementation of the superclass method.
207 */
208 BoxLayout.prototype.moveWidget = function (fromIndex, toIndex, widget) {
209 // Move the layout item for the widget.
210 algorithm_1.ArrayExt.move(this._items, fromIndex, toIndex);
211 // Move the sizer for the widget.
212 algorithm_1.ArrayExt.move(this._sizers, fromIndex, toIndex);
213 // Post an update request for the parent widget.
214 this.parent.update();
215 };
216 /**
217 * Detach a widget from the parent's DOM node.
218 *
219 * @param index - The previous index of the widget in the layout.
220 *
221 * @param widget - The widget to detach from the parent.
222 *
223 * #### Notes
224 * This is a reimplementation of the superclass method.
225 */
226 BoxLayout.prototype.detachWidget = function (index, widget) {
227 // Remove the layout item for the widget.
228 var item = algorithm_1.ArrayExt.removeAt(this._items, index);
229 // Remove the sizer for the widget.
230 algorithm_1.ArrayExt.removeAt(this._sizers, index);
231 // Send a `'before-detach'` message if the parent is attached.
232 if (this.parent.isAttached) {
233 messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.BeforeDetach);
234 }
235 // Remove the widget's node from the parent.
236 this.parent.node.removeChild(widget.node);
237 // Send an `'after-detach'` message if the parent is attached.
238 if (this.parent.isAttached) {
239 messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.AfterDetach);
240 }
241 // Dispose of the layout item.
242 item.dispose();
243 // Post a fit request for the parent widget.
244 this.parent.fit();
245 };
246 /**
247 * A message handler invoked on a `'before-show'` message.
248 */
249 BoxLayout.prototype.onBeforeShow = function (msg) {
250 _super.prototype.onBeforeShow.call(this, msg);
251 this.parent.update();
252 };
253 /**
254 * A message handler invoked on a `'before-attach'` message.
255 */
256 BoxLayout.prototype.onBeforeAttach = function (msg) {
257 _super.prototype.onBeforeAttach.call(this, msg);
258 this.parent.fit();
259 };
260 /**
261 * A message handler invoked on a `'child-shown'` message.
262 */
263 BoxLayout.prototype.onChildShown = function (msg) {
264 this.parent.fit();
265 };
266 /**
267 * A message handler invoked on a `'child-hidden'` message.
268 */
269 BoxLayout.prototype.onChildHidden = function (msg) {
270 this.parent.fit();
271 };
272 /**
273 * A message handler invoked on a `'resize'` message.
274 */
275 BoxLayout.prototype.onResize = function (msg) {
276 if (this.parent.isVisible) {
277 this._update(msg.width, msg.height);
278 }
279 };
280 /**
281 * A message handler invoked on an `'update-request'` message.
282 */
283 BoxLayout.prototype.onUpdateRequest = function (msg) {
284 if (this.parent.isVisible) {
285 this._update(-1, -1);
286 }
287 };
288 /**
289 * A message handler invoked on a `'fit-request'` message.
290 */
291 BoxLayout.prototype.onFitRequest = function (msg) {
292 if (this.parent.isAttached) {
293 this._fit();
294 }
295 };
296 /**
297 * Fit the layout to the total size required by the widgets.
298 */
299 BoxLayout.prototype._fit = function () {
300 // Compute the visible item count.
301 var nVisible = 0;
302 for (var i = 0, n = this._items.length; i < n; ++i) {
303 nVisible += +!this._items[i].isHidden;
304 }
305 // Update the fixed space for the visible items.
306 this._fixed = this._spacing * Math.max(0, nVisible - 1);
307 // Setup the computed minimum size.
308 var horz = Private.isHorizontal(this._direction);
309 var minW = horz ? this._fixed : 0;
310 var minH = horz ? 0 : this._fixed;
311 // Update the sizers and computed minimum size.
312 for (var i = 0, n = this._items.length; i < n; ++i) {
313 // Fetch the item and corresponding box sizer.
314 var item = this._items[i];
315 var sizer = this._sizers[i];
316 // If the item is hidden, it should consume zero size.
317 if (item.isHidden) {
318 sizer.minSize = 0;
319 sizer.maxSize = 0;
320 continue;
321 }
322 // Update the size limits for the item.
323 item.fit();
324 // Update the size basis and stretch factor.
325 sizer.sizeHint = BoxLayout.getSizeBasis(item.widget);
326 sizer.stretch = BoxLayout.getStretch(item.widget);
327 // Update the sizer limits and computed min size.
328 if (horz) {
329 sizer.minSize = item.minWidth;
330 sizer.maxSize = item.maxWidth;
331 minW += item.minWidth;
332 minH = Math.max(minH, item.minHeight);
333 }
334 else {
335 sizer.minSize = item.minHeight;
336 sizer.maxSize = item.maxHeight;
337 minH += item.minHeight;
338 minW = Math.max(minW, item.minWidth);
339 }
340 }
341 // Update the box sizing and add it to the computed min size.
342 var box = this._box = domutils_1.ElementExt.boxSizing(this.parent.node);
343 minW += box.horizontalSum;
344 minH += box.verticalSum;
345 // Update the parent's min size constraints.
346 var style = this.parent.node.style;
347 style.minWidth = minW + "px";
348 style.minHeight = minH + "px";
349 // Set the dirty flag to ensure only a single update occurs.
350 this._dirty = true;
351 // Notify the ancestor that it should fit immediately. This may
352 // cause a resize of the parent, fulfilling the required update.
353 if (this.parent.parent) {
354 messaging_1.MessageLoop.sendMessage(this.parent.parent, widget_1.Widget.Msg.FitRequest);
355 }
356 // If the dirty flag is still set, the parent was not resized.
357 // Trigger the required update on the parent widget immediately.
358 if (this._dirty) {
359 messaging_1.MessageLoop.sendMessage(this.parent, widget_1.Widget.Msg.UpdateRequest);
360 }
361 };
362 /**
363 * Update the layout position and size of the widgets.
364 *
365 * The parent offset dimensions should be `-1` if unknown.
366 */
367 BoxLayout.prototype._update = function (offsetWidth, offsetHeight) {
368 // Clear the dirty flag to indicate the update occurred.
369 this._dirty = false;
370 // Compute the visible item count.
371 var nVisible = 0;
372 for (var i = 0, n = this._items.length; i < n; ++i) {
373 nVisible += +!this._items[i].isHidden;
374 }
375 // Bail early if there are no visible items to layout.
376 if (nVisible === 0) {
377 return;
378 }
379 // Measure the parent if the offset dimensions are unknown.
380 if (offsetWidth < 0) {
381 offsetWidth = this.parent.node.offsetWidth;
382 }
383 if (offsetHeight < 0) {
384 offsetHeight = this.parent.node.offsetHeight;
385 }
386 // Ensure the parent box sizing data is computed.
387 if (!this._box) {
388 this._box = domutils_1.ElementExt.boxSizing(this.parent.node);
389 }
390 // Compute the layout area adjusted for border and padding.
391 var top = this._box.paddingTop;
392 var left = this._box.paddingLeft;
393 var width = offsetWidth - this._box.horizontalSum;
394 var height = offsetHeight - this._box.verticalSum;
395 // Distribute the layout space and adjust the start position.
396 var delta;
397 switch (this._direction) {
398 case 'left-to-right':
399 delta = boxengine_1.BoxEngine.calc(this._sizers, Math.max(0, width - this._fixed));
400 break;
401 case 'top-to-bottom':
402 delta = boxengine_1.BoxEngine.calc(this._sizers, Math.max(0, height - this._fixed));
403 break;
404 case 'right-to-left':
405 delta = boxengine_1.BoxEngine.calc(this._sizers, Math.max(0, width - this._fixed));
406 left += width;
407 break;
408 case 'bottom-to-top':
409 delta = boxengine_1.BoxEngine.calc(this._sizers, Math.max(0, height - this._fixed));
410 top += height;
411 break;
412 default:
413 throw 'unreachable';
414 }
415 // Setup the variables for justification and alignment offset.
416 var extra = 0;
417 var offset = 0;
418 // Account for alignment if there is extra layout space.
419 if (delta > 0) {
420 switch (this._alignment) {
421 case 'start':
422 break;
423 case 'center':
424 extra = 0;
425 offset = delta / 2;
426 break;
427 case 'end':
428 extra = 0;
429 offset = delta;
430 break;
431 case 'justify':
432 extra = delta / nVisible;
433 offset = 0;
434 break;
435 default:
436 throw 'unreachable';
437 }
438 }
439 // Layout the items using the computed box sizes.
440 for (var i = 0, n = this._items.length; i < n; ++i) {
441 // Fetch the item.
442 var item = this._items[i];
443 // Ignore hidden items.
444 if (item.isHidden) {
445 continue;
446 }
447 // Fetch the computed size for the widget.
448 var size = this._sizers[i].size;
449 // Update the widget geometry and advance the relevant edge.
450 switch (this._direction) {
451 case 'left-to-right':
452 item.update(left + offset, top, size + extra, height);
453 left += size + extra + this._spacing;
454 break;
455 case 'top-to-bottom':
456 item.update(left, top + offset, width, size + extra);
457 top += size + extra + this._spacing;
458 break;
459 case 'right-to-left':
460 item.update(left - offset - size - extra, top, size + extra, height);
461 left -= size + extra + this._spacing;
462 break;
463 case 'bottom-to-top':
464 item.update(left, top - offset - size - extra, width, size + extra);
465 top -= size + extra + this._spacing;
466 break;
467 default:
468 throw 'unreachable';
469 }
470 }
471 };
472 return BoxLayout;
473}(panellayout_1.PanelLayout));
474exports.BoxLayout = BoxLayout;
475/**
476 * The namespace for the `BoxLayout` class statics.
477 */
478(function (BoxLayout) {
479 /**
480 * Get the box layout stretch factor for the given widget.
481 *
482 * @param widget - The widget of interest.
483 *
484 * @returns The box layout stretch factor for the widget.
485 */
486 function getStretch(widget) {
487 return Private.stretchProperty.get(widget);
488 }
489 BoxLayout.getStretch = getStretch;
490 /**
491 * Set the box layout stretch factor for the given widget.
492 *
493 * @param widget - The widget of interest.
494 *
495 * @param value - The value for the stretch factor.
496 */
497 function setStretch(widget, value) {
498 Private.stretchProperty.set(widget, value);
499 }
500 BoxLayout.setStretch = setStretch;
501 /**
502 * Get the box layout size basis for the given widget.
503 *
504 * @param widget - The widget of interest.
505 *
506 * @returns The box layout size basis for the widget.
507 */
508 function getSizeBasis(widget) {
509 return Private.sizeBasisProperty.get(widget);
510 }
511 BoxLayout.getSizeBasis = getSizeBasis;
512 /**
513 * Set the box layout size basis for the given widget.
514 *
515 * @param widget - The widget of interest.
516 *
517 * @param value - The value for the size basis.
518 */
519 function setSizeBasis(widget, value) {
520 Private.sizeBasisProperty.set(widget, value);
521 }
522 BoxLayout.setSizeBasis = setSizeBasis;
523})(BoxLayout = exports.BoxLayout || (exports.BoxLayout = {}));
524exports.BoxLayout = BoxLayout;
525/**
526 * The namespace for the module implementation details.
527 */
528var Private;
529(function (Private) {
530 /**
531 * The property descriptor for a widget stretch factor.
532 */
533 Private.stretchProperty = new properties_1.AttachedProperty({
534 name: 'stretch',
535 create: function () { return 0; },
536 coerce: function (owner, value) { return Math.max(0, Math.floor(value)); },
537 changed: onChildSizingChanged
538 });
539 /**
540 * The property descriptor for a widget size basis.
541 */
542 Private.sizeBasisProperty = new properties_1.AttachedProperty({
543 name: 'sizeBasis',
544 create: function () { return 0; },
545 coerce: function (owner, value) { return Math.max(0, Math.floor(value)); },
546 changed: onChildSizingChanged
547 });
548 /**
549 * Test whether a direction has horizontal orientation.
550 */
551 function isHorizontal(dir) {
552 return dir === 'left-to-right' || dir === 'right-to-left';
553 }
554 Private.isHorizontal = isHorizontal;
555 /**
556 * Clamp a spacing value to an integer >= 0.
557 */
558 function clampSpacing(value) {
559 return Math.max(0, Math.floor(value));
560 }
561 Private.clampSpacing = clampSpacing;
562 /**
563 * The change handler for the attached sizing properties.
564 */
565 function onChildSizingChanged(child) {
566 if (child.parent && child.parent.layout instanceof BoxLayout) {
567 child.parent.fit();
568 }
569 }
570})(Private || (Private = {}));