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
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.
29 * @extends {WebInspector.Object}
31 WebInspector.View = function()
33 this.element = document.createElement("div");
34 this.element.className = "view";
35 this.element.__view = this;
38 this._isShowing = false;
40 this._hideOnDetach = false;
42 this._notificationDepth = 0;
45 WebInspector.View._cssFileToVisibleViewCount = {};
46 WebInspector.View._cssFileToStyleElement = {};
47 WebInspector.View._cssUnloadTimeout = 2000;
49 WebInspector.View.prototype = {
50 markAsRoot: function()
52 WebInspector.View._assert(!this.element.parentElement, "Attempt to mark as root attached node");
56 makeLayoutBoundary: function()
58 this._isLayoutBoundary = true;
62 * @return {?WebInspector.View}
64 parentView: function()
66 return this._parentView;
74 return this._isShowing;
77 setHideOnDetach: function()
79 this._hideOnDetach = true;
85 _inNotification: function()
87 return !!this._notificationDepth || (this._parentView && this._parentView._inNotification());
90 _parentIsShowing: function()
94 return this._parentView && this._parentView.isShowing();
98 * @param {function(this:WebInspector.View)} method
100 _callOnVisibleChildren: function(method)
102 var copy = this._children.slice();
103 for (var i = 0; i < copy.length; ++i) {
104 if (copy[i]._parentView === this && copy[i]._visible)
105 method.call(copy[i]);
109 _processWillShow: function()
111 this._loadCSSIfNeeded();
112 this._callOnVisibleChildren(this._processWillShow);
113 this._isShowing = true;
116 _processWasShown: function()
118 if (this._inNotification())
120 this.restoreScrollPositions();
121 this._notify(this.wasShown);
122 this._notify(this.onResize);
123 this._callOnVisibleChildren(this._processWasShown);
126 _processWillHide: function()
128 if (this._inNotification())
130 this.storeScrollPositions();
132 this._callOnVisibleChildren(this._processWillHide);
133 this._notify(this.willHide);
134 this._isShowing = false;
137 _processWasHidden: function()
139 this._disableCSSIfNeeded();
140 this._callOnVisibleChildren(this._processWasHidden);
143 _processOnResize: function()
145 if (this._inNotification())
147 if (!this.isShowing())
149 this._notify(this.onResize);
150 this._callOnVisibleChildren(this._processOnResize);
153 _processDiscardCachedSize: function()
155 if (this._isLayoutBoundary) {
156 this.element.style.removeProperty("width");
157 this.element.style.removeProperty("height");
159 this._callOnVisibleChildren(this._processDiscardCachedSize);
162 _cacheSize: function()
164 this._prepareCacheSize();
165 this._applyCacheSize();
168 _prepareCacheSize: function()
170 if (this._isLayoutBoundary) {
171 this._cachedOffsetWidth = this.element.offsetWidth;
172 this._cachedOffsetHeight = this.element.offsetHeight;
174 this._callOnVisibleChildren(this._prepareCacheSize);
177 _applyCacheSize: function()
179 if (this._isLayoutBoundary) {
180 this.element.style.setProperty("width", this._cachedOffsetWidth + "px");
181 this.element.style.setProperty("height", this._cachedOffsetHeight + "px");
182 delete this._cachedOffsetWidth;
183 delete this._cachedOffsetHeight;
185 this._callOnVisibleChildren(this._applyCacheSize);
189 * @param {function(this:WebInspector.View)} notification
191 _notify: function(notification)
193 ++this._notificationDepth;
195 notification.call(this);
197 --this._notificationDepth;
214 * @param {?Element} parentElement
215 * @param {!Element=} insertBefore
217 show: function(parentElement, insertBefore)
219 WebInspector.View._assert(parentElement, "Attempt to attach view with no parent element");
221 // Update view hierarchy
222 if (this.element.parentElement !== parentElement) {
223 if (this.element.parentElement)
226 var currentParent = parentElement;
227 while (currentParent && !currentParent.__view)
228 currentParent = currentParent.parentElement;
231 this._parentView = currentParent.__view;
232 this._parentView._children.push(this);
233 this._isRoot = false;
235 WebInspector.View._assert(this._isRoot, "Attempt to attach view to orphan node");
236 } else if (this._visible) {
240 this._visible = true;
242 if (this._parentIsShowing())
243 this._processWillShow();
245 this.element.classList.add("visible");
248 if (this.element.parentElement !== parentElement) {
249 WebInspector.View._incrementViewCounter(parentElement, this.element);
251 WebInspector.View._originalInsertBefore.call(parentElement, this.element, insertBefore);
253 WebInspector.View._originalAppendChild.call(parentElement, this.element);
256 if (this._parentIsShowing()) {
257 this._processWasShown();
263 * @param {boolean=} overrideHideOnDetach
265 detach: function(overrideHideOnDetach)
267 var parentElement = this.element.parentElement;
271 if (this._parentIsShowing()) {
272 this._processDiscardCachedSize();
273 this._processWillHide();
276 if (this._hideOnDetach && !overrideHideOnDetach) {
277 this.element.classList.remove("visible");
278 this._visible = false;
279 if (this._parentIsShowing())
280 this._processWasHidden();
284 // Force legal removal
285 WebInspector.View._decrementViewCounter(parentElement, this.element);
286 WebInspector.View._originalRemoveChild.call(parentElement, this.element);
288 this._visible = false;
289 if (this._parentIsShowing())
290 this._processWasHidden();
292 // Update view hierarchy
293 if (this._parentView) {
294 var childIndex = this._parentView._children.indexOf(this);
295 WebInspector.View._assert(childIndex >= 0, "Attempt to remove non-child view");
296 this._parentView._children.splice(childIndex, 1);
297 this._parentView = null;
299 WebInspector.View._assert(this._isRoot, "Removing non-root view from DOM");
302 detachChildViews: function()
304 var children = this._children.slice();
305 for (var i = 0; i < children.length; ++i)
306 children[i].detach();
310 * @return {!Array.<!Element>}
312 elementsToRestoreScrollPositionsFor: function()
314 return [this.element];
317 storeScrollPositions: function()
319 var elements = this.elementsToRestoreScrollPositionsFor();
320 for (var i = 0; i < elements.length; ++i) {
321 var container = elements[i];
322 container._scrollTop = container.scrollTop;
323 container._scrollLeft = container.scrollLeft;
327 restoreScrollPositions: function()
329 var elements = this.elementsToRestoreScrollPositionsFor();
330 for (var i = 0; i < elements.length; ++i) {
331 var container = elements[i];
332 if (container._scrollTop)
333 container.scrollTop = container._scrollTop;
334 if (container._scrollLeft)
335 container.scrollLeft = container._scrollLeft;
342 canHighlightPosition: function()
348 * @param {number} line
349 * @param {number=} column
351 highlightPosition: function(line, column)
357 if (!this.isShowing())
359 this._processDiscardCachedSize();
360 // No matter what notification we are in, dispatching onResize is not needed.
361 if (!this._inNotification())
362 this._callOnVisibleChildren(this._processOnResize);
366 registerRequiredCSS: function(cssFile)
368 if (window.flattenImports)
369 cssFile = cssFile.split("/").reverse()[0];
370 this._cssFiles.push(cssFile);
373 _loadCSSIfNeeded: function()
375 for (var i = 0; i < this._cssFiles.length; ++i) {
376 var cssFile = this._cssFiles[i];
378 var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile];
379 WebInspector.View._cssFileToVisibleViewCount[cssFile] = (viewsWithCSSFile || 0) + 1;
380 if (!viewsWithCSSFile)
381 this._doLoadCSS(cssFile);
385 _doLoadCSS: function(cssFile)
387 var styleElement = WebInspector.View._cssFileToStyleElement[cssFile];
389 styleElement.disabled = false;
393 if (window.debugCSS) { /* debugging support */
394 styleElement = document.createElement("link");
395 styleElement.rel = "stylesheet";
396 styleElement.type = "text/css";
397 styleElement.href = cssFile;
399 var xhr = new XMLHttpRequest();
400 xhr.open("GET", cssFile, false);
403 styleElement = document.createElement("style");
404 styleElement.type = "text/css";
405 styleElement.textContent = xhr.responseText + this._buildSourceURL(cssFile);
407 document.head.insertBefore(styleElement, document.head.firstChild);
409 WebInspector.View._cssFileToStyleElement[cssFile] = styleElement;
412 _buildSourceURL: function(cssFile)
414 return "\n/*# sourceURL=" + WebInspector.ParsedURL.completeURL(window.location.href, cssFile) + " */";
417 _disableCSSIfNeeded: function()
419 var scheduleUnload = !!WebInspector.View._cssUnloadTimer;
421 for (var i = 0; i < this._cssFiles.length; ++i) {
422 var cssFile = this._cssFiles[i];
424 if (!--WebInspector.View._cssFileToVisibleViewCount[cssFile])
425 scheduleUnload = true;
428 function doUnloadCSS()
430 delete WebInspector.View._cssUnloadTimer;
432 for (cssFile in WebInspector.View._cssFileToVisibleViewCount) {
433 if (WebInspector.View._cssFileToVisibleViewCount.hasOwnProperty(cssFile)
434 && !WebInspector.View._cssFileToVisibleViewCount[cssFile])
435 WebInspector.View._cssFileToStyleElement[cssFile].disabled = true;
439 if (scheduleUnload) {
440 if (WebInspector.View._cssUnloadTimer)
441 clearTimeout(WebInspector.View._cssUnloadTimer);
443 WebInspector.View._cssUnloadTimer = setTimeout(doUnloadCSS, WebInspector.View._cssUnloadTimeout)
447 printViewHierarchy: function()
450 this._collectViewHierarchy("", lines);
451 console.log(lines.join("\n"));
454 _collectViewHierarchy: function(prefix, lines)
456 lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : ""));
458 for (var i = 0; i < this._children.length; ++i)
459 this._children[i]._collectViewHierarchy(prefix + " ", lines);
461 if (this._children.length)
462 lines.push(prefix + "}");
468 defaultFocusedElement: function()
470 return this._defaultFocusedElement || this.element;
474 * @param {!Element} element
476 setDefaultFocusedElement: function(element)
478 this._defaultFocusedElement = element;
483 var element = this.defaultFocusedElement();
484 if (!element || element.isAncestor(document.activeElement))
487 WebInspector.setCurrentFocusElement(element);
493 measurePreferredSize: function()
495 this._loadCSSIfNeeded();
496 WebInspector.View._originalAppendChild.call(document.body, this.element);
497 this.element.positionAt(0, 0);
498 var result = new Size(this.element.offsetWidth, this.element.offsetHeight);
499 this.element.positionAt(undefined, undefined);
500 WebInspector.View._originalRemoveChild.call(document.body, this.element);
501 this._disableCSSIfNeeded();
505 __proto__: WebInspector.Object.prototype
508 WebInspector.View._originalAppendChild = Element.prototype.appendChild;
509 WebInspector.View._originalInsertBefore = Element.prototype.insertBefore;
510 WebInspector.View._originalRemoveChild = Element.prototype.removeChild;
511 WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren;
513 WebInspector.View._incrementViewCounter = function(parentElement, childElement)
515 var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
519 while (parentElement) {
520 parentElement.__viewCounter = (parentElement.__viewCounter || 0) + count;
521 parentElement = parentElement.parentElement;
525 WebInspector.View._decrementViewCounter = function(parentElement, childElement)
527 var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0);
531 while (parentElement) {
532 parentElement.__viewCounter -= count;
533 parentElement = parentElement.parentElement;
537 WebInspector.View._assert = function(condition, message)
541 throw new Error(message);
547 * @extends {WebInspector.View}
548 * @param {function()} resizeCallback
550 WebInspector.ViewWithResizeCallback = function(resizeCallback)
552 WebInspector.View.call(this);
553 this._resizeCallback = resizeCallback;
556 WebInspector.ViewWithResizeCallback.prototype = {
559 this._resizeCallback();
562 __proto__: WebInspector.View.prototype
565 Element.prototype.appendChild = function(child)
567 WebInspector.View._assert(!child.__view || child.parentElement === this, "Attempt to add view via regular DOM operation.");
568 return WebInspector.View._originalAppendChild.call(this, child);
571 Element.prototype.insertBefore = function(child, anchor)
573 WebInspector.View._assert(!child.__view || child.parentElement === this, "Attempt to add view via regular DOM operation.");
574 return WebInspector.View._originalInsertBefore.call(this, child, anchor);
578 Element.prototype.removeChild = function(child)
580 WebInspector.View._assert(!child.__viewCounter && !child.__view, "Attempt to remove element containing view via regular DOM operation");
581 return WebInspector.View._originalRemoveChild.call(this, child);
584 Element.prototype.removeChildren = function()
586 WebInspector.View._assert(!this.__viewCounter, "Attempt to remove element containing view via regular DOM operation");
587 WebInspector.View._originalRemoveChildren.call(this);