blob: 611890d3f8185e2115a9e25b9f8ecddd7e25f63e [file] [log] [blame]
Blink Reformat4c46d092018-04-07 15:32:371/*
2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3 * Copyright (C) 2011 Google Inc. All Rights Reserved.
4 *
5 * Redistribution and use in source and binary forms, with or without
6 * modification, are permitted provided that the following conditions
7 * are met:
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 *
14 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
15 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
16 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
17 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
18 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
19 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
20 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
21 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
22 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25 */
26
27/**
28 * @unrestricted
29 */
30UI.Widget = class extends Common.Object {
31 /**
32 * @param {boolean=} isWebComponent
Joel Einbinder7fbe24c2019-01-24 05:19:0133 * @param {boolean=} delegatesFocus
Blink Reformat4c46d092018-04-07 15:32:3734 */
Joel Einbinder7fbe24c2019-01-24 05:19:0135 constructor(isWebComponent, delegatesFocus) {
Blink Reformat4c46d092018-04-07 15:32:3736 super();
37 this.contentElement = createElementWithClass('div', 'widget');
38 if (isWebComponent) {
39 this.element = createElementWithClass('div', 'vbox flex-auto');
Joel Einbinder7fbe24c2019-01-24 05:19:0140 this._shadowRoot = UI.createShadowRootWithCoreStyles(this.element, undefined, delegatesFocus);
Blink Reformat4c46d092018-04-07 15:32:3741 this._shadowRoot.appendChild(this.contentElement);
42 } else {
43 this.element = this.contentElement;
44 }
45 this._isWebComponent = isWebComponent;
46 this.element.__widget = this;
47 this._visible = false;
48 this._isRoot = false;
49 this._isShowing = false;
50 this._children = [];
51 this._hideOnDetach = false;
52 this._notificationDepth = 0;
53 this._invalidationsSuspended = 0;
54 this._defaultFocusedChild = null;
55 }
56
57 static _incrementWidgetCounter(parentElement, childElement) {
58 const count = (childElement.__widgetCounter || 0) + (childElement.__widget ? 1 : 0);
59 if (!count)
60 return;
61
62 while (parentElement) {
63 parentElement.__widgetCounter = (parentElement.__widgetCounter || 0) + count;
64 parentElement = parentElement.parentElementOrShadowHost();
65 }
66 }
67
68 static _decrementWidgetCounter(parentElement, childElement) {
69 const count = (childElement.__widgetCounter || 0) + (childElement.__widget ? 1 : 0);
70 if (!count)
71 return;
72
73 while (parentElement) {
74 parentElement.__widgetCounter -= count;
75 parentElement = parentElement.parentElementOrShadowHost();
76 }
77 }
78
79 static __assert(condition, message) {
80 if (!condition)
81 throw new Error(message);
82 }
83
84 /**
85 * @param {?Node} node
86 */
87 static focusWidgetForNode(node) {
88 while (node) {
89 if (node.__widget)
90 break;
91 node = node.parentNodeOrShadowHost();
92 }
93 if (!node)
94 return;
95
96 let widget = node.__widget;
97 while (widget._parentWidget) {
98 widget._parentWidget._defaultFocusedChild = widget;
99 widget = widget._parentWidget;
100 }
101 }
102
103 markAsRoot() {
104 UI.Widget.__assert(!this.element.parentElement, 'Attempt to mark as root attached node');
105 this._isRoot = true;
106 }
107
108 /**
109 * @return {?UI.Widget}
110 */
111 parentWidget() {
112 return this._parentWidget;
113 }
114
115 /**
116 * @return {!Array.<!UI.Widget>}
117 */
118 children() {
119 return this._children;
120 }
121
122 /**
123 * @param {!UI.Widget} widget
124 * @protected
125 */
126 childWasDetached(widget) {
127 }
128
129 /**
130 * @return {boolean}
131 */
132 isShowing() {
133 return this._isShowing;
134 }
135
136 /**
137 * @return {boolean}
138 */
139 shouldHideOnDetach() {
140 if (!this.element.parentElement)
141 return false;
142 if (this._hideOnDetach)
143 return true;
144 for (const child of this._children) {
145 if (child.shouldHideOnDetach())
146 return true;
147 }
148 return false;
149 }
150
151 setHideOnDetach() {
152 this._hideOnDetach = true;
153 }
154
155 /**
156 * @return {boolean}
157 */
158 _inNotification() {
159 return !!this._notificationDepth || (this._parentWidget && this._parentWidget._inNotification());
160 }
161
162 _parentIsShowing() {
163 if (this._isRoot)
164 return true;
165 return !!this._parentWidget && this._parentWidget.isShowing();
166 }
167
168 /**
169 * @param {function(this:UI.Widget)} method
170 */
171 _callOnVisibleChildren(method) {
172 const copy = this._children.slice();
173 for (let i = 0; i < copy.length; ++i) {
174 if (copy[i]._parentWidget === this && copy[i]._visible)
175 method.call(copy[i]);
176 }
177 }
178
179 _processWillShow() {
180 this._callOnVisibleChildren(this._processWillShow);
181 this._isShowing = true;
182 }
183
184 _processWasShown() {
185 if (this._inNotification())
186 return;
187 this.restoreScrollPositions();
188 this._notify(this.wasShown);
189 this._callOnVisibleChildren(this._processWasShown);
190 }
191
192 _processWillHide() {
193 if (this._inNotification())
194 return;
195 this.storeScrollPositions();
196
197 this._callOnVisibleChildren(this._processWillHide);
198 this._notify(this.willHide);
199 this._isShowing = false;
200 }
201
202 _processWasHidden() {
203 this._callOnVisibleChildren(this._processWasHidden);
204 }
205
206 _processOnResize() {
207 if (this._inNotification())
208 return;
209 if (!this.isShowing())
210 return;
211 this._notify(this.onResize);
212 this._callOnVisibleChildren(this._processOnResize);
213 }
214
215 /**
216 * @param {function(this:UI.Widget)} notification
217 */
218 _notify(notification) {
219 ++this._notificationDepth;
220 try {
221 notification.call(this);
222 } finally {
223 --this._notificationDepth;
224 }
225 }
226
227 wasShown() {
228 }
229
230 willHide() {
231 }
232
233 onResize() {
234 }
235
236 onLayout() {
237 }
238
239 ownerViewDisposed() {
240 }
241
242 /**
243 * @param {!Element} parentElement
244 * @param {?Node=} insertBefore
245 */
246 show(parentElement, insertBefore) {
247 UI.Widget.__assert(parentElement, 'Attempt to attach widget with no parent element');
248
249 if (!this._isRoot) {
250 // Update widget hierarchy.
251 let currentParent = parentElement;
252 while (currentParent && !currentParent.__widget)
253 currentParent = currentParent.parentElementOrShadowHost();
254 UI.Widget.__assert(currentParent, 'Attempt to attach widget to orphan node');
255 this._attach(currentParent.__widget);
256 }
257
258 this._showWidget(parentElement, insertBefore);
259 }
260
261 /**
262 * @param {!UI.Widget} parentWidget
263 */
264 _attach(parentWidget) {
265 if (parentWidget === this._parentWidget)
266 return;
267 if (this._parentWidget)
268 this.detach();
269 this._parentWidget = parentWidget;
270 this._parentWidget._children.push(this);
271 this._isRoot = false;
272 }
273
274 showWidget() {
275 if (this._visible)
276 return;
277 UI.Widget.__assert(this.element.parentElement, 'Attempt to show widget that is not hidden using hideWidget().');
278 this._showWidget(/** @type {!Element} */ (this.element.parentElement), this.element.nextSibling);
279 }
280
281 /**
282 * @param {!Element} parentElement
283 * @param {?Node=} insertBefore
284 */
285 _showWidget(parentElement, insertBefore) {
286 let currentParent = parentElement;
287 while (currentParent && !currentParent.__widget)
288 currentParent = currentParent.parentElementOrShadowHost();
289
290 if (this._isRoot) {
291 UI.Widget.__assert(!currentParent, 'Attempt to show root widget under another widget');
292 } else {
293 UI.Widget.__assert(
294 currentParent && currentParent.__widget === this._parentWidget,
295 'Attempt to show under node belonging to alien widget');
296 }
297
298 const wasVisible = this._visible;
299 if (wasVisible && this.element.parentElement === parentElement)
300 return;
301
302 this._visible = true;
303
304 if (!wasVisible && this._parentIsShowing())
305 this._processWillShow();
306
307 this.element.classList.remove('hidden');
308
309 // Reparent
310 if (this.element.parentElement !== parentElement) {
311 UI.Widget._incrementWidgetCounter(parentElement, this.element);
312 if (insertBefore)
313 UI.Widget._originalInsertBefore.call(parentElement, this.element, insertBefore);
314 else
315 UI.Widget._originalAppendChild.call(parentElement, this.element);
316 }
317
318 if (!wasVisible && this._parentIsShowing())
319 this._processWasShown();
320
321 if (this._parentWidget && this._hasNonZeroConstraints())
322 this._parentWidget.invalidateConstraints();
323 else
324 this._processOnResize();
325 }
326
327 hideWidget() {
328 if (!this._visible)
329 return;
330 this._hideWidget(false);
331 }
332
333 /**
334 * @param {boolean} removeFromDOM
335 */
336 _hideWidget(removeFromDOM) {
337 this._visible = false;
338 const parentElement = this.element.parentElement;
339
340 if (this._parentIsShowing())
341 this._processWillHide();
342
343 if (removeFromDOM) {
344 // Force legal removal
345 UI.Widget._decrementWidgetCounter(parentElement, this.element);
346 UI.Widget._originalRemoveChild.call(parentElement, this.element);
347 } else {
348 this.element.classList.add('hidden');
349 }
350
351 if (this._parentIsShowing())
352 this._processWasHidden();
353 if (this._parentWidget && this._hasNonZeroConstraints())
354 this._parentWidget.invalidateConstraints();
355 }
356
357 /**
358 * @param {boolean=} overrideHideOnDetach
359 */
360 detach(overrideHideOnDetach) {
361 if (!this._parentWidget && !this._isRoot)
362 return;
363
364 // hideOnDetach means that we should never remove element from dom - content
365 // has iframes and detaching it will hurt.
366 //
367 // overrideHideOnDetach will override hideOnDetach and the client takes
368 // responsibility for the consequences.
369 const removeFromDOM = overrideHideOnDetach || !this.shouldHideOnDetach();
370 if (this._visible) {
371 this._hideWidget(removeFromDOM);
372 } else if (removeFromDOM && this.element.parentElement) {
373 const parentElement = this.element.parentElement;
374 // Force kick out from DOM.
375 UI.Widget._decrementWidgetCounter(parentElement, this.element);
376 UI.Widget._originalRemoveChild.call(parentElement, this.element);
377 }
378
379 // Update widget hierarchy.
380 if (this._parentWidget) {
381 const childIndex = this._parentWidget._children.indexOf(this);
382 UI.Widget.__assert(childIndex >= 0, 'Attempt to remove non-child widget');
383 this._parentWidget._children.splice(childIndex, 1);
384 if (this._parentWidget._defaultFocusedChild === this)
385 this._parentWidget._defaultFocusedChild = null;
386 this._parentWidget.childWasDetached(this);
387 this._parentWidget = null;
388 } else {
389 UI.Widget.__assert(this._isRoot, 'Removing non-root widget from DOM');
390 }
391 }
392
393 detachChildWidgets() {
394 const children = this._children.slice();
395 for (let i = 0; i < children.length; ++i)
396 children[i].detach();
397 }
398
399 /**
400 * @return {!Array.<!Element>}
401 */
402 elementsToRestoreScrollPositionsFor() {
403 return [this.element];
404 }
405
406 storeScrollPositions() {
407 const elements = this.elementsToRestoreScrollPositionsFor();
408 for (let i = 0; i < elements.length; ++i) {
409 const container = elements[i];
410 container._scrollTop = container.scrollTop;
411 container._scrollLeft = container.scrollLeft;
412 }
413 }
414
415 restoreScrollPositions() {
416 const elements = this.elementsToRestoreScrollPositionsFor();
417 for (let i = 0; i < elements.length; ++i) {
418 const container = elements[i];
419 if (container._scrollTop)
420 container.scrollTop = container._scrollTop;
421 if (container._scrollLeft)
422 container.scrollLeft = container._scrollLeft;
423 }
424 }
425
426 doResize() {
427 if (!this.isShowing())
428 return;
429 // No matter what notification we are in, dispatching onResize is not needed.
430 if (!this._inNotification())
431 this._callOnVisibleChildren(this._processOnResize);
432 }
433
434 doLayout() {
435 if (!this.isShowing())
436 return;
437 this._notify(this.onLayout);
438 this.doResize();
439 }
440
441 /**
442 * @param {string} cssFile
443 */
444 registerRequiredCSS(cssFile) {
445 UI.appendStyle(this._isWebComponent ? this._shadowRoot : this.element, cssFile);
446 }
447
448 printWidgetHierarchy() {
449 const lines = [];
450 this._collectWidgetHierarchy('', lines);
451 console.log(lines.join('\n')); // eslint-disable-line no-console
452 }
453
454 _collectWidgetHierarchy(prefix, lines) {
455 lines.push(prefix + '[' + this.element.className + ']' + (this._children.length ? ' {' : ''));
456
457 for (let i = 0; i < this._children.length; ++i)
458 this._children[i]._collectWidgetHierarchy(prefix + ' ', lines);
459
460 if (this._children.length)
461 lines.push(prefix + '}');
462 }
463
464 /**
465 * @param {?Element} element
466 */
467 setDefaultFocusedElement(element) {
468 this._defaultFocusedElement = element;
469 }
470
471 /**
472 * @param {!UI.Widget} child
473 */
474 setDefaultFocusedChild(child) {
475 UI.Widget.__assert(child._parentWidget === this, 'Attempt to set non-child widget as default focused.');
476 this._defaultFocusedChild = child;
477 }
478
479 focus() {
480 if (!this.isShowing())
481 return;
482
483 const element = this._defaultFocusedElement;
484 if (element) {
485 if (!element.hasFocus())
486 element.focus();
487 return;
488 }
489
490 if (this._defaultFocusedChild && this._defaultFocusedChild._visible) {
491 this._defaultFocusedChild.focus();
492 } else {
493 for (const child of this._children) {
494 if (child._visible) {
495 child.focus();
496 return;
497 }
498 }
499 let child = this.contentElement.traverseNextNode(this.contentElement);
500 while (child) {
501 if (child instanceof UI.XWidget) {
502 child.focus();
503 return;
504 }
505 child = child.traverseNextNode(this.contentElement);
506 }
507 }
508 }
509
510 /**
511 * @return {boolean}
512 */
513 hasFocus() {
514 return this.element.hasFocus();
515 }
516
517 /**
518 * @return {!UI.Constraints}
519 */
520 calculateConstraints() {
521 return new UI.Constraints();
522 }
523
524 /**
525 * @return {!UI.Constraints}
526 */
527 constraints() {
528 if (typeof this._constraints !== 'undefined')
529 return this._constraints;
530 if (typeof this._cachedConstraints === 'undefined')
531 this._cachedConstraints = this.calculateConstraints();
532 return this._cachedConstraints;
533 }
534
535 /**
536 * @param {number} width
537 * @param {number} height
538 * @param {number} preferredWidth
539 * @param {number} preferredHeight
540 */
541 setMinimumAndPreferredSizes(width, height, preferredWidth, preferredHeight) {
542 this._constraints = new UI.Constraints(new UI.Size(width, height), new UI.Size(preferredWidth, preferredHeight));
543 this.invalidateConstraints();
544 }
545
546 /**
547 * @param {number} width
548 * @param {number} height
549 */
550 setMinimumSize(width, height) {
551 this._constraints = new UI.Constraints(new UI.Size(width, height));
552 this.invalidateConstraints();
553 }
554
555 /**
556 * @return {boolean}
557 */
558 _hasNonZeroConstraints() {
559 const constraints = this.constraints();
560 return !!(
561 constraints.minimum.width || constraints.minimum.height || constraints.preferred.width ||
562 constraints.preferred.height);
563 }
564
565 suspendInvalidations() {
566 ++this._invalidationsSuspended;
567 }
568
569 resumeInvalidations() {
570 --this._invalidationsSuspended;
571 if (!this._invalidationsSuspended && this._invalidationsRequested)
572 this.invalidateConstraints();
573 }
574
575 invalidateConstraints() {
576 if (this._invalidationsSuspended) {
577 this._invalidationsRequested = true;
578 return;
579 }
580 this._invalidationsRequested = false;
581 const cached = this._cachedConstraints;
582 delete this._cachedConstraints;
583 const actual = this.constraints();
584 if (!actual.isEqual(cached) && this._parentWidget)
585 this._parentWidget.invalidateConstraints();
586 else
587 this.doLayout();
588 }
589};
590
591UI.Widget._originalAppendChild = Element.prototype.appendChild;
592UI.Widget._originalInsertBefore = Element.prototype.insertBefore;
593UI.Widget._originalRemoveChild = Element.prototype.removeChild;
594UI.Widget._originalRemoveChildren = Element.prototype.removeChildren;
595
596
597/**
598 * @unrestricted
599 */
600UI.VBox = class extends UI.Widget {
601 /**
602 * @param {boolean=} isWebComponent
Joel Einbinder7fbe24c2019-01-24 05:19:01603 * @param {boolean=} delegatesFocus
Blink Reformat4c46d092018-04-07 15:32:37604 */
Joel Einbinder7fbe24c2019-01-24 05:19:01605 constructor(isWebComponent, delegatesFocus) {
606 super(isWebComponent, delegatesFocus);
Blink Reformat4c46d092018-04-07 15:32:37607 this.contentElement.classList.add('vbox');
608 }
609
610 /**
611 * @override
612 * @return {!UI.Constraints}
613 */
614 calculateConstraints() {
615 let constraints = new UI.Constraints();
616
617 /**
618 * @this {!UI.Widget}
619 * @suppressReceiverCheck
620 */
621 function updateForChild() {
622 const child = this.constraints();
623 constraints = constraints.widthToMax(child);
624 constraints = constraints.addHeight(child);
625 }
626
627 this._callOnVisibleChildren(updateForChild);
628 return constraints;
629 }
630};
631
632/**
633 * @unrestricted
634 */
635UI.HBox = class extends UI.Widget {
636 /**
637 * @param {boolean=} isWebComponent
638 */
639 constructor(isWebComponent) {
640 super(isWebComponent);
641 this.contentElement.classList.add('hbox');
642 }
643
644 /**
645 * @override
646 * @return {!UI.Constraints}
647 */
648 calculateConstraints() {
649 let constraints = new UI.Constraints();
650
651 /**
652 * @this {!UI.Widget}
653 * @suppressReceiverCheck
654 */
655 function updateForChild() {
656 const child = this.constraints();
657 constraints = constraints.addWidth(child);
658 constraints = constraints.heightToMax(child);
659 }
660
661 this._callOnVisibleChildren(updateForChild);
662 return constraints;
663 }
664};
665
666/**
667 * @unrestricted
668 */
669UI.VBoxWithResizeCallback = class extends UI.VBox {
670 /**
671 * @param {function()} resizeCallback
672 */
673 constructor(resizeCallback) {
674 super();
675 this._resizeCallback = resizeCallback;
676 }
677
678 /**
679 * @override
680 */
681 onResize() {
682 this._resizeCallback();
683 }
684};
685
686/**
687 * @unrestricted
688 */
689UI.WidgetFocusRestorer = class {
690 /**
691 * @param {!UI.Widget} widget
692 */
693 constructor(widget) {
694 this._widget = widget;
695 this._previous = widget.element.ownerDocument.deepActiveElement();
696 widget.focus();
697 }
698
699 restore() {
700 if (!this._widget)
701 return;
702 if (this._widget.hasFocus() && this._previous)
703 this._previous.focus();
704 this._previous = null;
705 this._widget = null;
706 }
707};
708
709/**
710 * @override
711 * @param {?Node} child
712 * @return {!Node}
713 * @suppress {duplicate}
714 */
715Element.prototype.appendChild = function(child) {
716 UI.Widget.__assert(
717 !child.__widget || child.parentElement === this, 'Attempt to add widget via regular DOM operation.');
718 return UI.Widget._originalAppendChild.call(this, child);
719};
720
721/**
722 * @override
723 * @param {?Node} child
724 * @param {?Node} anchor
725 * @return {!Node}
726 * @suppress {duplicate}
727 */
728Element.prototype.insertBefore = function(child, anchor) {
729 UI.Widget.__assert(
730 !child.__widget || child.parentElement === this, 'Attempt to add widget via regular DOM operation.');
731 return UI.Widget._originalInsertBefore.call(this, child, anchor);
732};
733
734/**
735 * @override
736 * @param {?Node} child
737 * @return {!Node}
738 * @suppress {duplicate}
739 */
740Element.prototype.removeChild = function(child) {
741 UI.Widget.__assert(
742 !child.__widgetCounter && !child.__widget,
743 'Attempt to remove element containing widget via regular DOM operation');
744 return UI.Widget._originalRemoveChild.call(this, child);
745};
746
747Element.prototype.removeChildren = function() {
748 UI.Widget.__assert(!this.__widgetCounter, 'Attempt to remove element containing widget via regular DOM operation');
749 UI.Widget._originalRemoveChildren.call(this);
750};