View.js 15 KB

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