mirror of
https://github.com/scratchfoundation/scratch-blocks.git
synced 2025-06-06 01:44:35 -04:00
309 lines
11 KiB
JavaScript
309 lines
11 KiB
JavaScript
/**
|
|
* @license
|
|
* Visual Blocks Editor
|
|
*
|
|
* Copyright 2016 Massachusetts Institute of Technology
|
|
* All rights reserved.
|
|
*
|
|
* 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 Icon picker input field.
|
|
* This is primarily for use in Scratch Horizontal blocks.
|
|
* Pops open a drop-down with icons; when an icon is selected, it replaces
|
|
* the icon (image field) in the original block.
|
|
* @author tmickel@mit.edu (Tim Mickel)
|
|
*/
|
|
'use strict';
|
|
|
|
goog.provide('Blockly.FieldIconMenu');
|
|
|
|
goog.require('Blockly.DropDownDiv');
|
|
|
|
/**
|
|
* Class for an icon menu field.
|
|
* @param {Object} icons List of icons. These take the same options as an Image Field.
|
|
* @extends {Blockly.Field}
|
|
* @constructor
|
|
*/
|
|
Blockly.FieldIconMenu = function(icons) {
|
|
/** @type {object} */
|
|
this.icons_ = icons;
|
|
// Example:
|
|
// [{src: '...', width: 20, height: 20, alt: '...', value: 'machine_value'}, ...]
|
|
// First icon provides the default values.
|
|
var defaultValue = icons[0].value;
|
|
Blockly.FieldIconMenu.superClass_.constructor.call(this, defaultValue);
|
|
this.addArgType('iconmenu');
|
|
};
|
|
goog.inherits(Blockly.FieldIconMenu, Blockly.Field);
|
|
|
|
/**
|
|
* Construct a FieldIconMenu from a JSON arg object.
|
|
* @param {!Object} element A JSON object with options.
|
|
* @returns {!Blockly.FieldIconMenu} The new field instance.
|
|
* @package
|
|
* @nocollapse
|
|
*/
|
|
Blockly.FieldIconMenu.fromJson = function(element) {
|
|
return new Blockly.FieldIconMenu(element['options']);
|
|
};
|
|
|
|
/**
|
|
* Fixed width of the drop-down, in px. Icon buttons will flow inside this width.
|
|
* @type {number}
|
|
* @const
|
|
*/
|
|
Blockly.FieldIconMenu.DROPDOWN_WIDTH = 168;
|
|
|
|
/**
|
|
* Save the primary colour of the source block while the menu is open, for reset.
|
|
* @type {number|string}
|
|
* @private
|
|
*/
|
|
Blockly.FieldIconMenu.savedPrimary_ = null;
|
|
|
|
/**
|
|
* Called when the field is placed on a block.
|
|
* @param {Block} block The owning block.
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.init = function(block) {
|
|
if (this.fieldGroup_) {
|
|
// Icon menu has already been initialized once.
|
|
return;
|
|
}
|
|
// Render the arrow icon
|
|
// Fixed sizes in px. Saved for creating the flip transform of the menu renders above the button.
|
|
var arrowSize = 12;
|
|
/** @type {Number} */
|
|
this.arrowX_ = 18;
|
|
/** @type {Number} */
|
|
this.arrowY_ = 10;
|
|
if (block.RTL) {
|
|
// In RTL, the icon position is flipped and rendered from the right (offset by width)
|
|
this.arrowX_ = -this.arrowX_ - arrowSize;
|
|
}
|
|
/** @type {Element} */
|
|
this.arrowIcon_ = Blockly.utils.createSvgElement('image', {
|
|
'height': arrowSize + 'px',
|
|
'width': arrowSize + 'px',
|
|
'transform': 'translate(' + this.arrowX_ + ',' + this.arrowY_ + ')'
|
|
});
|
|
this.arrowIcon_.setAttributeNS('http://www.w3.org/1999/xlink',
|
|
'xlink:href', Blockly.mainWorkspace.options.pathToMedia + 'dropdown-arrow.svg');
|
|
block.getSvgRoot().appendChild(this.arrowIcon_);
|
|
Blockly.FieldIconMenu.superClass_.init.call(this, block);
|
|
};
|
|
|
|
/**
|
|
* Mouse cursor style when over the hotspot that initiates the editor.
|
|
* @const
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.CURSOR = 'default';
|
|
|
|
/**
|
|
* Set the language-neutral value for this icon drop-down menu.
|
|
* @param {?string} newValue New value.
|
|
* @override
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.setValue = function(newValue) {
|
|
if (newValue === null || newValue === this.value_) {
|
|
return; // No change
|
|
}
|
|
if (this.sourceBlock_ && Blockly.Events.isEnabled()) {
|
|
Blockly.Events.fire(new Blockly.Events.Change(
|
|
this.sourceBlock_, 'field', this.name, this.value_, newValue));
|
|
}
|
|
this.value_ = newValue;
|
|
// Find the relevant icon in this.icons_ to get the image src.
|
|
this.setParentFieldImage(this.getSrcForValue(this.value_));
|
|
};
|
|
|
|
/**
|
|
* Find the parent block's FieldImage and set its src.
|
|
* @param {?string} src New src for the parent block FieldImage.
|
|
* @private
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.setParentFieldImage = function(src) {
|
|
// Only attempt if we have a set sourceBlock_ and parentBlock_
|
|
// It's possible that this function could be called before
|
|
// a parent block is set; in that case, fail silently.
|
|
if (this.sourceBlock_ && this.sourceBlock_.parentBlock_) {
|
|
var parentBlock = this.sourceBlock_.parentBlock_;
|
|
// Loop through all inputs' fields to find the first FieldImage
|
|
for (var i = 0, input; input = parentBlock.inputList[i]; i++) {
|
|
for (var j = 0, field; field = input.fieldRow[j]; j++) {
|
|
if (field instanceof Blockly.FieldImage) {
|
|
// Src for a FieldImage is stored in its value.
|
|
field.setValue(src);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get the language-neutral value from this drop-down menu.
|
|
* @return {string} Current language-neutral value.
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.getValue = function() {
|
|
return this.value_;
|
|
};
|
|
|
|
/**
|
|
* For a language-neutral value, get the src for the image that represents it.
|
|
* @param {string} value Language-neutral value to look up.
|
|
* @return {string} Src to image representing value
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.getSrcForValue = function(value) {
|
|
for (var i = 0, icon; icon = this.icons_[i]; i++) {
|
|
if (icon.value === value) {
|
|
return icon.src;
|
|
}
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Show the drop-down menu for editing this field.
|
|
* @private
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.showEditor_ = function() {
|
|
// If there is an existing drop-down we own, this is a request to hide the drop-down.
|
|
if (Blockly.DropDownDiv.hideIfOwner(this)) {
|
|
return;
|
|
}
|
|
// If there is an existing drop-down someone else owns, hide it immediately and clear it.
|
|
Blockly.DropDownDiv.hideWithoutAnimation();
|
|
Blockly.DropDownDiv.clearContent();
|
|
// Populate the drop-down with the icons for this field.
|
|
var contentDiv = Blockly.DropDownDiv.getContentDiv();
|
|
// Accessibility properties
|
|
contentDiv.setAttribute('role', 'menu');
|
|
contentDiv.setAttribute('aria-haspopup', 'true');
|
|
for (var i = 0, icon; icon = this.icons_[i]; i++) {
|
|
// Icons with the type property placeholder take up space but don't have any functionality
|
|
// Use for special-case layouts
|
|
if (icon.type == 'placeholder') {
|
|
var placeholder = document.createElement('span');
|
|
placeholder.setAttribute('class', 'blocklyDropDownPlaceholder');
|
|
placeholder.style.width = icon.width + 'px';
|
|
placeholder.style.height = icon.height + 'px';
|
|
contentDiv.appendChild(placeholder);
|
|
continue;
|
|
}
|
|
var button = document.createElement('button');
|
|
button.setAttribute('id', ':' + i); // For aria-activedescendant
|
|
button.setAttribute('role', 'menuitem');
|
|
button.setAttribute('class', 'blocklyDropDownButton');
|
|
button.title = icon.alt;
|
|
button.style.width = icon.width + 'px';
|
|
button.style.height = icon.height + 'px';
|
|
var backgroundColor = this.sourceBlock_.getColour();
|
|
if (icon.value == this.getValue()) {
|
|
// This icon is selected, show it in a different colour
|
|
backgroundColor = this.sourceBlock_.getColourTertiary();
|
|
button.setAttribute('aria-selected', 'true');
|
|
}
|
|
button.style.backgroundColor = backgroundColor;
|
|
button.style.borderColor = this.sourceBlock_.getColourTertiary();
|
|
Blockly.bindEvent_(button, 'click', this, this.buttonClick_);
|
|
Blockly.bindEvent_(button, 'mouseup', this, this.buttonClick_);
|
|
// These are applied manually instead of using the :hover pseudoclass
|
|
// because Android has a bad long press "helper" menu and green highlight
|
|
// that we must prevent with ontouchstart preventDefault
|
|
Blockly.bindEvent_(button, 'mousedown', button, function(e) {
|
|
this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover');
|
|
e.preventDefault();
|
|
});
|
|
Blockly.bindEvent_(button, 'mouseover', button, function() {
|
|
this.setAttribute('class', 'blocklyDropDownButton blocklyDropDownButtonHover');
|
|
contentDiv.setAttribute('aria-activedescendant', this.id);
|
|
});
|
|
Blockly.bindEvent_(button, 'mouseout', button, function() {
|
|
this.setAttribute('class', 'blocklyDropDownButton');
|
|
contentDiv.removeAttribute('aria-activedescendant');
|
|
});
|
|
var buttonImg = document.createElement('img');
|
|
buttonImg.src = icon.src;
|
|
//buttonImg.alt = icon.alt;
|
|
// Upon click/touch, we will be able to get the clicked element as e.target
|
|
// Store a data attribute on all possible click targets so we can match it to the icon.
|
|
button.setAttribute('data-value', icon.value);
|
|
buttonImg.setAttribute('data-value', icon.value);
|
|
button.appendChild(buttonImg);
|
|
contentDiv.appendChild(button);
|
|
}
|
|
contentDiv.style.width = Blockly.FieldIconMenu.DROPDOWN_WIDTH + 'px';
|
|
|
|
Blockly.DropDownDiv.setColour(this.sourceBlock_.getColour(), this.sourceBlock_.getColourTertiary());
|
|
Blockly.DropDownDiv.setCategory(this.sourceBlock_.parentBlock_.getCategory());
|
|
|
|
// Update source block colour to look selected
|
|
this.savedPrimary_ = this.sourceBlock_.getColour();
|
|
this.sourceBlock_.setColour(this.sourceBlock_.getColourSecondary(),
|
|
this.sourceBlock_.getColourSecondary(),
|
|
this.sourceBlock_.getColourTertiary(),
|
|
this.sourceBlock_.getColourQuaternary());
|
|
|
|
var scale = this.sourceBlock_.workspace.scale;
|
|
// Offset for icon-type horizontal blocks.
|
|
var secondaryYOffset = (
|
|
-(Blockly.BlockSvg.MIN_BLOCK_Y * scale) - (Blockly.BlockSvg.FIELD_Y_OFFSET * scale)
|
|
);
|
|
var renderedPrimary = Blockly.DropDownDiv.showPositionedByBlock(
|
|
this, this.sourceBlock_, this.onHide_.bind(this), secondaryYOffset);
|
|
if (!renderedPrimary) {
|
|
// Adjust for rotation
|
|
var arrowX = this.arrowX_ + Blockly.DropDownDiv.ARROW_SIZE / 1.5 + 1;
|
|
var arrowY = this.arrowY_ + Blockly.DropDownDiv.ARROW_SIZE / 1.5;
|
|
// Flip the arrow on the button
|
|
this.arrowIcon_.setAttribute('transform',
|
|
'translate(' + arrowX + ',' + arrowY + ') rotate(180)');}
|
|
};
|
|
|
|
/**
|
|
* Callback for when a button is clicked inside the drop-down.
|
|
* Should be bound to the FieldIconMenu.
|
|
* @param {Event} e DOM event for the click/touch
|
|
* @private
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.buttonClick_ = function(e) {
|
|
var value = e.target.getAttribute('data-value');
|
|
this.setValue(value);
|
|
Blockly.DropDownDiv.hide();
|
|
};
|
|
|
|
/**
|
|
* Callback for when the drop-down is hidden.
|
|
*/
|
|
Blockly.FieldIconMenu.prototype.onHide_ = function() {
|
|
// Reset the button colour and clear accessibility properties
|
|
// Only attempt to do this reset if sourceBlock_ is not disposed.
|
|
// It could become disposed before an onHide_, for example,
|
|
// when a block is dragged from the flyout.
|
|
if (this.sourceBlock_) {
|
|
this.sourceBlock_.setColour(this.savedPrimary_,
|
|
this.sourceBlock_.getColourSecondary(),
|
|
this.sourceBlock_.getColourTertiary(),
|
|
this.sourceBlock_.getColourQuaternary());
|
|
}
|
|
Blockly.DropDownDiv.content_.removeAttribute('role');
|
|
Blockly.DropDownDiv.content_.removeAttribute('aria-haspopup');
|
|
Blockly.DropDownDiv.content_.removeAttribute('aria-activedescendant');
|
|
// Unflip the arrow if appropriate
|
|
this.arrowIcon_.setAttribute('transform', 'translate(' + this.arrowX_ + ',' + this.arrowY_ + ')');
|
|
};
|
|
|
|
Blockly.Field.register('field_iconmenu', Blockly.FieldIconMenu);
|