diff --git a/core/field_dropdown.js b/core/field_dropdown.js index be44fff35..0f3568072 100644 --- a/core/field_dropdown.js +++ b/core/field_dropdown.js @@ -39,8 +39,8 @@ goog.require('goog.userAgent'); /** * Class for an editable dropdown field. - * @param {(!Array.>|!Function)} menuGenerator An array of - * options for a dropdown list, or a function which generates these options. + * @param {(!Array.|!Function)} menuGenerator An array of options + * for a dropdown list, or a function which generates these options. * @param {Function=} opt_validator A function that is executed when a new * option is selected, with the newly selected value as its sole argument. * If it returns a value, that value (which must be one of the options) will @@ -76,12 +76,27 @@ Blockly.FieldDropdown.ARROW_CHAR = goog.userAgent.ANDROID ? '\u25BC' : '\u25BE'; Blockly.FieldDropdown.prototype.CURSOR = 'default'; /** - * Language-neutral currently selected string. - * @type {string} + * Language-neutral currently selected string or image object. + * @type {string|!Object} * @private */ Blockly.FieldDropdown.prototype.value_ = ''; +/** + * SVG image element if currently selected option is an image, or null. + * @type {SVGElement} + * @private + */ +Blockly.FieldDropdown.prototype.imageElement_ = null; + +/** + * Object wih src, height, width, and alt attributes if currently selected + * option is an image, or null. + * @type {Object} + * @private + */ +Blockly.FieldDropdown.prototype.imageJson_ = null; + /** * Install this dropdown on a block. */ @@ -124,9 +139,16 @@ Blockly.FieldDropdown.prototype.showEditor_ = function() { menu.setRightToLeft(this.sourceBlock_.RTL); var options = this.getOptions_(); for (var i = 0; i < options.length; i++) { - var text = options[i][0]; // Human-readable text. - var value = options[i][1]; // Language-neutral value. - var menuItem = new goog.ui.MenuItem(text); + var content = options[i][0]; // Human-readable text or image. + var value = options[i][1]; // Language-neutral value. + if (typeof content == 'object') { + // An image, not text. + var image = new Image(content['width'], content['height']); + image.src = content['src']; + image.alt = content['alt'] || ''; + content = image; + } + var menuItem = new goog.ui.MenuItem(content); menuItem.setRightToLeft(this.sourceBlock_.RTL); menuItem.setValue(value); menuItem.setCheckable(true); @@ -221,7 +243,13 @@ Blockly.FieldDropdown.prototype.trimOptions_ = function() { if (!goog.isArray(options) || options.length < 2) { return; } - var strings = options.map(function(t) {return t[0];}); + var strings = []; + for (var i = 0; i < options.length; i++) { + var text = options[i][0]; + if (typeof text == 'string') { + strings.push(text); + } + } var shortest = Blockly.shortestStringLength(strings); var prefixLength = Blockly.commonWordPrefix(strings, shortest); var suffixLength = Blockly.commonWordSuffix(strings, shortest); @@ -242,17 +270,19 @@ Blockly.FieldDropdown.prototype.trimOptions_ = function() { var newOptions = []; for (var i = 0; i < options.length; i++) { var text = options[i][0]; - var value = options[i][1]; - text = text.substring(prefixLength, text.length - suffixLength); - newOptions[i] = [text, value]; + if (typeof text == 'string') { + var value = options[i][1]; + text = text.substring(prefixLength, text.length - suffixLength); + newOptions[i] = [text, value]; + } } this.menuGenerator_ = newOptions; }; /** * Return a list of the options for this dropdown. - * @return {!Array.>} Array of option tuples: - * (human-readable text, language-neutral name). + * @return {!Array.} Array of option tuples: + * (human-readable text or image, language-neutral name). * @private */ Blockly.FieldDropdown.prototype.getOptions_ = function() { @@ -288,7 +318,22 @@ Blockly.FieldDropdown.prototype.setValue = function(newValue) { for (var i = 0; i < options.length; i++) { // Options are tuples of human-readable text and language-neutral values. if (options[i][1] == newValue) { - this.setText(options[i][0]); + var content = options[i][0]; + goog.dom.removeNode(this.imageElement_); + if (typeof content == 'object') { + this.imageJson_ = content; + this.imageElement_ = Blockly.createSvgElement('image', + {'y': 5, + 'height': content.height + 'px', + 'width': content.width + 'px'}, this.fieldGroup_); + this.imageElement_.setAttributeNS('http://www.w3.org/1999/xlink', + 'xlink:href', content.src); + this.setText(content.alt); + } else { + this.imageJson_ = null; + this.imageElement_ = null; + this.setText(content); + } return; } } @@ -306,12 +351,28 @@ Blockly.FieldDropdown.prototype.setText = function(text) { // Update arrow's colour. this.arrow_.style.fill = this.sourceBlock_.getColour(); } - if (text === null || text === this.text_) { + if (text === null) { // No change if null. return; } this.text_ = text; - this.updateTextNode_(); + if (this.imageJson_) { + if (this.textElement_) { + this.textElement_.style.display = 'none'; + } + this.size_.height = Number(this.imageJson_.height) + 19; + this.render_(); + } else { + if (this.textElement_) { + this.textElement_.style.display = 'block'; + } + this.size_.height = Blockly.BlockSvg.MIN_BLOCK_Y; + this.updateTextNode_(); + } + // Unlike other editable fields, a dropdown can change height. + if (this.borderRect_) { + this.borderRect_.setAttribute('height', this.size_.height - 9); + } if (this.textElement_) { // Insert dropdown arrow. @@ -328,6 +389,20 @@ Blockly.FieldDropdown.prototype.setText = function(text) { } }; +/** + * Draws the border with the correct width. + * @private + */ +Blockly.FieldDropdown.prototype.render_ = function() { + if (!this.imageJson_) { + Blockly.FieldDropdown.superClass_.render_.call(this); + } else if (this.visible_ && this.borderRect_) { + this.size_.width = Number(this.imageJson_.width); + this.borderRect_.setAttribute('width', + this.size_.width + Blockly.BlockSvg.SEP_SPACE_X); + } +}; + /** * Close the dropdown menu if this input is being deleted. */ diff --git a/demos/blockfactory/blocks.js b/demos/blockfactory/blocks.js index 78e351fcd..d4ea11652 100644 --- a/demos/blockfactory/blocks.js +++ b/demos/blockfactory/blocks.js @@ -318,11 +318,12 @@ Blockly.Blocks['field_dropdown'] = { this.appendDummyInput() .appendField('dropdown') .appendField(new Blockly.FieldTextInput('NAME'), 'FIELDNAME'); - this.optionCount_ = 3; + this.optionList_ = ['text', 'text', 'text']; this.updateShape_(); this.setPreviousStatement(true, 'Field'); this.setNextStatement(true, 'Field'); - this.setMutator(new Blockly.Mutator(['field_dropdown_option'])); + this.setMutator(new Blockly.Mutator(['field_dropdown_option_text', + 'field_dropdown_option_image'])); this.setColour(160); this.setTooltip('Dropdown menu with a list of options.'); this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); @@ -330,12 +331,21 @@ Blockly.Blocks['field_dropdown'] = { mutationToDom: function(workspace) { // Create XML to represent menu options. var container = document.createElement('mutation'); - container.setAttribute('options', this.optionCount_); + container.setAttribute('options', JSON.stringify(this.optionList_)); return container; }, domToMutation: function(container) { // Parse XML to restore the menu options. - this.optionCount_ = parseInt(container.getAttribute('options'), 10); + var value = JSON.parse(container.getAttribute('options')); + if (typeof value == 'number') { + // Old format from before images were added. November 2016. + this.optionList_ = []; + for (var i = 0; i < value; i++) { + this.optionList_.push('text'); + } + } else { + this.optionList_ = value; + } this.updateShape_(); }, decompose: function(workspace) { @@ -343,8 +353,9 @@ Blockly.Blocks['field_dropdown'] = { var containerBlock = workspace.newBlock('field_dropdown_container'); containerBlock.initSvg(); var connection = containerBlock.getInput('STACK').connection; - for (var i = 0; i < this.optionCount_; i++) { - var optionBlock = workspace.newBlock('field_dropdown_option'); + for (var i = 0; i < this.optionList_.length; i++) { + var optionBlock = workspace.newBlock( + 'field_dropdown_option_' + this.optionList_[i]); optionBlock.initSvg(); connection.connect(optionBlock.previousConnection); connection = optionBlock.nextConnection; @@ -355,26 +366,41 @@ Blockly.Blocks['field_dropdown'] = { // Reconfigure this block based on the mutator dialog's components. var optionBlock = containerBlock.getInputTargetBlock('STACK'); // Count number of inputs. + this.optionList_.length = 0; var data = []; while (optionBlock) { + if (optionBlock.type == 'field_dropdown_option_text') { + this.optionList_.push('text'); + } else if (optionBlock.type == 'field_dropdown_option_image') { + this.optionList_.push('image'); + } data.push([optionBlock.userData_, optionBlock.cpuData_]); optionBlock = optionBlock.nextConnection && optionBlock.nextConnection.targetBlock(); } - this.optionCount_ = data.length; this.updateShape_(); // Restore any data. - for (var i = 0; i < this.optionCount_; i++) { - this.setFieldValue(data[i][0] || 'option', 'USER' + i); - this.setFieldValue(data[i][1] || 'OPTIONNAME', 'CPU' + i); + for (var i = 0; i < this.optionList_.length; i++) { + var userData = data[i][0]; + if (userData !== undefined) { + if (typeof userData == 'string') { + this.setFieldValue(userData || 'option', 'USER' + i); + } else { + this.setFieldValue(userData.src, 'SRC' + i); + this.setFieldValue(userData.width, 'WIDTH' + i); + this.setFieldValue(userData.height, 'HEIGHT' + i); + this.setFieldValue(userData.alt, 'ALT' + i); + } + this.setFieldValue(data[i][1] || 'OPTIONNAME', 'CPU' + i); + } } }, saveConnections: function(containerBlock) { - // Store names and values for each option. + // Store all data for each option. var optionBlock = containerBlock.getInputTargetBlock('STACK'); var i = 0; while (optionBlock) { - optionBlock.userData_ = this.getFieldValue('USER' + i); + optionBlock.userData_ = this.getUserData(i); optionBlock.cpuData_ = this.getFieldValue('CPU' + i); i++; optionBlock = optionBlock.nextConnection && @@ -382,28 +408,61 @@ Blockly.Blocks['field_dropdown'] = { } }, updateShape_: function() { - // Modify this block to have the correct number of options. - // Add new options. - for (var i = 0; i < this.optionCount_; i++) { - if (!this.getInput('OPTION' + i)) { + // Delete everything. + var i = 0; + while (this.getInput('OPTION' + i)) { + this.removeInput('OPTION' + i); + this.removeInput('OPTION_IMAGE' + i, true); + i++; + } + // Rebuild block. + var src = 'https://www.gstatic.com/codesite/ph/images/star_on.gif'; + for (var i = 0; i <= this.optionList_.length; i++) { + var type = this.optionList_[i]; + if (type == 'text') { this.appendDummyInput('OPTION' + i) + .appendField('•') .appendField(new Blockly.FieldTextInput('option'), 'USER' + i) .appendField(',') .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU' + i); + } else if (type == 'image') { + this.appendDummyInput('OPTION' + i) + .appendField('•') + .appendField('image') + .appendField(new Blockly.FieldTextInput(src), 'SRC' + i); + this.appendDummyInput('OPTION_IMAGE' + i) + .appendField(' ') + .appendField('width') + .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'WIDTH' + i) + .appendField('height') + .appendField(new Blockly.FieldNumber('15', 0, NaN, 1), 'HEIGHT' + i) + .appendField('alt text') + .appendField(new Blockly.FieldTextInput('*'), 'ALT' + i) + .appendField(',') + .appendField(new Blockly.FieldTextInput('OPTIONNAME'), 'CPU' + i); } } - // Remove deleted options. - while (this.getInput('OPTION' + i)) { - this.removeInput('OPTION' + i); - i++; - } }, onchange: function() { - if (this.workspace && this.optionCount_ < 1) { + if (this.workspace && this.optionList_.length < 1) { this.setWarningText('Drop down menu must\nhave at least one option.'); } else { fieldNameCheck(this); } + }, + getUserData: function(n) { + if (this.optionList_[n] == 'text') { + return this.getFieldValue('USER' + n); + } + if (this.optionList_[n] == 'image') { + return { + src: this.getFieldValue('SRC' + n), + width: Number(this.getFieldValue('WIDTH' + n)), + height: Number(this.getFieldValue('HEIGHT' + n)), + alt: this.getFieldValue('ALT' + n) + }; + } + throw 'Unknown dropdown type'; } }; @@ -421,15 +480,29 @@ Blockly.Blocks['field_dropdown_container'] = { } }; -Blockly.Blocks['field_dropdown_option'] = { - // Add option. +Blockly.Blocks['field_dropdown_option_text'] = { + // Add text option. init: function() { this.setColour(160); this.appendDummyInput() - .appendField('option'); + .appendField('text option'); this.setPreviousStatement(true); this.setNextStatement(true); - this.setTooltip('Add a new option to the dropdown menu.'); + this.setTooltip('Add a new text option to the dropdown menu.'); + this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); + this.contextMenu = false; + } +}; + +Blockly.Blocks['field_dropdown_option_image'] = { + // Add image option. + init: function() { + this.setColour(160); + this.appendDummyInput() + .appendField('image option'); + this.setPreviousStatement(true); + this.setNextStatement(true); + this.setTooltip('Add a new image option to the dropdown menu.'); this.setHelpUrl('https://www.youtube.com/watch?v=s2_xaEvcVI0#t=386'); this.contextMenu = false; } diff --git a/demos/blockfactory/factory_utils.js b/demos/blockfactory/factory_utils.js index 1792630d9..bdfdeee56 100644 --- a/demos/blockfactory/factory_utils.js +++ b/demos/blockfactory/factory_utils.js @@ -278,7 +278,7 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) { // Dummy inputs don't have names. Other inputs do. if (contentsBlock.type != 'input_dummy') { name = - FactoryUtils.escapeString(contentsBlock.getFieldValue('INPUTNAME')); + JSON.stringify(contentsBlock.getFieldValue('INPUTNAME')); } code.push(' this.' + TYPES[contentsBlock.type] + '(' + name + ')'); var check = FactoryUtils.getOptTypesFrom(contentsBlock, 'TYPE'); @@ -373,13 +373,13 @@ FactoryUtils.getFieldsJs_ = function(block) { switch (block.type) { case 'field_static': // Result: 'hello' - fields.push(FactoryUtils.escapeString(block.getFieldValue('TEXT'))); + fields.push(JSON.stringify(block.getFieldValue('TEXT'))); break; case 'field_input': // Result: new Blockly.FieldTextInput('Hello'), 'GREET' fields.push('new Blockly.FieldTextInput(' + - FactoryUtils.escapeString(block.getFieldValue('TEXT')) + '), ' + - FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + JSON.stringify(block.getFieldValue('TEXT')) + '), ' + + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; case 'field_number': // Result: new Blockly.FieldNumber(10, 0, 100, 1), 'NUMBER' @@ -400,63 +400,61 @@ FactoryUtils.getFieldsJs_ = function(block) { } } fields.push('new Blockly.FieldNumber(' + args.join(', ') + '), ' + - FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; case 'field_angle': // Result: new Blockly.FieldAngle(90), 'ANGLE' fields.push('new Blockly.FieldAngle(' + parseFloat(block.getFieldValue('ANGLE')) + '), ' + - FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; case 'field_checkbox': // Result: new Blockly.FieldCheckbox('TRUE'), 'CHECK' fields.push('new Blockly.FieldCheckbox(' + - FactoryUtils.escapeString(block.getFieldValue('CHECKED')) + + JSON.stringify(block.getFieldValue('CHECKED')) + '), ' + - FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; case 'field_colour': // Result: new Blockly.FieldColour('#ff0000'), 'COLOUR' fields.push('new Blockly.FieldColour(' + - FactoryUtils.escapeString(block.getFieldValue('COLOUR')) + + JSON.stringify(block.getFieldValue('COLOUR')) + '), ' + - FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; case 'field_date': // Result: new Blockly.FieldDate('2015-02-04'), 'DATE' fields.push('new Blockly.FieldDate(' + - FactoryUtils.escapeString(block.getFieldValue('DATE')) + '), ' + - FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + JSON.stringify(block.getFieldValue('DATE')) + '), ' + + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; case 'field_variable': // Result: new Blockly.FieldVariable('item'), 'VAR' var varname - = FactoryUtils.escapeString(block.getFieldValue('TEXT') || null); + = JSON.stringify(block.getFieldValue('TEXT') || null); fields.push('new Blockly.FieldVariable(' + varname + '), ' + - FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + JSON.stringify(block.getFieldValue('FIELDNAME'))); break; case 'field_dropdown': // Result: // new Blockly.FieldDropdown([['yes', '1'], ['no', '0']]), 'TOGGLE' var options = []; - for (var i = 0; i < block.optionCount_; i++) { - options[i] = '[' + - FactoryUtils.escapeString(block.getFieldValue('USER' + i)) + - ', ' + - FactoryUtils.escapeString(block.getFieldValue('CPU' + i)) + ']'; + for (var i = 0; i < block.optionList_.length; i++) { + options[i] = JSON.stringify([block.getUserData(i), + block.getFieldValue('CPU' + i)]); } if (options.length) { fields.push('new Blockly.FieldDropdown([' + options.join(', ') + ']), ' + - FactoryUtils.escapeString(block.getFieldValue('FIELDNAME'))); + JSON.stringify(block.getFieldValue('FIELDNAME'))); } break; case 'field_image': - // Result: new Blockly.FieldImage('http://...', 80, 60) - var src = FactoryUtils.escapeString(block.getFieldValue('SRC')); + // Result: new Blockly.FieldImage('http://...', 80, 60, '*') + var src = JSON.stringify(block.getFieldValue('SRC')); var width = Number(block.getFieldValue('WIDTH')); var height = Number(block.getFieldValue('HEIGHT')); - var alt = FactoryUtils.escapeString(block.getFieldValue('ALT')); + var alt = JSON.stringify(block.getFieldValue('ALT')); fields.push('new Blockly.FieldImage(' + src + ', ' + width + ', ' + height + ', ' + alt + ')'); break; @@ -546,8 +544,8 @@ FactoryUtils.getFieldsJson_ = function(block) { break; case 'field_dropdown': var options = []; - for (var i = 0; i < block.optionCount_; i++) { - options[i] = [block.getFieldValue('USER' + i), + for (var i = 0; i < block.optionList_.length; i++) { + options[i] = [block.getUserData(i), block.getFieldValue('CPU' + i)]; } if (options.length) { @@ -608,7 +606,7 @@ FactoryUtils.getTypesFrom_ = function(block, name) { if (!typeBlock || typeBlock.disabled) { types = []; } else if (typeBlock.type == 'type_other') { - types = [FactoryUtils.escapeString(typeBlock.getFieldValue('TYPE'))]; + types = [JSON.stringify(typeBlock.getFieldValue('TYPE'))]; } else if (typeBlock.type == 'type_group') { types = []; for (var n = 0; n < typeBlock.typeCount_; n++) { @@ -623,20 +621,11 @@ FactoryUtils.getTypesFrom_ = function(block, name) { hash[types[n]] = true; } } else { - types = [FactoryUtils.escapeString(typeBlock.valueType)]; + types = [JSON.stringify(typeBlock.valueType)]; } return types; }; -/** - * Escape a string. - * @param {string} string String to escape. - * @return {string} Escaped string surrouned by quotes. - */ -FactoryUtils.escapeString = function(string) { - return JSON.stringify(string); -}; - /** * Return the uneditable container block that everything else attaches to in * given workspace.