From b3d543cc3560fbe44b16ac8085e8d6cb141f8dbe Mon Sep 17 00:00:00 2001 From: Maribeth Bottorff Date: Wed, 23 Feb 2022 13:49:49 -0800 Subject: [PATCH] refactor: make Tooltip a class that is accessed via a singleton --- core/block.js | 9 +- core/block_svg.js | 8 +- core/blockly.js | 3 +- core/field.js | 15 +- core/flyout_base.js | 6 +- core/gesture.js | 6 +- core/inject.js | 4 +- core/tooltip.js | 812 ++++++++++++++++------------------ core/workspace_svg.js | 4 +- scripts/gulpfiles/chunks.json | 23 +- tests/deps.js | 2 +- 11 files changed, 426 insertions(+), 466 deletions(-) diff --git a/core/block.js b/core/block.js index 3c6a7541453..ae561676413 100644 --- a/core/block.js +++ b/core/block.js @@ -16,7 +16,6 @@ goog.module('Blockly.Block'); const Extensions = goog.require('Blockly.Extensions'); -const Tooltip = goog.require('Blockly.Tooltip'); const arrayUtils = goog.require('Blockly.utils.array'); const common = goog.require('Blockly.common'); const constants = goog.require('Blockly.constants'); @@ -51,6 +50,8 @@ const {VariableModel} = goog.requireType('Blockly.VariableModel'); /* eslint-disable-next-line no-unused-vars */ const {Workspace} = goog.requireType('Blockly.Workspace'); const {inputTypes} = goog.require('Blockly.inputTypes'); +/* eslint-disable-next-line no-unused-vars */ +const {tooltipManager, TipInfo} = goog.require('Blockly.Tooltip'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.BlockChange'); /** @suppress {extraRequire} */ @@ -191,7 +192,7 @@ const Block = function(workspace, prototypeName, opt_id) { * @private */ this.disabled = false; - /** @type {!Tooltip.TipInfo} */ + /** @type {!TipInfo} */ this.tooltip = ''; /** @type {boolean} */ this.contextMenu = true; @@ -990,7 +991,7 @@ Block.prototype.setHelpUrl = function(url) { /** * Sets the tooltip for this block. - * @param {!Tooltip.TipInfo} newTip The text for the tooltip, a function + * @param {!TipInfo} newTip The text for the tooltip, a function * that returns the text for the tooltip, or a parent object whose tooltip * will be used. To not display a tooltip pass the empty string. */ @@ -1003,7 +1004,7 @@ Block.prototype.setTooltip = function(newTip) { * @return {!string} The tooltip text for this block. */ Block.prototype.getTooltip = function() { - return Tooltip.getTooltipOfObject(this); + return tooltipManager.getTooltipOfObject(this); }; /** diff --git a/core/block_svg.js b/core/block_svg.js index ffd6eb48f73..dfc7e9cf56d 100644 --- a/core/block_svg.js +++ b/core/block_svg.js @@ -16,7 +16,6 @@ goog.module('Blockly.BlockSvg'); const ContextMenu = goog.require('Blockly.ContextMenu'); -const Tooltip = goog.require('Blockly.Tooltip'); const blockAnimations = goog.require('Blockly.blockAnimations'); const blocks = goog.require('Blockly.serialization.blocks'); const browserEvents = goog.require('Blockly.browserEvents'); @@ -73,6 +72,7 @@ const {Theme} = goog.requireType('Blockly.Theme'); const {Warning} = goog.requireType('Blockly.Warning'); /* eslint-disable-next-line no-unused-vars */ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg'); +const {tooltipManager} = goog.require('Blockly.Tooltip'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.BlockMove'); /** @suppress {extraRequire} */ @@ -240,7 +240,7 @@ const BlockSvg = function(workspace, prototypeName, opt_id) { const svgPath = this.pathObject.svgPath; svgPath.tooltip = this; - Tooltip.bindMouseEvents(svgPath); + tooltipManager.bindMouseEvents(svgPath); BlockSvg.superClass_.constructor.call(this, workspace, prototypeName, opt_id); // Expose this block's ID on its top-level SVG group. @@ -903,8 +903,8 @@ BlockSvg.prototype.dispose = function(healStack, animate) { // The block has already been deleted. return; } - Tooltip.dispose(); - Tooltip.unbindMouseEvents(this.pathObject.svgPath); + tooltipManager.dispose(); + tooltipManager.unbindMouseEvents(this.pathObject.svgPath); dom.startTextWidthCache(); // Save the block's workspace temporarily so we can resize the // contents once the block is disposed. diff --git a/core/blockly.js b/core/blockly.js index e393ebc4686..2d2e663ee11 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -24,7 +24,6 @@ const Extensions = goog.require('Blockly.Extensions'); const Procedures = goog.require('Blockly.Procedures'); const ShortcutItems = goog.require('Blockly.ShortcutItems'); const Themes = goog.require('Blockly.Themes'); -const Tooltip = goog.require('Blockly.Tooltip'); const Touch = goog.require('Blockly.Touch'); const Variables = goog.require('Blockly.Variables'); const VariablesDynamic = goog.require('Blockly.VariablesDynamic'); @@ -151,6 +150,7 @@ const {ToolboxCategory} = goog.require('Blockly.ToolboxCategory'); const {ToolboxItem} = goog.require('Blockly.ToolboxItem'); const {ToolboxSeparator} = goog.require('Blockly.ToolboxSeparator'); const {Toolbox} = goog.require('Blockly.Toolbox'); +const {Tooltip, tooltipManager} = goog.require('Blockly.Tooltip'); const {TouchGesture} = goog.require('Blockly.TouchGesture'); const {Trashcan} = goog.require('Blockly.Trashcan'); const {VariableMap} = goog.require('Blockly.VariableMap'); @@ -838,6 +838,7 @@ exports.serialization = { ISerializer: ISerializer, }; exports.thrasos = thrasos; +exports.tooltipManager = tooltipManager; exports.uiPosition = uiPosition; exports.utils = utils; exports.zelos = zelos; diff --git a/core/field.js b/core/field.js index f1fd06b2fa6..d7d08f7ab43 100644 --- a/core/field.js +++ b/core/field.js @@ -19,7 +19,6 @@ */ goog.module('Blockly.Field'); -const Tooltip = goog.require('Blockly.Tooltip'); const WidgetDiv = goog.require('Blockly.WidgetDiv'); const Xml = goog.require('Blockly.Xml'); const browserEvents = goog.require('Blockly.browserEvents'); @@ -55,6 +54,8 @@ const {ShortcutRegistry} = goog.requireType('Blockly.ShortcutRegistry'); const {Size} = goog.require('Blockly.utils.Size'); const {Svg} = goog.require('Blockly.utils.Svg'); /* eslint-disable-next-line no-unused-vars */ +const {TipInfo, tooltipManager} = goog.require('Blockly.Tooltip'); +/* eslint-disable-next-line no-unused-vars */ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.BlockChange'); @@ -98,7 +99,7 @@ const Field = function(value, opt_validator, opt_config) { /** * Used to cache the field's tooltip value if setTooltip is called when the * field is not yet initialized. Is *not* guaranteed to be accurate. - * @type {?Tooltip.TipInfo} + * @type {?TipInfo} * @private */ this.tooltip_ = null; @@ -411,7 +412,7 @@ Field.prototype.createTextElement_ = function() { * @protected */ Field.prototype.bindEvents_ = function() { - Tooltip.bindMouseEvents(this.getClickTarget_()); + tooltipManager.bindMouseEvents(this.getClickTarget_()); this.mouseDownWrapper_ = browserEvents.conditionalBind( this.getClickTarget_(), 'mousedown', this, this.onMouseDown_); }; @@ -519,7 +520,7 @@ Field.prototype.loadLegacyState = function(callingClass, state) { Field.prototype.dispose = function() { DropDownDiv.hideIfOwner(this); WidgetDiv.hideIfOwner(this); - Tooltip.unbindMouseEvents(this.getClickTarget_()); + tooltipManager.unbindMouseEvents(this.getClickTarget_()); if (this.mouseDownWrapper_) { browserEvents.unbind(this.mouseDownWrapper_); @@ -1052,7 +1053,7 @@ Field.prototype.onMouseDown_ = function(e) { /** * Sets the tooltip for this field. - * @param {?Tooltip.TipInfo} newTip The + * @param {?TipInfo} newTip The * text for the tooltip, a function that returns the text for the tooltip, a * parent object whose tooltip will be used, or null to display the tooltip * of the parent block. To not display a tooltip pass the empty string. @@ -1077,10 +1078,10 @@ Field.prototype.setTooltip = function(newTip) { Field.prototype.getTooltip = function() { const clickTarget = this.getClickTarget_(); if (clickTarget) { - return Tooltip.getTooltipOfObject(clickTarget); + return tooltipManager.getTooltipOfObject(clickTarget); } // Field has not been initialized yet. Return stashed this.tooltip_ value. - return Tooltip.getTooltipOfObject({tooltip: this.tooltip_}); + return tooltipManager.getTooltipOfObject({tooltip: this.tooltip_}); }; /** diff --git a/core/flyout_base.js b/core/flyout_base.js index 4490617e00b..7bcb6bd5022 100644 --- a/core/flyout_base.js +++ b/core/flyout_base.js @@ -15,7 +15,6 @@ */ goog.module('Blockly.Flyout'); -const Tooltip = goog.require('Blockly.Tooltip'); const Variables = goog.require('Blockly.Variables'); const Xml = goog.require('Blockly.Xml'); const blocks = goog.require('Blockly.serialization.blocks'); @@ -44,6 +43,7 @@ const {Rect} = goog.require('Blockly.utils.Rect'); const {ScrollbarPair} = goog.require('Blockly.ScrollbarPair'); const {Svg} = goog.require('Blockly.utils.Svg'); const {WorkspaceSvg} = goog.require('Blockly.WorkspaceSvg'); +const {tooltipManager} = goog.require('Blockly.Tooltip'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.BlockCreate'); /** @suppress {extraRequire} */ @@ -836,7 +836,7 @@ class Flyout extends DeleteArea { for (let j = 0; j < this.mats_.length; j++) { const rect = this.mats_[j]; if (rect) { - Tooltip.unbindMouseEvents(rect); + tooltipManager.unbindMouseEvents(rect); dom.removeNode(rect); } } @@ -1041,7 +1041,7 @@ class Flyout extends DeleteArea { }, null); rect.tooltip = block; - Tooltip.bindMouseEvents(rect); + tooltipManager.bindMouseEvents(rect); // Add the rectangles under the blocks, so that the blocks' tooltips work. this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot()); diff --git a/core/gesture.js b/core/gesture.js index 63e27b16ae8..907746e57c9 100644 --- a/core/gesture.js +++ b/core/gesture.js @@ -17,7 +17,6 @@ */ goog.module('Blockly.Gesture'); -const Tooltip = goog.require('Blockly.Tooltip'); const Touch = goog.require('Blockly.Touch'); const blockAnimations = goog.require('Blockly.blockAnimations'); const browserEvents = goog.require('Blockly.browserEvents'); @@ -42,6 +41,7 @@ const {WorkspaceDragger} = goog.require('Blockly.WorkspaceDragger'); /* eslint-disable-next-line no-unused-vars */ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg'); const {Workspace} = goog.require('Blockly.Workspace'); +const {tooltipManager} = goog.require('Blockly.Tooltip'); /** @suppress {extraRequire} */ goog.require('Blockly.BlockDragger'); /** @suppress {extraRequire} */ @@ -253,7 +253,7 @@ class Gesture { */ dispose() { Touch.clearTouchIdentifier(); - Tooltip.unblock(); + tooltipManager.unblock(); // Clear the owner's reference to this gesture. this.creatorWorkspace_.clearGesture(); @@ -508,7 +508,7 @@ class Gesture { this.startWorkspace_.markFocused(); this.mostRecentEvent_ = e; - Tooltip.block(); + tooltipManager.block(); if (this.targetBlock_) { this.targetBlock_.select(); diff --git a/core/inject.js b/core/inject.js index ef6c0ed88b0..8f3411ba5c4 100644 --- a/core/inject.js +++ b/core/inject.js @@ -16,7 +16,6 @@ goog.module('Blockly.inject'); const Css = goog.require('Blockly.Css'); -const Tooltip = goog.require('Blockly.Tooltip'); const Touch = goog.require('Blockly.Touch'); const WidgetDiv = goog.require('Blockly.WidgetDiv'); const aria = goog.require('Blockly.utils.aria'); @@ -38,6 +37,7 @@ const {Svg} = goog.require('Blockly.utils.Svg'); const {WorkspaceDragSurfaceSvg} = goog.require('Blockly.WorkspaceDragSurfaceSvg'); const {WorkspaceSvg} = goog.require('Blockly.WorkspaceSvg'); const {Workspace} = goog.require('Blockly.Workspace'); +const {tooltipManager} = goog.require('Blockly.Tooltip'); /** @@ -193,7 +193,7 @@ const createMainWorkspace = function( common.svgResize(mainWorkspace); WidgetDiv.createDom(); DropDownDiv.createDom(); - Tooltip.createDom(); + tooltipManager.createDom(); return mainWorkspace; }; diff --git a/core/tooltip.js b/core/tooltip.js index 85ba611ef93..90daf34bee6 100644 --- a/core/tooltip.js +++ b/core/tooltip.js @@ -4,216 +4,14 @@ * SPDX-License-Identifier: Apache-2.0 */ -/** - * @fileoverview Library to create tooltips for Blockly. - * First, call 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 bindMouseEvents(e) passing the SVG element. - */ 'use strict'; -/** - * Library to create tooltips for Blockly. - * First, call 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 bindMouseEvents(e) passing the SVG element. - * @namespace Blockly.Tooltip - */ goog.module('Blockly.Tooltip'); const blocklyString = goog.require('Blockly.utils.string'); const browserEvents = goog.require('Blockly.browserEvents'); const common = goog.require('Blockly.common'); -const deprecation = goog.require('Blockly.utils.deprecation'); - - -/** - * A type which can define a tooltip. - * Either a string, an object containing a tooltip property, or a function which - * returns either a string, or another arbitrarily nested function which - * eventually unwinds to a string. - * @typedef {string|{tooltip}|function(): (string|!Function)} - * @alias Blockly.Tooltip.TipInfo - */ -let TipInfo; -exports.TipInfo = TipInfo; - -/** - * Is a tooltip currently showing? - * @type {boolean} - */ -let visible = false; - -/** - * Returns whether or not a tooltip is showing - * @returns {boolean} True if a tooltip is showing - * @alias Blockly.Tooltip.isVisible - */ -const isVisible = function() { - return visible; -}; -exports.isVisible = isVisible; -Object.defineProperties(exports, { - /** - * Is a tooltip currently showing? - * @name Blockly.Tooltip.visible - * @type {boolean} - * @deprecated Use Blockly.Tooltip.isVisible() instead. (September - * 2021) - * @suppress {checkTypes} - */ - visible: { - get: function() { - deprecation.warn( - 'Blockly.Tooltip.visible', 'September 2021', 'September 2022', - 'Blockly.Tooltip.isVisible()'); - return isVisible(); - }, - }, -}); - -/** - * Is someone else blocking the tooltip from being shown? - * @type {boolean} - */ -let blocked = false; - -/** - * Maximum width (in characters) of a tooltip. - * @alias Blockly.Tooltip.LIMIT - */ -const LIMIT = 50; -exports.LIMIT = LIMIT; - -/** - * PID of suspended thread to clear tooltip on mouse out. - */ -let mouseOutPid = 0; - -/** - * PID of suspended thread to show the tooltip. - */ -let showPid = 0; - -/** - * Last observed X location of the mouse pointer (freezes when tooltip appears). - */ -let lastX = 0; - -/** - * Last observed Y location of the mouse pointer (freezes when tooltip appears). - */ -let lastY = 0; - -/** - * Current element being pointed at. - * @type {Element} - */ -let 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. - * @type {Element} - */ -let poisonedElement = null; - -/** - * Horizontal offset between mouse cursor and tooltip. - * @alias Blockly.Tooltip.OFFSET_X - */ -const OFFSET_X = 0; -exports.OFFSET_X = OFFSET_X; - -/** - * Vertical offset between mouse cursor and tooltip. - * @alias Blockly.Tooltip.OFFSET_Y - */ -const OFFSET_Y = 10; -exports.OFFSET_Y = OFFSET_Y; - -/** - * Radius mouse can move before killing tooltip. - * @alias Blockly.Tooltip.RADIUS_OK - */ -const RADIUS_OK = 10; -exports.RADIUS_OK = RADIUS_OK; - -/** - * Delay before tooltip appears. - * @alias Blockly.Tooltip.HOVER_MS - */ -const HOVER_MS = 750; -exports.HOVER_MS = HOVER_MS; - -/** - * Horizontal padding between tooltip and screen edge. - * @alias Blockly.Tooltip.MARGINS - */ -const MARGINS = 5; -exports.MARGINS = MARGINS; - -/** - * The HTML container. Set once by createDom. - * @type {Element} - */ -let DIV = null; - -/** - * Returns the HTML tooltip container. - * @returns {Element} The HTML tooltip container. - * @alias Blockly.Tooltip.getDiv - */ -const getDiv = function() { - return DIV; -}; -exports.getDiv = getDiv; - -Object.defineProperties(exports, { - /** - * The HTML container. Set once by createDom. - * @name Blockly.Tooltip.DIV - * @type {Element} - * @deprecated Use Blockly.Tooltip.getDiv() and .setDiv(). - * (September 2021) - * @suppress {checkTypes} - */ - DIV: { - get: function() { - deprecation.warn( - 'Blockly.Tooltip.DIV', 'September 2021', 'September 2022', - 'Blockly.Tooltip.getDiv()'); - return getDiv(); - }, - }, -}); - -/** - * Returns the tooltip text for the given element. - * @param {?Object} object The object to get the tooltip text of. - * @return {string} The tooltip text of the element. - * @alias Blockly.Tooltip.getTooltipOfObject - */ -const getTooltipOfObject = function(object) { - const obj = getTargetObject(object); - if (obj) { - let tooltip = obj.tooltip; - while (typeof tooltip === 'function') { - tooltip = tooltip(); - } - if (typeof tooltip !== 'string') { - throw Error('Tooltip function must return a string.'); - } - return tooltip; - } - return ''; -}; -exports.getTooltipOfObject = getTooltipOfObject; /** * Returns the target object that the given object is targeting for its @@ -234,261 +32,421 @@ const getTargetObject = function(obj) { }; /** - * Create the tooltip div and inject it onto the page. - * @alias Blockly.Tooltip.createDom + * Class to create tooltips. This class represents a singleton and should not be + * constructed outside of this file. Use `Blockly.tooltipManager` to interact + * with the singleton. First, call createDom() after onload. Second, set the + * 'tooltip' property on any SVG element that needs a tooltip. If the tooltip is + * a string, or a function that returns 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 bindMouseEvents(e) passing the SVG element. */ -const createDom = function() { - if (DIV) { - return; // Already created. +class Tooltip { + /** + * Class to create tooltips. Do not instantiate this class outside of this + * file. + * @protected + * @alias Blockly.Tooltip + */ + constructor() { + /** + * Is a tooltip currently showing? + * @type {boolean} + * @protected + */ + this.visible = false; + + /** + * Is someone else blocking the tooltip from being shown? + * @type {boolean} + * @protected + */ + this.blocked = false; + + /** + * PID of suspended thread to clear tooltip on mouse out. + * @protected + */ + this.mouseOutPid = 0; + + /** + * PID of suspended thread to show the tooltip. + * @protected + */ + this.showPid = 0; + + /** + * Last observed X location of the mouse pointer (freezes when tooltip + * appears). + * @protected + */ + this.lastX = 0; + + /** + * Last observed Y location of the mouse pointer (freezes when tooltip + * appears). + * @protected + */ + this.lastY = 0; + + /** + * Current element being pointed at. + * @type {?Element} + * @protected + */ + this.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. + * @type {?Element} + * @protected + */ + this.poisonedElement = null; + + /** + * Maximum width (in characters) of a tooltip. + * @alias Blockly.Tooltip.LIMIT + */ + this.LIMIT = 50; + + /** + * Horizontal offset between mouse cursor and tooltip. + * @alias Blockly.Tooltip.OFFSET_X + */ + this.OFFSET_X = 0; + + /** + * Vertical offset between mouse cursor and tooltip. + * @alias Blockly.Tooltip.OFFSET_Y + */ + this.OFFSET_Y = 10; + + /** + * Radius mouse can move before killing tooltip. + * @alias Blockly.Tooltip.RADIUS_OK + */ + this.RADIUS_OK = 10; + + /** + * Delay before tooltip appears. + * @alias Blockly.Tooltip.HOVER_MS + */ + this.HOVER_MS = 750; + + /** + * Horizontal padding between tooltip and screen edge. + * @alias Blockly.Tooltip.MARGINS + */ + this.MARGINS = 5; + + /** + * The HTML container. Set once by createDom. + * @type {?Element} + * @protected + */ + this.DIV = null; } - // Create an HTML container for popup overlays (e.g. editor widgets). - DIV = document.createElement('div'); - DIV.className = 'blocklyTooltipDiv'; - const container = common.getParentContainer() || document.body; - container.appendChild(DIV); -}; -exports.createDom = createDom; -/** - * Binds the required mouse events onto an SVG element. - * @param {!Element} element SVG element onto which tooltip is to be bound. - * @alias Blockly.Tooltip.bindMouseEvents - */ -const bindMouseEvents = function(element) { - element.mouseOverWrapper_ = - browserEvents.bind(element, 'mouseover', null, onMouseOver); - element.mouseOutWrapper_ = - browserEvents.bind(element, 'mouseout', null, 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', onMouseMove, false); -}; -exports.bindMouseEvents = bindMouseEvents; - -/** - * Unbinds tooltip mouse events from the SVG element. - * @param {!Element} element SVG element onto which tooltip is bound. - * @alias Blockly.Tooltip.unbindMouseEvents - */ -const unbindMouseEvents = function(element) { - if (!element) { - return; + /** + * Returns the tooltip text for the given element. + * @param {?Object} object The object to get the tooltip text of. + * @return {string} The tooltip text of the element. + * @alias Blockly.Tooltip.getTooltipOfObject + */ + getTooltipOfObject(object) { + const obj = getTargetObject(object); + if (obj) { + let tooltip = obj.tooltip; + while (typeof tooltip === 'function') { + tooltip = tooltip(); + } + if (typeof tooltip !== 'string') { + throw Error('Tooltip function must return a string.'); + } + return tooltip; + } + return ''; } - browserEvents.unbind(element.mouseOverWrapper_); - browserEvents.unbind(element.mouseOutWrapper_); - element.removeEventListener('mousemove', onMouseMove); -}; -exports.unbindMouseEvents = unbindMouseEvents; -/** - * 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. - */ -const onMouseOver = function(e) { - if (blocked) { - // Someone doesn't want us to show tooltips. - return; + /** + * Create the tooltip div and inject it onto the page. + * @alias Blockly.Tooltip.createDom + */ + createDom() { + if (this.DIV) { + return; // Already created. + } + // Create an HTML container for popup overlays (e.g. editor widgets). + this.DIV = document.createElement('div'); + this.DIV.className = 'blocklyTooltipDiv'; + const container = common.getParentContainer() || document.body; + container.appendChild(this.DIV); } - // 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. - const newElement = /** @type {Element} */ (getTargetObject(e.currentTarget)); - if (element !== newElement) { - hide(); - poisonedElement = null; - element = newElement; + /** + * Binds the required mouse events onto an SVG element. + * @param {!Element} element SVG element onto which tooltip is to be bound. + * @alias Blockly.Tooltip.bindMouseEvents + */ + bindMouseEvents(element) { + element.mouseOverWrapper_ = + browserEvents.bind(element, 'mouseover', this, this.onMouseOver); + element.mouseOutWrapper_ = + browserEvents.bind(element, 'mouseout', this, this.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', (e) => { + this.onMouseMove(e); + }, false); } - // Forget about any immediately preceding mouseOut event. - clearTimeout(mouseOutPid); -}; -/** - * Hide the tooltip if the mouse leaves the object and enters the workspace. - * @param {!Event} _e Mouse event. - */ -const onMouseOut = function(_e) { - if (blocked) { - // Someone doesn't want us to show tooltips. - return; + /** + * Unbinds tooltip mouse events from the SVG element. + * @param {!Element} element SVG element onto which tooltip is bound. + * @alias Blockly.Tooltip.unbindMouseEvents + */ + unbindMouseEvents(element) { + if (!element) { + return; + } + browserEvents.unbind(element.mouseOverWrapper_); + browserEvents.unbind(element.mouseOutWrapper_); + element.removeEventListener('mousemove', this.onMouseMove); } - // 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. - mouseOutPid = setTimeout(function() { - element = null; - poisonedElement = null; - hide(); - }, 1); - clearTimeout(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. - */ -const onMouseMove = function(e) { - if (!element || !element.tooltip) { - // No tooltip here to show. - return; - } else if (blocked) { - // Someone doesn't want us to show tooltips. We are probably handling a - // user gesture, such as a click or drag. - return; + /** + * 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. + * @protected + */ + onMouseOver(e) { + if (this.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. + const newElement = + /** @type {Element} */ (getTargetObject(e.currentTarget)); + if (this.element !== newElement) { + this.hide(); + this.poisonedElement = null; + this.element = newElement; + } + // Forget about any immediately preceding mouseOut event. + clearTimeout(this.mouseOutPid); } - if (visible) { - // Compute the distance between the mouse position when the tooltip was - // shown and the current mouse position. Pythagorean theorem. - const dx = lastX - e.pageX; - const dy = lastY - e.pageY; - if (Math.sqrt(dx * dx + dy * dy) > RADIUS_OK) { - hide(); + + /** + * Hide the tooltip if the mouse leaves the object and enters the workspace. + * @param {!Event} _e Mouse event. + * @protected + */ + onMouseOut(_e) { + if (this.blocked) { + // Someone doesn't want us to show tooltips. + return; } - } else if (poisonedElement !== element) { - // The mouse moved, clear any previously scheduled tooltip. - clearTimeout(showPid); - // Maybe this time the mouse will stay put. Schedule showing of tooltip. - lastX = e.pageX; - lastY = e.pageY; - showPid = setTimeout(show, HOVER_MS); + // 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. + this.mouseOutPid = setTimeout(() => { + this.element = null; + this.poisonedElement = null; + this.hide(); + }, 1); + clearTimeout(this.showPid); } -}; -/** - * Dispose of the tooltip. - * @alias Blockly.Tooltip.dispose - * @package - */ -const dispose = function() { - element = null; - poisonedElement = null; - hide(); -}; -exports.dispose = dispose; -/** - * Hide the tooltip. - * @alias Blockly.Tooltip.hide - */ -const hide = function() { - if (visible) { - visible = false; - if (DIV) { - DIV.style.display = 'none'; + /** + * 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. + * @protected + */ + onMouseMove(e) { + if (!this.element || !this.element.tooltip) { + // No tooltip here to show. + return; + } else if (this.blocked) { + // Someone doesn't want us to show tooltips. We are probably handling a + // user gesture, such as a click or drag. + return; + } + if (this.visible) { + // Compute the distance between the mouse position when the tooltip was + // shown and the current mouse position. Pythagorean theorem. + const dx = this.lastX - e.pageX; + const dy = this.lastY - e.pageY; + if (Math.sqrt(dx * dx + dy * dy) > this.RADIUS_OK) { + this.hide(); + } + } else if (this.poisonedElement !== this.element) { + // The mouse moved, clear any previously scheduled tooltip. + clearTimeout(this.showPid); + // Maybe this time the mouse will stay put. Schedule showing of tooltip. + this.lastX = e.pageX; + this.lastY = e.pageY; + this.showPid = setTimeout(() => { + this.show(); + }, this.HOVER_MS); } } - if (showPid) { - clearTimeout(showPid); - } -}; -exports.hide = hide; -/** - * Hide any in-progress tooltips and block showing new tooltips until the next - * call to unblock(). - * @alias Blockly.Tooltip.block - * @package - */ -const block = function() { - hide(); - blocked = true; -}; -exports.block = block; + /** + * Dispose of the tooltip. + * @alias Blockly.Tooltip.dispose + * @package + */ + dispose() { + this.element = null; + this.poisonedElement = null; + this.hide(); + } -/** - * Unblock tooltips: allow them to be scheduled and shown according to their own - * logic. - * @alias Blockly.Tooltip.unblock - * @package - */ -const unblock = function() { - blocked = false; -}; -exports.unblock = unblock; + /** + * Hide the tooltip. + * @alias Blockly.Tooltip.hide + */ + hide() { + if (this.visible) { + this.visible = false; + if (this.DIV) { + this.DIV.style.display = 'none'; + } + } + if (this.showPid) { + clearTimeout(this.showPid); + } + } -/** - * Renders the tooltip content into the tooltip div. - */ -const renderContent = function() { - let tip = getTooltipOfObject(element); - tip = blocklyString.wrap(tip, LIMIT); - // Create new text, line by line. - const lines = tip.split('\n'); - for (let i = 0; i < lines.length; i++) { - const div = document.createElement('div'); - div.appendChild(document.createTextNode(lines[i])); - DIV.appendChild(div); + /** + * Hide any in-progress tooltips and block showing new tooltips until the next + * call to unblock(). + * @alias Blockly.Tooltip.block + * @package + */ + block() { + this.hide(); + this.blocked = true; } -}; -/** - * Gets the coordinates for the tooltip div, taking into account the edges of the screen to - * prevent showing the tooltip offscreen. - * @param {boolean} rtl True if the tooltip should be in right-to-left layout. - * @returns {{x: number, y: number}} Coordinates at which the tooltip div should be placed. - */ -const getPosition = function(rtl) { - // Position the tooltip just below the cursor. - const windowWidth = document.documentElement.clientWidth; - const windowHeight = document.documentElement.clientHeight; - - let anchorX = lastX; - if (rtl) { - anchorX -= OFFSET_X + DIV.offsetWidth; - } else { - anchorX += OFFSET_X; + /** + * Unblock tooltips: allow them to be scheduled and shown according to their + * own logic. + * @alias Blockly.Tooltip.unblock + * @package + */ + unblock() { + this.blocked = false; } - let anchorY = lastY + OFFSET_Y; - if (anchorY + DIV.offsetHeight > windowHeight + window.scrollY) { - // Falling off the bottom of the screen; shift the tooltip up. - anchorY -= DIV.offsetHeight + 2 * OFFSET_Y; + /** + * Renders the tooltip content into the tooltip div. + * @protected + */ + renderContent() { + let tip = this.getTooltipOfObject(this.element); + tip = blocklyString.wrap(tip, this.LIMIT); + // Create new text, line by line. + const lines = tip.split('\n'); + for (let i = 0; i < lines.length; i++) { + const div = document.createElement('div'); + div.appendChild(document.createTextNode(lines[i])); + this.DIV.appendChild(div); + } } - if (rtl) { - // Prevent falling off left edge in RTL mode. - anchorX = Math.max(MARGINS - window.scrollX, anchorX); - } else { - if (anchorX + DIV.offsetWidth > - windowWidth + window.scrollX - 2 * MARGINS) { - // Falling off the right edge of the screen; - // clamp the tooltip on the edge. - anchorX = windowWidth - DIV.offsetWidth - 2 * MARGINS; + /** + * Gets the coordinates for the tooltip div, taking into account the edges of + * the screen to prevent showing the tooltip offscreen. + * @param {boolean} rtl True if the tooltip should be in right-to-left layout. + * @returns {{x: number, y: number}} Coordinates at which the tooltip div + * should be placed. + * @protected + */ + getPosition(rtl) { + // Position the tooltip just below the cursor. + const windowWidth = document.documentElement.clientWidth; + const windowHeight = document.documentElement.clientHeight; + + let anchorX = this.lastX; + if (rtl) { + anchorX -= this.OFFSET_X + this.DIV.offsetWidth; + } else { + anchorX += this.OFFSET_X; + } + + let anchorY = this.lastY + this.OFFSET_Y; + if (anchorY + this.DIV.offsetHeight > windowHeight + window.scrollY) { + // Falling off the bottom of the screen; shift the tooltip up. + anchorY -= this.DIV.offsetHeight + 2 * this.OFFSET_Y; + } + + if (rtl) { + // Prevent falling off left edge in RTL mode. + anchorX = Math.max(this.MARGINS - window.scrollX, anchorX); + } else { + if (anchorX + this.DIV.offsetWidth > + windowWidth + window.scrollX - 2 * this.MARGINS) { + // Falling off the right edge of the screen; + // clamp the tooltip on the edge. + anchorX = windowWidth - this.DIV.offsetWidth - 2 * this.MARGINS; + } } + + return {x: anchorX, y: anchorY}; } - return {x: anchorX, y: anchorY}; -}; + /** + * Create the tooltip and show it. + */ + show() { + if (this.blocked) { + // Someone doesn't want us to show tooltips. + return; + } + this.poisonedElement = this.element; + if (!this.DIV) { + return; + } + // Erase all existing text. + this.DIV.textContent = ''; + + // Add new content. + this.renderContent(); + + // Display the tooltip. + const rtl = /** @type {{RTL: boolean}} */ (this.element).RTL; + this.DIV.style.direction = rtl ? 'rtl' : 'ltr'; + this.DIV.style.display = 'block'; + this.visible = true; + + const {x, y} = this.getPosition(rtl); + this.DIV.style.left = x + 'px'; + this.DIV.style.top = y + 'px'; + } +} +exports.Tooltip = Tooltip; /** - * Create the tooltip and show it. + * A type which can define a tooltip. + * Either a string, an object containing a tooltip property, or a function which + * returns either a string, or another arbitrarily nested function which + * eventually unwinds to a string. + * @typedef {string|{tooltip}|function(): (string|!Function)} + * @alias Blockly.Tooltip.TipInfo */ -const show = function() { - if (blocked) { - // Someone doesn't want us to show tooltips. - return; - } - poisonedElement = element; - if (!DIV) { - return; - } - // Erase all existing text. - DIV.textContent = ''; - - // Add new content. - renderContent(); - - // Display the tooltip. - const rtl = /** @type {{RTL: boolean}} */ (element).RTL; - DIV.style.direction = rtl ? 'rtl' : 'ltr'; - DIV.style.display = 'block'; - visible = true; - - const {x, y} = getPosition(rtl); - DIV.style.left = x + 'px'; - DIV.style.top = y + 'px'; -}; +let TipInfo; +exports.TipInfo = TipInfo; + +// Singleton tooltip manager. Only interact with this object. +// TODO(maribethb): Get this class from the registry. +exports.tooltipManager = new Tooltip(); diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 28849b0c37e..6689c14fe91 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -18,7 +18,6 @@ goog.module('Blockly.WorkspaceSvg'); const ContextMenu = goog.require('Blockly.ContextMenu'); /* eslint-disable-next-line no-unused-vars */ const Procedures = goog.requireType('Blockly.Procedures'); -const Tooltip = goog.require('Blockly.Tooltip'); /* eslint-disable-next-line no-unused-vars */ const Variables = goog.requireType('Blockly.Variables'); /* eslint-disable-next-line no-unused-vars */ @@ -102,6 +101,7 @@ const {WorkspaceDragSurfaceSvg} = goog.requireType('Blockly.WorkspaceDragSurface const {Workspace} = goog.require('Blockly.Workspace'); /* eslint-disable-next-line no-unused-vars */ const {ZoomControls} = goog.requireType('Blockly.ZoomControls'); +const {tooltipManager} = goog.require('Blockly.Tooltip'); /** @suppress {extraRequire} */ goog.require('Blockly.Events.BlockCreate'); /** @suppress {extraRequire} */ @@ -2673,7 +2673,7 @@ class WorkspaceSvg extends Workspace { * @param {boolean=} opt_onlyClosePopups Whether only popups should be closed. */ hideChaff(opt_onlyClosePopups) { - Tooltip.hide(); + tooltipManager.hide(); WidgetDiv.hide(); DropDownDiv.hideWithoutAnimation(); diff --git a/scripts/gulpfiles/chunks.json b/scripts/gulpfiles/chunks.json index 3c299828575..4275d868e94 100644 --- a/scripts/gulpfiles/chunks.json +++ b/scripts/gulpfiles/chunks.json @@ -80,11 +80,9 @@ "./core/utils/math.js", "./core/utils/array.js", "./core/workspace.js", - "./core/events/events_block_delete.js", "./core/keyboard_nav/basic_cursor.js", "./core/keyboard_nav/tab_navigate_cursor.js", "./core/warning.js", - "./core/events/events_bubble_open.js", "./core/comment.js", "./core/events/events_block_drag.js", "./core/events/events_block_move.js", @@ -99,21 +97,22 @@ "./core/zoom_controls.js", "./core/workspace_drag_surface_svg.js", "./core/events/events_selected.js", - "./core/events/events_comment_move.js", - "./core/events/events_comment_delete.js", - "./core/events/events_comment_create.js", - "./core/events/events_comment_base.js", - "./core/events/events_comment_change.js", - "./core/workspace_comment.js", "./core/interfaces/i_movable.js", "./core/interfaces/i_selectable.js", "./core/interfaces/i_copyable.js", + "./core/events/events_comment_delete.js", + "./core/events/events_comment_change.js", + "./core/workspace_comment.js", + "./core/events/events_comment_create.js", + "./core/events/events_comment_base.js", + "./core/events/events_comment_move.js", "./core/workspace_comment_svg.js", "./core/workspace_audio.js", "./core/events/events_trashcan_open.js", "./core/sprites.js", "./core/drag_target.js", "./core/delete_area.js", + "./core/events/events_block_delete.js", "./core/positionable_helpers.js", "./core/trashcan.js", "./core/touch_gesture.js", @@ -197,7 +196,7 @@ "./core/events/events_var_delete.js", "./core/variable_map.js", "./core/names.js", - "./core/events/events_ui_base.js", + "./core/tooltip.js", "./core/events/events_marker_move.js", "./core/renderers/common/marker_svg.js", "./core/keyboard_nav/marker.js", @@ -226,6 +225,8 @@ "./core/utils/svg_paths.js", "./core/renderers/common/constants.js", "./core/field.js", + "./core/events/events_ui_base.js", + "./core/events/events_bubble_open.js", "./core/procedures.js", "./core/workspace_svg.js", "./core/utils/rect.js", @@ -237,11 +238,10 @@ "./core/bubble_dragger.js", "./core/connection_type.js", "./core/internal_constants.js", - "./core/block_animations.js", "./core/gesture.js", "./core/touch.js", "./core/browser_events.js", - "./core/tooltip.js", + "./core/block_animations.js", "./core/block_svg.js", "./core/events/events_block_base.js", "./core/events/events_block_change.js", @@ -253,7 +253,6 @@ "./core/extensions.js", "./core/block.js", "./core/utils/string.js", - "./core/utils/object.js", "./core/dialog.js", "./core/events/events_var_base.js", "./core/events/events_var_create.js", diff --git a/tests/deps.js b/tests/deps.js index 6118ba8b1b3..1e1e5d0bed5 100644 --- a/tests/deps.js +++ b/tests/deps.js @@ -223,7 +223,7 @@ goog.addDependency('../../core/toolbox/collapsible_category.js', ['Blockly.Colla goog.addDependency('../../core/toolbox/separator.js', ['Blockly.ToolboxSeparator'], ['Blockly.Css', 'Blockly.ToolboxItem', 'Blockly.registry', 'Blockly.utils.dom', 'Blockly.utils.object'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/toolbox/toolbox.js', ['Blockly.Toolbox'], ['Blockly.BlockSvg', 'Blockly.CollapsibleToolboxCategory', 'Blockly.ComponentManager', 'Blockly.Css', 'Blockly.DeleteArea', 'Blockly.Events.ToolboxItemSelect', 'Blockly.Events.utils', 'Blockly.IAutoHideable', 'Blockly.IKeyboardAccessible', 'Blockly.IStyleable', 'Blockly.IToolbox', 'Blockly.Options', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.common', 'Blockly.registry', 'Blockly.utils.KeyCodes', 'Blockly.utils.Rect', 'Blockly.utils.aria', 'Blockly.utils.dom', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/toolbox/toolbox_item.js', ['Blockly.ToolboxItem'], ['Blockly.IToolboxItem', 'Blockly.utils.idGenerator'], {'lang': 'es6', 'module': 'goog'}); -goog.addDependency('../../core/tooltip.js', ['Blockly.Tooltip'], ['Blockly.browserEvents', 'Blockly.common', 'Blockly.utils.deprecation', 'Blockly.utils.string'], {'lang': 'es6', 'module': 'goog'}); +goog.addDependency('../../core/tooltip.js', ['Blockly.Tooltip'], ['Blockly.browserEvents', 'Blockly.common', 'Blockly.utils.string'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/touch.js', ['Blockly.Touch'], ['Blockly.utils.global', 'Blockly.utils.string'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/touch_gesture.js', ['Blockly.TouchGesture'], ['Blockly.Gesture', 'Blockly.Touch', 'Blockly.browserEvents', 'Blockly.utils.Coordinate'], {'lang': 'es6', 'module': 'goog'}); goog.addDependency('../../core/trashcan.js', ['Blockly.Trashcan'], ['Blockly.ComponentManager', 'Blockly.DeleteArea', 'Blockly.Events.TrashcanOpen', 'Blockly.Events.utils', 'Blockly.IAutoHideable', 'Blockly.IPositionable', 'Blockly.Options', 'Blockly.browserEvents', 'Blockly.registry', 'Blockly.sprite', 'Blockly.uiPosition', 'Blockly.utils.Rect', 'Blockly.utils.Size', 'Blockly.utils.Svg', 'Blockly.utils.dom', 'Blockly.utils.toolbox'], {'lang': 'es6', 'module': 'goog'});