| /* |
| * Copyright (C) 2008 Apple Inc. All Rights Reserved. |
| * Copyright (C) 2011 Google Inc. All Rights Reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE |
| * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR |
| * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY |
| * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
| * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.Object} |
| */ |
| WebInspector.View = function() |
| { |
| this.element = document.createElement("div"); |
| this.element.__view = this; |
| this._visible = true; |
| this._isRoot = false; |
| this._isShowing = false; |
| this._children = []; |
| this._hideOnDetach = false; |
| this._cssFiles = []; |
| this._notificationDepth = 0; |
| } |
| |
| WebInspector.View._cssFileToVisibleViewCount = {}; |
| WebInspector.View._cssFileToStyleElement = {}; |
| WebInspector.View._cssUnloadTimeout = 2000; |
| |
| WebInspector.View.prototype = { |
| markAsRoot: function() |
| { |
| WebInspector.View._assert(!this.element.parentElement, "Attempt to mark as root attached node"); |
| this._isRoot = true; |
| }, |
| |
| /** |
| * @return {?WebInspector.View} |
| */ |
| parentView: function() |
| { |
| return this._parentView; |
| }, |
| |
| isShowing: function() |
| { |
| return this._isShowing; |
| }, |
| |
| setHideOnDetach: function() |
| { |
| this._hideOnDetach = true; |
| }, |
| |
| /** |
| * @return {boolean} |
| */ |
| _inNotification: function() |
| { |
| return !!this._notificationDepth || (this._parentView && this._parentView._inNotification()); |
| }, |
| |
| _parentIsShowing: function() |
| { |
| if (this._isRoot) |
| return true; |
| return this._parentView && this._parentView.isShowing(); |
| }, |
| |
| /** |
| * @param {function(this:WebInspector.View)} method |
| */ |
| _callOnVisibleChildren: function(method) |
| { |
| var copy = this._children.slice(); |
| for (var i = 0; i < copy.length; ++i) { |
| if (copy[i]._parentView === this && copy[i]._visible) |
| method.call(copy[i]); |
| } |
| }, |
| |
| _processWillShow: function() |
| { |
| this._loadCSSIfNeeded(); |
| this._callOnVisibleChildren(this._processWillShow); |
| this._isShowing = true; |
| }, |
| |
| _processWasShown: function() |
| { |
| if (this._inNotification()) |
| return; |
| this.restoreScrollPositions(); |
| this._notify(this.wasShown); |
| this._notify(this.onResize); |
| this._callOnVisibleChildren(this._processWasShown); |
| }, |
| |
| _processWillHide: function() |
| { |
| if (this._inNotification()) |
| return; |
| this.storeScrollPositions(); |
| |
| this._callOnVisibleChildren(this._processWillHide); |
| this._notify(this.willHide); |
| this._isShowing = false; |
| }, |
| |
| _processWasHidden: function() |
| { |
| this._disableCSSIfNeeded(); |
| this._callOnVisibleChildren(this._processWasHidden); |
| }, |
| |
| _processOnResize: function() |
| { |
| if (this._inNotification()) |
| return; |
| if (!this.isShowing()) |
| return; |
| this._notify(this.onResize); |
| this._callOnVisibleChildren(this._processOnResize); |
| }, |
| |
| /** |
| * @param {function(this:WebInspector.View)} notification |
| */ |
| _notify: function(notification) |
| { |
| ++this._notificationDepth; |
| try { |
| notification.call(this); |
| } finally { |
| --this._notificationDepth; |
| } |
| }, |
| |
| wasShown: function() |
| { |
| }, |
| |
| willHide: function() |
| { |
| }, |
| |
| onResize: function() |
| { |
| }, |
| |
| /** |
| * @param {Element} parentElement |
| * @param {Element=} insertBefore |
| */ |
| show: function(parentElement, insertBefore) |
| { |
| WebInspector.View._assert(parentElement, "Attempt to attach view with no parent element"); |
| |
| // Update view hierarchy |
| if (this.element.parentElement !== parentElement) { |
| if (this.element.parentElement) |
| this.detach(); |
| |
| var currentParent = parentElement; |
| while (currentParent && !currentParent.__view) |
| currentParent = currentParent.parentElement; |
| |
| if (currentParent) { |
| this._parentView = currentParent.__view; |
| this._parentView._children.push(this); |
| this._isRoot = false; |
| } else |
| WebInspector.View._assert(this._isRoot, "Attempt to attach view to orphan node"); |
| } else if (this._visible) |
| return; |
| |
| this._visible = true; |
| |
| if (this._parentIsShowing()) |
| this._processWillShow(); |
| |
| this.element.addStyleClass("visible"); |
| |
| // Reparent |
| if (this.element.parentElement !== parentElement) { |
| WebInspector.View._incrementViewCounter(parentElement, this.element); |
| if (insertBefore) |
| WebInspector.View._originalInsertBefore.call(parentElement, this.element, insertBefore); |
| else |
| WebInspector.View._originalAppendChild.call(parentElement, this.element); |
| } |
| |
| if (this._parentIsShowing()) |
| this._processWasShown(); |
| }, |
| |
| /** |
| * @param {boolean=} overrideHideOnDetach |
| */ |
| detach: function(overrideHideOnDetach) |
| { |
| var parentElement = this.element.parentElement; |
| if (!parentElement) |
| return; |
| |
| if (this._parentIsShowing()) |
| this._processWillHide(); |
| |
| if (this._hideOnDetach && !overrideHideOnDetach) { |
| this.element.removeStyleClass("visible"); |
| this._visible = false; |
| if (this._parentIsShowing()) |
| this._processWasHidden(); |
| return; |
| } |
| |
| // Force legal removal |
| WebInspector.View._decrementViewCounter(parentElement, this.element); |
| WebInspector.View._originalRemoveChild.call(parentElement, this.element); |
| |
| this._visible = false; |
| if (this._parentIsShowing()) |
| this._processWasHidden(); |
| |
| // Update view hierarchy |
| if (this._parentView) { |
| var childIndex = this._parentView._children.indexOf(this); |
| WebInspector.View._assert(childIndex >= 0, "Attempt to remove non-child view"); |
| this._parentView._children.splice(childIndex, 1); |
| this._parentView = null; |
| } else |
| WebInspector.View._assert(this._isRoot, "Removing non-root view from DOM"); |
| }, |
| |
| detachChildViews: function() |
| { |
| var children = this._children.slice(); |
| for (var i = 0; i < children.length; ++i) |
| children[i].detach(); |
| }, |
| |
| elementsToRestoreScrollPositionsFor: function() |
| { |
| return [this.element]; |
| }, |
| |
| storeScrollPositions: function() |
| { |
| var elements = this.elementsToRestoreScrollPositionsFor(); |
| for (var i = 0; i < elements.length; ++i) { |
| var container = elements[i]; |
| container._scrollTop = container.scrollTop; |
| container._scrollLeft = container.scrollLeft; |
| } |
| }, |
| |
| restoreScrollPositions: function() |
| { |
| var elements = this.elementsToRestoreScrollPositionsFor(); |
| for (var i = 0; i < elements.length; ++i) { |
| var container = elements[i]; |
| if (container._scrollTop) |
| container.scrollTop = container._scrollTop; |
| if (container._scrollLeft) |
| container.scrollLeft = container._scrollLeft; |
| } |
| }, |
| |
| canHighlightPosition: function() |
| { |
| return false; |
| }, |
| |
| /** |
| * @param {number} line |
| * @param {number=} column |
| */ |
| highlightPosition: function(line, column) |
| { |
| }, |
| |
| doResize: function() |
| { |
| this._processOnResize(); |
| }, |
| |
| registerRequiredCSS: function(cssFile) |
| { |
| if (window.flattenImports) |
| cssFile = cssFile.split("/").reverse()[0]; |
| this._cssFiles.push(cssFile); |
| }, |
| |
| _loadCSSIfNeeded: function() |
| { |
| for (var i = 0; i < this._cssFiles.length; ++i) { |
| var cssFile = this._cssFiles[i]; |
| |
| var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile]; |
| WebInspector.View._cssFileToVisibleViewCount[cssFile] = (viewsWithCSSFile || 0) + 1; |
| if (!viewsWithCSSFile) |
| this._doLoadCSS(cssFile); |
| } |
| }, |
| |
| _doLoadCSS: function(cssFile) |
| { |
| var styleElement = WebInspector.View._cssFileToStyleElement[cssFile]; |
| if (styleElement) { |
| styleElement.disabled = false; |
| return; |
| } |
| |
| if (window.debugCSS) { /* debugging support */ |
| styleElement = document.createElement("link"); |
| styleElement.rel = "stylesheet"; |
| styleElement.type = "text/css"; |
| styleElement.href = cssFile; |
| } else { |
| var xhr = new XMLHttpRequest(); |
| xhr.open("GET", cssFile, false); |
| xhr.send(null); |
| |
| styleElement = document.createElement("style"); |
| styleElement.type = "text/css"; |
| styleElement.textContent = xhr.responseText + this._buildSourceURL(cssFile); |
| } |
| document.head.insertBefore(styleElement, document.head.firstChild); |
| |
| WebInspector.View._cssFileToStyleElement[cssFile] = styleElement; |
| }, |
| |
| _buildSourceURL: function(cssFile) |
| { |
| return "\n/*# sourceURL=" + WebInspector.ParsedURL.completeURL(window.location.href, cssFile) + " */"; |
| }, |
| |
| _disableCSSIfNeeded: function() |
| { |
| var scheduleUnload = !!WebInspector.View._cssUnloadTimer; |
| |
| for (var i = 0; i < this._cssFiles.length; ++i) { |
| var cssFile = this._cssFiles[i]; |
| |
| if (!--WebInspector.View._cssFileToVisibleViewCount[cssFile]) |
| scheduleUnload = true; |
| } |
| |
| function doUnloadCSS() |
| { |
| delete WebInspector.View._cssUnloadTimer; |
| |
| for (cssFile in WebInspector.View._cssFileToVisibleViewCount) { |
| if (WebInspector.View._cssFileToVisibleViewCount.hasOwnProperty(cssFile) |
| && !WebInspector.View._cssFileToVisibleViewCount[cssFile]) |
| WebInspector.View._cssFileToStyleElement[cssFile].disabled = true; |
| } |
| } |
| |
| if (scheduleUnload) { |
| if (WebInspector.View._cssUnloadTimer) |
| clearTimeout(WebInspector.View._cssUnloadTimer); |
| |
| WebInspector.View._cssUnloadTimer = setTimeout(doUnloadCSS, WebInspector.View._cssUnloadTimeout) |
| } |
| }, |
| |
| printViewHierarchy: function() |
| { |
| var lines = []; |
| this._collectViewHierarchy("", lines); |
| console.log(lines.join("\n")); |
| }, |
| |
| _collectViewHierarchy: function(prefix, lines) |
| { |
| lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : "")); |
| |
| for (var i = 0; i < this._children.length; ++i) |
| this._children[i]._collectViewHierarchy(prefix + " ", lines); |
| |
| if (this._children.length) |
| lines.push(prefix + "}"); |
| }, |
| |
| /** |
| * @return {Element} |
| */ |
| defaultFocusedElement: function() |
| { |
| return this._defaultFocusedElement || this.element; |
| }, |
| |
| /** |
| * @param {Element} element |
| */ |
| setDefaultFocusedElement: function(element) |
| { |
| this._defaultFocusedElement = element; |
| }, |
| |
| focus: function() |
| { |
| var element = this.defaultFocusedElement(); |
| if (!element || element.isAncestor(document.activeElement)) |
| return; |
| |
| WebInspector.setCurrentFocusElement(element); |
| }, |
| |
| /** |
| * @return {Size} |
| */ |
| measurePreferredSize: function() |
| { |
| this._loadCSSIfNeeded(); |
| WebInspector.View._originalAppendChild.call(document.body, this.element); |
| this.element.positionAt(0, 0); |
| var result = new Size(this.element.offsetWidth, this.element.offsetHeight); |
| this.element.positionAt(undefined, undefined); |
| WebInspector.View._originalRemoveChild.call(document.body, this.element); |
| this._disableCSSIfNeeded(); |
| return result; |
| }, |
| |
| __proto__: WebInspector.Object.prototype |
| } |
| |
| WebInspector.View._originalAppendChild = Element.prototype.appendChild; |
| WebInspector.View._originalInsertBefore = Element.prototype.insertBefore; |
| WebInspector.View._originalRemoveChild = Element.prototype.removeChild; |
| WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren; |
| |
| WebInspector.View._incrementViewCounter = function(parentElement, childElement) |
| { |
| var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0); |
| if (!count) |
| return; |
| |
| while (parentElement) { |
| parentElement.__viewCounter = (parentElement.__viewCounter || 0) + count; |
| parentElement = parentElement.parentElement; |
| } |
| } |
| |
| WebInspector.View._decrementViewCounter = function(parentElement, childElement) |
| { |
| var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0); |
| if (!count) |
| return; |
| |
| while (parentElement) { |
| parentElement.__viewCounter -= count; |
| parentElement = parentElement.parentElement; |
| } |
| } |
| |
| WebInspector.View._assert = function(condition, message) |
| { |
| if (!condition) { |
| console.trace(); |
| throw new Error(message); |
| } |
| } |
| |
| /** |
| * @interface |
| */ |
| WebInspector.ViewFactory = function() |
| { |
| } |
| |
| WebInspector.ViewFactory.prototype = { |
| /** |
| * @param {string=} id |
| * @return {WebInspector.View} |
| */ |
| createView: function(id) {} |
| } |
| |
| /** |
| * @constructor |
| * @extends {WebInspector.View} |
| * @param {function()} resizeCallback |
| */ |
| WebInspector.ViewWithResizeCallback = function(resizeCallback) |
| { |
| WebInspector.View.call(this); |
| this._resizeCallback = resizeCallback; |
| } |
| |
| WebInspector.ViewWithResizeCallback.prototype = { |
| onResize: function() |
| { |
| this._resizeCallback(); |
| }, |
| |
| __proto__: WebInspector.View.prototype |
| } |
| |
| |
| Element.prototype.appendChild = function(child) |
| { |
| WebInspector.View._assert(!child.__view, "Attempt to add view via regular DOM operation."); |
| return WebInspector.View._originalAppendChild.call(this, child); |
| } |
| |
| Element.prototype.insertBefore = function(child, anchor) |
| { |
| WebInspector.View._assert(!child.__view, "Attempt to add view via regular DOM operation."); |
| return WebInspector.View._originalInsertBefore.call(this, child, anchor); |
| } |
| |
| |
| Element.prototype.removeChild = function(child) |
| { |
| WebInspector.View._assert(!child.__viewCounter && !child.__view, "Attempt to remove element containing view via regular DOM operation"); |
| return WebInspector.View._originalRemoveChild.call(this, child); |
| } |
| |
| Element.prototype.removeChildren = function() |
| { |
| WebInspector.View._assert(!this.__viewCounter, "Attempt to remove element containing view via regular DOM operation"); |
| WebInspector.View._originalRemoveChildren.call(this); |
| } |