Skip to content

Commit

Permalink
feat: Allow developers to set a custom tooltip rendering function. (#…
Browse files Browse the repository at this point in the history
…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 b3d543c.

* 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
  • Loading branch information
maribethb authored Mar 4, 2022
1 parent 8058df2 commit 6841ccc
Show file tree
Hide file tree
Showing 2 changed files with 156 additions and 48 deletions.
133 changes: 103 additions & 30 deletions core/tooltip.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand All @@ -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}
Expand Down Expand Up @@ -410,19 +442,24 @@ const unblock = function() {
exports.unblock = unblock;

/**
* Create the tooltip and show it.
* Renders the tooltip content into the tooltip div.
*/
const show = function() {
if (blocked) {
// Someone doesn't want us to show tooltips.
const renderContent = function() {
if (!DIV || !element) {
// This shouldn't happen, but if it does, we can't render.
return;
}
poisonedElement = element;
if (!DIV) {
return;
if (typeof customTooltip === 'function') {
customTooltip(DIV, element);
} else {
renderDefaultContent();
}
// Erase all existing text.
DIV.textContent = '';
};

/**
* Renders the default tooltip UI.
*/
const renderDefaultContent = function() {
let tip = getTooltipOfObject(element);
tip = blocklyString.wrap(tip, LIMIT);
// Create new text, line by line.
Expand All @@ -432,26 +469,33 @@ const show = function() {
div.appendChild(document.createTextNode(lines[i]));
DIV.appendChild(div);
}
const rtl = /** @type {{RTL: boolean}} */ (element).RTL;
};

/**
* 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;
// Display the tooltip.
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;

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);
Expand All @@ -463,6 +507,35 @@ const show = function() {
anchorX = windowWidth - DIV.offsetWidth - 2 * MARGINS;
}
}
DIV.style.top = anchorY + 'px';
DIV.style.left = anchorX + 'px';

return {x: anchorX, y: anchorY};
};

/**
* Create the tooltip and show it.
*/
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';
};
71 changes: 53 additions & 18 deletions tests/mocha/tooltip_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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.');
});

Expand Down Expand Up @@ -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.');
});

Expand Down Expand Up @@ -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.');
});

Expand Down Expand Up @@ -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.');
});

Expand Down

0 comments on commit 6841ccc

Please sign in to comment.