UNPKG

23.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 { AttachedProperty } from '@lumino/properties';
17
18import { BoxEngine, BoxSizer } from './boxengine';
19
20import { LayoutItem } from './layout';
21
22import { PanelLayout } from './panellayout';
23
24import { Utils } from './utils';
25
26import { Widget } from './widget';
27
28/**
29 * A layout which arranges its widgets into resizable sections.
30 */
31export class SplitLayout extends PanelLayout {
32 /**
33 * Construct a new split layout.
34 *
35 * @param options - The options for initializing the layout.
36 */
37 constructor(options: SplitLayout.IOptions) {
38 super();
39 this.renderer = options.renderer;
40 if (options.orientation !== undefined) {
41 this._orientation = options.orientation;
42 }
43 if (options.alignment !== undefined) {
44 this._alignment = options.alignment;
45 }
46 if (options.spacing !== undefined) {
47 this._spacing = Utils.clampDimension(options.spacing);
48 }
49 }
50
51 /**
52 * Dispose of the resources held by the layout.
53 */
54 dispose(): void {
55 // Dispose of the layout items.
56 each(this._items, item => {
57 item.dispose();
58 });
59
60 // Clear the layout state.
61 this._box = null;
62 this._items.length = 0;
63 this._sizers.length = 0;
64 this._handles.length = 0;
65
66 // Dispose of the rest of the layout.
67 super.dispose();
68 }
69
70 /**
71 * The renderer used by the split layout.
72 */
73 readonly renderer: SplitLayout.IRenderer;
74
75 /**
76 * Get the layout orientation for the split layout.
77 */
78 get orientation(): SplitLayout.Orientation {
79 return this._orientation;
80 }
81
82 /**
83 * Set the layout orientation for the split layout.
84 */
85 set orientation(value: SplitLayout.Orientation) {
86 if (this._orientation === value) {
87 return;
88 }
89 this._orientation = value;
90 if (!this.parent) {
91 return;
92 }
93 this.parent.dataset['orientation'] = value;
94 this.parent.fit();
95 }
96
97 /**
98 * Get the content alignment for the split layout.
99 *
100 * #### Notes
101 * This is the alignment of the widgets in the layout direction.
102 *
103 * The alignment has no effect if the widgets can expand to fill the
104 * entire split layout.
105 */
106 get alignment(): SplitLayout.Alignment {
107 return this._alignment;
108 }
109
110 /**
111 * Set the content alignment for the split layout.
112 *
113 * #### Notes
114 * This is the alignment of the widgets in the layout direction.
115 *
116 * The alignment has no effect if the widgets can expand to fill the
117 * entire split layout.
118 */
119 set alignment(value: SplitLayout.Alignment) {
120 if (this._alignment === value) {
121 return;
122 }
123 this._alignment = value;
124 if (!this.parent) {
125 return;
126 }
127 this.parent.dataset['alignment'] = value;
128 this.parent.update();
129 }
130
131 /**
132 * Get the inter-element spacing for the split layout.
133 */
134 get spacing(): number {
135 return this._spacing;
136 }
137
138 /**
139 * Set the inter-element spacing for the split layout.
140 */
141 set spacing(value: number) {
142 value = Utils.clampDimension(value);
143 if (this._spacing === value) {
144 return;
145 }
146 this._spacing = value;
147 if (!this.parent) {
148 return;
149 }
150 this.parent.fit();
151 }
152
153 /**
154 * A read-only array of the split handles in the layout.
155 */
156 get handles(): ReadonlyArray<HTMLDivElement> {
157 return this._handles;
158 }
159
160 /**
161 * Get the absolute sizes of the widgets in the layout.
162 *
163 * @returns A new array of the absolute sizes of the widgets.
164 *
165 * This method **does not** measure the DOM nodes.
166 */
167 absoluteSizes(): number[] {
168 return this._sizers.map(sizer => sizer.size);
169 }
170
171 /**
172 * Get the relative sizes of the widgets in the layout.
173 *
174 * @returns A new array of the relative sizes of the widgets.
175 *
176 * #### Notes
177 * The returned sizes reflect the sizes of the widgets normalized
178 * relative to their siblings.
179 *
180 * This method **does not** measure the DOM nodes.
181 */
182 relativeSizes(): number[] {
183 return Private.normalize(this._sizers.map(sizer => sizer.size));
184 }
185
186 /**
187 * Set the relative sizes for the widgets in the layout.
188 *
189 * @param sizes - The relative sizes for the widgets in the panel.
190 * @param update - Update the layout after setting relative sizes.
191 * Default is True.
192 *
193 * #### Notes
194 * Extra values are ignored, too few will yield an undefined layout.
195 *
196 * The actual geometry of the DOM nodes is updated asynchronously.
197 */
198 setRelativeSizes(sizes: number[], update = true): void {
199 // Copy the sizes and pad with zeros as needed.
200 let n = this._sizers.length;
201 let temp = sizes.slice(0, n);
202 while (temp.length < n) {
203 temp.push(0);
204 }
205
206 // Normalize the padded sizes.
207 let normed = Private.normalize(temp);
208
209 // Apply the normalized sizes to the sizers.
210 for (let i = 0; i < n; ++i) {
211 let sizer = this._sizers[i];
212 sizer.sizeHint = normed[i];
213 sizer.size = normed[i];
214 }
215
216 // Set the flag indicating the sizes are normalized.
217 this._hasNormedSizes = true;
218
219 // Trigger an update of the parent widget.
220 if (update && this.parent) {
221 this.parent.update();
222 }
223 }
224
225 /**
226 * Move the offset position of a split handle.
227 *
228 * @param index - The index of the handle of the interest.
229 *
230 * @param position - The desired offset position of the handle.
231 *
232 * #### Notes
233 * The position is relative to the offset parent.
234 *
235 * This will move the handle as close as possible to the desired
236 * position. The sibling widgets will be adjusted as necessary.
237 */
238 moveHandle(index: number, position: number): void {
239 // Bail if the index is invalid or the handle is hidden.
240 let handle = this._handles[index];
241 if (!handle || handle.classList.contains('lm-mod-hidden')) {
242 return;
243 }
244
245 // Compute the desired delta movement for the handle.
246 let delta: number;
247 if (this._orientation === 'horizontal') {
248 delta = position - handle.offsetLeft;
249 } else {
250 delta = position - handle.offsetTop;
251 }
252
253 // Bail if there is no handle movement.
254 if (delta === 0) {
255 return;
256 }
257
258 // Prevent widget resizing unless needed.
259 for (let sizer of this._sizers) {
260 if (sizer.size > 0) {
261 sizer.sizeHint = sizer.size;
262 }
263 }
264
265 // Adjust the sizers to reflect the handle movement.
266 BoxEngine.adjust(this._sizers, index, delta);
267
268 // Update the layout of the widgets.
269 if (this.parent) {
270 this.parent.update();
271 }
272 }
273
274 /**
275 * Perform layout initialization which requires the parent widget.
276 */
277 protected init(): void {
278 this.parent!.dataset['orientation'] = this.orientation;
279 this.parent!.dataset['alignment'] = this.alignment;
280 super.init();
281 }
282
283 /**
284 * Attach a widget to the parent's DOM node.
285 *
286 * @param index - The current index of the widget in the layout.
287 *
288 * @param widget - The widget to attach to the parent.
289 *
290 * #### Notes
291 * This is a reimplementation of the superclass method.
292 */
293 protected attachWidget(index: number, widget: Widget): void {
294 // Create the item, handle, and sizer for the new widget.
295 let item = new LayoutItem(widget);
296 let handle = Private.createHandle(this.renderer);
297 let average = Private.averageSize(this._sizers);
298 let sizer = Private.createSizer(average);
299
300 // Insert the item, handle, and sizer into the internal arrays.
301 ArrayExt.insert(this._items, index, item);
302 ArrayExt.insert(this._sizers, index, sizer);
303 ArrayExt.insert(this._handles, index, handle);
304
305 // Send a `'before-attach'` message if the parent is attached.
306 if (this.parent!.isAttached) {
307 MessageLoop.sendMessage(widget, Widget.Msg.BeforeAttach);
308 }
309
310 // Add the widget and handle nodes to the parent.
311 this.parent!.node.appendChild(widget.node);
312 this.parent!.node.appendChild(handle);
313
314 // Send an `'after-attach'` message if the parent is attached.
315 if (this.parent!.isAttached) {
316 MessageLoop.sendMessage(widget, Widget.Msg.AfterAttach);
317 }
318
319 // Post a fit request for the parent widget.
320 this.parent!.fit();
321 }
322
323 /**
324 * Move a widget in the parent's DOM node.
325 *
326 * @param fromIndex - The previous index of the widget in the layout.
327 *
328 * @param toIndex - The current index of the widget in the layout.
329 *
330 * @param widget - The widget to move in the parent.
331 *
332 * #### Notes
333 * This is a reimplementation of the superclass method.
334 */
335 protected moveWidget(
336 fromIndex: number,
337 toIndex: number,
338 widget: Widget
339 ): void {
340 // Move the item, sizer, and handle for the widget.
341 ArrayExt.move(this._items, fromIndex, toIndex);
342 ArrayExt.move(this._sizers, fromIndex, toIndex);
343 ArrayExt.move(this._handles, fromIndex, toIndex);
344
345 // Post a fit request to the parent to show/hide last handle.
346 this.parent!.fit();
347 }
348
349 /**
350 * Detach a widget from the parent's DOM node.
351 *
352 * @param index - The previous index of the widget in the layout.
353 *
354 * @param widget - The widget to detach from the parent.
355 *
356 * #### Notes
357 * This is a reimplementation of the superclass method.
358 */
359 protected detachWidget(index: number, widget: Widget): void {
360 // Remove the item, handle, and sizer for the widget.
361 let item = ArrayExt.removeAt(this._items, index);
362 let handle = ArrayExt.removeAt(this._handles, index);
363 ArrayExt.removeAt(this._sizers, index);
364
365 // Send a `'before-detach'` message if the parent is attached.
366 if (this.parent!.isAttached) {
367 MessageLoop.sendMessage(widget, Widget.Msg.BeforeDetach);
368 }
369
370 // Remove the widget and handle nodes from the parent.
371 this.parent!.node.removeChild(widget.node);
372 this.parent!.node.removeChild(handle!);
373
374 // Send an `'after-detach'` message if the parent is attached.
375 if (this.parent!.isAttached) {
376 MessageLoop.sendMessage(widget, Widget.Msg.AfterDetach);
377 }
378
379 // Dispose of the layout item.
380 item!.dispose();
381
382 // Post a fit request for the parent widget.
383 this.parent!.fit();
384 }
385
386 /**
387 * A message handler invoked on a `'before-show'` message.
388 */
389 protected onBeforeShow(msg: Message): void {
390 super.onBeforeShow(msg);
391 this.parent!.update();
392 }
393
394 /**
395 * A message handler invoked on a `'before-attach'` message.
396 */
397 protected onBeforeAttach(msg: Message): void {
398 super.onBeforeAttach(msg);
399 this.parent!.fit();
400 }
401
402 /**
403 * A message handler invoked on a `'child-shown'` message.
404 */
405 protected onChildShown(msg: Widget.ChildMessage): void {
406 this.parent!.fit();
407 }
408
409 /**
410 * A message handler invoked on a `'child-hidden'` message.
411 */
412 protected onChildHidden(msg: Widget.ChildMessage): void {
413 this.parent!.fit();
414 }
415
416 /**
417 * A message handler invoked on a `'resize'` message.
418 */
419 protected onResize(msg: Widget.ResizeMessage): void {
420 if (this.parent!.isVisible) {
421 this._update(msg.width, msg.height);
422 }
423 }
424
425 /**
426 * A message handler invoked on an `'update-request'` message.
427 */
428 protected onUpdateRequest(msg: Message): void {
429 if (this.parent!.isVisible) {
430 this._update(-1, -1);
431 }
432 }
433
434 /**
435 * A message handler invoked on a `'fit-request'` message.
436 */
437 protected onFitRequest(msg: Message): void {
438 if (this.parent!.isAttached) {
439 this._fit();
440 }
441 }
442
443 /**
444 * Update the item position.
445 *
446 * @param i Item index
447 * @param isHorizontal Whether the layout is horizontal or not
448 * @param left Left position in pixels
449 * @param top Top position in pixels
450 * @param height Item height
451 * @param width Item width
452 * @param size Item size
453 */
454 protected updateItemPosition(
455 i: number,
456 isHorizontal: boolean,
457 left: number,
458 top: number,
459 height: number,
460 width: number,
461 size: number
462 ): void {
463 const item = this._items[i];
464 if (item.isHidden) {
465 return;
466 }
467
468 // Fetch the style for the handle.
469 let handleStyle = this._handles[i].style;
470
471 // Update the widget and handle, and advance the relevant edge.
472 if (isHorizontal) {
473 left += this.widgetOffset;
474 item.update(left, top, size, height);
475 left += size;
476 handleStyle.top = `${top}px`;
477 handleStyle.left = `${left}px`;
478 handleStyle.width = `${this._spacing}px`;
479 handleStyle.height = `${height}px`;
480 } else {
481 top += this.widgetOffset;
482 item.update(left, top, width, size);
483 top += size;
484 handleStyle.top = `${top}px`;
485 handleStyle.left = `${left}px`;
486 handleStyle.width = `${width}px`;
487 handleStyle.height = `${this._spacing}px`;
488 }
489 }
490
491 /**
492 * Fit the layout to the total size required by the widgets.
493 */
494 private _fit(): void {
495 // Update the handles and track the visible widget count.
496 let nVisible = 0;
497 let lastHandleIndex = -1;
498 for (let i = 0, n = this._items.length; i < n; ++i) {
499 if (this._items[i].isHidden) {
500 this._handles[i].classList.add('lm-mod-hidden');
501 /* <DEPRECATED> */
502 this._handles[i].classList.add('p-mod-hidden');
503 /* </DEPRECATED> */
504 } else {
505 this._handles[i].classList.remove('lm-mod-hidden');
506 /* <DEPRECATED> */
507 this._handles[i].classList.remove('p-mod-hidden');
508 /* </DEPRECATED> */
509 lastHandleIndex = i;
510 nVisible++;
511 }
512 }
513
514 // Hide the handle for the last visible widget.
515 if (lastHandleIndex !== -1) {
516 this._handles[lastHandleIndex].classList.add('lm-mod-hidden');
517 /* <DEPRECATED> */
518 this._handles[lastHandleIndex].classList.add('p-mod-hidden');
519 /* </DEPRECATED> */
520 }
521
522 // Update the fixed space for the visible items.
523 this._fixed =
524 this._spacing * Math.max(0, nVisible - 1) +
525 this.widgetOffset * this._items.length;
526
527 // Setup the computed minimum size.
528 let horz = this._orientation === 'horizontal';
529 let minW = horz ? this._fixed : 0;
530 let minH = horz ? 0 : this._fixed;
531
532 // Update the sizers and computed size limits.
533 for (let i = 0, n = this._items.length; i < n; ++i) {
534 // Fetch the item and corresponding box sizer.
535 let item = this._items[i];
536 let sizer = this._sizers[i];
537
538 // Prevent resizing unless necessary.
539 if (sizer.size > 0) {
540 sizer.sizeHint = sizer.size;
541 }
542
543 // If the item is hidden, it should consume zero size.
544 if (item.isHidden) {
545 sizer.minSize = 0;
546 sizer.maxSize = 0;
547 continue;
548 }
549
550 // Update the size limits for the item.
551 item.fit();
552
553 // Update the stretch factor.
554 sizer.stretch = SplitLayout.getStretch(item.widget);
555
556 // Update the sizer limits and computed min size.
557 if (horz) {
558 sizer.minSize = item.minWidth;
559 sizer.maxSize = item.maxWidth;
560 minW += item.minWidth;
561 minH = Math.max(minH, item.minHeight);
562 } else {
563 sizer.minSize = item.minHeight;
564 sizer.maxSize = item.maxHeight;
565 minH += item.minHeight;
566 minW = Math.max(minW, item.minWidth);
567 }
568 }
569
570 // Update the box sizing and add it to the computed min size.
571 let box = (this._box = ElementExt.boxSizing(this.parent!.node));
572 minW += box.horizontalSum;
573 minH += box.verticalSum;
574
575 // Update the parent's min size constraints.
576 let style = this.parent!.node.style;
577 style.minWidth = `${minW}px`;
578 style.minHeight = `${minH}px`;
579
580 // Set the dirty flag to ensure only a single update occurs.
581 this._dirty = true;
582
583 // Notify the ancestor that it should fit immediately. This may
584 // cause a resize of the parent, fulfilling the required update.
585 if (this.parent!.parent) {
586 MessageLoop.sendMessage(this.parent!.parent!, Widget.Msg.FitRequest);
587 }
588
589 // If the dirty flag is still set, the parent was not resized.
590 // Trigger the required update on the parent widget immediately.
591 if (this._dirty) {
592 MessageLoop.sendMessage(this.parent!, Widget.Msg.UpdateRequest);
593 }
594 }
595
596 /**
597 * Update the layout position and size of the widgets.
598 *
599 * The parent offset dimensions should be `-1` if unknown.
600 */
601 private _update(offsetWidth: number, offsetHeight: number): void {
602 // Clear the dirty flag to indicate the update occurred.
603 this._dirty = false;
604
605 // Compute the visible item count.
606 let nVisible = 0;
607 for (let i = 0, n = this._items.length; i < n; ++i) {
608 nVisible += +!this._items[i].isHidden;
609 }
610
611 // Bail early if there are no visible items to layout.
612 if (nVisible === 0 && this.widgetOffset === 0) {
613 return;
614 }
615
616 // Measure the parent if the offset dimensions are unknown.
617 if (offsetWidth < 0) {
618 offsetWidth = this.parent!.node.offsetWidth;
619 }
620 if (offsetHeight < 0) {
621 offsetHeight = this.parent!.node.offsetHeight;
622 }
623
624 // Ensure the parent box sizing data is computed.
625 if (!this._box) {
626 this._box = ElementExt.boxSizing(this.parent!.node);
627 }
628
629 // Compute the actual layout bounds adjusted for border and padding.
630 let top = this._box.paddingTop;
631 let left = this._box.paddingLeft;
632 let width = offsetWidth - this._box.horizontalSum;
633 let height = offsetHeight - this._box.verticalSum;
634
635 // Set up the variables for justification and alignment offset.
636 let extra = 0;
637 let offset = 0;
638 let horz = this._orientation === 'horizontal';
639
640 if (nVisible > 0) {
641 // Compute the adjusted layout space.
642 let space: number;
643 if (horz) {
644 // left += this.widgetOffset;
645 space = Math.max(0, width - this._fixed);
646 } else {
647 // top += this.widgetOffset;
648 space = Math.max(0, height - this._fixed);
649 }
650
651 // Scale the size hints if they are normalized.
652 if (this._hasNormedSizes) {
653 for (let sizer of this._sizers) {
654 sizer.sizeHint *= space;
655 }
656 this._hasNormedSizes = false;
657 }
658
659 // Distribute the layout space to the box sizers.
660 let delta = BoxEngine.calc(this._sizers, space);
661
662 // Account for alignment if there is extra layout space.
663 if (delta > 0) {
664 switch (this._alignment) {
665 case 'start':
666 break;
667 case 'center':
668 extra = 0;
669 offset = delta / 2;
670 break;
671 case 'end':
672 extra = 0;
673 offset = delta;
674 break;
675 case 'justify':
676 extra = delta / nVisible;
677 offset = 0;
678 break;
679 default:
680 throw 'unreachable';
681 }
682 }
683 }
684
685 // Layout the items using the computed box sizes.
686 for (let i = 0, n = this._items.length; i < n; ++i) {
687 // Fetch the item.
688 const item = this._items[i];
689
690 // Fetch the computed size for the widget.
691 const size = item.isHidden ? 0 : this._sizers[i].size + extra;
692
693 this.updateItemPosition(
694 i,
695 horz,
696 horz ? left + offset : left,
697 horz ? top : top + offset,
698 height,
699 width,
700 size
701 );
702
703 const fullOffset =
704 this.widgetOffset +
705 (this._handles[i].classList.contains('lm-mod-hidden')
706 ? 0
707 : this._spacing);
708
709 if (horz) {
710 left += size + fullOffset;
711 } else {
712 top += size + fullOffset;
713 }
714 }
715 }
716
717 protected widgetOffset = 0;
718 private _fixed = 0;
719 private _spacing = 4;
720 private _dirty = false;
721 private _hasNormedSizes = false;
722 private _sizers: BoxSizer[] = [];
723 private _items: LayoutItem[] = [];
724 private _handles: HTMLDivElement[] = [];
725 private _box: ElementExt.IBoxSizing | null = null;
726 private _alignment: SplitLayout.Alignment = 'start';
727 private _orientation: SplitLayout.Orientation = 'horizontal';
728}
729
730/**
731 * The namespace for the `SplitLayout` class statics.
732 */
733export namespace SplitLayout {
734 /**
735 * A type alias for a split layout orientation.
736 */
737 export type Orientation = 'horizontal' | 'vertical';
738
739 /**
740 * A type alias for a split layout alignment.
741 */
742 export type Alignment = 'start' | 'center' | 'end' | 'justify';
743
744 /**
745 * An options object for initializing a split layout.
746 */
747 export interface IOptions {
748 /**
749 * The renderer to use for the split layout.
750 */
751 renderer: IRenderer;
752
753 /**
754 * The orientation of the layout.
755 *
756 * The default is `'horizontal'`.
757 */
758 orientation?: Orientation;
759
760 /**
761 * The content alignment of the layout.
762 *
763 * The default is `'start'`.
764 */
765 alignment?: Alignment;
766
767 /**
768 * The spacing between items in the layout.
769 *
770 * The default is `4`.
771 */
772 spacing?: number;
773 }
774
775 /**
776 * A renderer for use with a split layout.
777 */
778 export interface IRenderer {
779 /**
780 * Create a new handle for use with a split layout.
781 *
782 * @returns A new handle element.
783 */
784 createHandle(): HTMLDivElement;
785 }
786
787 /**
788 * Get the split layout stretch factor for the given widget.
789 *
790 * @param widget - The widget of interest.
791 *
792 * @returns The split layout stretch factor for the widget.
793 */
794 export function getStretch(widget: Widget): number {
795 return Private.stretchProperty.get(widget);
796 }
797
798 /**
799 * Set the split layout stretch factor for the given widget.
800 *
801 * @param widget - The widget of interest.
802 *
803 * @param value - The value for the stretch factor.
804 */
805 export function setStretch(widget: Widget, value: number): void {
806 Private.stretchProperty.set(widget, value);
807 }
808}
809
810/**
811 * The namespace for the module implementation details.
812 */
813namespace Private {
814 /**
815 * The property descriptor for a widget stretch factor.
816 */
817 export const stretchProperty = new AttachedProperty<Widget, number>({
818 name: 'stretch',
819 create: () => 0,
820 coerce: (owner, value) => Math.max(0, Math.floor(value)),
821 changed: onChildSizingChanged
822 });
823
824 /**
825 * Create a new box sizer with the given size hint.
826 */
827 export function createSizer(size: number): BoxSizer {
828 let sizer = new BoxSizer();
829 sizer.sizeHint = Math.floor(size);
830 return sizer;
831 }
832
833 /**
834 * Create a new split handle node using the given renderer.
835 */
836 export function createHandle(
837 renderer: SplitLayout.IRenderer
838 ): HTMLDivElement {
839 let handle = renderer.createHandle();
840 handle.style.position = 'absolute';
841 return handle;
842 }
843
844 /**
845 * Compute the average size of an array of box sizers.
846 */
847 export function averageSize(sizers: BoxSizer[]): number {
848 return sizers.reduce((v, s) => v + s.size, 0) / sizers.length || 0;
849 }
850
851 /**
852 * Normalize an array of values.
853 */
854 export function normalize(values: number[]): number[] {
855 let n = values.length;
856 if (n === 0) {
857 return [];
858 }
859 let sum = values.reduce((a, b) => a + Math.abs(b), 0);
860 return sum === 0 ? values.map(v => 1 / n) : values.map(v => v / sum);
861 }
862
863 /**
864 * The change handler for the attached sizing properties.
865 */
866 function onChildSizingChanged(child: Widget): void {
867 if (child.parent && child.parent.layout instanceof SplitLayout) {
868 child.parent.fit();
869 }
870 }
871}