fix: make getSourceBlock nullable again (#6542)

* fix: make getSourceBlock nullable again

* chore: format

* chore: move to a specific error

* chore: also update procedures with new error

* chore: format
This commit is contained in:
Beka Westberg
2022-10-18 12:57:44 -07:00
committed by GitHub
parent b0c897224a
commit df660af66c
9 changed files with 161 additions and 55 deletions

View File

@@ -255,10 +255,7 @@ export abstract class Field implements IASTNodeLocationSvg,
* @returns The block containing this field.
* @throws An error if the source block is not defined.
*/
getSourceBlock(): Block {
if (!this.sourceBlock_) {
throw new Error(`The source block is ${this.sourceBlock_}.`);
}
getSourceBlock(): Block|null {
return this.sourceBlock_;
}
@@ -476,10 +473,11 @@ export abstract class Field implements IASTNodeLocationSvg,
/** Add or remove the UI indicating if this field is editable or not. */
updateEditable() {
const group = this.fieldGroup_;
if (!this.EDITABLE || !group) {
const block = this.getSourceBlock();
if (!this.EDITABLE || !group || !block) {
return;
}
if (this.enabled_ && this.getSourceBlock().isEditable()) {
if (this.enabled_ && block.isEditable()) {
dom.addClass(group, 'blocklyEditableText');
dom.removeClass(group, 'blocklyNonEditableText');
group.style.cursor = this.CURSOR;
@@ -756,7 +754,7 @@ export abstract class Field implements IASTNodeLocationSvg,
this.textElement_.setAttribute(
'x',
`${
this.getSourceBlock().RTL ?
this.getSourceBlock()?.RTL ?
this.size_.width - contentWidth - xOffset :
xOffset}`);
this.textElement_.setAttribute(
@@ -819,12 +817,17 @@ export abstract class Field implements IASTNodeLocationSvg,
let scaledWidth;
let scaledHeight;
let xy;
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (!this.borderRect_) {
// Browsers are inconsistent in what they return for a bounding box.
// - Webkit / Blink: fill-box / object bounding box
// - Gecko: stroke-box
const bBox = (this.sourceBlock_ as BlockSvg).getHeightWidth();
const scale = (this.getSourceBlock().workspace as WorkspaceSvg).scale;
const scale = (block.workspace as WorkspaceSvg).scale;
xy = this.getAbsoluteXY_();
scaledWidth = (bBox.width + 1) * scale;
scaledHeight = (bBox.height + 1) * scale;
@@ -1158,6 +1161,9 @@ export abstract class Field implements IASTNodeLocationSvg,
getParentInput(): Input {
let parentInput = null;
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const inputs = block.inputList;
for (let idx = 0; idx < block.inputList.length; idx++) {
@@ -1241,7 +1247,11 @@ export abstract class Field implements IASTNodeLocationSvg,
/** Redraw any attached marker or cursor svgs if needed. */
protected updateMarkers_() {
const workspace = this.getSourceBlock().workspace as WorkspaceSvg;
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const workspace = block.workspace as WorkspaceSvg;
if (workspace.keyboardAccessibilityMode && this.cursorSvg_) {
workspace.getCursor()!.draw();
}
@@ -1264,3 +1274,17 @@ export interface FieldConfig {
* in descendants, though they should contain all of Field's prototype methods.
*/
export type FieldProto = Pick<typeof Field, 'prototype'>;
/**
* Represents an error where the field is trying to access its block or
* information about its block before it has actually been attached to said
* block.
*/
export class UnattachedFieldError extends Error {
/** @internal */
constructor() {
super(
'The field has not yet been attached to its input. ' +
'Call appendField to attach it.');
}
}

View File

@@ -16,7 +16,7 @@ import {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as Css from './css.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Field} from './field.js';
import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
import * as dom from './utils/dom.js';
@@ -412,15 +412,19 @@ export class FieldAngle extends FieldTextInput {
*/
protected override onHtmlInputKeyDown_(e: Event) {
super.onHtmlInputKeyDown_(e);
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const keyboardEvent = e as KeyboardEvent;
let multiplier;
if (keyboardEvent.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL)
multiplier = this.getSourceBlock().RTL ? 1 : -1;
multiplier = block.RTL ? 1 : -1;
} else if (keyboardEvent.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL)
multiplier = this.getSourceBlock().RTL ? -1 : 1;
multiplier = block.RTL ? -1 : 1;
} else if (keyboardEvent.keyCode === KeyCodes.DOWN) {
// decrement
multiplier = -1;

View File

@@ -16,7 +16,7 @@ goog.declareModuleId('Blockly.FieldDropdown');
import type {BlockSvg} from './block_svg.js';
import * as dropDownDiv from './dropdowndiv.js';
import {FieldConfig, Field} from './field.js';
import {FieldConfig, Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
@@ -217,16 +217,16 @@ export class FieldDropdown extends Field {
protected shouldAddBorderRect_(): boolean {
return !this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW &&
!this.getSourceBlock().isShadow();
!this.getSourceBlock()?.isShadow();
}
/** Create a tspan based arrow. */
protected createTextArrow_() {
this.arrow_ = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_);
this.arrow_!.appendChild(document.createTextNode(
this.getSourceBlock().RTL ? FieldDropdown.ARROW_CHAR + ' ' :
' ' + FieldDropdown.ARROW_CHAR));
if (this.getSourceBlock().RTL) {
this.getSourceBlock()?.RTL ? FieldDropdown.ARROW_CHAR + ' ' :
' ' + FieldDropdown.ARROW_CHAR));
if (this.getSourceBlock()?.RTL) {
// AnyDuringMigration because: Argument of type 'SVGTSpanElement | null'
// is not assignable to parameter of type 'Node'.
this.getTextElement().insertBefore(
@@ -258,6 +258,10 @@ export class FieldDropdown extends Field {
* undefined if triggered programmatically.
*/
protected override showEditor_(opt_e?: Event) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
this.dropdownCreate_();
// AnyDuringMigration because: Property 'clientX' does not exist on type
// 'Event'.
@@ -279,11 +283,10 @@ export class FieldDropdown extends Field {
dom.addClass(menuElement, 'blocklyDropdownMenu');
if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) {
const primaryColour = this.getSourceBlock().isShadow() ?
this.getSourceBlock().getParent()!.getColour() :
this.getSourceBlock().getColour();
const borderColour = this.getSourceBlock().isShadow() ?
(this.getSourceBlock().getParent() as BlockSvg).style.colourTertiary :
const primaryColour =
block.isShadow() ? block.getParent()!.getColour() : block.getColour();
const borderColour = block.isShadow() ?
(block.getParent() as BlockSvg).style.colourTertiary :
(this.sourceBlock_ as BlockSvg).style.colourTertiary;
dropDownDiv.setColour(primaryColour, borderColour);
}
@@ -304,6 +307,10 @@ export class FieldDropdown extends Field {
/** Create the dropdown editor. */
private dropdownCreate_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const menu = new Menu();
menu.setRole(aria.Role.LISTBOX);
this.menu_ = menu;
@@ -322,7 +329,7 @@ export class FieldDropdown extends Field {
}
const menuItem = new MenuItem(content, value);
menuItem.setRole(aria.Role.OPTION);
menuItem.setRightToLeft(this.getSourceBlock().RTL);
menuItem.setRightToLeft(block.RTL);
menuItem.setCheckable(true);
menu.addChild(menuItem);
menuItem.setChecked(value === this.value_);
@@ -540,6 +547,10 @@ export class FieldDropdown extends Field {
* @param imageJson Selected option that must be an image.
*/
private renderSelectedImage_(imageJson: ImageProperties) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
this.imageElement_!.style.display = '';
this.imageElement_!.setAttributeNS(
dom.XLINK_NS, 'xlink:href', imageJson.src);
@@ -578,7 +589,7 @@ export class FieldDropdown extends Field {
this.size_.height = height;
let arrowX = 0;
if (this.getSourceBlock().RTL) {
if (block.RTL) {
const imageX = xPadding + arrowWidth;
this.imageElement_!.setAttribute('x', imageX.toString());
} else {
@@ -634,12 +645,16 @@ export class FieldDropdown extends Field {
if (!this.svgArrow_) {
return 0;
}
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const hasBorder = !!this.borderRect_;
const xPadding =
hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0;
const textPadding = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_PADDING;
const svgArrowSize = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE;
const arrowX = this.getSourceBlock().RTL ? xPadding : x + textPadding;
const arrowX = block.RTL ? xPadding : x + textPadding;
this.svgArrow_.setAttribute(
'transform', 'translate(' + arrowX + ',' + y + ')');
return svgArrowSize + textPadding;

View File

@@ -13,7 +13,7 @@ import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldMultilineInput');
import * as Css from './css.js';
import {Field} from './field.js';
import {Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
import * as aria from './utils/aria.js';
@@ -163,6 +163,10 @@ export class FieldMultilineInput extends FieldTextInput {
* @returns Currently displayed text.
*/
protected override getDisplayText_(): string {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
let textLines = this.getText();
if (!textLines) {
// Prevent the field from disappearing if empty.
@@ -189,7 +193,7 @@ export class FieldMultilineInput extends FieldTextInput {
textLines += '\n';
}
}
if (this.getSourceBlock().RTL) {
if (block.RTL) {
// The SVG is LTR, force value to be RTL.
textLines += '\u200F';
}
@@ -212,6 +216,10 @@ export class FieldMultilineInput extends FieldTextInput {
/** Updates the text of the textElement. */
protected override render_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
// Remove all text group children.
let currentChild;
while (currentChild = this.textGroup_.firstChild) {
@@ -248,7 +256,7 @@ export class FieldMultilineInput extends FieldTextInput {
this.updateSize_();
if (this.isBeingEdited_) {
if (this.getSourceBlock().RTL) {
if (block.RTL) {
// in RTL, we need to let the browser reflow before resizing
// in order to get the correct bounding box of the borderRect
// avoiding issue #2777.

View File

@@ -21,7 +21,7 @@ import * as dialog from './dialog.js';
import * as dom from './utils/dom.js';
import * as dropDownDiv from './dropdowndiv.js';
import * as eventUtils from './events/utils.js';
import {FieldConfig, Field} from './field.js';
import {FieldConfig, Field, UnattachedFieldError} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
@@ -127,13 +127,17 @@ export class FieldTextInput extends Field {
/** @internal */
override initView() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (this.getConstants()!.FULL_BLOCK_FIELDS) {
// Step one: figure out if this is the only field on this block.
// Rendering is quite different in that case.
let nFields = 0;
let nConnections = 0;
// Count the number of fields, excluding text fields
for (let i = 0, input; input = this.getSourceBlock().inputList[i]; i++) {
for (let i = 0, input; input = block.inputList[i]; i++) {
for (let j = 0; input.fieldRow[j]; j++) {
nFields++;
}
@@ -143,8 +147,8 @@ export class FieldTextInput extends Field {
}
// The special case is when this is the only non-label field on the block
// and it has an output but no inputs.
this.fullBlockClickTarget_ = nFields <= 1 &&
this.getSourceBlock().outputConnection && !nConnections;
this.fullBlockClickTarget_ =
nFields <= 1 && block.outputConnection && !nConnections;
} else {
this.fullBlockClickTarget_ = false;
}
@@ -307,8 +311,11 @@ export class FieldTextInput extends Field {
* @param quietInput True if editor should be created without focus.
*/
private showInlineEditor_(quietInput: boolean) {
WidgetDiv.show(
this, this.getSourceBlock().RTL, this.widgetDispose_.bind(this));
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
WidgetDiv.show(this, block.RTL, this.widgetDispose_.bind(this));
this.htmlInput_ = this.widgetCreate_() as HTMLInputElement;
this.isBeingEdited_ = true;
@@ -326,6 +333,10 @@ export class FieldTextInput extends Field {
* @returns The newly created text input editor.
*/
protected widgetCreate_(): HTMLElement {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
eventUtils.setGroup(true);
const div = WidgetDiv.getDiv();
@@ -351,8 +362,8 @@ export class FieldTextInput extends Field {
// Override border radius.
borderRadius = (bBox.bottom - bBox.top) / 2 + 'px';
// Pull stroke colour from the existing shadow block
const strokeColour = this.getSourceBlock().getParent() ?
(this.getSourceBlock().getParent() as BlockSvg).style.colourTertiary :
const strokeColour = block.getParent() ?
(block.getParent() as BlockSvg).style.colourTertiary :
(this.sourceBlock_ as BlockSvg).style.colourTertiary;
htmlInput.style.border = 1 * scale + 'px solid ' + strokeColour;
div!.style.borderRadius = borderRadius;
@@ -510,6 +521,10 @@ export class FieldTextInput extends Field {
/** Resize the editor to fit the text. */
protected resizeEditor_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const div = WidgetDiv.getDiv();
const bBox = this.getScaledBBox();
div!.style.width = bBox.right - bBox.left + 'px';
@@ -517,8 +532,7 @@ export class FieldTextInput extends Field {
// In RTL mode block fields and LTR input fields the left edge moves,
// whereas the right edge is fixed. Reposition the editor.
const x =
this.getSourceBlock().RTL ? bBox.right - div!.offsetWidth : bBox.left;
const x = block.RTL ? bBox.right - div!.offsetWidth : bBox.left;
const xy = new Coordinate(x, bBox.top);
div!.style.left = xy.x + 'px';

View File

@@ -16,7 +16,7 @@ goog.declareModuleId('Blockly.FieldVariable');
import './events/events_block_change.js';
import type {Block} from './block.js';
import {Field, FieldConfig} from './field.js';
import {Field, FieldConfig, UnattachedFieldError} from './field.js';
import {FieldDropdown} from './field_dropdown.js';
import * as fieldRegistry from './field_registry.js';
import * as internalConstants from './internal_constants.js';
@@ -135,20 +135,27 @@ export class FieldVariable extends FieldDropdown {
* @internal
*/
override initModel() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (this.variable_) {
return; // Initialization already happened.
}
const variable = Variables.getOrCreateVariablePackage(
this.getSourceBlock().workspace, null, this.defaultVariableName,
this.defaultType_);
block.workspace, null, this.defaultVariableName, this.defaultType_);
// Don't call setValue because we don't want to cause a rerender.
this.doValueUpdate_(variable.getId());
}
override shouldAddBorderRect_() {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
return super.shouldAddBorderRect_() &&
(!this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.getSourceBlock().type !== 'variables_get');
block.type !== 'variables_get');
}
/**
@@ -158,6 +165,10 @@ export class FieldVariable extends FieldDropdown {
* field's state.
*/
override fromXml(fieldElement: Element) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const id = fieldElement.getAttribute('id');
const variableName = fieldElement.textContent;
// 'variabletype' should be lowercase, but until July 2019 it was sometimes
@@ -168,8 +179,7 @@ export class FieldVariable extends FieldDropdown {
// AnyDuringMigration because: Argument of type 'string | null' is not
// assignable to parameter of type 'string | undefined'.
const variable = Variables.getOrCreateVariablePackage(
this.getSourceBlock().workspace, id, variableName as AnyDuringMigration,
variableType);
block.workspace, id, variableName as AnyDuringMigration, variableType);
// This should never happen :)
if (variableType !== null && variableType !== variable.type) {
@@ -233,12 +243,16 @@ export class FieldVariable extends FieldDropdown {
* @internal
*/
override loadState(state: AnyDuringMigration) {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (this.loadLegacyState(FieldVariable, state)) {
return;
}
// This is necessary so that blocks in the flyout can have custom var names.
const variable = Variables.getOrCreateVariablePackage(
this.getSourceBlock().workspace, state['id'] || null, state['name'],
block.workspace, state['id'] || null, state['name'],
state['type'] || '');
this.setValue(variable.getId());
}
@@ -315,9 +329,12 @@ export class FieldVariable extends FieldDropdown {
if (opt_newValue === null) {
return null;
}
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const newId = opt_newValue as string;
const variable =
Variables.getVariable(this.getSourceBlock().workspace, newId);
const variable = Variables.getVariable(block.workspace, newId);
if (!variable) {
console.warn(
'Variable id doesn\'t point to a real variable! ' +
@@ -343,8 +360,11 @@ export class FieldVariable extends FieldDropdown {
* @param newId The value to be saved.
*/
protected override doValueUpdate_(newId: AnyDuringMigration) {
this.variable_ =
Variables.getVariable(this.getSourceBlock().workspace, newId as string);
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
this.variable_ = Variables.getVariable(block.workspace, newId as string);
super.doValueUpdate_(newId);
}

View File

@@ -28,5 +28,5 @@ export interface IASTNodeLocationWithBlock extends IASTNodeLocation {
*
* @returns The source block.
*/
getSourceBlock(): Block;
getSourceBlock(): Block|null;
}

View File

@@ -176,6 +176,10 @@ export class ASTNode {
const location = this.location_ as Field;
const input = location.getParentInput();
const block = location.getSourceBlock();
if (!block) {
throw new Error(
'The current AST location is not associated with a block');
}
const curIdx = block.inputList.indexOf((input));
let fieldIdx = input.fieldRow.indexOf(location) + 1;
for (let i = curIdx; i < block.inputList.length; i++) {
@@ -235,6 +239,10 @@ export class ASTNode {
const location = this.location_ as Field;
const parentInput = location.getParentInput();
const block = location.getSourceBlock();
if (!block) {
throw new Error(
'The current AST location is not associated with a block');
}
const curIdx = block.inputList.indexOf((parentInput));
let fieldIdx = parentInput.fieldRow.indexOf(location) - 1;
for (let i = curIdx; i >= 0; i--) {
@@ -270,7 +278,10 @@ export class ASTNode {
// TODO(#6097): Use instanceof checks to exit early for values of
// curLocation that don't make sense.
if ((curLocation as IASTNodeLocationWithBlock).getSourceBlock) {
curLocation = (curLocation as IASTNodeLocationWithBlock).getSourceBlock();
const block = (curLocation as IASTNodeLocationWithBlock).getSourceBlock();
if (block) {
curLocation = block;
}
}
// TODO(#6097): Use instanceof checks to exit early for values of
// curLocation that don't make sense.
@@ -531,7 +542,12 @@ export class ASTNode {
}
case ASTNode.types.FIELD: {
const field = this.location_ as Field;
return ASTNode.createBlockNode(field.getSourceBlock());
const block = field.getSourceBlock();
if (!block) {
throw new Error(
'The current AST location is not associated with a block');
}
return ASTNode.createBlockNode(block);
}
case ASTNode.types.INPUT: {
const connection = this.location_ as Connection;

View File

@@ -22,7 +22,7 @@ import * as common from './common.js';
import type {Abstract} from './events/events_abstract.js';
import type {BubbleOpen} from './events/events_bubble_open.js';
import * as eventUtils from './events/utils.js';
import type {Field} from './field.js';
import {Field, UnattachedFieldError} from './field.js';
import {Msg} from './msg.js';
import {Names} from './names.js';
import * as utilsXml from './utils/xml.js';
@@ -180,14 +180,19 @@ export function isNameUsed(
* @alias Blockly.Procedures.rename
*/
export function rename(this: Field, name: string): string {
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
// Strip leading and trailing whitespace. Beyond this, all names are legal.
name = name.trim();
const legalName = findLegalName(name, (this.getSourceBlock()));
const legalName = findLegalName(name, block);
const oldName = this.getValue();
if (oldName !== name && oldName !== legalName) {
// Rename any callers.
const blocks = this.getSourceBlock().workspace.getAllBlocks(false);
const blocks = block.workspace.getAllBlocks(false);
for (let i = 0; i < blocks.length; i++) {
// Assume it is a procedure so we can check.
const procedureBlock = blocks[i] as unknown as ProcedureBlock;