Text+drop-downs and number+drop-downs (#662)

* Don't force text-centering around FIELD_WIDTH if arrow present

* Add dark arrow asset

* Refactor FieldNumber restrictor generator to be static

* Allow disabling of FieldDropDown colour changing

* Add FieldTextDropDown and FieldNumberDropDown

* Add number drop-downs to list blocks

* Add docs

* Update `setArgType` -> `addArgType`

* Ensure arrow only created once

* Update list index min

* Refactor shared components of num restriction

* Line breaks

* Clear touch identifier

* Clear touch identifier when text drop-down closed

* Inheritance simplification
This commit is contained in:
Tim Mickel 2016-10-05 17:14:33 -04:00 committed by GitHub
parent 38ae0dcaf8
commit d65c274652
12 changed files with 374 additions and 49 deletions

View file

@ -206,6 +206,66 @@ Blockly.Blocks['data_listcontents'] = {
}
};
Blockly.Blocks['data_listindexall'] = {
/**
* List index menu, with all option.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": "%1",
"args0": [
{
"type": "field_numberdropdown",
"name": "INDEX",
"value": "1",
"min": 1,
"precision": 1,
"options": [
["1", "1"],
["last", "last"],
["all", "all"]
]
}
],
"output": "String",
"category": Blockly.Categories.data,
"outputShape": Blockly.OUTPUT_SHAPE_ROUND,
"colour": Blockly.Colours.textField
});
}
};
Blockly.Blocks['data_listindexrandom'] = {
/**
* List index menu, with random option.
* @this Blockly.Block
*/
init: function() {
this.jsonInit({
"message0": "%1",
"args0": [
{
"type": "field_numberdropdown",
"name": "INDEX",
"value": "1",
"min": 1,
"precision": 1,
"options": [
["1", "1"],
["last", "last"],
["random", "random"]
]
}
],
"output": "String",
"category": Blockly.Categories.data,
"outputShape": Blockly.OUTPUT_SHAPE_ROUND,
"colour": Blockly.Colours.textField
});
}
};
Blockly.Blocks['data_addtolist'] = {
/**
* Block to add item to list.

View file

@ -132,7 +132,7 @@ Blockly.Block = function(workspace, prototypeName, opt_id) {
* @private
*/
this.outputShape_ = null;
/**
* @type {?string}
* @private
@ -1243,6 +1243,18 @@ Blockly.Block.prototype.interpolate_ = function(message, args, lastDummyAlign) {
field.setSpellcheck(element['spellcheck']);
}
break;
case 'field_textdropdown':
field = new Blockly.FieldTextDropdown(element['text'], element['options']);
if (typeof element['spellcheck'] == 'boolean') {
field.setSpellcheck(element['spellcheck']);
}
break;
case 'field_numberdropdown':
field = new Blockly.FieldNumberDropdown(
element['value'], element['options'],
element['min'], element['max'], element['precision']
);
break;
case 'field_angle':
field = new Blockly.FieldAngle(element['angle']);
break;

View file

@ -40,7 +40,9 @@ goog.require('Blockly.FieldDropdown');
goog.require('Blockly.FieldIconMenu');
goog.require('Blockly.FieldImage');
goog.require('Blockly.FieldTextInput');
goog.require('Blockly.FieldTextDropdown');
goog.require('Blockly.FieldNumber');
goog.require('Blockly.FieldNumberDropdown');
goog.require('Blockly.FieldVariable');
goog.require('Blockly.Generator');
goog.require('Blockly.Msg');

View file

@ -198,6 +198,10 @@ Blockly.Css.CONTENT = [
'-webkit-transform-origin: 0 0;',
'}',
'.blocklyTextDropDownArrow {',
'position: absolute;',
'}',
'.blocklyNonSelectable {',
'user-select: none;',
'-moz-user-select: none;',

View file

@ -366,8 +366,9 @@ Blockly.Field.prototype.render_ = function() {
}
// In a text-editing shadow block's field,
// if half the text length is not at least center of
// visible field (FIELD_WIDTH), center it there instead.
if (this.sourceBlock_.isShadow()) {
// visible field (FIELD_WIDTH), center it there instead,
// unless there is a drop-down arrow.
if (this.sourceBlock_.isShadow() && !this.positionArrow) {
var minOffset = Blockly.BlockSvg.FIELD_WIDTH / 2;
if (this.sourceBlock_.RTL) {
// X position starts at the left edge of the block, in both RTL and LTR.

View file

@ -129,6 +129,7 @@ Blockly.FieldDropdown.prototype.init = function() {
* @private
*/
Blockly.FieldDropdown.prototype.showEditor_ = function() {
this.dropDownOpen_ = true;
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
Blockly.DropDownDiv.hideWithoutAnimation();
Blockly.DropDownDiv.clearContent();
@ -224,12 +225,14 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() {
menuDom.focus();
// Update colour to look selected.
if (this.sourceBlock_.isShadow()) {
this.savedPrimary_ = this.sourceBlock_.getColour();
this.sourceBlock_.setColour(this.sourceBlock_.getColourTertiary(),
this.sourceBlock_.getColourSecondary(), this.sourceBlock_.getColourTertiary());
} else if (this.box_) {
this.box_.setAttribute('fill', this.sourceBlock_.getColourTertiary());
if (!this.disableColourChange_) {
if (this.sourceBlock_.isShadow()) {
this.savedPrimary_ = this.sourceBlock_.getColour();
this.sourceBlock_.setColour(this.sourceBlock_.getColourTertiary(),
this.sourceBlock_.getColourSecondary(), this.sourceBlock_.getColourTertiary());
} else if (this.box_) {
this.box_.setAttribute('fill', this.sourceBlock_.getColourTertiary());
}
}
};
@ -237,12 +240,15 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() {
* Callback for when the drop-down is hidden.
*/
Blockly.FieldDropdown.prototype.onHide = function() {
this.dropDownOpen_ = false;
// Update colour to look selected.
if (this.sourceBlock_.isShadow()) {
this.sourceBlock_.setColour(this.savedPrimary_,
this.sourceBlock_.getColourSecondary(), this.sourceBlock_.getColourTertiary());
} else if (this.box_) {
this.box_.setAttribute('fill', this.sourceBlock_.getColour());
if (!this.disableColourChange_) {
if (this.sourceBlock_.isShadow()) {
this.sourceBlock_.setColour(this.savedPrimary_,
this.sourceBlock_.getColourSecondary(), this.sourceBlock_.getColourTertiary());
} else if (this.box_) {
this.box_.setAttribute('fill', this.sourceBlock_.getColour());
}
}
};
@ -366,6 +372,11 @@ Blockly.FieldDropdown.prototype.setText = function(text) {
}
};
/**
* Position a drop-down arrow at the appropriate location at render-time.
* @param {number} x X position the arrow is being rendered at, in px.
* @return {number} Amount of space the arrow is taking up, in px.
*/
Blockly.FieldDropdown.prototype.positionArrow = function(x) {
var addedWidth = 0;
if (this.sourceBlock_.RTL) {

View file

@ -30,25 +30,6 @@ goog.require('Blockly.FieldTextInput');
goog.require('goog.math');
goog.require('goog.userAgent');
/**
* Return an appropriate restrictor, depending on whether this FieldNumber
* allows decimal or negative numbers.
* @param {boolean} decimalAllowed Whether number may have decimal/float component.
* @param {boolean} negativeAllowed Whether number may be negative.
* @return {!RegExp} Regular expression for this FieldNumber's restrictor.
*/
var getNumRestrictor = function(decimalAllowed, negativeAllowed) {
var pattern = "[\\d]"; // Always allow digits.
if (decimalAllowed) {
pattern += "|[\\.]";
}
if (negativeAllowed) {
pattern += "|[-]";
}
return new RegExp(pattern);
};
/**
* Class for an editable number field.
* In scratch-blocks, the min/max/precision properties are only used
@ -67,11 +48,7 @@ var getNumRestrictor = function(decimalAllowed, negativeAllowed) {
* @constructor
*/
Blockly.FieldNumber = function(value, opt_min, opt_max, opt_precision, opt_validator) {
this.decimalAllowed_ = (typeof opt_precision == 'undefined') || isNaN(opt_precision) ||
(opt_precision == 0) ||
(Math.floor(opt_precision) != opt_precision);
this.negativeAllowed_ = (typeof opt_min == 'undefined') || isNaN(opt_min) || opt_min < 0;
var numRestrictor = getNumRestrictor(this.decimalAllowed_, this.negativeAllowed_);
var numRestrictor = this.getNumRestrictor(opt_min, opt_max, opt_precision);
Blockly.FieldNumber.superClass_.constructor.call(this, value, opt_validator, numRestrictor);
this.addArgType('number');
};
@ -126,6 +103,28 @@ Blockly.FieldNumber.NUMPAD_DELETE_ICON = 'data:image/svg+xml;utf8,' +
*/
Blockly.FieldNumber.activeField_ = null;
/**
* Return an appropriate restrictor, depending on whether this FieldNumber
* allows decimal or negative numbers.
* @param {number|string|undefined} opt_min Minimum value.
* @param {number|string|undefined} opt_max Maximum value.
* @param {number|string|undefined} opt_precision Precision for value.
* @return {!RegExp} Regular expression for this FieldNumber's restrictor.
*/
Blockly.FieldNumber.prototype.getNumRestrictor = function(opt_min, opt_max, opt_precision) {
this.decimalAllowed_ = (typeof opt_precision == 'undefined') || isNaN(opt_precision) ||
(opt_precision == 0) || (Math.floor(opt_precision) != opt_precision);
this.negativeAllowed_ = (typeof opt_min == 'undefined') || isNaN(opt_min) || opt_min < 0;
var pattern = "[\\d]"; // Always allow digits.
if (this.decimalAllowed_) {
pattern += "|[\\.]";
}
if (this.negativeAllowed_) {
pattern += "|[-]";
}
return new RegExp(pattern);
};
/**
* Set the constraints for this field.
* @param {number=} opt_min Minimum number allowed.
@ -134,8 +133,7 @@ Blockly.FieldNumber.activeField_ = null;
*/
Blockly.FieldNumber.prototype.setConstraints_ = function(opt_min, opt_max, opt_precision) {
this.decimalAllowed_ = (typeof opt_precision == 'undefined') || isNaN(opt_precision) ||
(opt_precision == 0) ||
(Math.floor(opt_precision) != opt_precision);
(opt_precision == 0) || (Math.floor(opt_precision) != opt_precision);
this.negativeAllowed_ = (typeof opt_min == 'undefined') || isNaN(opt_min) || opt_min < 0;
};

View file

@ -0,0 +1,58 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 Combination number + drop-down field
* @author tmickel@mit.edu (Tim Mickel)
*/
'use strict';
goog.provide('Blockly.FieldNumberDropdown');
goog.require('Blockly.FieldTextDropdown');
goog.require('goog.userAgent');
/**
* Class for a combination number + drop-down field.
* @param {number|string} value The initial content of the field.
* @param {(!Array.<!Array.<string>>|!Function)} menuGenerator An array of
* options for a dropdown list, or a function which generates these options.
* @param {number|string|undefined} opt_min Minimum value.
* @param {number|string|undefined} opt_max Maximum value.
* @param {number|string|undefined} opt_precision Precision for value.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns the accepted text or null to abort
* the change.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldNumberDropdown = function(value, menuGenerator, opt_min, opt_max, opt_precision, opt_validator) {
var numRestrictor = Blockly.FieldNumber.prototype.getNumRestrictor.call(
this, opt_min, opt_max, opt_precision
);
Blockly.FieldNumberDropdown.superClass_.constructor.call(
this, value, menuGenerator, opt_validator, numRestrictor
);
this.addArgType('numberdropdown');
};
goog.inherits(Blockly.FieldNumberDropdown, Blockly.FieldTextDropdown);

141
core/field_textdropdown.js Normal file
View file

@ -0,0 +1,141 @@
/**
* @license
* Visual Blocks Editor
*
* Copyright 2013 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 Combination text + drop-down field
* @author tmickel@mit.edu (Tim Mickel)
*/
'use strict';
goog.provide('Blockly.FieldTextDropdown');
goog.require('Blockly.DropDownDiv');
goog.require('Blockly.FieldDropdown');
goog.require('Blockly.FieldTextInput');
goog.require('goog.userAgent');
/**
* Class for a combination text + drop-down field.
* @param {string} text The initial content of the text field.
* @param {(!Array.<!Array.<string>>|!Function)} menuGenerator An array of
* options for a dropdown list, or a function which generates these options.
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns the accepted text or null to abort
* the change.
* @param {RegExp=} opt_restrictor An optional regular expression to restrict
* typed text to. Text that doesn't match the restrictor will never show
* in the text field.
* @extends {Blockly.FieldTextInput}
* @constructor
*/
Blockly.FieldTextDropdown = function(text, menuGenerator, opt_validator, opt_restrictor) {
this.menuGenerator_ = menuGenerator;
Blockly.FieldDropdown.prototype.trimOptions_.call(this);
Blockly.FieldTextDropdown.superClass_.constructor.call(this, text, opt_validator, opt_restrictor);
this.addArgType('textdropdown');
};
goog.inherits(Blockly.FieldTextDropdown, Blockly.FieldTextInput);
/**
* Install this text drop-down field on a block.
*/
Blockly.FieldTextDropdown.prototype.init = function() {
Blockly.FieldTextDropdown.superClass_.init.call(this);
// Add dropdown arrow: "option ▾" (LTR) or "▾ אופציה" (RTL)
// Positioned on render, after text size is calculated.
if (!this.arrow_) {
/** @type {Number} */
this.arrowSize_ = 12;
/** @type {Number} */
this.arrowX_ = 0;
/** @type {Number} */
this.arrowY_ = 11;
this.arrow_ = Blockly.createSvgElement('image', {
'height': this.arrowSize_ + 'px',
'width': this.arrowSize_ + 'px'
});
this.arrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'dropdown-arrow-dark.svg');
this.arrow_.style.cursor = 'pointer';
this.fieldGroup_.appendChild(this.arrow_);
this.mouseUpWrapper_ =
Blockly.bindEvent_(this.arrow_, 'mouseup', this,
this.showDropdown_);
}
// Prevent the drop-down handler from changing the field colour on open.
this.disableColourChange_ = true;
};
/**
* Close the input widget if this input is being deleted.
*/
Blockly.FieldTextDropdown.prototype.dispose = function() {
if (this.mouseUpWrapper_) {
Blockly.unbindEvent_(this.mouseUpWrapper_);
this.mouseUpWrapper_ = null;
Blockly.Touch.clearTouchIdentifier();
}
Blockly.FieldTextDropdown.superClass_.dispose.call(this);
};
/**
* If the drop-down isn't open, show the text editor.
*/
Blockly.FieldTextDropdown.prototype.showEditor_ = function() {
if (!this.dropDownOpen_) {
Blockly.FieldTextDropdown.superClass_.showEditor_.call(this, null, null,
true, function() {
// When the drop-down arrow is clicked, hide text editor and show drop-down.
Blockly.WidgetDiv.hide();
this.showDropdown_();
Blockly.Touch.clearTouchIdentifier();
});
}
};
/**
* Return a list of the options for this dropdown.
* See: Blockly.FieldDropDown.prototype.getOptions_.
* @return {!Array.<!Array.<string>>} Array of option tuples:
* (human-readable text, language-neutral name).
* @private
*/
Blockly.FieldTextDropdown.prototype.getOptions_ = Blockly.FieldDropdown.prototype.getOptions_;
/**
* Position a drop-down arrow at the appropriate location at render-time.
* See: Blockly.FieldDropDown.prototype.positionArrow.
* @param {number} x X position the arrow is being rendered at, in px.
* @return {number} Amount of space the arrow is taking up, in px.
*/
Blockly.FieldTextDropdown.prototype.positionArrow = Blockly.FieldDropdown.prototype.positionArrow;
/**
* Create the dropdown menu.
* @private
*/
Blockly.FieldTextDropdown.prototype.showDropdown_ = Blockly.FieldDropdown.prototype.showEditor_;
/**
* Callback when the drop-down menu is hidden.
*/
Blockly.FieldTextDropdown.prototype.onHide = Blockly.FieldDropdown.prototype.onHide;

View file

@ -152,9 +152,12 @@ Blockly.FieldTextInput.prototype.setRestrictor = function(restrictor) {
* focus. Defaults to false.
* @param {boolean=} opt_readOnly True if editor should be created with HTML
* input set to read-only, to prevent virtual keyboards.
* @param {boolean=} opt_withArrow True to show drop-down arrow in text editor.
* @param {Function=} opt_arrowCallback Callback for when drop-down arrow clicked.
* @private
*/
Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput, opt_readOnly) {
Blockly.FieldTextInput.prototype.showEditor_ = function(
opt_quietInput, opt_readOnly, opt_withArrow, opt_arrowCallback) {
this.workspace_ = this.sourceBlock_.workspace;
var quietInput = opt_quietInput || false;
var readOnly = opt_readOnly || false;
@ -175,6 +178,36 @@ Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput, opt_read
Blockly.FieldTextInput.htmlInput_ = htmlInput;
div.appendChild(htmlInput);
if (opt_withArrow) {
// Move text in input to account for displayed drop-down arrow.
if (this.sourceBlock_.RTL) {
htmlInput.style.paddingLeft = (this.arrowSize_+ Blockly.BlockSvg.DROPDOWN_ARROW_PADDING) + 'px';
} else {
htmlInput.style.paddingRight = (this.arrowSize_ + Blockly.BlockSvg.DROPDOWN_ARROW_PADDING) + 'px';
}
// Create the arrow.
var dropDownArrow =
goog.dom.createDom(goog.dom.TagName.IMG, 'blocklyTextDropDownArrow');
dropDownArrow.setAttribute('src',
Blockly.mainWorkspace.options.pathToMedia + 'dropdown-arrow-dark.svg');
dropDownArrow.style.width = this.arrowSize_ + 'px';
dropDownArrow.style.height = this.arrowSize_ + 'px';
dropDownArrow.style.top = this.arrowY_ + 'px';
dropDownArrow.style.cursor = 'pointer';
// Magic number for positioning the drop-down arrow on top of the text editor.
var dropdownArrowMagic = '11px';
if (this.sourceBlock_.RTL) {
dropDownArrow.style.left = dropdownArrowMagic;
} else {
dropDownArrow.style.right = dropdownArrowMagic;
}
if (opt_arrowCallback) {
htmlInput.dropDownArrowMouseWrapper_ = Blockly.bindEvent_(dropDownArrow,
'mousedown', this, opt_arrowCallback);
}
div.appendChild(dropDownArrow);
}
htmlInput.value = htmlInput.defaultValue = this.text_;
htmlInput.oldValue_ = null;
this.validate_();
@ -379,6 +412,7 @@ Blockly.FieldTextInput.prototype.resizeEditor_ = function() {
if (this.sourceBlock_.RTL) {
xy.x += width;
xy.x -= div.offsetWidth * scale;
xy.x += 1 * scale;
}
// Shift by a few pixels to line up exactly.
xy.y += 1 * scale;
@ -438,6 +472,9 @@ Blockly.FieldTextInput.prototype.widgetDispose_ = function() {
Blockly.unbindEvent_(htmlInput.onKeyUpWrapper_);
Blockly.unbindEvent_(htmlInput.onKeyPressWrapper_);
Blockly.unbindEvent_(htmlInput.onInputWrapper_);
if (htmlInput.dropDownArrowMouseWrapper_) {
Blockly.unbindEvent_(htmlInput.dropDownArrowMouseWrapper_);
}
thisField.workspace_.removeChangeListener(
htmlInput.onWorkspaceChangeWrapper_);

View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" width="12.71" height="8.79" viewBox="0 0 12.71 8.79"><title>dropdown-arrow</title><g opacity="0.1"><path d="M12.71,2.44A2.41,2.41,0,0,1,12,4.16L8.08,8.08a2.45,2.45,0,0,1-3.45,0L0.72,4.16A2.42,2.42,0,0,1,0,2.44,2.48,2.48,0,0,1,.71.71C1,0.47,1.43,0,6.36,0S11.75,0.46,12,.71A2.44,2.44,0,0,1,12.71,2.44Z" fill="#231f20"/></g><path d="M6.36,7.79a1.43,1.43,0,0,1-1-.42L1.42,3.45a1.44,1.44,0,0,1,0-2c0.56-.56,9.31-0.56,9.87,0a1.44,1.44,0,0,1,0,2L7.37,7.37A1.43,1.43,0,0,1,6.36,7.79Z" fill="#575E75"/></svg>

After

Width:  |  Height:  |  Size: 573 B

View file

@ -750,15 +750,15 @@
</block>
<block type="data_deleteoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
<shadow type="data_listindexall">
<field name="INDEX">1</field>
</shadow>
</value>
</block>
<block type="data_insertatlist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
<shadow type="data_listindexrandom">
<field name="INDEX">1</field>
</shadow>
</value>
<value name="ITEM">
@ -769,8 +769,8 @@
</block>
<block type="data_replaceitemoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
<shadow type="data_listindexrandom">
<field name="INDEX">1</field>
</shadow>
</value>
<value name="ITEM">
@ -781,8 +781,8 @@
</block>
<block type="data_itemoflist">
<value name="INDEX">
<shadow type="math_integer">
<field name="NUM">1</field>
<shadow type="data_listindexrandom">
<field name="INDEX">1</field>
</shadow>
</value>
</block>