Add getTooltip (#4254)

* Add getTooltip

* Add tests

* Fix typings?

* Fix typings?

* PR Comments
This commit is contained in:
Beka Westberg
2020-09-10 08:57:05 -07:00
committed by GitHub
parent 74cf05535c
commit 47cc3b97b0
5 changed files with 319 additions and 24 deletions

View File

@@ -24,6 +24,7 @@ goog.require('Blockly.Extensions');
goog.require('Blockly.fieldRegistry');
goog.require('Blockly.Input');
goog.require('Blockly.navigation');
goog.require('Blockly.Tooltip');
goog.require('Blockly.utils');
goog.require('Blockly.utils.deprecation');
goog.require('Blockly.utils.Coordinate');
@@ -73,7 +74,7 @@ Blockly.Block = function(workspace, prototypeName, opt_id) {
* @private
*/
this.disabled = false;
/** @type {string|!Function} */
/** @type {!Blockly.Tooltip.TipInfo} */
this.tooltip = '';
/** @type {boolean} */
this.contextMenu = true;
@@ -905,14 +906,23 @@ Blockly.Block.prototype.setHelpUrl = function(url) {
};
/**
* Change the tooltip text for a block.
* @param {string|!Function} newTip Text for tooltip or a parent element to
* link to for its tooltip. May be a function that returns a string.
* Sets the tooltip for this block.
* @param {!Blockly.Tooltip.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.
*/
Blockly.Block.prototype.setTooltip = function(newTip) {
this.tooltip = newTip;
};
/**
* Returns the tooltip text for this block.
* @returns {!string} The tooltip text for this block.
*/
Blockly.Block.prototype.getTooltip = function() {
return Blockly.Tooltip.getTooltipOfObject(this);
};
/**
* Get the colour of a block.
* @return {string} #RRGGBB string.

View File

@@ -17,6 +17,7 @@ goog.provide('Blockly.Field');
goog.require('Blockly.Events');
goog.require('Blockly.Events.BlockChange');
goog.require('Blockly.Gesture');
goog.require('Blockly.Tooltip');
goog.require('Blockly.utils');
goog.require('Blockly.utils.deprecation');
goog.require('Blockly.utils.dom');
@@ -66,7 +67,7 @@ Blockly.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 {string|Function|!SVGElement}
* @type {?Blockly.Tooltip.TipInfo}
* @private
*/
this.tooltip_ = null;
@@ -996,23 +997,36 @@ Blockly.Field.prototype.onMouseDown_ = function(e) {
};
/**
* Change the tooltip text for this field.
* @param {string|Function|!SVGElement} newTip Text for tooltip or a parent
* element to link to for its tooltip.
* Sets the tooltip for this field.
* @param {?Blockly.Tooltip.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.
*/
Blockly.Field.prototype.setTooltip = function(newTip) {
if (!newTip && newTip !== '') { // If null or undefined.
newTip = this.sourceBlock_;
}
var clickTarget = this.getClickTarget_();
if (!clickTarget) {
if (clickTarget) {
clickTarget.tooltip = newTip;
} else {
// Field has not been initialized yet.
this.tooltip_ = newTip;
return;
}
};
if (!newTip && newTip !== '') { // If null or undefined.
clickTarget.tooltip = this.sourceBlock_;
} else {
clickTarget.tooltip = newTip;
/**
* Returns the tooltip text for this field.
* @returns {string} The tooltip text for this field.
*/
Blockly.Field.prototype.getTooltip = function() {
var clickTarget = this.getClickTarget_();
if (clickTarget) {
return Blockly.Tooltip.getTooltipOfObject(clickTarget);
}
// Field has not been initialized yet. Return stashed this.tooltip_ value.
return Blockly.Tooltip.getTooltipOfObject({tooltip: this.tooltip_});
};
/**

View File

@@ -23,6 +23,14 @@ goog.provide('Blockly.Tooltip');
goog.require('Blockly.utils.string');
/**
* 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)}
*/
Blockly.Tooltip.TipInfo;
/**
* Is a tooltip currently showing?
@@ -111,6 +119,45 @@ Blockly.Tooltip.MARGINS = 5;
*/
Blockly.Tooltip.DIV = null;
/**
* Returns the tooltip text for the given element.
* @param {?Object} object The object to get the the tooltip text of.
* @returns {string} The tooltip text of the element.
*/
Blockly.Tooltip.getTooltipOfObject = function(object) {
var obj = Blockly.Tooltip.getTargetObject_(object);
if (obj) {
var tooltip = obj.tooltip;
while (typeof tooltip == 'function') {
tooltip = tooltip();
}
if (typeof tooltip != 'string') {
throw Error('Tooltip function must return a string.');
}
return tooltip;
}
return '';
};
/**
* Returns the target object that the given object is targeting for its
* tooltip. Could be the object itself.
* @param {?Object} obj The object are trying to find the target tooltip
* object of.
* @returns {?{tooltip}} The target tooltip object.
* @private
*/
Blockly.Tooltip.getTargetObject_ = function(obj) {
while (obj && obj.tooltip) {
if ((typeof obj.tooltip == 'string') ||
(typeof obj.tooltip == 'function')) {
return obj;
}
obj = obj.tooltip;
}
return null;
};
/**
* Create the tooltip div and inject it onto the page.
*/
@@ -167,11 +214,8 @@ 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.currentTarget;
while ((typeof element.tooltip != 'string') &&
(typeof element.tooltip != 'function')) {
element = element.tooltip;
}
var element = /** @type {Element} */ (Blockly.Tooltip.getTargetObject_(
e.currentTarget));
if (Blockly.Tooltip.element_ != element) {
Blockly.Tooltip.hide();
Blockly.Tooltip.poisonedElement_ = null;
@@ -296,11 +340,7 @@ Blockly.Tooltip.show_ = function() {
}
// Erase all existing text.
Blockly.Tooltip.DIV.textContent = '';
// Get the new text.
var tip = Blockly.Tooltip.element_.tooltip;
while (typeof tip == 'function') {
tip = tip();
}
var tip = Blockly.Tooltip.getTooltipOfObject(Blockly.Tooltip.element_);
tip = Blockly.utils.string.wrap(tip, Blockly.Tooltip.LIMIT);
// Create new text, line by line.
var lines = tip.split('\n');

View File

@@ -83,6 +83,7 @@
<script src="registry_test.js"></script>
<script src="theme_test.js"></script>
<script src="toolbox_test.js"></script>
<script src="tooltip_test.js"></script>
<script src="trashcan_test.js"></script>
<script src="utils_test.js"></script>
<script src="variable_map_test.js"></script>

230
tests/mocha/tooltip_test.js Normal file
View File

@@ -0,0 +1,230 @@
/**
* @license
* Copyright 2020 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
suite('Tooltip', function() {
setup(function() {
sharedTestSetup.call(this);
this.workspace = new Blockly.Workspace();
});
teardown(function() {
sharedTestTeardown.call(this);
});
suite('set/getTooltip', function() {
setup(function() {
Blockly.defineBlocksWithJsonArray([
{
"type": "test_block",
"message0": "%1",
"args0": [
{
"type": "field_input",
"name": "FIELD"
}
]
}
]);
});
teardown(function() {
delete Blockly.Blocks["test_block"];
});
var tooltipText = 'testTooltip';
function assertTooltip(obj) {
chai.assert.equal(obj.getTooltip(), tooltipText);
}
function setStringTooltip(obj) {
obj.setTooltip(tooltipText);
}
function setFunctionTooltip(obj) {
obj.setTooltip(() => tooltipText);
}
function setNestedFunctionTooltip(obj) {
function nestFunction(fn, count) {
if (!count) {
return fn;
}
return () => nestFunction(fn, --count);
}
obj.setTooltip(nestFunction(() => tooltipText, 5));
}
function setFunctionReturningObjectTooltip(obj) {
obj.setTooltip(() => {
return {
tooltip: tooltipText
};
});
}
function setObjectTooltip(obj) {
obj.setTooltip({tooltip: tooltipText});
}
suite('Headless Blocks', function() {
setup(function() {
this.block = this.workspace.newBlock('test_block');
});
test('String', function() {
setStringTooltip(this.block);
assertTooltip(this.block);
});
test('Function', function() {
setFunctionTooltip(this.block);
assertTooltip(this.block);
});
test('Nested Function', function() {
setNestedFunctionTooltip(this.block);
assertTooltip(this.block);
});
test('Function returning object', function() {
setFunctionReturningObjectTooltip(this.block);
chai.assert.throws(this.block.getTooltip.bind(this.block),
'Tooltip function must return a string.');
});
test('Object', function() {
setObjectTooltip(this.block);
assertTooltip(this.block);
});
});
suite('Rendered Blocks', function() {
setup(function() {
this.renderedWorkspace = Blockly.inject('blocklyDiv');
this.block = this.renderedWorkspace.newBlock('test_block');
this.block.initSvg();
this.block.render();
});
teardown(function() {
workspaceTeardown.call(this, this.renderedWorkspace);
});
test('String', function() {
setStringTooltip(this.block);
assertTooltip(this.block);
});
test('Function', function() {
setFunctionTooltip(this.block);
assertTooltip(this.block);
});
test('Nested Function', function() {
setNestedFunctionTooltip(this.block);
assertTooltip(this.block);
});
test('Function returning object', function() {
setFunctionReturningObjectTooltip(this.block);
chai.assert.throws(this.block.getTooltip.bind(this.block),
'Tooltip function must return a string.');
});
test('Object', function() {
setObjectTooltip(this.block);
assertTooltip(this.block);
});
});
suite('Headless Fields', function() {
setup(function() {
this.block = this.workspace.newBlock('test_block');
this.field = this.block.getField('FIELD');
});
test('String', function() {
setStringTooltip(this.field);
assertTooltip(this.field);
});
test('Function', function() {
setFunctionTooltip(this.field);
assertTooltip(this.field);
});
test('Nested Function', function() {
setNestedFunctionTooltip(this.field);
assertTooltip(this.field);
});
test('Function returning object', function() {
setFunctionReturningObjectTooltip(this.field);
chai.assert.throws(this.field.getTooltip.bind(this.field),
'Tooltip function must return a string.');
});
test('Object', function() {
setObjectTooltip(this.field);
assertTooltip(this.field);
});
test('Null', function() {
setStringTooltip(this.block);
this.field.setTooltip(null);
assertTooltip(this.field);
});
});
suite('Rendered Fields', function() {
setup(function() {
this.renderedWorkspace = Blockly.inject('blocklyDiv');
this.block = this.renderedWorkspace.newBlock('test_block');
this.block.initSvg();
this.block.render();
this.field = this.block.getField('FIELD');
});
teardown(function() {
workspaceTeardown.call(this, this.renderedWorkspace);
});
test('String', function() {
setStringTooltip(this.field);
assertTooltip(this.field);
});
test('Function', function() {
setFunctionTooltip(this.field);
assertTooltip(this.field);
});
test('Nested Function', function() {
setNestedFunctionTooltip(this.field);
assertTooltip(this.field);
});
test('Function returning object', function() {
setFunctionReturningObjectTooltip(this.field);
chai.assert.throws(this.field.getTooltip.bind(this.field),
'Tooltip function must return a string.');
});
test('Object', function() {
setObjectTooltip(this.field);
assertTooltip(this.field);
});
test('Null', function() {
setStringTooltip(this.block);
this.field.setTooltip(null);
assertTooltip(this.field);
});
});
});
});