• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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