mirror of
https://github.com/google/blockly.git
synced 2026-01-11 19:07:08 +01:00
feat: Allow developers to set a custom tooltip rendering function. (#5956)
* refactor: refactor tooltip show method
* refactor: make Tooltip a class that is accessed via a singleton
* revert: "refactor: make Tooltip a class that is accessed via a singleton"
This reverts commit b3d543cc35.
* feat: add the ability to set a custom tooltip function
* fix: check for null where it matters for types
* feat: Add a test for the custom tooltip function
* fix: fix formatting
* fix: format test
* fix: remove unnecessary teardown call in test
This commit is contained in:
committed by
GitHub
parent
8058df2a71
commit
6841ccc99f
171
core/tooltip.js
171
core/tooltip.js
@@ -4,23 +4,15 @@
|
||||
* 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.
|
||||
* First, call createDom() 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.
|
||||
* If the tooltip is a string, or a function that returns a string, 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');
|
||||
@@ -42,6 +34,46 @@ const deprecation = goog.require('Blockly.utils.deprecation');
|
||||
let TipInfo;
|
||||
exports.TipInfo = TipInfo;
|
||||
|
||||
/**
|
||||
* A function that renders custom tooltip UI.
|
||||
* 1st parameter: the div element to render content into.
|
||||
* 2nd parameter: the element being moused over (i.e., the element for which the
|
||||
* tooltip should be shown).
|
||||
* @typedef {function(!Element, !Element)}
|
||||
* @alias Blockly.Tooltip.CustomTooltip
|
||||
*/
|
||||
let CustomTooltip;
|
||||
exports.CustomTooltip = CustomTooltip;
|
||||
|
||||
/**
|
||||
* An optional function that renders custom tooltips into the provided DIV. If
|
||||
* this is defined, the function will be called instead of rendering the default
|
||||
* tooltip UI.
|
||||
* @type {!CustomTooltip|undefined}
|
||||
*/
|
||||
let customTooltip = undefined;
|
||||
|
||||
/**
|
||||
* Sets a custom function that will be called if present instead of the default
|
||||
* tooltip UI.
|
||||
* @param {!CustomTooltip} customFn A custom tooltip used to render an alternate
|
||||
* tooltip UI.
|
||||
* @alias Blockly.Tooltip.setCustomTooltip
|
||||
*/
|
||||
const setCustomTooltip = function(customFn) {
|
||||
customTooltip = customFn;
|
||||
};
|
||||
exports.setCustomTooltip = setCustomTooltip;
|
||||
|
||||
/**
|
||||
* Gets the custom tooltip function.
|
||||
* @returns {!CustomTooltip|undefined} The custom tooltip function, if defined.
|
||||
*/
|
||||
const getCustomTooltip = function() {
|
||||
return customTooltip;
|
||||
};
|
||||
exports.getCustomTooltip = getCustomTooltip;
|
||||
|
||||
/**
|
||||
* Is a tooltip currently showing?
|
||||
* @type {boolean}
|
||||
@@ -409,6 +441,76 @@ const unblock = function() {
|
||||
};
|
||||
exports.unblock = unblock;
|
||||
|
||||
/**
|
||||
* Renders the tooltip content into the tooltip div.
|
||||
*/
|
||||
const renderContent = function() {
|
||||
if (!DIV || !element) {
|
||||
// This shouldn't happen, but if it does, we can't render.
|
||||
return;
|
||||
}
|
||||
if (typeof customTooltip === 'function') {
|
||||
customTooltip(DIV, element);
|
||||
} else {
|
||||
renderDefaultContent();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders the default tooltip UI.
|
||||
*/
|
||||
const renderDefaultContent = 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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return {x: anchorX, y: anchorY};
|
||||
};
|
||||
|
||||
/**
|
||||
* Create the tooltip and show it.
|
||||
*/
|
||||
@@ -423,46 +525,17 @@ const show = function() {
|
||||
}
|
||||
// Erase all existing text.
|
||||
DIV.textContent = '';
|
||||
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);
|
||||
}
|
||||
const rtl = /** @type {{RTL: boolean}} */ (element).RTL;
|
||||
const windowWidth = document.documentElement.clientWidth;
|
||||
const windowHeight = document.documentElement.clientHeight;
|
||||
|
||||
// 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;
|
||||
// Move the tooltip to just below the cursor.
|
||||
let anchorX = lastX;
|
||||
if (rtl) {
|
||||
anchorX -= OFFSET_X + DIV.offsetWidth;
|
||||
} else {
|
||||
anchorX += OFFSET_X;
|
||||
}
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
DIV.style.top = anchorY + 'px';
|
||||
DIV.style.left = anchorX + 'px';
|
||||
const {x, y} = getPosition(rtl);
|
||||
DIV.style.left = x + 'px';
|
||||
DIV.style.top = y + 'px';
|
||||
};
|
||||
|
||||
@@ -13,32 +13,63 @@ suite('Tooltip', function() {
|
||||
setup(function() {
|
||||
sharedTestSetup.call(this);
|
||||
this.workspace = new Blockly.Workspace();
|
||||
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'test_block',
|
||||
'message0': '%1',
|
||||
'args0': [
|
||||
{
|
||||
'type': 'field_input',
|
||||
'name': 'FIELD',
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
delete Blockly.Blocks['test_block'];
|
||||
sharedTestTeardown.call(this);
|
||||
});
|
||||
|
||||
suite('set/getTooltip', function() {
|
||||
suite('Custom Tooltip', function() {
|
||||
setup(function() {
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
"type": "test_block",
|
||||
"message0": "%1",
|
||||
"args0": [
|
||||
{
|
||||
"type": "field_input",
|
||||
"name": "FIELD",
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
this.renderedWorkspace = Blockly.inject('blocklyDiv', {});
|
||||
});
|
||||
|
||||
teardown(function() {
|
||||
delete Blockly.Blocks["test_block"];
|
||||
workspaceTeardown.call(this, this.renderedWorkspace);
|
||||
});
|
||||
|
||||
test('Custom function is called', function() {
|
||||
// Custom tooltip function is registered and should be called when mouse
|
||||
// events are fired.
|
||||
let wasCalled = false;
|
||||
const customFn = function() {
|
||||
wasCalled = true;
|
||||
};
|
||||
Blockly.Tooltip.setCustomTooltip(customFn);
|
||||
|
||||
this.block = this.renderedWorkspace.newBlock('test_block');
|
||||
this.block.setTooltip('Test Tooltip');
|
||||
|
||||
// Fire pointer events directly on the relevant SVG.
|
||||
// Note the 'pointerover', due to the events registered through
|
||||
// Blockly.browserEvents.bind being registered as pointer events rather
|
||||
// than mouse events. Mousemove event is registered directly on the
|
||||
// element rather than through browserEvents.
|
||||
this.block.pathObject.svgPath.dispatchEvent(
|
||||
new MouseEvent('pointerover'));
|
||||
this.block.pathObject.svgPath.dispatchEvent(new MouseEvent('mousemove'));
|
||||
this.clock.runAll();
|
||||
|
||||
chai.assert.isTrue(
|
||||
wasCalled, 'Expected custom tooltip function to have been called');
|
||||
});
|
||||
});
|
||||
|
||||
suite('set/getTooltip', function() {
|
||||
const tooltipText = 'testTooltip';
|
||||
|
||||
function assertTooltip(obj) {
|
||||
@@ -97,7 +128,8 @@ suite('Tooltip', function() {
|
||||
|
||||
test('Function returning object', function() {
|
||||
setFunctionReturningObjectTooltip(this.block);
|
||||
chai.assert.throws(this.block.getTooltip.bind(this.block),
|
||||
chai.assert.throws(
|
||||
this.block.getTooltip.bind(this.block),
|
||||
'Tooltip function must return a string.');
|
||||
});
|
||||
|
||||
@@ -136,7 +168,8 @@ suite('Tooltip', function() {
|
||||
|
||||
test('Function returning object', function() {
|
||||
setFunctionReturningObjectTooltip(this.block);
|
||||
chai.assert.throws(this.block.getTooltip.bind(this.block),
|
||||
chai.assert.throws(
|
||||
this.block.getTooltip.bind(this.block),
|
||||
'Tooltip function must return a string.');
|
||||
});
|
||||
|
||||
@@ -169,7 +202,8 @@ suite('Tooltip', function() {
|
||||
|
||||
test('Function returning object', function() {
|
||||
setFunctionReturningObjectTooltip(this.field);
|
||||
chai.assert.throws(this.field.getTooltip.bind(this.field),
|
||||
chai.assert.throws(
|
||||
this.field.getTooltip.bind(this.field),
|
||||
'Tooltip function must return a string.');
|
||||
});
|
||||
|
||||
@@ -215,7 +249,8 @@ suite('Tooltip', function() {
|
||||
|
||||
test('Function returning object', function() {
|
||||
setFunctionReturningObjectTooltip(this.field);
|
||||
chai.assert.throws(this.field.getTooltip.bind(this.field),
|
||||
chai.assert.throws(
|
||||
this.field.getTooltip.bind(this.field),
|
||||
'Tooltip function must return a string.');
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user