2018-04-10 00:59:28 +02:00
|
|
|
/// <reference path="../node_modules/pxt-core/localtypings/blockly.d.ts"/>
|
|
|
|
/// <reference path="../node_modules/pxt-core/built/pxtsim.d.ts"/>
|
|
|
|
|
|
|
|
export interface FieldMotorsOptions extends Blockly.FieldCustomDropdownOptions {
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
export class FieldMotors extends Blockly.FieldDropdown implements Blockly.FieldCustom {
|
|
|
|
public isFieldCustom_ = true;
|
|
|
|
|
|
|
|
private box2_: SVGRectElement;
|
|
|
|
private textElement2_: SVGTextElement;
|
|
|
|
private arrow2_: SVGImageElement;
|
|
|
|
|
|
|
|
// Width in pixels
|
|
|
|
protected itemWidth_: number;
|
|
|
|
|
|
|
|
// Number of rows to display (if there are extra rows, the picker will be scrollable)
|
|
|
|
protected maxRows_: number;
|
|
|
|
|
|
|
|
protected backgroundColour_: string;
|
|
|
|
protected itemColour_: string;
|
|
|
|
protected borderColour_: string;
|
|
|
|
|
|
|
|
private isFirst_: boolean; // which of the two dropdowns is selected
|
|
|
|
|
|
|
|
constructor(text: string, options: FieldMotorsOptions, validator?: Function) {
|
|
|
|
super(options.data, validator);
|
|
|
|
|
|
|
|
this.itemWidth_ = 75;
|
|
|
|
this.backgroundColour_ = pxtblockly.parseColour(options.colour);
|
|
|
|
this.itemColour_ = "rgba(255, 255, 255, 0.6)";
|
2018-04-14 06:46:19 +02:00
|
|
|
this.borderColour_ = pxt.toolbox.fadeColor(this.backgroundColour_, 0.4, false);
|
2018-04-10 00:59:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
init() {
|
|
|
|
if (this.fieldGroup_) {
|
|
|
|
// Field has already been initialized once.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
// Add dropdown arrow: "option ▾" (LTR) or "▾ אופציה" (RTL)
|
|
|
|
// Positioned on render, after text size is calculated.
|
|
|
|
/** @type {Number} */
|
|
|
|
(this as any).arrowSize_ = 12;
|
|
|
|
/** @type {Number} */
|
|
|
|
(this as any).arrowX_ = 0;
|
|
|
|
/** @type {Number} */
|
|
|
|
this.arrowY_ = 11;
|
|
|
|
this.arrow_ = Blockly.utils.createSvgElement('image', {
|
|
|
|
'height': (this as any).arrowSize_ + 'px',
|
|
|
|
'width': (this as any).arrowSize_ + 'px'
|
|
|
|
});
|
|
|
|
this.arrow_.setAttributeNS('http://www.w3.org/1999/xlink',
|
|
|
|
'xlink:href', (Blockly.FieldDropdown as any).DROPDOWN_SVG_DATAURI);
|
|
|
|
|
|
|
|
this.arrow2_ = Blockly.utils.createSvgElement('image', {
|
|
|
|
'height': (this as any).arrowSize_ + 'px',
|
|
|
|
'width': (this as any).arrowSize_ + 'px'
|
|
|
|
});
|
|
|
|
this.arrow2_.setAttributeNS('http://www.w3.org/1999/xlink',
|
|
|
|
'xlink:href', (Blockly.FieldDropdown as any).DROPDOWN_SVG_DATAURI);
|
|
|
|
(this as any).className_ += ' blocklyDropdownText';
|
|
|
|
|
|
|
|
// Build the DOM.
|
|
|
|
this.fieldGroup_ = Blockly.utils.createSvgElement('g', {}, null);
|
|
|
|
if (!this.visible_) {
|
|
|
|
(this.fieldGroup_ as any).style.display = 'none';
|
|
|
|
}
|
|
|
|
// Adjust X to be flipped for RTL. Position is relative to horizontal start of source block.
|
|
|
|
var size = this.getSize();
|
|
|
|
var fieldX = (this.sourceBlock_.RTL) ? -size.width / 2 : size.width / 2;
|
|
|
|
/** @type {!Element} */
|
|
|
|
this.textElement_ = Blockly.utils.createSvgElement('text',
|
|
|
|
{
|
|
|
|
'class': (this as any).className_,
|
|
|
|
'x': fieldX,
|
|
|
|
'dy': '0.7ex',
|
|
|
|
'y': size.height / 2
|
|
|
|
},
|
|
|
|
this.fieldGroup_);
|
|
|
|
fieldX += 10; // size of first group.
|
|
|
|
this.textElement2_ = Blockly.utils.createSvgElement('text',
|
|
|
|
{
|
|
|
|
'class': (this as any).className_,
|
|
|
|
'x': fieldX,
|
|
|
|
'dy': '0.7ex',
|
|
|
|
'y': this.size_.height / 2
|
|
|
|
},
|
|
|
|
this.fieldGroup_);
|
|
|
|
|
|
|
|
this.updateEditable();
|
|
|
|
this.sourceBlock_.getSvgRoot().appendChild(this.fieldGroup_);
|
|
|
|
// Force a render.
|
|
|
|
this.render_();
|
|
|
|
this.size_.width = 0;
|
|
|
|
(this as any).mouseDownWrapper_ =
|
|
|
|
Blockly.bindEventWithChecks_((this as any).getClickTarget_(), 'mousedown', this,
|
|
|
|
(this as any).onMouseDown_);
|
|
|
|
|
|
|
|
// Add second dropdown
|
|
|
|
if (this.shouldShowRect_()) {
|
|
|
|
this.box_ = Blockly.utils.createSvgElement('rect', {
|
|
|
|
'rx': (Blockly.BlockSvg as any).CORNER_RADIUS,
|
|
|
|
'ry': (Blockly.BlockSvg as any).CORNER_RADIUS,
|
|
|
|
'x': 0,
|
|
|
|
'y': 0,
|
|
|
|
'width': this.size_.width,
|
|
|
|
'height': this.size_.height,
|
|
|
|
'stroke': this.sourceBlock_.getColourTertiary(),
|
|
|
|
'fill': this.sourceBlock_.getColour(),
|
|
|
|
'class': 'blocklyBlockBackground',
|
|
|
|
'fill-opacity': 1
|
|
|
|
}, null);
|
|
|
|
this.fieldGroup_.insertBefore(this.box_, this.textElement_);
|
|
|
|
this.box2_ = Blockly.utils.createSvgElement('rect', {
|
|
|
|
'rx': (Blockly.BlockSvg as any).CORNER_RADIUS,
|
|
|
|
'ry': (Blockly.BlockSvg as any).CORNER_RADIUS,
|
|
|
|
'x': 0,
|
|
|
|
'y': 0,
|
|
|
|
'width': this.size_.width,
|
|
|
|
'height': this.size_.height,
|
|
|
|
'stroke': this.sourceBlock_.getColourTertiary(),
|
|
|
|
'fill': this.sourceBlock_.getColour(),
|
|
|
|
'class': 'blocklyBlockBackground',
|
|
|
|
'fill-opacity': 1
|
|
|
|
}, null);
|
|
|
|
this.fieldGroup_.insertBefore(this.box2_, this.textElement2_);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Force a reset of the text to add the arrow.
|
|
|
|
var text = this.text_;
|
|
|
|
this.text_ = null;
|
|
|
|
this.setText(text);
|
|
|
|
}
|
|
|
|
|
|
|
|
getFirstValue(text: string) {
|
|
|
|
// Get first set of words up until last space
|
|
|
|
return this.normalizeText_(text.substring(0, text.lastIndexOf(' ')));
|
|
|
|
}
|
|
|
|
|
|
|
|
getSecondValue(text: string) {
|
|
|
|
// Get last word
|
|
|
|
return this.normalizeText_(text.match(/\S*$/)[0]);
|
|
|
|
}
|
|
|
|
|
|
|
|
private normalizeText_(text: string) {
|
|
|
|
if (!text) {
|
|
|
|
// Prevent the field from disappearing if empty.
|
|
|
|
return Blockly.Field.NBSP;
|
|
|
|
}
|
|
|
|
if (text.length > this.maxDisplayLength) {
|
|
|
|
// Truncate displayed string and add an ellipsis ('...').
|
|
|
|
text = text.substring(0, this.maxDisplayLength - 2) + '\u2026';
|
|
|
|
}
|
|
|
|
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
|
|
|
|
text = text.replace(/\s/g, Blockly.Field.NBSP);
|
|
|
|
if (this.sourceBlock_.RTL) {
|
|
|
|
// The SVG is LTR, force text to be RTL.
|
|
|
|
text += '\u200F';
|
|
|
|
}
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
updateTextNode2_() {
|
|
|
|
if (!this.textElement2_) {
|
|
|
|
// Not rendered yet.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
var text = this.text_;
|
|
|
|
if (text.length > this.maxDisplayLength) {
|
|
|
|
// Truncate displayed string and add an ellipsis ('...').
|
|
|
|
text = text.substring(0, this.maxDisplayLength - 2) + '\u2026';
|
|
|
|
// Add special class for sizing font when truncated
|
|
|
|
this.textElement2_.setAttribute('class', (this as any).className_ + ' blocklyTextTruncated');
|
|
|
|
} else {
|
|
|
|
this.textElement2_.setAttribute('class', (this as any).className_);
|
|
|
|
}
|
|
|
|
// Empty the text element.
|
|
|
|
goog.dom.removeChildren(/** @type {!Element} */(this.textElement2_));
|
|
|
|
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
|
|
|
|
text = text.replace(/\s/g, Blockly.Field.NBSP);
|
|
|
|
if (this.sourceBlock_.RTL && text) {
|
|
|
|
// The SVG is LTR, force text to be RTL.
|
|
|
|
text += '\u200F';
|
|
|
|
}
|
|
|
|
if (!text) {
|
|
|
|
// Prevent the field from disappearing if empty.
|
|
|
|
text = Blockly.Field.NBSP;
|
|
|
|
}
|
|
|
|
var textNode = document.createTextNode(text);
|
|
|
|
this.textElement2_.appendChild(textNode);
|
|
|
|
|
|
|
|
// Cached width is obsolete. Clear it.
|
|
|
|
this.size_.width = 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
patchDualMotorText(text: string) {
|
|
|
|
if (text === null) {
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
if (text.indexOf(' ') == -1) {
|
|
|
|
text = `large motors ${text}`;
|
|
|
|
}
|
|
|
|
return text;
|
|
|
|
}
|
|
|
|
|
|
|
|
setText(text: string) {
|
|
|
|
if (text === null || text === this.text_) {
|
|
|
|
// No change if null.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
text = this.patchDualMotorText(text);
|
|
|
|
this.text_ = text;
|
|
|
|
this.updateTextNode_();
|
|
|
|
this.updateTextNode2_();
|
|
|
|
|
|
|
|
if (this.textElement_) {
|
|
|
|
this.textElement_.parentNode.appendChild(this.arrow_);
|
|
|
|
}
|
|
|
|
if (this.textElement2_) {
|
|
|
|
this.textElement2_.parentNode.appendChild(this.arrow2_);
|
|
|
|
}
|
|
|
|
if (this.sourceBlock_ && this.sourceBlock_.rendered) {
|
|
|
|
this.sourceBlock_.render();
|
|
|
|
this.sourceBlock_.bumpNeighbours_();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
positionArrow2(start: number, x: number) {
|
|
|
|
if (!this.arrow2_) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
var addedWidth = 0;
|
|
|
|
if (this.sourceBlock_.RTL) {
|
|
|
|
(this as any).arrow2X_ = (this as any).arrowSize_ - (Blockly.BlockSvg as any).DROPDOWN_ARROW_PADDING;
|
|
|
|
addedWidth = (this as any).arrowSize_ + (Blockly.BlockSvg as any).DROPDOWN_ARROW_PADDING;
|
|
|
|
} else {
|
|
|
|
(this as any).arrow2X_ = x + (Blockly.BlockSvg as any).DROPDOWN_ARROW_PADDING / 2;
|
|
|
|
addedWidth = (this as any).arrowSize_ + (Blockly.BlockSvg as any).DROPDOWN_ARROW_PADDING;
|
|
|
|
}
|
|
|
|
if (this.box_) {
|
|
|
|
// Bump positioning to the right for a box-type drop-down.
|
|
|
|
(this as any).arrow2X_ += Blockly.BlockSvg.BOX_FIELD_PADDING;
|
|
|
|
}
|
|
|
|
(this as any).arrow2X_ += start;
|
|
|
|
this.arrow2_.setAttribute('transform',
|
|
|
|
'translate(' + (this as any).arrow2X_ + ',' + this.arrowY_ + ')'
|
|
|
|
);
|
|
|
|
return addedWidth;
|
|
|
|
};
|
|
|
|
|
|
|
|
updateWidth() {
|
|
|
|
// Calculate width of field
|
|
|
|
var width = Blockly.Field.getCachedWidth(this.textElement_);
|
|
|
|
var width2 = Blockly.Field.getCachedWidth(this.textElement2_);
|
|
|
|
|
|
|
|
// Add padding to left and right of text.
|
|
|
|
if (this.EDITABLE) {
|
|
|
|
width += Blockly.BlockSvg.EDITABLE_FIELD_PADDING;
|
|
|
|
width2 += Blockly.BlockSvg.EDITABLE_FIELD_PADDING;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Adjust width for drop-down arrow.
|
|
|
|
this.arrowWidth_ = 0;
|
|
|
|
if (this.positionArrow) {
|
|
|
|
this.arrowWidth_ = this.positionArrow(width);
|
|
|
|
width += this.arrowWidth_;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add padding to any drawn box.
|
|
|
|
if (this.box_) {
|
|
|
|
width += 2 * Blockly.BlockSvg.BOX_FIELD_PADDING;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Adjust width for second drop-down arrow.
|
|
|
|
(this as any).arrowWidth2_ = 0;
|
|
|
|
if (this.positionArrow2) {
|
|
|
|
(this as any).arrowWidth2_ = this.positionArrow2(width + Blockly.BlockSvg.BOX_FIELD_PADDING, width2);
|
|
|
|
width2 += (this as any).arrowWidth2_;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Add padding to any drawn box.
|
|
|
|
if (this.box2_) {
|
|
|
|
width2 += 2 * Blockly.BlockSvg.BOX_FIELD_PADDING;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set width of the field.
|
|
|
|
this.size_.width = width + Blockly.BlockSvg.BOX_FIELD_PADDING + width2;
|
|
|
|
(this as any).width1 = width;
|
|
|
|
(this as any).width2 = width2;
|
|
|
|
};
|
|
|
|
|
|
|
|
render_() {
|
|
|
|
if (this.visible_ && this.textElement_) {
|
|
|
|
goog.dom.removeChildren(/** @type {!Element} */(this.textElement_));
|
|
|
|
goog.dom.removeChildren(/** @type {!Element} */(this.textElement2_));
|
|
|
|
|
|
|
|
var text = this.text_;
|
|
|
|
text = this.patchDualMotorText(text);
|
|
|
|
|
|
|
|
// First dropdown
|
|
|
|
const textNode1 = document.createTextNode(this.getFirstValue(text));
|
|
|
|
this.textElement_.appendChild(textNode1);
|
|
|
|
|
|
|
|
// Second dropdown
|
|
|
|
if (this.textElement2_) {
|
|
|
|
const textNode2 = document.createTextNode(this.getSecondValue(text));
|
|
|
|
this.textElement2_.appendChild(textNode2);
|
|
|
|
}
|
|
|
|
this.updateWidth();
|
|
|
|
|
|
|
|
// Update text centering, based on newly calculated width.
|
|
|
|
let centerTextX = ((this as any).width1 - this.arrowWidth_) / 2;
|
|
|
|
if (this.sourceBlock_.RTL) {
|
|
|
|
centerTextX += this.arrowWidth_;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
// unless there is a drop-down arrow.
|
|
|
|
if (this.sourceBlock_.isShadow() && !this.positionArrow) {
|
|
|
|
let minOffset = (Blockly.BlockSvg as any).FIELD_WIDTH / 2;
|
|
|
|
if (this.sourceBlock_.RTL) {
|
|
|
|
// X position starts at the left edge of the block, in both RTL and LTR.
|
|
|
|
// First offset by the width of the block to move to the right edge,
|
|
|
|
// and then subtract to move to the same position as LTR.
|
|
|
|
let minCenter = (this as any).width1 - minOffset;
|
|
|
|
centerTextX = Math.min(minCenter, centerTextX);
|
|
|
|
} else {
|
|
|
|
// (width / 2) should exceed Blockly.BlockSvg.FIELD_WIDTH / 2
|
|
|
|
// if the text is longer.
|
|
|
|
centerTextX = Math.max(minOffset, centerTextX);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply new text element x position.
|
|
|
|
var width = Blockly.Field.getCachedWidth(this.textElement_);
|
|
|
|
var newX = centerTextX - width / 2;
|
|
|
|
this.textElement_.setAttribute('x', `${newX}`);
|
|
|
|
|
|
|
|
// Update text centering, based on newly calculated width.
|
|
|
|
let centerTextX2 = ((this as any).width2 - (this as any).arrowWidth2_) / 2;
|
|
|
|
if (this.sourceBlock_.RTL) {
|
|
|
|
centerTextX2 += (this as any).arrowWidth2_;
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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,
|
|
|
|
// unless there is a drop-down arrow.
|
|
|
|
if (this.sourceBlock_.isShadow() && !this.positionArrow2) {
|
|
|
|
let minOffset = (Blockly.BlockSvg as any).FIELD_WIDTH / 2;
|
|
|
|
if (this.sourceBlock_.RTL) {
|
|
|
|
// X position starts at the left edge of the block, in both RTL and LTR.
|
|
|
|
// First offset by the width of the block to move to the right edge,
|
|
|
|
// and then subtract to move to the same position as LTR.
|
|
|
|
let minCenter = (this as any).width2 - minOffset;
|
|
|
|
centerTextX2 = Math.min(minCenter, centerTextX2);
|
|
|
|
} else {
|
|
|
|
// (width / 2) should exceed Blockly.BlockSvg.FIELD_WIDTH / 2
|
|
|
|
// if the text is longer.
|
|
|
|
centerTextX2 = Math.max(minOffset, centerTextX2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Apply new text element x position.
|
|
|
|
var width2 = Blockly.Field.getCachedWidth(this.textElement2_);
|
|
|
|
var newX2 = centerTextX2 - width2 / 2;
|
|
|
|
this.textElement2_.setAttribute('x', `${newX2 + (this as any).width1 + Blockly.BlockSvg.BOX_FIELD_PADDING}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update any drawn box to the correct width and height.
|
|
|
|
if (this.box_) {
|
|
|
|
this.box_.setAttribute('width', `${(this as any).width1}`);
|
|
|
|
this.box_.setAttribute('height', `${this.size_.height}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Update any drawn box to the correct width and height.
|
|
|
|
if (this.box2_) {
|
|
|
|
this.box2_.setAttribute('x', `${(this as any).width1 + Blockly.BlockSvg.BOX_FIELD_PADDING}`);
|
|
|
|
this.box2_.setAttribute('width', `${(this as any).width2}`);
|
|
|
|
this.box2_.setAttribute('height', `${this.size_.height}`);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
showEditor_(e?: MouseEvent) {
|
|
|
|
// If there is an existing drop-down we own, this is a request to hide the drop-down.
|
|
|
|
if (Blockly.DropDownDiv.hideIfOwner(this)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.isFirst_ = e.clientX - this.getScaledBBox_().left < ((this as any).width1 * this.sourceBlock_.workspace.scale);
|
|
|
|
// 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.
|
|
|
|
let dropdownDiv = Blockly.DropDownDiv.getContentDiv();
|
|
|
|
let contentDiv = document.createElement('div');
|
|
|
|
// Accessibility properties
|
|
|
|
contentDiv.setAttribute('role', 'menu');
|
|
|
|
contentDiv.setAttribute('aria-haspopup', 'true');
|
|
|
|
let options = this.getOptions();
|
|
|
|
|
|
|
|
// Hashmap of options
|
|
|
|
let opts = {};
|
|
|
|
let conts = {};
|
|
|
|
let vals = {};
|
2018-04-14 00:21:41 +02:00
|
|
|
for (let opt = 0; opt < options.length; opt++) {
|
2018-04-10 00:59:28 +02:00
|
|
|
let text = options[opt][0].alt ? options[opt][0].alt : options[opt][0];
|
|
|
|
if (text.indexOf(' ') == -1) {
|
|
|
|
// Patch dual motors as they don't have prefixes.
|
|
|
|
text = this.patchDualMotorText(text);
|
|
|
|
if (options[opt][0].alt) options[opt][0].alt = text;
|
|
|
|
}
|
|
|
|
const value = options[opt][1];
|
|
|
|
const firstValue = this.getFirstValue(text);
|
|
|
|
const secondValue = this.getSecondValue(text);
|
|
|
|
if (!opts[firstValue]) opts[firstValue] = [secondValue];
|
|
|
|
else opts[firstValue].push(secondValue);
|
|
|
|
// Store a hash of the original key value pairs for later
|
|
|
|
conts[text] = options[opt][0];
|
|
|
|
vals[text] = value;
|
|
|
|
}
|
|
|
|
|
|
|
|
const currentFirst = this.getFirstValue(this.text_);
|
|
|
|
const currentSecond = this.getSecondValue(this.text_);
|
|
|
|
|
|
|
|
if (!this.isFirst_) {
|
|
|
|
options = opts[currentFirst];
|
|
|
|
} else {
|
|
|
|
options = Object.keys(opts);
|
|
|
|
// Flip the first and second options to make it sorted the way we want it (medium, large, dual)
|
|
|
|
if (options.length == 3) {
|
|
|
|
const temp = options[1];
|
|
|
|
options[1] = options[0];
|
|
|
|
options[0] = temp;
|
|
|
|
} else {
|
|
|
|
options.reverse();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const isFirstUrl = {
|
|
|
|
'large motors': FieldMotors.DUAL_MOTORS_DATAURI,
|
|
|
|
'large motor': FieldMotors.MOTORS_LARGE_DATAURI,
|
|
|
|
'medium motor': FieldMotors.MOTORS_MEDIUM_DATAURI
|
|
|
|
}
|
|
|
|
const columns = options.length;
|
|
|
|
|
|
|
|
for (let i = 0, option: any; option = options[i]; i++) {
|
|
|
|
let text = this.isFirst_ ? option + ' ' + opts[option][0] : currentFirst + ' ' + option;
|
|
|
|
text = text.replace(/\xA0/g, ' ');
|
|
|
|
const content: any = conts[text];
|
|
|
|
const value = vals[text];
|
|
|
|
// Icons with the type property placeholder take up space but don't have any functionality
|
|
|
|
// Use for special-case layouts
|
|
|
|
if (content.type == 'placeholder') {
|
|
|
|
let placeholder = document.createElement('span');
|
|
|
|
placeholder.setAttribute('class', 'blocklyDropDownPlaceholder');
|
|
|
|
placeholder.style.width = content.width + 'px';
|
|
|
|
placeholder.style.height = content.height + 'px';
|
|
|
|
contentDiv.appendChild(placeholder);
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
let button = document.createElement('button');
|
|
|
|
button.setAttribute('id', ':' + i); // For aria-activedescendant
|
|
|
|
button.setAttribute('role', 'menuitem');
|
|
|
|
button.setAttribute('class', 'blocklyDropDownButton');
|
|
|
|
button.title = this.isFirst_ ? this.getFirstValue(content.alt) : content.alt;
|
|
|
|
button.style.width = ((this.itemWidth_) - 8) + 'px';
|
|
|
|
button.style.height = ((this.itemWidth_) - 8) + 'px';
|
|
|
|
let backgroundColor = this.backgroundColour_;
|
|
|
|
if (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.borderColour_;
|
|
|
|
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');
|
|
|
|
});
|
|
|
|
let buttonImg = document.createElement('img');
|
|
|
|
buttonImg.src = this.isFirst_ ? isFirstUrl[option.replace(/\xA0/g, ' ')] : content.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', value);
|
|
|
|
buttonImg.setAttribute('data-value', value);
|
|
|
|
button.appendChild(buttonImg);
|
|
|
|
contentDiv.appendChild(button);
|
|
|
|
}
|
|
|
|
contentDiv.style.width = (this.itemWidth_ * columns) + 'px';
|
|
|
|
dropdownDiv.appendChild(contentDiv);
|
|
|
|
|
|
|
|
Blockly.DropDownDiv.setColour(this.backgroundColour_, this.borderColour_);
|
|
|
|
|
|
|
|
// Calculate positioning based on the field position.
|
|
|
|
let scale = this.sourceBlock_.workspace.scale;
|
|
|
|
let width = this.isFirst_ ? (this as any).width1 : (this as any).width2;
|
|
|
|
let bBox = { width: this.size_.width, height: this.size_.height };
|
|
|
|
width *= scale;
|
|
|
|
bBox.height *= scale;
|
|
|
|
let position = this.fieldGroup_.getBoundingClientRect();
|
|
|
|
let leftPosition = this.isFirst_ ? position.left : position.left + (scale * (this as any).width1) + Blockly.BlockSvg.BOX_FIELD_PADDING;
|
|
|
|
let primaryX = leftPosition + width / 2;
|
|
|
|
let primaryY = position.top + bBox.height;
|
|
|
|
let secondaryX = primaryX;
|
|
|
|
let secondaryY = position.top;
|
|
|
|
// Set bounds to workspace; show the drop-down.
|
|
|
|
(Blockly.DropDownDiv as any).setBoundsElement(this.sourceBlock_.workspace.getParentSvg().parentNode);
|
|
|
|
(Blockly.DropDownDiv as any).show(this, primaryX, primaryY, secondaryX, secondaryY,
|
|
|
|
this.onHide_.bind(this));
|
|
|
|
|
|
|
|
// Update colour to look selected.
|
|
|
|
if (this.isFirst_ && this.box_) {
|
|
|
|
this.box_.setAttribute('fill', this.sourceBlock_.getColourTertiary());
|
|
|
|
} else if (!this.isFirst_ && this.box2_) {
|
|
|
|
this.box2_.setAttribute('fill', this.sourceBlock_.getColourTertiary());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
protected buttonClick_ = function (e: any) {
|
|
|
|
let value = e.target.getAttribute('data-value');
|
|
|
|
this.setValue(value);
|
|
|
|
Blockly.DropDownDiv.hide();
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Callback for when the drop-down is hidden.
|
|
|
|
*/
|
2018-04-18 07:16:19 +02:00
|
|
|
protected onHide_() {
|
2018-04-10 00:59:28 +02:00
|
|
|
Blockly.DropDownDiv.content_.removeAttribute('role');
|
|
|
|
Blockly.DropDownDiv.content_.removeAttribute('aria-haspopup');
|
|
|
|
Blockly.DropDownDiv.content_.removeAttribute('aria-activedescendant');
|
|
|
|
Blockly.DropDownDiv.getContentDiv().style.width = '';
|
|
|
|
if (this.isFirst_ && this.box_) {
|
|
|
|
this.box_.setAttribute('fill', this.sourceBlock_.getColour());
|
|
|
|
} else if (!this.isFirst_ && this.box2_) {
|
|
|
|
this.box2_.setAttribute('fill', this.sourceBlock_.getColour());
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
static MOTORS_MEDIUM_DATAURI = 'data:image/png;base64,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
|
|
|
|
|
|
|
|
|
|
|
|
static MOTORS_LARGE_DATAURI = 'data:image/png;base64,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
|
|
|
|
|
|
|
|
static DUAL_MOTORS_DATAURI = 'data:image/png;base64,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
|
|
|
|
}
|