Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / third_party / WebKit / Source / devtools / front_end / ui / Widget.js
blob8ca45405faf8aad6bf7d3ff02813f8a1c91caa9a
1 /*
2 * Copyright (C) 2008 Apple Inc. All Rights Reserved.
3 * Copyright (C) 2011 Google Inc. All Rights Reserved.
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.
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.
27 /**
28 * @constructor
29 * @extends {WebInspector.Object}
30 * @param {boolean=} isWebComponent
32 WebInspector.Widget = function(isWebComponent)
34 this.contentElement = createElementWithClass("div", "widget");
35 if (isWebComponent) {
36 this.element = createElementWithClass("div", "vbox flex-auto");
37 this._shadowRoot = WebInspector.createShadowRootWithCoreStyles(this.element);
38 this._shadowRoot.appendChild(this.contentElement);
39 } else {
40 this.element = this.contentElement;
42 this._isWebComponent = isWebComponent;
43 this.element.__widget = this;
44 this._visible = true;
45 this._isRoot = false;
46 this._isShowing = false;
47 this._children = [];
48 this._hideOnDetach = false;
49 this._notificationDepth = 0;
52 /**
53 * @param {string} cssFile
54 * @return {!Element}
56 WebInspector.Widget.createStyleElement = function(cssFile)
58 var content = Runtime.cachedResources[cssFile] || "";
59 if (!content)
60 console.error(cssFile + " not preloaded. Check module.json");
61 var styleElement = createElement("style");
62 styleElement.type = "text/css";
63 styleElement.textContent = content;
64 return styleElement;
67 WebInspector.Widget.prototype = {
68 markAsRoot: function()
70 WebInspector.Widget.__assert(!this.element.parentElement, "Attempt to mark as root attached node");
71 this._isRoot = true;
74 /**
75 * @return {?WebInspector.Widget}
77 parentWidget: function()
79 return this._parentWidget;
82 /**
83 * @return {!Array.<!WebInspector.Widget>}
85 children: function()
87 return this._children;
90 /**
91 * @param {!WebInspector.Widget} widget
92 * @protected
94 childWasDetached: function(widget)
98 /**
99 * @return {boolean}
101 isShowing: function()
103 return this._isShowing;
107 * @return {boolean}
109 shouldHideOnDetach: function()
111 if (this._hideOnDetach)
112 return true;
113 for (var child of this._children) {
114 if (child.shouldHideOnDetach())
115 return true;
117 return false;
120 setHideOnDetach: function()
122 this._hideOnDetach = true;
126 * @return {boolean}
128 _inNotification: function()
130 return !!this._notificationDepth || (this._parentWidget && this._parentWidget._inNotification());
133 _parentIsShowing: function()
135 if (this._isRoot)
136 return true;
137 return this._parentWidget && this._parentWidget.isShowing();
141 * @param {function(this:WebInspector.Widget)} method
143 _callOnVisibleChildren: function(method)
145 var copy = this._children.slice();
146 for (var i = 0; i < copy.length; ++i) {
147 if (copy[i]._parentWidget === this && copy[i]._visible)
148 method.call(copy[i]);
152 _processWillShow: function()
154 this._callOnVisibleChildren(this._processWillShow);
155 this._isShowing = true;
158 _processWasShown: function()
160 if (this._inNotification())
161 return;
162 this.restoreScrollPositions();
163 this._notify(this.wasShown);
164 this._callOnVisibleChildren(this._processWasShown);
167 _processWillHide: function()
169 if (this._inNotification())
170 return;
171 this.storeScrollPositions();
173 this._callOnVisibleChildren(this._processWillHide);
174 this._notify(this.willHide);
175 this._isShowing = false;
178 _processWasHidden: function()
180 this._callOnVisibleChildren(this._processWasHidden);
183 _processOnResize: function()
185 if (this._inNotification())
186 return;
187 if (!this.isShowing())
188 return;
189 this._notify(this.onResize);
190 this._callOnVisibleChildren(this._processOnResize);
194 * @param {function(this:WebInspector.Widget)} notification
196 _notify: function(notification)
198 ++this._notificationDepth;
199 try {
200 notification.call(this);
201 } finally {
202 --this._notificationDepth;
206 wasShown: function()
210 willHide: function()
214 onResize: function()
218 onLayout: function()
223 * @param {?Element} parentElement
224 * @param {?Element=} insertBefore
226 show: function(parentElement, insertBefore)
228 WebInspector.Widget.__assert(parentElement, "Attempt to attach widget with no parent element");
230 // Update widget hierarchy.
231 if (this.element.parentElement !== parentElement) {
232 if (this.element.parentElement)
233 this.detach();
235 var currentParent = parentElement;
236 while (currentParent && !currentParent.__widget)
237 currentParent = currentParent.parentElementOrShadowHost();
239 if (currentParent) {
240 this._parentWidget = currentParent.__widget;
241 this._parentWidget._children.push(this);
242 this._isRoot = false;
243 } else
244 WebInspector.Widget.__assert(this._isRoot, "Attempt to attach widget to orphan node");
245 } else if (this._visible) {
246 return;
249 this._visible = true;
251 if (this._parentIsShowing())
252 this._processWillShow();
254 this.element.classList.remove("hidden");
256 // Reparent
257 if (this.element.parentElement !== parentElement) {
258 WebInspector.Widget._incrementWidgetCounter(parentElement, this.element);
259 if (insertBefore)
260 WebInspector.Widget._originalInsertBefore.call(parentElement, this.element, insertBefore);
261 else
262 WebInspector.Widget._originalAppendChild.call(parentElement, this.element);
265 if (this._parentIsShowing())
266 this._processWasShown();
268 if (this._parentWidget && this._hasNonZeroConstraints())
269 this._parentWidget.invalidateConstraints();
270 else
271 this._processOnResize();
275 * @param {boolean=} overrideHideOnDetach
277 detach: function(overrideHideOnDetach)
279 var parentElement = this.element.parentElement;
280 if (!parentElement)
281 return;
283 if (this._parentIsShowing())
284 this._processWillHide();
286 if (!overrideHideOnDetach && this.shouldHideOnDetach()) {
287 this.element.classList.add("hidden");
288 this._visible = false;
289 if (this._parentIsShowing())
290 this._processWasHidden();
291 if (this._parentWidget && this._hasNonZeroConstraints())
292 this._parentWidget.invalidateConstraints();
293 return;
296 // Force legal removal
297 WebInspector.Widget._decrementWidgetCounter(parentElement, this.element);
298 WebInspector.Widget._originalRemoveChild.call(parentElement, this.element);
300 this._visible = false;
301 if (this._parentIsShowing())
302 this._processWasHidden();
304 // Update widget hierarchy.
305 if (this._parentWidget) {
306 var childIndex = this._parentWidget._children.indexOf(this);
307 WebInspector.Widget.__assert(childIndex >= 0, "Attempt to remove non-child widget");
308 this._parentWidget._children.splice(childIndex, 1);
309 this._parentWidget.childWasDetached(this);
310 var parent = this._parentWidget;
311 this._parentWidget = null;
312 if (this._hasNonZeroConstraints())
313 parent.invalidateConstraints();
314 } else
315 WebInspector.Widget.__assert(this._isRoot, "Removing non-root widget from DOM");
318 detachChildWidgets: function()
320 var children = this._children.slice();
321 for (var i = 0; i < children.length; ++i)
322 children[i].detach();
326 * @return {!Array.<!Element>}
328 elementsToRestoreScrollPositionsFor: function()
330 return [this.element];
333 storeScrollPositions: function()
335 var elements = this.elementsToRestoreScrollPositionsFor();
336 for (var i = 0; i < elements.length; ++i) {
337 var container = elements[i];
338 container._scrollTop = container.scrollTop;
339 container._scrollLeft = container.scrollLeft;
343 restoreScrollPositions: function()
345 var elements = this.elementsToRestoreScrollPositionsFor();
346 for (var i = 0; i < elements.length; ++i) {
347 var container = elements[i];
348 if (container._scrollTop)
349 container.scrollTop = container._scrollTop;
350 if (container._scrollLeft)
351 container.scrollLeft = container._scrollLeft;
355 doResize: function()
357 if (!this.isShowing())
358 return;
359 // No matter what notification we are in, dispatching onResize is not needed.
360 if (!this._inNotification())
361 this._callOnVisibleChildren(this._processOnResize);
364 doLayout: function()
366 if (!this.isShowing())
367 return;
368 this._notify(this.onLayout);
369 this.doResize();
373 * @param {string} cssFile
375 registerRequiredCSS: function(cssFile)
377 (this._isWebComponent ? this._shadowRoot : this.element).appendChild(WebInspector.Widget.createStyleElement(cssFile));
380 printWidgetHierarchy: function()
382 var lines = [];
383 this._collectWidgetHierarchy("", lines);
384 console.log(lines.join("\n"));
387 _collectWidgetHierarchy: function(prefix, lines)
389 lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : ""));
391 for (var i = 0; i < this._children.length; ++i)
392 this._children[i]._collectWidgetHierarchy(prefix + " ", lines);
394 if (this._children.length)
395 lines.push(prefix + "}");
399 * @return {!Element}
401 defaultFocusedElement: function()
403 return this._defaultFocusedElement || this.element;
407 * @param {!Element} element
409 setDefaultFocusedElement: function(element)
411 this._defaultFocusedElement = element;
414 focus: function()
416 var element = this.defaultFocusedElement();
417 if (!element || element.isAncestor(this.element.ownerDocument.activeElement))
418 return;
420 WebInspector.setCurrentFocusElement(element);
424 * @return {boolean}
426 hasFocus: function()
428 var activeElement = this.element.ownerDocument.activeElement;
429 return activeElement && activeElement.isSelfOrDescendant(this.element);
433 * @return {!Size}
435 measurePreferredSize: function()
437 var document = this.element.ownerDocument;
438 WebInspector.Widget._originalAppendChild.call(document.body, this.element);
439 this.element.positionAt(0, 0);
440 var result = new Size(this.element.offsetWidth, this.element.offsetHeight);
441 this.element.positionAt(undefined, undefined);
442 WebInspector.Widget._originalRemoveChild.call(document.body, this.element);
443 return result;
447 * @return {!Constraints}
449 calculateConstraints: function()
451 return new Constraints();
455 * @return {!Constraints}
457 constraints: function()
459 if (typeof this._constraints !== "undefined")
460 return this._constraints;
461 if (typeof this._cachedConstraints === "undefined")
462 this._cachedConstraints = this.calculateConstraints();
463 return this._cachedConstraints;
467 * @param {number} width
468 * @param {number} height
469 * @param {number} preferredWidth
470 * @param {number} preferredHeight
472 setMinimumAndPreferredSizes: function(width, height, preferredWidth, preferredHeight)
474 this._constraints = new Constraints(new Size(width, height), new Size(preferredWidth, preferredHeight));
475 this.invalidateConstraints();
479 * @param {number} width
480 * @param {number} height
482 setMinimumSize: function(width, height)
484 this._constraints = new Constraints(new Size(width, height));
485 this.invalidateConstraints();
489 * @return {boolean}
491 _hasNonZeroConstraints: function()
493 var constraints = this.constraints();
494 return !!(constraints.minimum.width || constraints.minimum.height || constraints.preferred.width || constraints.preferred.height);
497 invalidateConstraints: function()
499 var cached = this._cachedConstraints;
500 delete this._cachedConstraints;
501 var actual = this.constraints();
502 if (!actual.isEqual(cached) && this._parentWidget)
503 this._parentWidget.invalidateConstraints();
504 else
505 this.doLayout();
508 __proto__: WebInspector.Object.prototype
511 WebInspector.Widget._originalAppendChild = Element.prototype.appendChild;
512 WebInspector.Widget._originalInsertBefore = Element.prototype.insertBefore;
513 WebInspector.Widget._originalRemoveChild = Element.prototype.removeChild;
514 WebInspector.Widget._originalRemoveChildren = Element.prototype.removeChildren;
516 WebInspector.Widget._incrementWidgetCounter = function(parentElement, childElement)
518 var count = (childElement.__widgetCounter || 0) + (childElement.__widget ? 1 : 0);
519 if (!count)
520 return;
522 while (parentElement) {
523 parentElement.__widgetCounter = (parentElement.__widgetCounter || 0) + count;
524 parentElement = parentElement.parentElementOrShadowHost();
528 WebInspector.Widget._decrementWidgetCounter = function(parentElement, childElement)
530 var count = (childElement.__widgetCounter || 0) + (childElement.__widget ? 1 : 0);
531 if (!count)
532 return;
534 while (parentElement) {
535 parentElement.__widgetCounter -= count;
536 parentElement = parentElement.parentElementOrShadowHost();
540 WebInspector.Widget.__assert = function(condition, message)
542 if (!condition) {
543 console.trace();
544 throw new Error(message);
549 * @constructor
550 * @extends {WebInspector.Widget}
551 * @param {boolean=} isWebComponent
553 WebInspector.VBox = function(isWebComponent)
555 WebInspector.Widget.call(this, isWebComponent);
556 this.contentElement.classList.add("vbox");
559 WebInspector.VBox.prototype = {
561 * @override
562 * @return {!Constraints}
564 calculateConstraints: function()
566 var constraints = new Constraints();
569 * @this {!WebInspector.Widget}
570 * @suppressReceiverCheck
572 function updateForChild()
574 var child = this.constraints();
575 constraints = constraints.widthToMax(child);
576 constraints = constraints.addHeight(child);
579 this._callOnVisibleChildren(updateForChild);
580 return constraints;
583 __proto__: WebInspector.Widget.prototype
587 * @constructor
588 * @extends {WebInspector.Widget}
589 * @param {boolean=} isWebComponent
591 WebInspector.HBox = function(isWebComponent)
593 WebInspector.Widget.call(this, isWebComponent);
594 this.contentElement.classList.add("hbox");
597 WebInspector.HBox.prototype = {
599 * @override
600 * @return {!Constraints}
602 calculateConstraints: function()
604 var constraints = new Constraints();
607 * @this {!WebInspector.Widget}
608 * @suppressReceiverCheck
610 function updateForChild()
612 var child = this.constraints();
613 constraints = constraints.addWidth(child);
614 constraints = constraints.heightToMax(child);
617 this._callOnVisibleChildren(updateForChild);
618 return constraints;
621 __proto__: WebInspector.Widget.prototype
625 * @constructor
626 * @extends {WebInspector.VBox}
627 * @param {function()} resizeCallback
629 WebInspector.VBoxWithResizeCallback = function(resizeCallback)
631 WebInspector.VBox.call(this);
632 this._resizeCallback = resizeCallback;
635 WebInspector.VBoxWithResizeCallback.prototype = {
636 onResize: function()
638 this._resizeCallback();
641 __proto__: WebInspector.VBox.prototype
645 * @override
646 * @param {?Node} child
647 * @return {?Node}
648 * @suppress {duplicate}
650 Element.prototype.appendChild = function(child)
652 WebInspector.Widget.__assert(!child.__widget || child.parentElement === this, "Attempt to add widget via regular DOM operation.");
653 return WebInspector.Widget._originalAppendChild.call(this, child);
657 * @override
658 * @param {?Node} child
659 * @param {?Node} anchor
660 * @return {!Node}
661 * @suppress {duplicate}
663 Element.prototype.insertBefore = function(child, anchor)
665 WebInspector.Widget.__assert(!child.__widget || child.parentElement === this, "Attempt to add widget via regular DOM operation.");
666 return WebInspector.Widget._originalInsertBefore.call(this, child, anchor);
670 * @override
671 * @param {?Node} child
672 * @return {!Node}
673 * @suppress {duplicate}
675 Element.prototype.removeChild = function(child)
677 WebInspector.Widget.__assert(!child.__widgetCounter && !child.__widget, "Attempt to remove element containing widget via regular DOM operation");
678 return WebInspector.Widget._originalRemoveChild.call(this, child);
681 Element.prototype.removeChildren = function()
683 WebInspector.Widget.__assert(!this.__widgetCounter, "Attempt to remove element containing widget via regular DOM operation");
684 WebInspector.Widget._originalRemoveChildren.call(this);