Popover.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466
  1. /*
  2. * Copyright (C) 2009 Google Inc. All rights reserved.
  3. *
  4. * Redistribution and use in source and binary forms, with or without
  5. * modification, are permitted provided that the following conditions are
  6. * met:
  7. *
  8. * * Redistributions of source code must retain the above copyright
  9. * notice, this list of conditions and the following disclaimer.
  10. * * Redistributions in binary form must reproduce the above
  11. * copyright notice, this list of conditions and the following disclaimer
  12. * in the documentation and/or other materials provided with the
  13. * distribution.
  14. * * Neither the name of Google Inc. nor the names of its
  15. * contributors may be used to endorse or promote products derived from
  16. * this software without specific prior written permission.
  17. *
  18. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  19. * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  20. * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  21. * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  22. * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  23. * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  24. * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  25. * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  26. * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  27. * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  28. * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  29. */
  30. /**
  31. * @constructor
  32. * @extends {WebInspector.View}
  33. * @param {WebInspector.PopoverHelper=} popoverHelper
  34. */
  35. WebInspector.Popover = function(popoverHelper)
  36. {
  37. WebInspector.View.call(this);
  38. this.markAsRoot();
  39. this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll";
  40. this._popupArrowElement = document.createElement("div");
  41. this._popupArrowElement.className = "arrow";
  42. this.element.appendChild(this._popupArrowElement);
  43. this._contentDiv = document.createElement("div");
  44. this._contentDiv.className = "content";
  45. this.element.appendChild(this._contentDiv);
  46. this._popoverHelper = popoverHelper;
  47. }
  48. WebInspector.Popover.prototype = {
  49. /**
  50. * @param {Element} element
  51. * @param {Element|AnchorBox} anchor
  52. * @param {?number=} preferredWidth
  53. * @param {?number=} preferredHeight
  54. * @param {?WebInspector.Popover.Orientation=} arrowDirection
  55. */
  56. show: function(element, anchor, preferredWidth, preferredHeight, arrowDirection)
  57. {
  58. this._innerShow(null, element, anchor, preferredWidth, preferredHeight, arrowDirection);
  59. },
  60. /**
  61. * @param {WebInspector.View} view
  62. * @param {Element|AnchorBox} anchor
  63. * @param {?number=} preferredWidth
  64. * @param {?number=} preferredHeight
  65. */
  66. showView: function(view, anchor, preferredWidth, preferredHeight)
  67. {
  68. this._innerShow(view, view.element, anchor, preferredWidth, preferredHeight);
  69. },
  70. /**
  71. * @param {WebInspector.View?} view
  72. * @param {Element} contentElement
  73. * @param {Element|AnchorBox} anchor
  74. * @param {?number=} preferredWidth
  75. * @param {?number=} preferredHeight
  76. * @param {?WebInspector.Popover.Orientation=} arrowDirection
  77. */
  78. _innerShow: function(view, contentElement, anchor, preferredWidth, preferredHeight, arrowDirection)
  79. {
  80. if (this._disposed)
  81. return;
  82. this.contentElement = contentElement;
  83. // This should not happen, but we hide previous popup to be on the safe side.
  84. if (WebInspector.Popover._popover)
  85. WebInspector.Popover._popover.detach();
  86. WebInspector.Popover._popover = this;
  87. // Temporarily attach in order to measure preferred dimensions.
  88. var preferredSize = view ? view.measurePreferredSize() : this.contentElement.measurePreferredSize();
  89. preferredWidth = preferredWidth || preferredSize.width;
  90. preferredHeight = preferredHeight || preferredSize.height;
  91. WebInspector.View.prototype.show.call(this, document.body);
  92. if (view)
  93. view.show(this._contentDiv);
  94. else
  95. this._contentDiv.appendChild(this.contentElement);
  96. this._positionElement(anchor, preferredWidth, preferredHeight, arrowDirection);
  97. if (this._popoverHelper) {
  98. this._contentDiv.addEventListener("mousemove", this._popoverHelper._killHidePopoverTimer.bind(this._popoverHelper), true);
  99. this.element.addEventListener("mouseout", this._popoverHelper._popoverMouseOut.bind(this._popoverHelper), true);
  100. }
  101. },
  102. hide: function()
  103. {
  104. this.detach();
  105. delete WebInspector.Popover._popover;
  106. },
  107. get disposed()
  108. {
  109. return this._disposed;
  110. },
  111. dispose: function()
  112. {
  113. if (this.isShowing())
  114. this.hide();
  115. this._disposed = true;
  116. },
  117. setCanShrink: function(canShrink)
  118. {
  119. this._hasFixedHeight = !canShrink;
  120. this._contentDiv.addStyleClass("fixed-height");
  121. },
  122. /**
  123. * @param {Element|AnchorBox} anchorElement
  124. * @param {number} preferredWidth
  125. * @param {number} preferredHeight
  126. * @param {?WebInspector.Popover.Orientation=} arrowDirection
  127. */
  128. _positionElement: function(anchorElement, preferredWidth, preferredHeight, arrowDirection)
  129. {
  130. const borderWidth = 25;
  131. const scrollerWidth = this._hasFixedHeight ? 0 : 11;
  132. const arrowHeight = 15;
  133. const arrowOffset = 10;
  134. const borderRadius = 10;
  135. // Skinny tooltips are not pretty, their arrow location is not nice.
  136. preferredWidth = Math.max(preferredWidth, 50);
  137. const totalWidth = window.innerWidth;
  138. const totalHeight = window.innerHeight;
  139. var anchorBox = anchorElement instanceof AnchorBox ? anchorElement : anchorElement.boxInWindow(window);
  140. var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight };
  141. var verticalAlignment;
  142. var roomAbove = anchorBox.y;
  143. var roomBelow = totalHeight - anchorBox.y - anchorBox.height;
  144. if ((roomAbove > roomBelow) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) {
  145. // Positioning above the anchor.
  146. if ((anchorBox.y > newElementPosition.height + arrowHeight + borderRadius) || (arrowDirection === WebInspector.Popover.Orientation.Bottom))
  147. newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight;
  148. else {
  149. newElementPosition.y = borderRadius;
  150. newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight;
  151. if (this._hasFixedHeight && newElementPosition.height < preferredHeight) {
  152. newElementPosition.y = borderRadius;
  153. newElementPosition.height = preferredHeight;
  154. }
  155. }
  156. verticalAlignment = WebInspector.Popover.Orientation.Bottom;
  157. } else {
  158. // Positioning below the anchor.
  159. newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight;
  160. if ((newElementPosition.y + newElementPosition.height + arrowHeight - borderWidth >= totalHeight) && (arrowDirection !== WebInspector.Popover.Orientation.Top)) {
  161. newElementPosition.height = totalHeight - anchorBox.y - anchorBox.height - borderRadius * 2 - arrowHeight;
  162. if (this._hasFixedHeight && newElementPosition.height < preferredHeight) {
  163. newElementPosition.y = totalHeight - preferredHeight - borderRadius;
  164. newElementPosition.height = preferredHeight;
  165. }
  166. }
  167. // Align arrow.
  168. verticalAlignment = WebInspector.Popover.Orientation.Top;
  169. }
  170. var horizontalAlignment;
  171. if (anchorBox.x + newElementPosition.width < totalWidth) {
  172. newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset);
  173. horizontalAlignment = "left";
  174. } else if (newElementPosition.width + borderRadius * 2 < totalWidth) {
  175. newElementPosition.x = totalWidth - newElementPosition.width - borderRadius;
  176. horizontalAlignment = "right";
  177. // Position arrow accurately.
  178. var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset);
  179. arrowRightPosition += anchorBox.width / 2;
  180. arrowRightPosition = Math.min(arrowRightPosition, newElementPosition.width - borderRadius - arrowOffset);
  181. this._popupArrowElement.style.right = arrowRightPosition + "px";
  182. } else {
  183. newElementPosition.x = borderRadius;
  184. newElementPosition.width = totalWidth - borderRadius * 2;
  185. newElementPosition.height += scrollerWidth;
  186. horizontalAlignment = "left";
  187. if (verticalAlignment === WebInspector.Popover.Orientation.Bottom)
  188. newElementPosition.y -= scrollerWidth;
  189. // Position arrow accurately.
  190. this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px";
  191. this._popupArrowElement.style.left += anchorBox.width / 2;
  192. }
  193. this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll " + verticalAlignment + "-" + horizontalAlignment + "-arrow";
  194. this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth);
  195. this.element.style.width = newElementPosition.width + borderWidth * 2 + "px";
  196. this.element.style.height = newElementPosition.height + borderWidth * 2 + "px";
  197. },
  198. __proto__: WebInspector.View.prototype
  199. }
  200. /**
  201. * @constructor
  202. * @param {Element} panelElement
  203. * @param {function(Element, Event):(Element|AnchorBox)|undefined} getAnchor
  204. * @param {function(Element, WebInspector.Popover):undefined} showPopover
  205. * @param {function()=} onHide
  206. * @param {boolean=} disableOnClick
  207. */
  208. WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopover, onHide, disableOnClick)
  209. {
  210. this._panelElement = panelElement;
  211. this._getAnchor = getAnchor;
  212. this._showPopover = showPopover;
  213. this._onHide = onHide;
  214. this._disableOnClick = !!disableOnClick;
  215. panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false);
  216. panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false);
  217. panelElement.addEventListener("mouseout", this._mouseOut.bind(this), false);
  218. this.setTimeout(1000);
  219. }
  220. WebInspector.PopoverHelper.prototype = {
  221. setTimeout: function(timeout)
  222. {
  223. this._timeout = timeout;
  224. },
  225. /**
  226. * @param {MouseEvent} event
  227. * @return {boolean}
  228. */
  229. _eventInHoverElement: function(event)
  230. {
  231. if (!this._hoverElement)
  232. return false;
  233. var box = this._hoverElement instanceof AnchorBox ? this._hoverElement : this._hoverElement.boxInWindow();
  234. return (box.x <= event.clientX && event.clientX <= box.x + box.width &&
  235. box.y <= event.clientY && event.clientY <= box.y + box.height);
  236. },
  237. _mouseDown: function(event)
  238. {
  239. if (this._disableOnClick || !this._eventInHoverElement(event))
  240. this.hidePopover();
  241. else {
  242. this._killHidePopoverTimer();
  243. this._handleMouseAction(event, true);
  244. }
  245. },
  246. _mouseMove: function(event)
  247. {
  248. // Pretend that nothing has happened.
  249. if (this._eventInHoverElement(event))
  250. return;
  251. this._startHidePopoverTimer();
  252. this._handleMouseAction(event, false);
  253. },
  254. _popoverMouseOut: function(event)
  255. {
  256. if (!this.isPopoverVisible())
  257. return;
  258. if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this._popover._contentDiv))
  259. this._startHidePopoverTimer();
  260. },
  261. _mouseOut: function(event)
  262. {
  263. if (!this.isPopoverVisible())
  264. return;
  265. if (!this._eventInHoverElement(event))
  266. this._startHidePopoverTimer();
  267. },
  268. _startHidePopoverTimer: function()
  269. {
  270. // User has 500ms (this._timeout / 2) to reach the popup.
  271. if (!this._popover || this._hidePopoverTimer)
  272. return;
  273. function doHide()
  274. {
  275. this._hidePopover();
  276. delete this._hidePopoverTimer;
  277. }
  278. this._hidePopoverTimer = setTimeout(doHide.bind(this), this._timeout / 2);
  279. },
  280. _handleMouseAction: function(event, isMouseDown)
  281. {
  282. this._resetHoverTimer();
  283. if (event.which && this._disableOnClick)
  284. return;
  285. this._hoverElement = this._getAnchor(event.target, event);
  286. if (!this._hoverElement)
  287. return;
  288. const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout);
  289. this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay);
  290. },
  291. _resetHoverTimer: function()
  292. {
  293. if (this._hoverTimer) {
  294. clearTimeout(this._hoverTimer);
  295. delete this._hoverTimer;
  296. }
  297. },
  298. isPopoverVisible: function()
  299. {
  300. return !!this._popover;
  301. },
  302. hidePopover: function()
  303. {
  304. this._resetHoverTimer();
  305. this._hidePopover();
  306. },
  307. _hidePopover: function()
  308. {
  309. if (!this._popover)
  310. return;
  311. if (this._onHide)
  312. this._onHide();
  313. this._popover.dispose();
  314. delete this._popover;
  315. this._hoverElement = null;
  316. },
  317. _mouseHover: function(element)
  318. {
  319. delete this._hoverTimer;
  320. this._hidePopover();
  321. this._popover = new WebInspector.Popover(this);
  322. this._showPopover(element, this._popover);
  323. },
  324. _killHidePopoverTimer: function()
  325. {
  326. if (this._hidePopoverTimer) {
  327. clearTimeout(this._hidePopoverTimer);
  328. delete this._hidePopoverTimer;
  329. // We know that we reached the popup, but we might have moved over other elements.
  330. // Discard pending command.
  331. this._resetHoverTimer();
  332. }
  333. }
  334. }
  335. /** @enum {string} */
  336. WebInspector.Popover.Orientation = {
  337. Top: "top",
  338. Bottom: "bottom"
  339. }
  340. /**
  341. * @constructor
  342. * @param {string} title
  343. */
  344. WebInspector.PopoverContentHelper = function(title)
  345. {
  346. this._contentTable = document.createElement("table");
  347. var titleCell = this._createCell(WebInspector.UIString("%s - Details", title), "popover-details-title");
  348. titleCell.colSpan = 2;
  349. var titleRow = document.createElement("tr");
  350. titleRow.appendChild(titleCell);
  351. this._contentTable.appendChild(titleRow);
  352. }
  353. WebInspector.PopoverContentHelper.prototype = {
  354. contentTable: function()
  355. {
  356. return this._contentTable;
  357. },
  358. /**
  359. * @param {string=} styleName
  360. */
  361. _createCell: function(content, styleName)
  362. {
  363. var text = document.createElement("label");
  364. text.appendChild(document.createTextNode(content));
  365. var cell = document.createElement("td");
  366. cell.className = "popover-details";
  367. if (styleName)
  368. cell.className += " " + styleName;
  369. cell.textContent = content;
  370. return cell;
  371. },
  372. appendTextRow: function(title, content)
  373. {
  374. var row = document.createElement("tr");
  375. row.appendChild(this._createCell(title, "popover-details-row-title"));
  376. row.appendChild(this._createCell(content, "popover-details-row-data"));
  377. this._contentTable.appendChild(row);
  378. },
  379. /**
  380. * @param {string=} titleStyle
  381. */
  382. appendElementRow: function(title, content, titleStyle)
  383. {
  384. var row = document.createElement("tr");
  385. var titleCell = this._createCell(title, "popover-details-row-title");
  386. if (titleStyle)
  387. titleCell.addStyleClass(titleStyle);
  388. row.appendChild(titleCell);
  389. var cell = document.createElement("td");
  390. cell.className = "details";
  391. cell.appendChild(content);
  392. row.appendChild(cell);
  393. this._contentTable.appendChild(row);
  394. },
  395. appendStackTrace: function(title, stackTrace, callFrameLinkifier)
  396. {
  397. this.appendTextRow("", "");
  398. var framesTable = document.createElement("table");
  399. for (var i = 0; i < stackTrace.length; ++i) {
  400. var stackFrame = stackTrace[i];
  401. var row = document.createElement("tr");
  402. row.className = "details";
  403. row.appendChild(this._createCell(stackFrame.functionName ? stackFrame.functionName : WebInspector.UIString("(anonymous function)"), "function-name"));
  404. row.appendChild(this._createCell(" @ "));
  405. var linkCell = document.createElement("td");
  406. var urlElement = callFrameLinkifier(stackFrame);
  407. linkCell.appendChild(urlElement);
  408. row.appendChild(linkCell);
  409. framesTable.appendChild(row);
  410. }
  411. this.appendElementRow(title, framesTable, "popover-stacktrace-title");
  412. }
  413. }