mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-06-05 09:24:48 -04:00
337 lines
9.8 KiB
JavaScript
337 lines
9.8 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2011 Google Inc.
|
|
* https://developers.google.com/blockly/
|
|
*
|
|
* 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';
|
|
|
|
/**
|
|
* @name Blockly.Tooltip
|
|
* @namespace
|
|
**/
|
|
goog.provide('Blockly.Tooltip');
|
|
|
|
goog.require('goog.dom');
|
|
goog.require('goog.dom.TagName');
|
|
|
|
|
|
/**
|
|
* Is a tooltip currently showing?
|
|
*/
|
|
Blockly.Tooltip.visible = false;
|
|
|
|
/**
|
|
* Is someone else blocking the tooltip from being shown?
|
|
* @type {boolean}
|
|
* @private
|
|
*/
|
|
Blockly.Tooltip.blocked_ = false;
|
|
|
|
/**
|
|
* Maximum width (in characters) of a tooltip.
|
|
*/
|
|
Blockly.Tooltip.LIMIT = 50;
|
|
|
|
/**
|
|
* 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 X location of the mouse pointer (freezes when tooltip appears).
|
|
* @private
|
|
*/
|
|
Blockly.Tooltip.lastX_ = 0;
|
|
|
|
/**
|
|
* Last observed Y location of the mouse pointer (freezes when tooltip appears).
|
|
* @private
|
|
*/
|
|
Blockly.Tooltip.lastY_ = 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;
|
|
|
|
/**
|
|
* 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 = 750;
|
|
|
|
/**
|
|
* Horizontal padding between tooltip and screen edge.
|
|
*/
|
|
Blockly.Tooltip.MARGINS = 5;
|
|
|
|
/**
|
|
* The HTML container. Set once by Blockly.Tooltip.createDom.
|
|
* @type {Element}
|
|
*/
|
|
Blockly.Tooltip.DIV = null;
|
|
|
|
/**
|
|
* Create the tooltip div and inject it onto the page.
|
|
*/
|
|
Blockly.Tooltip.createDom = function() {
|
|
if (Blockly.Tooltip.DIV) {
|
|
return; // Already created.
|
|
}
|
|
// Create an HTML container for popup overlays (e.g. editor widgets).
|
|
Blockly.Tooltip.DIV =
|
|
goog.dom.createDom(goog.dom.TagName.DIV, 'blocklyTooltipDiv');
|
|
document.body.appendChild(Blockly.Tooltip.DIV);
|
|
};
|
|
|
|
/**
|
|
* 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_);
|
|
|
|
// Don't use bindEvent_ for mousemove since that would create a
|
|
// corresponding touch handler, even though this only makes sense in the
|
|
// context of a mouseover/mouseout.
|
|
element.addEventListener('mousemove', Blockly.Tooltip.onMouseMove_, false);
|
|
};
|
|
|
|
/**
|
|
* 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 (Blockly.Tooltip.blocked_) {
|
|
// Someone doesn't want us to show tooltips.
|
|
return;
|
|
}
|
|
// 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 preceding mouseOut event.
|
|
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) {
|
|
if (Blockly.Tooltip.blocked_) {
|
|
// Someone doesn't want us to show tooltips.
|
|
return;
|
|
}
|
|
// 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_ = setTimeout(function() {
|
|
Blockly.Tooltip.element_ = null;
|
|
Blockly.Tooltip.poisonedElement_ = null;
|
|
Blockly.Tooltip.hide();
|
|
}, 1);
|
|
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.WidgetDiv.isVisible()) {
|
|
// Don't display a tooltip if a widget is open (tooltip would be under it).
|
|
return;
|
|
} else if (Blockly.Tooltip.blocked_) {
|
|
// Someone doesn't want us to show tooltips. We are probably handling a
|
|
// user gesture, such as a click or 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 dx = Blockly.Tooltip.lastX_ - e.pageX;
|
|
var dy = Blockly.Tooltip.lastY_ - e.pageY;
|
|
if (Math.sqrt(dx * dx + dy * dy) > Blockly.Tooltip.RADIUS_OK) {
|
|
Blockly.Tooltip.hide();
|
|
}
|
|
} else if (Blockly.Tooltip.poisonedElement_ != Blockly.Tooltip.element_) {
|
|
// The mouse moved, clear any previously scheduled tooltip.
|
|
clearTimeout(Blockly.Tooltip.showPid_);
|
|
// Maybe this time the mouse will stay put. Schedule showing of tooltip.
|
|
Blockly.Tooltip.lastX_ = e.pageX;
|
|
Blockly.Tooltip.lastY_ = e.pageY;
|
|
Blockly.Tooltip.showPid_ =
|
|
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.DIV) {
|
|
Blockly.Tooltip.DIV.style.display = 'none';
|
|
}
|
|
}
|
|
if (Blockly.Tooltip.showPid_) {
|
|
clearTimeout(Blockly.Tooltip.showPid_);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Hide any in-progress tooltips and block showing new tooltips until the next
|
|
* call to unblock().
|
|
* @package
|
|
*/
|
|
Blockly.Tooltip.block = function() {
|
|
Blockly.Tooltip.hide();
|
|
Blockly.Tooltip.blocked_ = true;
|
|
};
|
|
|
|
/**
|
|
* Unblock tooltips: allow them to be scheduled and shown according to their own
|
|
* logic.
|
|
* @package
|
|
*/
|
|
Blockly.Tooltip.unblock = function() {
|
|
Blockly.Tooltip.blocked_ = false;
|
|
};
|
|
|
|
/**
|
|
* Create the tooltip and show it.
|
|
* @private
|
|
*/
|
|
Blockly.Tooltip.show_ = function() {
|
|
if (Blockly.Tooltip.blocked_) {
|
|
// Someone doesn't want us to show tooltips.
|
|
return;
|
|
}
|
|
Blockly.Tooltip.poisonedElement_ = Blockly.Tooltip.element_;
|
|
if (!Blockly.Tooltip.DIV) {
|
|
return;
|
|
}
|
|
// Erase all existing text.
|
|
goog.dom.removeChildren(/** @type {!Element} */ (Blockly.Tooltip.DIV));
|
|
// Get the new text.
|
|
var tip = Blockly.Tooltip.element_.tooltip;
|
|
while (goog.isFunction(tip)) {
|
|
tip = tip();
|
|
}
|
|
tip = Blockly.utils.wrap(tip, Blockly.Tooltip.LIMIT);
|
|
// Create new text, line by line.
|
|
var lines = tip.split('\n');
|
|
for (var i = 0; i < lines.length; i++) {
|
|
var div = document.createElement('div');
|
|
div.appendChild(document.createTextNode(lines[i]));
|
|
Blockly.Tooltip.DIV.appendChild(div);
|
|
}
|
|
var rtl = Blockly.Tooltip.element_.RTL;
|
|
var windowSize = goog.dom.getViewportSize();
|
|
// Display the tooltip.
|
|
Blockly.Tooltip.DIV.style.direction = rtl ? 'rtl' : 'ltr';
|
|
Blockly.Tooltip.DIV.style.display = 'block';
|
|
Blockly.Tooltip.visible = true;
|
|
// Move the tooltip to just below the cursor.
|
|
var anchorX = Blockly.Tooltip.lastX_;
|
|
if (rtl) {
|
|
anchorX -= Blockly.Tooltip.OFFSET_X + Blockly.Tooltip.DIV.offsetWidth;
|
|
} else {
|
|
anchorX += Blockly.Tooltip.OFFSET_X;
|
|
}
|
|
var anchorY = Blockly.Tooltip.lastY_ + Blockly.Tooltip.OFFSET_Y;
|
|
|
|
if (anchorY + Blockly.Tooltip.DIV.offsetHeight >
|
|
windowSize.height + window.scrollY) {
|
|
// Falling off the bottom of the screen; shift the tooltip up.
|
|
anchorY -= Blockly.Tooltip.DIV.offsetHeight + 2 * Blockly.Tooltip.OFFSET_Y;
|
|
}
|
|
if (rtl) {
|
|
// Prevent falling off left edge in RTL mode.
|
|
anchorX = Math.max(Blockly.Tooltip.MARGINS - window.scrollX, anchorX);
|
|
} else {
|
|
if (anchorX + Blockly.Tooltip.DIV.offsetWidth >
|
|
windowSize.width + window.scrollX - 2 * Blockly.Tooltip.MARGINS) {
|
|
// Falling off the right edge of the screen;
|
|
// clamp the tooltip on the edge.
|
|
anchorX = windowSize.width - Blockly.Tooltip.DIV.offsetWidth -
|
|
2 * Blockly.Tooltip.MARGINS;
|
|
}
|
|
}
|
|
Blockly.Tooltip.DIV.style.top = anchorY + 'px';
|
|
Blockly.Tooltip.DIV.style.left = anchorX + 'px';
|
|
};
|