1/* 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 * @constructor 29 * @extends {WebInspector.Object} 30 */ 31WebInspector.View = function() 32{ 33 this.element = document.createElement("div"); 34 this.element.__view = this; 35 this._visible = true; 36 this._isRoot = false; 37 this._isShowing = false; 38 this._children = []; 39 this._hideOnDetach = false; 40 this._cssFiles = []; 41 this._notificationDepth = 0; 42} 43 44WebInspector.View._cssFileToVisibleViewCount = {}; 45WebInspector.View._cssFileToStyleElement = {}; 46WebInspector.View._cssUnloadTimeout = 2000; 47 48WebInspector.View.prototype = { 49 markAsRoot: function() 50 { 51 WebInspector.View._assert(!this.element.parentElement, "Attempt to mark as root attached node"); 52 this._isRoot = true; 53 }, 54 55 /** 56 * @return {?WebInspector.View} 57 */ 58 parentView: function() 59 { 60 return this._parentView; 61 }, 62 63 isShowing: function() 64 { 65 return this._isShowing; 66 }, 67 68 setHideOnDetach: function() 69 { 70 this._hideOnDetach = true; 71 }, 72 73 /** 74 * @return {boolean} 75 */ 76 _inNotification: function() 77 { 78 return !!this._notificationDepth || (this._parentView && this._parentView._inNotification()); 79 }, 80 81 _parentIsShowing: function() 82 { 83 if (this._isRoot) 84 return true; 85 return this._parentView && this._parentView.isShowing(); 86 }, 87 88 /** 89 * @param {function(this:WebInspector.View)} method 90 */ 91 _callOnVisibleChildren: function(method) 92 { 93 var copy = this._children.slice(); 94 for (var i = 0; i < copy.length; ++i) { 95 if (copy[i]._parentView === this && copy[i]._visible) 96 method.call(copy[i]); 97 } 98 }, 99 100 _processWillShow: function() 101 { 102 this._loadCSSIfNeeded(); 103 this._callOnVisibleChildren(this._processWillShow); 104 this._isShowing = true; 105 }, 106 107 _processWasShown: function() 108 { 109 if (this._inNotification()) 110 return; 111 this.restoreScrollPositions(); 112 this._notify(this.wasShown); 113 this._notify(this.onResize); 114 this._callOnVisibleChildren(this._processWasShown); 115 }, 116 117 _processWillHide: function() 118 { 119 if (this._inNotification()) 120 return; 121 this.storeScrollPositions(); 122 123 this._callOnVisibleChildren(this._processWillHide); 124 this._notify(this.willHide); 125 this._isShowing = false; 126 }, 127 128 _processWasHidden: function() 129 { 130 this._disableCSSIfNeeded(); 131 this._callOnVisibleChildren(this._processWasHidden); 132 }, 133 134 _processOnResize: function() 135 { 136 if (this._inNotification()) 137 return; 138 if (!this.isShowing()) 139 return; 140 this._notify(this.onResize); 141 this._callOnVisibleChildren(this._processOnResize); 142 }, 143 144 /** 145 * @param {function(this:WebInspector.View)} notification 146 */ 147 _notify: function(notification) 148 { 149 ++this._notificationDepth; 150 try { 151 notification.call(this); 152 } finally { 153 --this._notificationDepth; 154 } 155 }, 156 157 wasShown: function() 158 { 159 }, 160 161 willHide: function() 162 { 163 }, 164 165 onResize: function() 166 { 167 }, 168 169 /** 170 * @param {?Element} parentElement 171 * @param {!Element=} insertBefore 172 */ 173 show: function(parentElement, insertBefore) 174 { 175 WebInspector.View._assert(parentElement, "Attempt to attach view with no parent element"); 176 177 // Update view hierarchy 178 if (this.element.parentElement !== parentElement) { 179 if (this.element.parentElement) 180 this.detach(); 181 182 var currentParent = parentElement; 183 while (currentParent && !currentParent.__view) 184 currentParent = currentParent.parentElement; 185 186 if (currentParent) { 187 this._parentView = currentParent.__view; 188 this._parentView._children.push(this); 189 this._isRoot = false; 190 } else 191 WebInspector.View._assert(this._isRoot, "Attempt to attach view to orphan node"); 192 } else if (this._visible) { 193 return; 194 } 195 196 this._visible = true; 197 198 if (this._parentIsShowing()) 199 this._processWillShow(); 200 201 this.element.classList.add("visible"); 202 203 // Reparent 204 if (this.element.parentElement !== parentElement) { 205 WebInspector.View._incrementViewCounter(parentElement, this.element); 206 if (insertBefore) 207 WebInspector.View._originalInsertBefore.call(parentElement, this.element, insertBefore); 208 else 209 WebInspector.View._originalAppendChild.call(parentElement, this.element); 210 } 211 212 if (this._parentIsShowing()) 213 this._processWasShown(); 214 }, 215 216 /** 217 * @param {boolean=} overrideHideOnDetach 218 */ 219 detach: function(overrideHideOnDetach) 220 { 221 var parentElement = this.element.parentElement; 222 if (!parentElement) 223 return; 224 225 if (this._parentIsShowing()) 226 this._processWillHide(); 227 228 if (this._hideOnDetach && !overrideHideOnDetach) { 229 this.element.classList.remove("visible"); 230 this._visible = false; 231 if (this._parentIsShowing()) 232 this._processWasHidden(); 233 return; 234 } 235 236 // Force legal removal 237 WebInspector.View._decrementViewCounter(parentElement, this.element); 238 WebInspector.View._originalRemoveChild.call(parentElement, this.element); 239 240 this._visible = false; 241 if (this._parentIsShowing()) 242 this._processWasHidden(); 243 244 // Update view hierarchy 245 if (this._parentView) { 246 var childIndex = this._parentView._children.indexOf(this); 247 WebInspector.View._assert(childIndex >= 0, "Attempt to remove non-child view"); 248 this._parentView._children.splice(childIndex, 1); 249 this._parentView = null; 250 } else 251 WebInspector.View._assert(this._isRoot, "Removing non-root view from DOM"); 252 }, 253 254 detachChildViews: function() 255 { 256 var children = this._children.slice(); 257 for (var i = 0; i < children.length; ++i) 258 children[i].detach(); 259 }, 260 261 elementsToRestoreScrollPositionsFor: function() 262 { 263 return [this.element]; 264 }, 265 266 storeScrollPositions: function() 267 { 268 var elements = this.elementsToRestoreScrollPositionsFor(); 269 for (var i = 0; i < elements.length; ++i) { 270 var container = elements[i]; 271 container._scrollTop = container.scrollTop; 272 container._scrollLeft = container.scrollLeft; 273 } 274 }, 275 276 restoreScrollPositions: function() 277 { 278 var elements = this.elementsToRestoreScrollPositionsFor(); 279 for (var i = 0; i < elements.length; ++i) { 280 var container = elements[i]; 281 if (container._scrollTop) 282 container.scrollTop = container._scrollTop; 283 if (container._scrollLeft) 284 container.scrollLeft = container._scrollLeft; 285 } 286 }, 287 288 canHighlightPosition: function() 289 { 290 return false; 291 }, 292 293 /** 294 * @param {number} line 295 * @param {number=} column 296 */ 297 highlightPosition: function(line, column) 298 { 299 }, 300 301 doResize: function() 302 { 303 this._processOnResize(); 304 }, 305 306 registerRequiredCSS: function(cssFile) 307 { 308 if (window.flattenImports) 309 cssFile = cssFile.split("/").reverse()[0]; 310 this._cssFiles.push(cssFile); 311 }, 312 313 _loadCSSIfNeeded: function() 314 { 315 for (var i = 0; i < this._cssFiles.length; ++i) { 316 var cssFile = this._cssFiles[i]; 317 318 var viewsWithCSSFile = WebInspector.View._cssFileToVisibleViewCount[cssFile]; 319 WebInspector.View._cssFileToVisibleViewCount[cssFile] = (viewsWithCSSFile || 0) + 1; 320 if (!viewsWithCSSFile) 321 this._doLoadCSS(cssFile); 322 } 323 }, 324 325 _doLoadCSS: function(cssFile) 326 { 327 var styleElement = WebInspector.View._cssFileToStyleElement[cssFile]; 328 if (styleElement) { 329 styleElement.disabled = false; 330 return; 331 } 332 333 if (window.debugCSS) { /* debugging support */ 334 styleElement = document.createElement("link"); 335 styleElement.rel = "stylesheet"; 336 styleElement.type = "text/css"; 337 styleElement.href = cssFile; 338 } else { 339 var xhr = new XMLHttpRequest(); 340 xhr.open("GET", cssFile, false); 341 xhr.send(null); 342 343 styleElement = document.createElement("style"); 344 styleElement.type = "text/css"; 345 styleElement.textContent = xhr.responseText + this._buildSourceURL(cssFile); 346 } 347 document.head.insertBefore(styleElement, document.head.firstChild); 348 349 WebInspector.View._cssFileToStyleElement[cssFile] = styleElement; 350 }, 351 352 _buildSourceURL: function(cssFile) 353 { 354 return "\n/*# sourceURL=" + WebInspector.ParsedURL.completeURL(window.location.href, cssFile) + " */"; 355 }, 356 357 _disableCSSIfNeeded: function() 358 { 359 var scheduleUnload = !!WebInspector.View._cssUnloadTimer; 360 361 for (var i = 0; i < this._cssFiles.length; ++i) { 362 var cssFile = this._cssFiles[i]; 363 364 if (!--WebInspector.View._cssFileToVisibleViewCount[cssFile]) 365 scheduleUnload = true; 366 } 367 368 function doUnloadCSS() 369 { 370 delete WebInspector.View._cssUnloadTimer; 371 372 for (cssFile in WebInspector.View._cssFileToVisibleViewCount) { 373 if (WebInspector.View._cssFileToVisibleViewCount.hasOwnProperty(cssFile) 374 && !WebInspector.View._cssFileToVisibleViewCount[cssFile]) 375 WebInspector.View._cssFileToStyleElement[cssFile].disabled = true; 376 } 377 } 378 379 if (scheduleUnload) { 380 if (WebInspector.View._cssUnloadTimer) 381 clearTimeout(WebInspector.View._cssUnloadTimer); 382 383 WebInspector.View._cssUnloadTimer = setTimeout(doUnloadCSS, WebInspector.View._cssUnloadTimeout) 384 } 385 }, 386 387 printViewHierarchy: function() 388 { 389 var lines = []; 390 this._collectViewHierarchy("", lines); 391 console.log(lines.join("\n")); 392 }, 393 394 _collectViewHierarchy: function(prefix, lines) 395 { 396 lines.push(prefix + "[" + this.element.className + "]" + (this._children.length ? " {" : "")); 397 398 for (var i = 0; i < this._children.length; ++i) 399 this._children[i]._collectViewHierarchy(prefix + " ", lines); 400 401 if (this._children.length) 402 lines.push(prefix + "}"); 403 }, 404 405 /** 406 * @return {!Element} 407 */ 408 defaultFocusedElement: function() 409 { 410 return this._defaultFocusedElement || this.element; 411 }, 412 413 /** 414 * @param {!Element} element 415 */ 416 setDefaultFocusedElement: function(element) 417 { 418 this._defaultFocusedElement = element; 419 }, 420 421 focus: function() 422 { 423 var element = this.defaultFocusedElement(); 424 if (!element || element.isAncestor(document.activeElement)) 425 return; 426 427 WebInspector.setCurrentFocusElement(element); 428 }, 429 430 /** 431 * @return {!Size} 432 */ 433 measurePreferredSize: function() 434 { 435 this._loadCSSIfNeeded(); 436 WebInspector.View._originalAppendChild.call(document.body, this.element); 437 this.element.positionAt(0, 0); 438 var result = new Size(this.element.offsetWidth, this.element.offsetHeight); 439 this.element.positionAt(undefined, undefined); 440 WebInspector.View._originalRemoveChild.call(document.body, this.element); 441 this._disableCSSIfNeeded(); 442 return result; 443 }, 444 445 __proto__: WebInspector.Object.prototype 446} 447 448WebInspector.View._originalAppendChild = Element.prototype.appendChild; 449WebInspector.View._originalInsertBefore = Element.prototype.insertBefore; 450WebInspector.View._originalRemoveChild = Element.prototype.removeChild; 451WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren; 452 453WebInspector.View._incrementViewCounter = function(parentElement, childElement) 454{ 455 var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0); 456 if (!count) 457 return; 458 459 while (parentElement) { 460 parentElement.__viewCounter = (parentElement.__viewCounter || 0) + count; 461 parentElement = parentElement.parentElement; 462 } 463} 464 465WebInspector.View._decrementViewCounter = function(parentElement, childElement) 466{ 467 var count = (childElement.__viewCounter || 0) + (childElement.__view ? 1 : 0); 468 if (!count) 469 return; 470 471 while (parentElement) { 472 parentElement.__viewCounter -= count; 473 parentElement = parentElement.parentElement; 474 } 475} 476 477WebInspector.View._assert = function(condition, message) 478{ 479 if (!condition) { 480 console.trace(); 481 throw new Error(message); 482 } 483} 484 485/** 486 * @interface 487 */ 488WebInspector.ViewFactory = function() 489{ 490} 491 492WebInspector.ViewFactory.prototype = { 493 /** 494 * @param {string=} id 495 * @return {?WebInspector.View} 496 */ 497 createView: function(id) {} 498} 499 500/** 501 * @constructor 502 * @extends {WebInspector.View} 503 * @param {function()} resizeCallback 504 */ 505WebInspector.ViewWithResizeCallback = function(resizeCallback) 506{ 507 WebInspector.View.call(this); 508 this._resizeCallback = resizeCallback; 509} 510 511WebInspector.ViewWithResizeCallback.prototype = { 512 onResize: function() 513 { 514 this._resizeCallback(); 515 }, 516 517 __proto__: WebInspector.View.prototype 518} 519 520 521Element.prototype.appendChild = function(child) 522{ 523 WebInspector.View._assert(!child.__view, "Attempt to add view via regular DOM operation."); 524 return WebInspector.View._originalAppendChild.call(this, child); 525} 526 527Element.prototype.insertBefore = function(child, anchor) 528{ 529 WebInspector.View._assert(!child.__view, "Attempt to add view via regular DOM operation."); 530 return WebInspector.View._originalInsertBefore.call(this, child, anchor); 531} 532 533 534Element.prototype.removeChild = function(child) 535{ 536 WebInspector.View._assert(!child.__viewCounter && !child.__view, "Attempt to remove element containing view via regular DOM operation"); 537 return WebInspector.View._originalRemoveChild.call(this, child); 538} 539 540Element.prototype.removeChildren = function() 541{ 542 WebInspector.View._assert(!this.__viewCounter, "Attempt to remove element containing view via regular DOM operation"); 543 WebInspector.View._originalRemoveChildren.call(this); 544} 545