pxt-ev3/fieldeditors/field_motors.ts

583 lines
53 KiB
TypeScript

/// <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)";
this.borderColour_ = pxt.toolbox.fadeColor(this.backgroundColour_, 0.4, false);
}
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.dom.createSvgElement('image', {
'height': (this as any).arrowSize_ + 'px',
'width': (this as any).arrowSize_ + 'px'
}, null);
this.arrow_.setAttributeNS('http://www.w3.org/1999/xlink',
'xlink:href', (Blockly.FieldDropdown as any).DROPDOWN_SVG_DATAURI);
this.arrow2_ = <SVGImageElement>Blockly.utils.dom.createSvgElement('image', {
'height': (this as any).arrowSize_ + 'px',
'width': (this as any).arrowSize_ + 'px'
}, null);
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.dom.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.
let size = this.getSize();
let fieldX = (this.sourceBlock_.RTL) ? -size.width / 2 : size.width / 2;
/** @type {!Element} */
this.textElement_ = Blockly.utils.dom.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_ = <SVGTextElement>Blockly.utils.dom.createSvgElement('text',
{
'class': (this as any).className_,
'x': fieldX,
'dy': '0.7ex',
'y': this.size_.height / 2
},
this.fieldGroup_);
this.updateEditable();
(this.sourceBlock_ as Blockly.BlockSvg).getSvgRoot().appendChild(this.fieldGroup_);
// Force a render.
this.render_();
this.isDirty_ = true;
(this as any).mouseDownWrapper_ =
Blockly.bindEventWithChecks_((this as any).getClickTarget_(), 'mousedown', this,
(this as any).onMouseDown_);
// Add second dropdown
if (this.shouldShowRect_()) {
this.box_ = <SVGRectElement>Blockly.utils.dom.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_ = <SVGRectElement>Blockly.utils.dom.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.
let text = this.text_;
this.text_ = null;
this.setText(text);
}
getFirstValue(value: string) {
const typeValue = value.indexOf('large') != -1 ? 'large' : 'medium';
const portValue = this.getSecondValue(value);
const isDual = portValue.length > 1;
return `${typeValue} motor${isDual ? 's' : ''}`;
}
getSecondValue(value: string) {
return (value.indexOf('large') != -1) ?
value.substring(value.indexOf('large') + 5) :
value.substring(value.indexOf('medium') + 6);
}
getFirstValueI11n(value: string) {
const firstValue = this.getFirstValue(value);
const motorOptions = {
'medium motor': lf("medium motor"),
'large motor': lf("large motor"),
'large motors': lf("large motors")
}
return motorOptions[firstValue];
}
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;
}
let 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;
}
let textNode = document.createTextNode(text);
this.textElement2_.appendChild(textNode);
// Cached width is obsolete. Clear it.
this.isDirty_ = true;
};
patchDualMotorText(text: string) {
if (text === null) {
return text;
}
if (text.indexOf('|') == -1) {
text = this.sourceBlock_.RTL ? `${text}|${lf("large motors")}` : `${lf("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_ && (<Blockly.BlockSvg>this.sourceBlock_).rendered) {
(<Blockly.BlockSvg>this.sourceBlock_).render();
this.sourceBlock_.bumpNeighbours_();
}
}
positionArrow2(start: number, x: number) {
if (!this.arrow2_) {
return 0;
}
let 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;
};
updateSize_() {
// Calculate width of field
let width = Blockly.Field.getCachedWidth(this.textElement_);
let 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_));
// First dropdown
// Use one of the following options, medium motor, large motor or large motors (translated)
const textNode1 = document.createTextNode(this.getFirstValueI11n(<string>this.value_));
this.textElement_.appendChild(textNode1);
// Second dropdown, no need to translate. Only port numbers
if (this.textElement2_) {
const textNode2 = document.createTextNode(this.getSecondValue(<string>this.value_));
this.textElement2_.appendChild(textNode2);
}
this.updateSize_();
// 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.
let width = Blockly.Field.getCachedWidth(this.textElement_);
let 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.
let width2 = Blockly.Field.getCachedWidth(this.textElement2_);
let 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 * (<Blockly.WorkspaceSvg>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');
const foptions = this.getOptions(); // [img info, text]
let opts = {};
let conts = {};
let vals = {};
// Go through all option values and split them into groups
for (let opt = 0; opt < foptions.length; opt++) {
const value = foptions[opt][1];
const motorValue = value.substring(value.indexOf('.') + 1);
const typeValue = motorValue.indexOf('large') == 0 ? 'large' : 'medium';
const portValue = motorValue.indexOf('large') == 0 ? motorValue.substring(5) : motorValue.substring(6);
const isDual = portValue.length > 1;
const text = `${typeValue} motor${isDual ? 's' : ''}|${portValue}`;
const key = `${typeValue} motor${isDual ? 's' : ''}`;
if (!opts[key]) opts[key] = [];
opts[key].push(portValue);
conts[text] = foptions[opt][0];
vals[text] = value;
}
const currentFirst = this.getFirstValue(<string>this.value_);
//const currentSecond = this.getSecondValue(<string>this.value_);
let options: string[];
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) {
options = [lf("medium motor"), lf("large motor"), lf("large motors")];
} 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 + '|' + (option.indexOf('motors') != -1 ? 'BC' : 'A') : 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.getFirstValueI11n(value) : this.getSecondValue(value);
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');
let imgSrc = content.src;
if (this.isFirst_) {
const motorValue = value.substring(value.indexOf('.') + 1);
// Find out what kind of motor this is based on it's value, possible values: mediumX, largeX, and largeXY
if (motorValue.indexOf('medium') == 0) imgSrc = isFirstUrl['medium motor'];
else if (motorValue.length == 6) imgSrc = isFirstUrl['large motor'];
else imgSrc = isFirstUrl['large motors'];
}
buttonImg.src = imgSrc;
//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 = (<Blockly.WorkspaceSvg>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((<Blockly.WorkspaceSvg>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.
*/
protected onHide_() {
const content = Blockly.DropDownDiv.getContentDiv();
content.removeAttribute('role');
content.removeAttribute('aria-haspopup');
content.removeAttribute('aria-activedescendant');
(content as HTMLElement).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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';
}