Files
blockly/core/tooltip.js

320 lines
10 KiB
JavaScript

/**
* Visual Blocks Editor
*
* Copyright 2011 Google Inc.
* http://blockly.googlecode.com/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* @fileoverview Library to create tooltips for Blockly.
* First, call Blockly.Tooltip.init() after onload.
* Second, set the 'tooltip' property on any SVG element that needs a tooltip.
* If the tooltip is a string, then that message will be displayed.
* If the tooltip is an SVG element, then that object's tooltip will be used.
* Third, call Blockly.Tooltip.bindMouseEvents(e) passing the SVG element.
* @author fraser@google.com (Neil Fraser)
*/
'use strict';
goog.provide('Blockly.Tooltip');
/**
* Is a tooltip currently showing?
*/
Blockly.Tooltip.visible = false;
/**
* PID of suspended thread to clear tooltip on mouse out.
* @private
*/
Blockly.Tooltip.mouseOutPid_ = 0;
/**
* PID of suspended thread to show the tooltip.
* @private
*/
Blockly.Tooltip.showPid_ = 0;
/**
* Last observed location of the mouse pointer (freezes when tooltip appears).
* @private
*/
Blockly.Tooltip.lastXY_ = {x: 0, y: 0};
/**
* Current element being pointed at.
* @private
*/
Blockly.Tooltip.element_ = null;
/**
* Once a tooltip has opened for an element, that element is 'poisoned' and
* cannot respawn a tooltip until the pointer moves over a different element.
* @private
*/
Blockly.Tooltip.poisonedElement_ = null;
/**
* Tooltip's SVG group element.
* @type {Element}
* @private
*/
Blockly.Tooltip.svgGroup_ = null;
/**
* Tooltip's SVG text element.
* @type {SVGTextElement}
* @private
*/
Blockly.Tooltip.svgText_ = null;
/**
* Tooltip's SVG background rectangle.
* @type {Element}
* @private
*/
Blockly.Tooltip.svgBackground_ = null;
/**
* Tooltip's SVG shadow rectangle.
* @type {Element}
* @private
*/
Blockly.Tooltip.svgShadow_ = null;
/**
* Horizontal offset between mouse cursor and tooltip.
*/
Blockly.Tooltip.OFFSET_X = 0;
/**
* Vertical offset between mouse cursor and tooltip.
*/
Blockly.Tooltip.OFFSET_Y = 10;
/**
* Radius mouse can move before killing tooltip.
*/
Blockly.Tooltip.RADIUS_OK = 10;
/**
* Delay before tooltip appears.
*/
Blockly.Tooltip.HOVER_MS = 1000;
/**
* Horizontal padding between text and background.
*/
Blockly.Tooltip.MARGINS = 5;
/**
* Create the tooltip elements. Only needs to be called once.
* @return {!SVGGElement} The tooltip's SVG group.
*/
Blockly.Tooltip.createDom = function() {
/*
<g class="blocklyHidden">
<rect class="blocklyTooltipShadow" x="2" y="2"/>
<rect class="blocklyTooltipBackground"/>
<text class="blocklyTooltipText"></text>
</g>
*/
var svgGroup = /** @type {!SVGGElement} */ (
Blockly.createSvgElement('g', {'class': 'blocklyHidden'}, null));
Blockly.Tooltip.svgGroup_ = svgGroup;
Blockly.Tooltip.svgShadow_ = /** @type {!SVGRectElement} */ (
Blockly.createSvgElement(
'rect', {'class': 'blocklyTooltipShadow', 'x': 2, 'y': 2}, svgGroup));
Blockly.Tooltip.svgBackground_ = /** @type {!SVGRectElement} */ (
Blockly.createSvgElement(
'rect', {'class': 'blocklyTooltipBackground'}, svgGroup));
Blockly.Tooltip.svgText_ = /** @type {!SVGTextElement} */ (
Blockly.createSvgElement(
'text', {'class': 'blocklyTooltipText'}, svgGroup));
return svgGroup;
};
/**
* Binds the required mouse events onto an SVG element.
* @param {!Element} element SVG element onto which tooltip is to be bound.
*/
Blockly.Tooltip.bindMouseEvents = function(element) {
Blockly.bindEvent_(element, 'mouseover', null, Blockly.Tooltip.onMouseOver_);
Blockly.bindEvent_(element, 'mouseout', null, Blockly.Tooltip.onMouseOut_);
Blockly.bindEvent_(element, 'mousemove', null, Blockly.Tooltip.onMouseMove_);
};
/**
* Hide the tooltip if the mouse is over a different object.
* Initialize the tooltip to potentially appear for this object.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.Tooltip.onMouseOver_ = function(e) {
// If the tooltip is an object, treat it as a pointer to the next object in
// the chain to look at. Terminate when a string or function is found.
var element = e.target;
while (!goog.isString(element.tooltip) && !goog.isFunction(element.tooltip)) {
element = element.tooltip;
}
if (Blockly.Tooltip.element_ != element) {
Blockly.Tooltip.hide();
Blockly.Tooltip.poisonedElement_ = null;
Blockly.Tooltip.element_ = element;
}
// Forget about any immediately preceeding mouseOut event.
window.clearTimeout(Blockly.Tooltip.mouseOutPid_);
};
/**
* Hide the tooltip if the mouse leaves the object and enters the workspace.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.Tooltip.onMouseOut_ = function(e) {
// Moving from one element to another (overlapping or with no gap) generates
// a mouseOut followed instantly by a mouseOver. Fork off the mouseOut
// event and kill it if a mouseOver is received immediately.
// This way the task only fully executes if mousing into the void.
Blockly.Tooltip.mouseOutPid_ = window.setTimeout(function() {
Blockly.Tooltip.element_ = null;
Blockly.Tooltip.poisonedElement_ = null;
Blockly.Tooltip.hide();
}, 1);
window.clearTimeout(Blockly.Tooltip.showPid_);
};
/**
* When hovering over an element, schedule a tooltip to be shown. If a tooltip
* is already visible, hide it if the mouse strays out of a certain radius.
* @param {!Event} e Mouse event.
* @private
*/
Blockly.Tooltip.onMouseMove_ = function(e) {
if (!Blockly.Tooltip.element_ || !Blockly.Tooltip.element_.tooltip) {
// No tooltip here to show.
return;
} else if ((Blockly.ContextMenu && Blockly.ContextMenu.visible) ||
Blockly.Block.dragMode_ != 0) {
// Don't display a tooltip when a context menu is active, or during a drag.
return;
}
if (Blockly.Tooltip.visible) {
// Compute the distance between the mouse position when the tooltip was
// shown and the current mouse position. Pythagorean theorem.
var mouseXY = Blockly.mouseToSvg(e);
var dx = Blockly.Tooltip.lastXY_.x - mouseXY.x;
var dy = Blockly.Tooltip.lastXY_.y - mouseXY.y;
var dr = Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2));
if (dr > Blockly.Tooltip.RADIUS_OK) {
Blockly.Tooltip.hide();
}
} else if (Blockly.Tooltip.poisonedElement_ != Blockly.Tooltip.element_) {
// The mouse moved, clear any previously scheduled tooltip.
window.clearTimeout(Blockly.Tooltip.showPid_);
// Maybe this time the mouse will stay put. Schedule showing of tooltip.
Blockly.Tooltip.lastXY_ = Blockly.mouseToSvg(e);
Blockly.Tooltip.showPid_ =
window.setTimeout(Blockly.Tooltip.show_, Blockly.Tooltip.HOVER_MS);
}
};
/**
* Hide the tooltip.
*/
Blockly.Tooltip.hide = function() {
if (Blockly.Tooltip.visible) {
Blockly.Tooltip.visible = false;
if (Blockly.Tooltip.svgGroup_) {
Blockly.Tooltip.svgGroup_.style.display = 'none';
}
}
window.clearTimeout(Blockly.Tooltip.showPid_);
};
/**
* Create the tooltip and show it.
* @private
*/
Blockly.Tooltip.show_ = function() {
Blockly.Tooltip.poisonedElement_ = Blockly.Tooltip.element_;
if (!Blockly.Tooltip.svgGroup_) {
return;
}
// Erase all existing text.
goog.dom.removeChildren(
/** @type {!Element} */ (Blockly.Tooltip.svgText_));
// Create new text, line by line.
var tip = Blockly.Tooltip.element_.tooltip;
if (goog.isFunction(tip)) {
tip = tip();
}
var lines = tip.split('\n');
for (var i = 0; i < lines.length; i++) {
var tspanElement = Blockly.createSvgElement('tspan',
{'dy': '1em', 'x': Blockly.Tooltip.MARGINS}, Blockly.Tooltip.svgText_);
var textNode = document.createTextNode(lines[i]);
tspanElement.appendChild(textNode);
}
// Display the tooltip.
Blockly.Tooltip.visible = true;
Blockly.Tooltip.svgGroup_.style.display = 'block';
// Resize the background and shadow to fit.
var bBox = Blockly.Tooltip.svgText_.getBBox();
var width = 2 * Blockly.Tooltip.MARGINS + bBox.width;
var height = bBox.height;
Blockly.Tooltip.svgBackground_.setAttribute('width', width);
Blockly.Tooltip.svgBackground_.setAttribute('height', height);
Blockly.Tooltip.svgShadow_.setAttribute('width', width);
Blockly.Tooltip.svgShadow_.setAttribute('height', height);
if (Blockly.RTL) {
// Right-align the paragraph.
// This cannot be done until the tooltip is rendered on screen.
var maxWidth = bBox.width;
for (var x = 0, textElement;
textElement = Blockly.Tooltip.svgText_.childNodes[x]; x++) {
textElement.setAttribute('text-anchor', 'end');
textElement.setAttribute('x', maxWidth + Blockly.Tooltip.MARGINS);
}
}
// Move the tooltip to just below the cursor.
var anchorX = Blockly.Tooltip.lastXY_.x;
if (Blockly.RTL) {
anchorX -= Blockly.Tooltip.OFFSET_X + width;
} else {
anchorX += Blockly.Tooltip.OFFSET_X;
}
var anchorY = Blockly.Tooltip.lastXY_.y + Blockly.Tooltip.OFFSET_Y;
var svgSize = Blockly.svgSize();
if (anchorY + bBox.height > svgSize.height) {
// Falling off the bottom of the screen; shift the tooltip up.
anchorY -= bBox.height + 2 * Blockly.Tooltip.OFFSET_Y;
}
if (Blockly.RTL) {
// Prevent falling off left edge in RTL mode.
anchorX = Math.max(Blockly.Tooltip.MARGINS, anchorX);
} else {
if (anchorX + bBox.width > svgSize.width - 2 * Blockly.Tooltip.MARGINS) {
// Falling off the right edge of the screen;
// clamp the tooltip on the edge.
anchorX = svgSize.width - bBox.width - 2 * Blockly.Tooltip.MARGINS;
}
}
Blockly.Tooltip.svgGroup_.setAttribute('transform',
'translate(' + anchorX + ',' + anchorY + ')');
};