View.js 15 KB

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