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. * @returns The block containing this field.
* @throws An error if the source block is not defined. * @throws An error if the source block is not defined.
*/ */
getSourceBlock(): Block { getSourceBlock(): Block|null {
if (!this.sourceBlock_) {
throw new Error(`The source block is ${this.sourceBlock_}.`);
}
return this.sourceBlock_; 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. */ /** Add or remove the UI indicating if this field is editable or not. */
updateEditable() { updateEditable() {
const group = this.fieldGroup_; const group = this.fieldGroup_;
if (!this.EDITABLE || !group) { const block = this.getSourceBlock();
if (!this.EDITABLE || !group || !block) {
return; return;
} }
if (this.enabled_ && this.getSourceBlock().isEditable()) { if (this.enabled_ && block.isEditable()) {
dom.addClass(group, 'blocklyEditableText'); dom.addClass(group, 'blocklyEditableText');
dom.removeClass(group, 'blocklyNonEditableText'); dom.removeClass(group, 'blocklyNonEditableText');
group.style.cursor = this.CURSOR; group.style.cursor = this.CURSOR;
@@ -756,7 +754,7 @@ export abstract class Field implements IASTNodeLocationSvg,
this.textElement_.setAttribute( this.textElement_.setAttribute(
'x', 'x',
`${ `${
this.getSourceBlock().RTL ? this.getSourceBlock()?.RTL ?
this.size_.width - contentWidth - xOffset : this.size_.width - contentWidth - xOffset :
xOffset}`); xOffset}`);
this.textElement_.setAttribute( this.textElement_.setAttribute(
@@ -819,12 +817,17 @@ export abstract class Field implements IASTNodeLocationSvg,
let scaledWidth; let scaledWidth;
let scaledHeight; let scaledHeight;
let xy; let xy;
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
if (!this.borderRect_) { if (!this.borderRect_) {
// Browsers are inconsistent in what they return for a bounding box. // Browsers are inconsistent in what they return for a bounding box.
// - Webkit / Blink: fill-box / object bounding box // - Webkit / Blink: fill-box / object bounding box
// - Gecko: stroke-box // - Gecko: stroke-box
const bBox = (this.sourceBlock_ as BlockSvg).getHeightWidth(); 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_(); xy = this.getAbsoluteXY_();
scaledWidth = (bBox.width + 1) * scale; scaledWidth = (bBox.width + 1) * scale;
scaledHeight = (bBox.height + 1) * scale; scaledHeight = (bBox.height + 1) * scale;
@@ -1158,6 +1161,9 @@ export abstract class Field implements IASTNodeLocationSvg,
getParentInput(): Input { getParentInput(): Input {
let parentInput = null; let parentInput = null;
const block = this.getSourceBlock(); const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const inputs = block.inputList; const inputs = block.inputList;
for (let idx = 0; idx < block.inputList.length; idx++) { 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. */ /** Redraw any attached marker or cursor svgs if needed. */
protected updateMarkers_() { 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_) { if (workspace.keyboardAccessibilityMode && this.cursorSvg_) {
workspace.getCursor()!.draw(); workspace.getCursor()!.draw();
} }
@@ -1264,3 +1274,17 @@ export interface FieldConfig {
* in descendants, though they should contain all of Field's prototype methods. * in descendants, though they should contain all of Field's prototype methods.
*/ */
export type FieldProto = Pick<typeof Field, 'prototype'>; 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 browserEvents from './browser_events.js';
import * as Css from './css.js'; import * as Css from './css.js';
import * as dropDownDiv from './dropdowndiv.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 * as fieldRegistry from './field_registry.js';
import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js'; import {FieldTextInputConfig, FieldTextInput} from './field_textinput.js';
import * as dom from './utils/dom.js'; import * as dom from './utils/dom.js';
@@ -412,15 +412,19 @@ export class FieldAngle extends FieldTextInput {
*/ */
protected override onHtmlInputKeyDown_(e: Event) { protected override onHtmlInputKeyDown_(e: Event) {
super.onHtmlInputKeyDown_(e); super.onHtmlInputKeyDown_(e);
const block = this.getSourceBlock();
if (!block) {
throw new UnattachedFieldError();
}
const keyboardEvent = e as KeyboardEvent; const keyboardEvent = e as KeyboardEvent;
let multiplier; let multiplier;
if (keyboardEvent.keyCode === KeyCodes.LEFT) { if (keyboardEvent.keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL) // decrement (increment in RTL)
multiplier = this.getSourceBlock().RTL ? 1 : -1; multiplier = block.RTL ? 1 : -1;
} else if (keyboardEvent.keyCode === KeyCodes.RIGHT) { } else if (keyboardEvent.keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL) // increment (decrement in RTL)
multiplier = this.getSourceBlock().RTL ? -1 : 1; multiplier = block.RTL ? -1 : 1;
} else if (keyboardEvent.keyCode === KeyCodes.DOWN) { } else if (keyboardEvent.keyCode === KeyCodes.DOWN) {
// decrement // decrement
multiplier = -1; multiplier = -1;

View File

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

View File

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

View File

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

View File

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

View File

@@ -28,5 +28,5 @@ export interface IASTNodeLocationWithBlock extends IASTNodeLocation {
* *
* @returns The source block. * @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 location = this.location_ as Field;
const input = location.getParentInput(); const input = location.getParentInput();
const block = location.getSourceBlock(); 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)); const curIdx = block.inputList.indexOf((input));
let fieldIdx = input.fieldRow.indexOf(location) + 1; let fieldIdx = input.fieldRow.indexOf(location) + 1;
for (let i = curIdx; i < block.inputList.length; i++) { for (let i = curIdx; i < block.inputList.length; i++) {
@@ -235,6 +239,10 @@ export class ASTNode {
const location = this.location_ as Field; const location = this.location_ as Field;
const parentInput = location.getParentInput(); const parentInput = location.getParentInput();
const block = location.getSourceBlock(); 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)); const curIdx = block.inputList.indexOf((parentInput));
let fieldIdx = parentInput.fieldRow.indexOf(location) - 1; let fieldIdx = parentInput.fieldRow.indexOf(location) - 1;
for (let i = curIdx; i >= 0; i--) { 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 // TODO(#6097): Use instanceof checks to exit early for values of
// curLocation that don't make sense. // curLocation that don't make sense.
if ((curLocation as IASTNodeLocationWithBlock).getSourceBlock) { 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 // TODO(#6097): Use instanceof checks to exit early for values of
// curLocation that don't make sense. // curLocation that don't make sense.
@@ -531,7 +542,12 @@ export class ASTNode {
} }
case ASTNode.types.FIELD: { case ASTNode.types.FIELD: {
const field = this.location_ as 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: { case ASTNode.types.INPUT: {
const connection = this.location_ as Connection; 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 {Abstract} from './events/events_abstract.js';
import type {BubbleOpen} from './events/events_bubble_open.js'; import type {BubbleOpen} from './events/events_bubble_open.js';
import * as eventUtils from './events/utils.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 {Msg} from './msg.js';
import {Names} from './names.js'; import {Names} from './names.js';
import * as utilsXml from './utils/xml.js'; import * as utilsXml from './utils/xml.js';
@@ -180,14 +180,19 @@ export function isNameUsed(
* @alias Blockly.Procedures.rename * @alias Blockly.Procedures.rename
*/ */
export function rename(this: Field, name: string): string { 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. // Strip leading and trailing whitespace. Beyond this, all names are legal.
name = name.trim(); name = name.trim();
const legalName = findLegalName(name, (this.getSourceBlock())); const legalName = findLegalName(name, block);
const oldName = this.getValue(); const oldName = this.getValue();
if (oldName !== name && oldName !== legalName) { if (oldName !== name && oldName !== legalName) {
// Rename any callers. // Rename any callers.
const blocks = this.getSourceBlock().workspace.getAllBlocks(false); const blocks = block.workspace.getAllBlocks(false);
for (let i = 0; i < blocks.length; i++) { for (let i = 0; i < blocks.length; i++) {
// Assume it is a procedure so we can check. // Assume it is a procedure so we can check.
const procedureBlock = blocks[i] as unknown as ProcedureBlock; const procedureBlock = blocks[i] as unknown as ProcedureBlock;