Home | History | Annotate | Download | only in front_end
      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  */
     31 WebInspector.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 
     44 WebInspector.View._cssFileToVisibleViewCount = {};
     45 WebInspector.View._cssFileToStyleElement = {};
     46 WebInspector.View._cssUnloadTimeout = 2000;
     47 
     48 WebInspector.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 
    448 WebInspector.View._originalAppendChild = Element.prototype.appendChild;
    449 WebInspector.View._originalInsertBefore = Element.prototype.insertBefore;
    450 WebInspector.View._originalRemoveChild = Element.prototype.removeChild;
    451 WebInspector.View._originalRemoveChildren = Element.prototype.removeChildren;
    452 
    453 WebInspector.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 
    465 WebInspector.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 
    477 WebInspector.View._assert = function(condition, message)
    478 {
    479     if (!condition) {
    480         console.trace();
    481         throw new Error(message);
    482     }
    483 }
    484 
    485 /**
    486  * @interface
    487  */
    488 WebInspector.ViewFactory = function()
    489 {
    490 }
    491 
    492 WebInspector.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  */
    505 WebInspector.ViewWithResizeCallback = function(resizeCallback)
    506 {
    507     WebInspector.View.call(this);
    508     this._resizeCallback = resizeCallback;
    509 }
    510 
    511 WebInspector.ViewWithResizeCallback.prototype = {
    512     onResize: function()
    513     {
    514         this._resizeCallback();
    515     },
    516 
    517     __proto__: WebInspector.View.prototype
    518 }
    519 
    520 
    521 Element.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 
    527 Element.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 
    534 Element.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 
    540 Element.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