tooltip.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. /**
  2. * @license
  3. * Visual Blocks Editor
  4. *
  5. * Copyright 2011 Google Inc.
  6. * https://developers.google.com/blockly/
  7. *
  8. * Licensed under the Apache License, Version 2.0 (the "License");
  9. * you may not use this file except in compliance with the License.
  10. * You may obtain a copy of the License at
  11. *
  12. * http://www.apache.org/licenses/LICENSE-2.0
  13. *
  14. * Unless required by applicable law or agreed to in writing, software
  15. * distributed under the License is distributed on an "AS IS" BASIS,
  16. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  17. * See the License for the specific language governing permissions and
  18. * limitations under the License.
  19. */
  20. /**
  21. * @fileoverview Library to create tooltips for Blockly.
  22. * First, call Blockly.Tooltip.init() after onload.
  23. * Second, set the 'tooltip' property on any SVG element that needs a tooltip.
  24. * If the tooltip is a string, then that message will be displayed.
  25. * If the tooltip is an SVG element, then that object's tooltip will be used.
  26. * Third, call Blockly.Tooltip.bindMouseEvents(e) passing the SVG element.
  27. * @author fraser@google.com (Neil Fraser)
  28. */
  29. 'use strict';
  30. goog.provide('Blockly.Tooltip');
  31. goog.require('goog.dom');
  32. /**
  33. * Is a tooltip currently showing?
  34. */
  35. Blockly.Tooltip.visible = false;
  36. /**
  37. * Maximum width (in characters) of a tooltip.
  38. */
  39. Blockly.Tooltip.LIMIT = 50;
  40. /**
  41. * PID of suspended thread to clear tooltip on mouse out.
  42. * @private
  43. */
  44. Blockly.Tooltip.mouseOutPid_ = 0;
  45. /**
  46. * PID of suspended thread to show the tooltip.
  47. * @private
  48. */
  49. Blockly.Tooltip.showPid_ = 0;
  50. /**
  51. * Last observed X location of the mouse pointer (freezes when tooltip appears).
  52. * @private
  53. */
  54. Blockly.Tooltip.lastX_ = 0;
  55. /**
  56. * Last observed Y location of the mouse pointer (freezes when tooltip appears).
  57. * @private
  58. */
  59. Blockly.Tooltip.lastY_ = 0;
  60. /**
  61. * Current element being pointed at.
  62. * @private
  63. */
  64. Blockly.Tooltip.element_ = null;
  65. /**
  66. * Once a tooltip has opened for an element, that element is 'poisoned' and
  67. * cannot respawn a tooltip until the pointer moves over a different element.
  68. * @private
  69. */
  70. Blockly.Tooltip.poisonedElement_ = null;
  71. /**
  72. * Horizontal offset between mouse cursor and tooltip.
  73. */
  74. Blockly.Tooltip.OFFSET_X = 0;
  75. /**
  76. * Vertical offset between mouse cursor and tooltip.
  77. */
  78. Blockly.Tooltip.OFFSET_Y = 10;
  79. /**
  80. * Radius mouse can move before killing tooltip.
  81. */
  82. Blockly.Tooltip.RADIUS_OK = 10;
  83. /**
  84. * Delay before tooltip appears.
  85. */
  86. Blockly.Tooltip.HOVER_MS = 1000;
  87. /**
  88. * Horizontal padding between tooltip and screen edge.
  89. */
  90. Blockly.Tooltip.MARGINS = 5;
  91. /**
  92. * The HTML container. Set once by Blockly.Tooltip.createDom.
  93. * @type {Element}
  94. */
  95. Blockly.Tooltip.DIV = null;
  96. /**
  97. * Create the tooltip div and inject it onto the page.
  98. */
  99. Blockly.Tooltip.createDom = function() {
  100. if (Blockly.Tooltip.DIV) {
  101. return; // Already created.
  102. }
  103. // Create an HTML container for popup overlays (e.g. editor widgets).
  104. Blockly.Tooltip.DIV = goog.dom.createDom('div', 'blocklyTooltipDiv');
  105. document.body.appendChild(Blockly.Tooltip.DIV);
  106. };
  107. /**
  108. * Binds the required mouse events onto an SVG element.
  109. * @param {!Element} element SVG element onto which tooltip is to be bound.
  110. */
  111. Blockly.Tooltip.bindMouseEvents = function(element) {
  112. Blockly.bindEvent_(element, 'mouseover', null, Blockly.Tooltip.onMouseOver_);
  113. Blockly.bindEvent_(element, 'mouseout', null, Blockly.Tooltip.onMouseOut_);
  114. Blockly.bindEvent_(element, 'mousemove', null, Blockly.Tooltip.onMouseMove_);
  115. };
  116. /**
  117. * Hide the tooltip if the mouse is over a different object.
  118. * Initialize the tooltip to potentially appear for this object.
  119. * @param {!Event} e Mouse event.
  120. * @private
  121. */
  122. Blockly.Tooltip.onMouseOver_ = function(e) {
  123. // If the tooltip is an object, treat it as a pointer to the next object in
  124. // the chain to look at. Terminate when a string or function is found.
  125. var element = e.target;
  126. while (!goog.isString(element.tooltip) && !goog.isFunction(element.tooltip)) {
  127. element = element.tooltip;
  128. }
  129. if (Blockly.Tooltip.element_ != element) {
  130. Blockly.Tooltip.hide();
  131. Blockly.Tooltip.poisonedElement_ = null;
  132. Blockly.Tooltip.element_ = element;
  133. }
  134. // Forget about any immediately preceeding mouseOut event.
  135. clearTimeout(Blockly.Tooltip.mouseOutPid_);
  136. };
  137. /**
  138. * Hide the tooltip if the mouse leaves the object and enters the workspace.
  139. * @param {!Event} e Mouse event.
  140. * @private
  141. */
  142. Blockly.Tooltip.onMouseOut_ = function(e) {
  143. // Moving from one element to another (overlapping or with no gap) generates
  144. // a mouseOut followed instantly by a mouseOver. Fork off the mouseOut
  145. // event and kill it if a mouseOver is received immediately.
  146. // This way the task only fully executes if mousing into the void.
  147. Blockly.Tooltip.mouseOutPid_ = setTimeout(function() {
  148. Blockly.Tooltip.element_ = null;
  149. Blockly.Tooltip.poisonedElement_ = null;
  150. Blockly.Tooltip.hide();
  151. }, 1);
  152. clearTimeout(Blockly.Tooltip.showPid_);
  153. };
  154. /**
  155. * When hovering over an element, schedule a tooltip to be shown. If a tooltip
  156. * is already visible, hide it if the mouse strays out of a certain radius.
  157. * @param {!Event} e Mouse event.
  158. * @private
  159. */
  160. Blockly.Tooltip.onMouseMove_ = function(e) {
  161. if (!Blockly.Tooltip.element_ || !Blockly.Tooltip.element_.tooltip) {
  162. // No tooltip here to show.
  163. return;
  164. } else if (Blockly.dragMode_ != Blockly.DRAG_NONE) {
  165. // Don't display a tooltip during a drag.
  166. return;
  167. } else if (Blockly.WidgetDiv.isVisible()) {
  168. // Don't display a tooltip if a widget is open (tooltip would be under it).
  169. return;
  170. }
  171. if (Blockly.Tooltip.visible) {
  172. // Compute the distance between the mouse position when the tooltip was
  173. // shown and the current mouse position. Pythagorean theorem.
  174. var dx = Blockly.Tooltip.lastX_ - e.pageX;
  175. var dy = Blockly.Tooltip.lastY_ - e.pageY;
  176. if (Math.sqrt(dx * dx + dy * dy) > Blockly.Tooltip.RADIUS_OK) {
  177. Blockly.Tooltip.hide();
  178. }
  179. } else if (Blockly.Tooltip.poisonedElement_ != Blockly.Tooltip.element_) {
  180. // The mouse moved, clear any previously scheduled tooltip.
  181. clearTimeout(Blockly.Tooltip.showPid_);
  182. // Maybe this time the mouse will stay put. Schedule showing of tooltip.
  183. Blockly.Tooltip.lastX_ = e.pageX;
  184. Blockly.Tooltip.lastY_ = e.pageY;
  185. Blockly.Tooltip.showPid_ =
  186. setTimeout(Blockly.Tooltip.show_, Blockly.Tooltip.HOVER_MS);
  187. }
  188. };
  189. /**
  190. * Hide the tooltip.
  191. */
  192. Blockly.Tooltip.hide = function() {
  193. if (Blockly.Tooltip.visible) {
  194. Blockly.Tooltip.visible = false;
  195. if (Blockly.Tooltip.DIV) {
  196. Blockly.Tooltip.DIV.style.display = 'none';
  197. }
  198. }
  199. clearTimeout(Blockly.Tooltip.showPid_);
  200. };
  201. /**
  202. * Create the tooltip and show it.
  203. * @private
  204. */
  205. Blockly.Tooltip.show_ = function() {
  206. Blockly.Tooltip.poisonedElement_ = Blockly.Tooltip.element_;
  207. if (!Blockly.Tooltip.DIV) {
  208. return;
  209. }
  210. // Erase all existing text.
  211. goog.dom.removeChildren(/** @type {!Element} */ (Blockly.Tooltip.DIV));
  212. // Get the new text.
  213. var tip = Blockly.Tooltip.element_.tooltip;
  214. while (goog.isFunction(tip)) {
  215. tip = tip();
  216. }
  217. var tooltip_is_image = 0;
  218. if (tip.match(/^<img /)) {
  219. var div = document.createElement('div');
  220. var tmp = document.createElement('div');
  221. tmp.innerHTML = tip;
  222. var elem = tmp.getElementsByTagName('img')[0];
  223. // if elem contains a src attribute I'll use it
  224. if (elem['src']) {
  225. div.appendChild(elem);
  226. tooltip_is_image = 1;
  227. Blockly.Tooltip.DIV.appendChild(div);
  228. }
  229. }
  230. if (!tooltip_is_image) {
  231. tip = Blockly.utils.wrap(tip, Blockly.Tooltip.LIMIT);
  232. // Create new text, line by line.
  233. var lines = tip.split('\n');
  234. for (var i = 0; i < lines.length; i++) {
  235. var div = document.createElement('div');
  236. div.appendChild(document.createTextNode(lines[i]));
  237. Blockly.Tooltip.DIV.appendChild(div);
  238. }
  239. }
  240. var rtl = Blockly.Tooltip.element_.RTL;
  241. var windowSize = goog.dom.getViewportSize();
  242. // Display the tooltip.
  243. Blockly.Tooltip.DIV.style.direction = rtl ? 'rtl' : 'ltr';
  244. Blockly.Tooltip.DIV.style.display = 'block';
  245. Blockly.Tooltip.visible = true;
  246. // Move the tooltip to just below the cursor.
  247. var anchorX = Blockly.Tooltip.lastX_;
  248. if (rtl) {
  249. anchorX -= Blockly.Tooltip.OFFSET_X + Blockly.Tooltip.DIV.offsetWidth;
  250. } else {
  251. anchorX += Blockly.Tooltip.OFFSET_X;
  252. }
  253. var anchorY = Blockly.Tooltip.lastY_ + Blockly.Tooltip.OFFSET_Y;
  254. if (anchorY + Blockly.Tooltip.DIV.offsetHeight >
  255. windowSize.height + window.scrollY) {
  256. // Falling off the bottom of the screen; shift the tooltip up.
  257. anchorY -= Blockly.Tooltip.DIV.offsetHeight + 2 * Blockly.Tooltip.OFFSET_Y;
  258. }
  259. if (rtl) {
  260. // Prevent falling off left edge in RTL mode.
  261. anchorX = Math.max(Blockly.Tooltip.MARGINS - window.scrollX, anchorX);
  262. } else {
  263. if (anchorX + Blockly.Tooltip.DIV.offsetWidth >
  264. windowSize.width + window.scrollX - 2 * Blockly.Tooltip.MARGINS) {
  265. // Falling off the right edge of the screen;
  266. // clamp the tooltip on the edge.
  267. anchorX = windowSize.width - Blockly.Tooltip.DIV.offsetWidth -
  268. 2 * Blockly.Tooltip.MARGINS;
  269. }
  270. }
  271. Blockly.Tooltip.DIV.style.top = anchorY + 'px';
  272. Blockly.Tooltip.DIV.style.left = anchorX + 'px';
  273. };