UNPKG

25.3 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 into resizable sections.
33 */
34var SplitLayout = /** @class */ (function (_super) {
35 __extends(SplitLayout, _super);
36 /**
37 * Construct a new split layout.
38 *
39 * @param options - The options for initializing the layout.
40 */
41 function SplitLayout(options) {
42 var _this = _super.call(this) || this;
43 _this._fixed = 0;
44 _this._spacing = 4;
45 _this._dirty = false;
46 _this._hasNormedSizes = false;
47 _this._sizers = [];
48 _this._items = [];
49 _this._handles = [];
50 _this._box = null;
51 _this._alignment = 'start';
52 _this._orientation = 'horizontal';
53 _this.renderer = options.renderer;
54 if (options.orientation !== undefined) {
55 _this._orientation = options.orientation;
56 }
57 if (options.alignment !== undefined) {
58 _this._alignment = options.alignment;
59 }
60 if (options.spacing !== undefined) {
61 _this._spacing = Private.clampSpacing(options.spacing);
62 }
63 return _this;
64 }
65 /**
66 * Dispose of the resources held by the layout.
67 */
68 SplitLayout.prototype.dispose = function () {
69 // Dispose of the layout items.
70 algorithm_1.each(this._items, function (item) { item.dispose(); });
71 // Clear the layout state.
72 this._box = null;
73 this._items.length = 0;
74 this._sizers.length = 0;
75 this._handles.length = 0;
76 // Dispose of the rest of the layout.
77 _super.prototype.dispose.call(this);
78 };
79 Object.defineProperty(SplitLayout.prototype, "orientation", {
80 /**
81 * Get the layout orientation for the split layout.
82 */
83 get: function () {
84 return this._orientation;
85 },
86 /**
87 * Set the layout orientation for the split layout.
88 */
89 set: function (value) {
90 if (this._orientation === value) {
91 return;
92 }
93 this._orientation = value;
94 if (!this.parent) {
95 return;
96 }
97 this.parent.dataset['orientation'] = value;
98 this.parent.fit();
99 },
100 enumerable: true,
101 configurable: true
102 });
103 Object.defineProperty(SplitLayout.prototype, "alignment", {
104 /**
105 * Get the content alignment for the split layout.
106 *
107 * #### Notes
108 * This is the alignment of the widgets in the layout direction.
109 *
110 * The alignment has no effect if the widgets can expand to fill the
111 * entire split layout.
112 */
113 get: function () {
114 return this._alignment;
115 },
116 /**
117 * Set the content alignment for the split layout.
118 *
119 * #### Notes
120 * This is the alignment of the widgets in the layout direction.
121 *
122 * The alignment has no effect if the widgets can expand to fill the
123 * entire split layout.
124 */
125 set: function (value) {
126 if (this._alignment === value) {
127 return;
128 }
129 this._alignment = value;
130 if (!this.parent) {
131 return;
132 }
133 this.parent.dataset['alignment'] = value;
134 this.parent.update();
135 },
136 enumerable: true,
137 configurable: true
138 });
139 Object.defineProperty(SplitLayout.prototype, "spacing", {
140 /**
141 * Get the inter-element spacing for the split layout.
142 */
143 get: function () {
144 return this._spacing;
145 },
146 /**
147 * Set the inter-element spacing for the split layout.
148 */
149 set: function (value) {
150 value = Private.clampSpacing(value);
151 if (this._spacing === value) {
152 return;
153 }
154 this._spacing = value;
155 if (!this.parent) {
156 return;
157 }
158 this.parent.fit();
159 },
160 enumerable: true,
161 configurable: true
162 });
163 Object.defineProperty(SplitLayout.prototype, "handles", {
164 /**
165 * A read-only array of the split handles in the layout.
166 */
167 get: function () {
168 return this._handles;
169 },
170 enumerable: true,
171 configurable: true
172 });
173 /**
174 * Get the relative sizes of the widgets in the layout.
175 *
176 * @returns A new array of the relative sizes of the widgets.
177 *
178 * #### Notes
179 * The returned sizes reflect the sizes of the widgets normalized
180 * relative to their siblings.
181 *
182 * This method **does not** measure the DOM nodes.
183 */
184 SplitLayout.prototype.relativeSizes = function () {
185 return Private.normalize(this._sizers.map(function (sizer) { return sizer.size; }));
186 };
187 /**
188 * Set the relative sizes for the widgets in the layout.
189 *
190 * @param sizes - The relative sizes for the widgets in the panel.
191 *
192 * #### Notes
193 * Extra values are ignored, too few will yield an undefined layout.
194 *
195 * The actual geometry of the DOM nodes is updated asynchronously.
196 */
197 SplitLayout.prototype.setRelativeSizes = function (sizes) {
198 // Copy the sizes and pad with zeros as needed.
199 var n = this._sizers.length;
200 var temp = sizes.slice(0, n);
201 while (temp.length < n) {
202 temp.push(0);
203 }
204 // Normalize the padded sizes.
205 var normed = Private.normalize(temp);
206 // Apply the normalized sizes to the sizers.
207 for (var i = 0; i < n; ++i) {
208 var sizer = this._sizers[i];
209 sizer.sizeHint = normed[i];
210 sizer.size = normed[i];
211 }
212 // Set the flag indicating the sizes are normalized.
213 this._hasNormedSizes = true;
214 // Trigger an update of the parent widget.
215 if (this.parent) {
216 this.parent.update();
217 }
218 };
219 /**
220 * Move the offset position of a split handle.
221 *
222 * @param index - The index of the handle of the interest.
223 *
224 * @param position - The desired offset position of the handle.
225 *
226 * #### Notes
227 * The position is relative to the offset parent.
228 *
229 * This will move the handle as close as possible to the desired
230 * position. The sibling widgets will be adjusted as necessary.
231 */
232 SplitLayout.prototype.moveHandle = function (index, position) {
233 // Bail if the index is invalid or the handle is hidden.
234 var handle = this._handles[index];
235 if (!handle || handle.classList.contains('p-mod-hidden')) {
236 return;
237 }
238 // Compute the desired delta movement for the handle.
239 var delta;
240 if (this._orientation === 'horizontal') {
241 delta = position - handle.offsetLeft;
242 }
243 else {
244 delta = position - handle.offsetTop;
245 }
246 // Bail if there is no handle movement.
247 if (delta === 0) {
248 return;
249 }
250 // Prevent widget resizing unless needed.
251 for (var _i = 0, _a = this._sizers; _i < _a.length; _i++) {
252 var sizer = _a[_i];
253 if (sizer.size > 0) {
254 sizer.sizeHint = sizer.size;
255 }
256 }
257 // Adjust the sizers to reflect the handle movement.
258 boxengine_1.BoxEngine.adjust(this._sizers, index, delta);
259 // Update the layout of the widgets.
260 if (this.parent) {
261 this.parent.update();
262 }
263 };
264 /**
265 * Perform layout initialization which requires the parent widget.
266 */
267 SplitLayout.prototype.init = function () {
268 this.parent.dataset['orientation'] = this.orientation;
269 this.parent.dataset['alignment'] = this.alignment;
270 _super.prototype.init.call(this);
271 };
272 /**
273 * Attach a widget to the parent's DOM node.
274 *
275 * @param index - The current index of the widget in the layout.
276 *
277 * @param widget - The widget to attach to the parent.
278 *
279 * #### Notes
280 * This is a reimplementation of the superclass method.
281 */
282 SplitLayout.prototype.attachWidget = function (index, widget) {
283 // Create the item, handle, and sizer for the new widget.
284 var item = new layout_1.LayoutItem(widget);
285 var handle = Private.createHandle(this.renderer);
286 var average = Private.averageSize(this._sizers);
287 var sizer = Private.createSizer(average);
288 // Insert the item, handle, and sizer into the internal arrays.
289 algorithm_1.ArrayExt.insert(this._items, index, item);
290 algorithm_1.ArrayExt.insert(this._sizers, index, sizer);
291 algorithm_1.ArrayExt.insert(this._handles, index, handle);
292 // Send a `'before-attach'` message if the parent is attached.
293 if (this.parent.isAttached) {
294 messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.BeforeAttach);
295 }
296 // Add the widget and handle nodes to the parent.
297 this.parent.node.appendChild(widget.node);
298 this.parent.node.appendChild(handle);
299 // Send an `'after-attach'` message if the parent is attached.
300 if (this.parent.isAttached) {
301 messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.AfterAttach);
302 }
303 // Post a fit request for the parent widget.
304 this.parent.fit();
305 };
306 /**
307 * Move a widget in the parent's DOM node.
308 *
309 * @param fromIndex - The previous index of the widget in the layout.
310 *
311 * @param toIndex - The current index of the widget in the layout.
312 *
313 * @param widget - The widget to move in the parent.
314 *
315 * #### Notes
316 * This is a reimplementation of the superclass method.
317 */
318 SplitLayout.prototype.moveWidget = function (fromIndex, toIndex, widget) {
319 // Move the item, sizer, and handle for the widget.
320 algorithm_1.ArrayExt.move(this._items, fromIndex, toIndex);
321 algorithm_1.ArrayExt.move(this._sizers, fromIndex, toIndex);
322 algorithm_1.ArrayExt.move(this._handles, fromIndex, toIndex);
323 // Post a fit request to the parent to show/hide last handle.
324 this.parent.fit();
325 };
326 /**
327 * Detach a widget from the parent's DOM node.
328 *
329 * @param index - The previous index of the widget in the layout.
330 *
331 * @param widget - The widget to detach from the parent.
332 *
333 * #### Notes
334 * This is a reimplementation of the superclass method.
335 */
336 SplitLayout.prototype.detachWidget = function (index, widget) {
337 // Remove the item, handle, and sizer for the widget.
338 var item = algorithm_1.ArrayExt.removeAt(this._items, index);
339 var handle = algorithm_1.ArrayExt.removeAt(this._handles, index);
340 algorithm_1.ArrayExt.removeAt(this._sizers, index);
341 // Send a `'before-detach'` message if the parent is attached.
342 if (this.parent.isAttached) {
343 messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.BeforeDetach);
344 }
345 // Remove the widget and handle nodes from the parent.
346 this.parent.node.removeChild(widget.node);
347 this.parent.node.removeChild(handle);
348 // Send an `'after-detach'` message if the parent is attached.
349 if (this.parent.isAttached) {
350 messaging_1.MessageLoop.sendMessage(widget, widget_1.Widget.Msg.AfterDetach);
351 }
352 // Dispose of the layout item.
353 item.dispose();
354 // Post a fit request for the parent widget.
355 this.parent.fit();
356 };
357 /**
358 * A message handler invoked on a `'before-show'` message.
359 */
360 SplitLayout.prototype.onBeforeShow = function (msg) {
361 _super.prototype.onBeforeShow.call(this, msg);
362 this.parent.update();
363 };
364 /**
365 * A message handler invoked on a `'before-attach'` message.
366 */
367 SplitLayout.prototype.onBeforeAttach = function (msg) {
368 _super.prototype.onBeforeAttach.call(this, msg);
369 this.parent.fit();
370 };
371 /**
372 * A message handler invoked on a `'child-shown'` message.
373 */
374 SplitLayout.prototype.onChildShown = function (msg) {
375 this.parent.fit();
376 };
377 /**
378 * A message handler invoked on a `'child-hidden'` message.
379 */
380 SplitLayout.prototype.onChildHidden = function (msg) {
381 this.parent.fit();
382 };
383 /**
384 * A message handler invoked on a `'resize'` message.
385 */
386 SplitLayout.prototype.onResize = function (msg) {
387 if (this.parent.isVisible) {
388 this._update(msg.width, msg.height);
389 }
390 };
391 /**
392 * A message handler invoked on an `'update-request'` message.
393 */
394 SplitLayout.prototype.onUpdateRequest = function (msg) {
395 if (this.parent.isVisible) {
396 this._update(-1, -1);
397 }
398 };
399 /**
400 * A message handler invoked on a `'fit-request'` message.
401 */
402 SplitLayout.prototype.onFitRequest = function (msg) {
403 if (this.parent.isAttached) {
404 this._fit();
405 }
406 };
407 /**
408 * Fit the layout to the total size required by the widgets.
409 */
410 SplitLayout.prototype._fit = function () {
411 // Update the handles and track the visible widget count.
412 var nVisible = 0;
413 var lastHandleIndex = -1;
414 for (var i = 0, n = this._items.length; i < n; ++i) {
415 if (this._items[i].isHidden) {
416 this._handles[i].classList.add('p-mod-hidden');
417 }
418 else {
419 this._handles[i].classList.remove('p-mod-hidden');
420 lastHandleIndex = i;
421 nVisible++;
422 }
423 }
424 // Hide the handle for the last visible widget.
425 if (lastHandleIndex !== -1) {
426 this._handles[lastHandleIndex].classList.add('p-mod-hidden');
427 }
428 // Update the fixed space for the visible items.
429 this._fixed = this._spacing * Math.max(0, nVisible - 1);
430 // Setup the computed minimum size.
431 var horz = this._orientation === 'horizontal';
432 var minW = horz ? this._fixed : 0;
433 var minH = horz ? 0 : this._fixed;
434 // Update the sizers and computed size limits.
435 for (var i = 0, n = this._items.length; i < n; ++i) {
436 // Fetch the item and corresponding box sizer.
437 var item = this._items[i];
438 var sizer = this._sizers[i];
439 // Prevent resizing unless necessary.
440 if (sizer.size > 0) {
441 sizer.sizeHint = sizer.size;
442 }
443 // If the item is hidden, it should consume zero size.
444 if (item.isHidden) {
445 sizer.minSize = 0;
446 sizer.maxSize = 0;
447 continue;
448 }
449 // Update the size limits for the item.
450 item.fit();
451 // Update the stretch factor.
452 sizer.stretch = SplitLayout.getStretch(item.widget);
453 // Update the sizer limits and computed min size.
454 if (horz) {
455 sizer.minSize = item.minWidth;
456 sizer.maxSize = item.maxWidth;
457 minW += item.minWidth;
458 minH = Math.max(minH, item.minHeight);
459 }
460 else {
461 sizer.minSize = item.minHeight;
462 sizer.maxSize = item.maxHeight;
463 minH += item.minHeight;
464 minW = Math.max(minW, item.minWidth);
465 }
466 }
467 // Update the box sizing and add it to the computed min size.
468 var box = this._box = domutils_1.ElementExt.boxSizing(this.parent.node);
469 minW += box.horizontalSum;
470 minH += box.verticalSum;
471 // Update the parent's min size constraints.
472 var style = this.parent.node.style;
473 style.minWidth = minW + "px";
474 style.minHeight = minH + "px";
475 // Set the dirty flag to ensure only a single update occurs.
476 this._dirty = true;
477 // Notify the ancestor that it should fit immediately. This may
478 // cause a resize of the parent, fulfilling the required update.
479 if (this.parent.parent) {
480 messaging_1.MessageLoop.sendMessage(this.parent.parent, widget_1.Widget.Msg.FitRequest);
481 }
482 // If the dirty flag is still set, the parent was not resized.
483 // Trigger the required update on the parent widget immediately.
484 if (this._dirty) {
485 messaging_1.MessageLoop.sendMessage(this.parent, widget_1.Widget.Msg.UpdateRequest);
486 }
487 };
488 /**
489 * Update the layout position and size of the widgets.
490 *
491 * The parent offset dimensions should be `-1` if unknown.
492 */
493 SplitLayout.prototype._update = function (offsetWidth, offsetHeight) {
494 // Clear the dirty flag to indicate the update occurred.
495 this._dirty = false;
496 // Compute the visible item count.
497 var nVisible = 0;
498 for (var i = 0, n = this._items.length; i < n; ++i) {
499 nVisible += +!this._items[i].isHidden;
500 }
501 // Bail early if there are no visible items to layout.
502 if (nVisible === 0) {
503 return;
504 }
505 // Measure the parent if the offset dimensions are unknown.
506 if (offsetWidth < 0) {
507 offsetWidth = this.parent.node.offsetWidth;
508 }
509 if (offsetHeight < 0) {
510 offsetHeight = this.parent.node.offsetHeight;
511 }
512 // Ensure the parent box sizing data is computed.
513 if (!this._box) {
514 this._box = domutils_1.ElementExt.boxSizing(this.parent.node);
515 }
516 // Compute the actual layout bounds adjusted for border and padding.
517 var top = this._box.paddingTop;
518 var left = this._box.paddingLeft;
519 var width = offsetWidth - this._box.horizontalSum;
520 var height = offsetHeight - this._box.verticalSum;
521 // Compute the adjusted layout space.
522 var space;
523 var horz = this._orientation === 'horizontal';
524 if (horz) {
525 space = Math.max(0, width - this._fixed);
526 }
527 else {
528 space = Math.max(0, height - this._fixed);
529 }
530 // Scale the size hints if they are normalized.
531 if (this._hasNormedSizes) {
532 for (var _i = 0, _a = this._sizers; _i < _a.length; _i++) {
533 var sizer = _a[_i];
534 sizer.sizeHint *= space;
535 }
536 this._hasNormedSizes = false;
537 }
538 // Distribute the layout space to the box sizers.
539 var delta = boxengine_1.BoxEngine.calc(this._sizers, space);
540 // Set up the variables for justification and alignment offset.
541 var extra = 0;
542 var offset = 0;
543 // Account for alignment if there is extra layout space.
544 if (delta > 0) {
545 switch (this._alignment) {
546 case 'start':
547 break;
548 case 'center':
549 extra = 0;
550 offset = delta / 2;
551 break;
552 case 'end':
553 extra = 0;
554 offset = delta;
555 break;
556 case 'justify':
557 extra = delta / nVisible;
558 offset = 0;
559 break;
560 default:
561 throw 'unreachable';
562 }
563 }
564 // Layout the items using the computed box sizes.
565 for (var i = 0, n = this._items.length; i < n; ++i) {
566 // Fetch the item.
567 var item = this._items[i];
568 // Ignore hidden items.
569 if (item.isHidden) {
570 continue;
571 }
572 // Fetch the computed size for the widget.
573 var size = this._sizers[i].size;
574 // Fetch the style for the handle.
575 var handleStyle = this._handles[i].style;
576 // Update the widget and handle, and advance the relevant edge.
577 if (horz) {
578 item.update(left + offset, top, size + extra, height);
579 left += size + extra;
580 handleStyle.top = top + "px";
581 handleStyle.left = left + offset + "px";
582 handleStyle.width = this._spacing + "px";
583 handleStyle.height = height + "px";
584 left += this._spacing;
585 }
586 else {
587 item.update(left, top + offset, width, size + extra);
588 top += size + extra;
589 handleStyle.top = top + offset + "px";
590 handleStyle.left = left + "px";
591 handleStyle.width = width + "px";
592 handleStyle.height = this._spacing + "px";
593 top += this._spacing;
594 }
595 }
596 };
597 return SplitLayout;
598}(panellayout_1.PanelLayout));
599exports.SplitLayout = SplitLayout;
600/**
601 * The namespace for the `SplitLayout` class statics.
602 */
603(function (SplitLayout) {
604 /**
605 * Get the split layout stretch factor for the given widget.
606 *
607 * @param widget - The widget of interest.
608 *
609 * @returns The split layout stretch factor for the widget.
610 */
611 function getStretch(widget) {
612 return Private.stretchProperty.get(widget);
613 }
614 SplitLayout.getStretch = getStretch;
615 /**
616 * Set the split layout stretch factor for the given widget.
617 *
618 * @param widget - The widget of interest.
619 *
620 * @param value - The value for the stretch factor.
621 */
622 function setStretch(widget, value) {
623 Private.stretchProperty.set(widget, value);
624 }
625 SplitLayout.setStretch = setStretch;
626})(SplitLayout = exports.SplitLayout || (exports.SplitLayout = {}));
627exports.SplitLayout = SplitLayout;
628/**
629 * The namespace for the module implementation details.
630 */
631var Private;
632(function (Private) {
633 /**
634 * The property descriptor for a widget stretch factor.
635 */
636 Private.stretchProperty = new properties_1.AttachedProperty({
637 name: 'stretch',
638 create: function () { return 0; },
639 coerce: function (owner, value) { return Math.max(0, Math.floor(value)); },
640 changed: onChildSizingChanged
641 });
642 /**
643 * Create a new box sizer with the given size hint.
644 */
645 function createSizer(size) {
646 var sizer = new boxengine_1.BoxSizer();
647 sizer.sizeHint = Math.floor(size);
648 return sizer;
649 }
650 Private.createSizer = createSizer;
651 /**
652 * Create a new split handle node using the given renderer.
653 */
654 function createHandle(renderer) {
655 var handle = renderer.createHandle();
656 handle.style.position = 'absolute';
657 return handle;
658 }
659 Private.createHandle = createHandle;
660 /**
661 * Clamp a spacing value to an integer >= 0.
662 */
663 function clampSpacing(value) {
664 return Math.max(0, Math.floor(value));
665 }
666 Private.clampSpacing = clampSpacing;
667 /**
668 * Compute the average size of an array of box sizers.
669 */
670 function averageSize(sizers) {
671 return sizers.reduce(function (v, s) { return v + s.size; }, 0) / sizers.length || 0;
672 }
673 Private.averageSize = averageSize;
674 /**
675 * Normalize an array of values.
676 */
677 function normalize(values) {
678 var n = values.length;
679 if (n === 0) {
680 return [];
681 }
682 var sum = values.reduce(function (a, b) { return a + Math.abs(b); }, 0);
683 return sum === 0 ? values.map(function (v) { return 1 / n; }) : values.map(function (v) { return v / sum; });
684 }
685 Private.normalize = normalize;
686 /**
687 * The change handler for the attached sizing properties.
688 */
689 function onChildSizingChanged(child) {
690 if (child.parent && child.parent.layout instanceof SplitLayout) {
691 child.parent.fit();
692 }
693 }
694})(Private || (Private = {}));