From df660af66ca62dcbe44560c05156160c4d91445b Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 18 Oct 2022 12:57:44 -0700 Subject: [PATCH] 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 --- core/field.ts | 42 +++++++++++++++---- core/field_angle.ts | 10 +++-- core/field_dropdown.ts | 41 ++++++++++++------ core/field_multilineinput.ts | 14 +++++-- core/field_textinput.ts | 34 ++++++++++----- core/field_variable.ts | 42 ++++++++++++++----- .../i_ast_node_location_with_block.ts | 2 +- core/keyboard_nav/ast_node.ts | 20 ++++++++- core/procedures.ts | 11 +++-- 9 files changed, 161 insertions(+), 55 deletions(-) diff --git a/core/field.ts b/core/field.ts index ac827bbd9..480a459d2 100644 --- a/core/field.ts +++ b/core/field.ts @@ -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; + +/** + * 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.'); + } +} diff --git a/core/field_angle.ts b/core/field_angle.ts index 678f3ef7f..69a5df239 100644 --- a/core/field_angle.ts +++ b/core/field_angle.ts @@ -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; diff --git a/core/field_dropdown.ts b/core/field_dropdown.ts index 4385c037d..87580726f 100644 --- a/core/field_dropdown.ts +++ b/core/field_dropdown.ts @@ -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; diff --git a/core/field_multilineinput.ts b/core/field_multilineinput.ts index 44bcd651a..403f725cc 100644 --- a/core/field_multilineinput.ts +++ b/core/field_multilineinput.ts @@ -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. diff --git a/core/field_textinput.ts b/core/field_textinput.ts index b639ffd2f..ca4e6053c 100644 --- a/core/field_textinput.ts +++ b/core/field_textinput.ts @@ -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'; diff --git a/core/field_variable.ts b/core/field_variable.ts index e337356bf..879eb1aa9 100644 --- a/core/field_variable.ts +++ b/core/field_variable.ts @@ -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); } diff --git a/core/interfaces/i_ast_node_location_with_block.ts b/core/interfaces/i_ast_node_location_with_block.ts index 72ef94b78..cf54ddc41 100644 --- a/core/interfaces/i_ast_node_location_with_block.ts +++ b/core/interfaces/i_ast_node_location_with_block.ts @@ -28,5 +28,5 @@ export interface IASTNodeLocationWithBlock extends IASTNodeLocation { * * @returns The source block. */ - getSourceBlock(): Block; + getSourceBlock(): Block|null; } diff --git a/core/keyboard_nav/ast_node.ts b/core/keyboard_nav/ast_node.ts index 24bf02803..27d0a4554 100644 --- a/core/keyboard_nav/ast_node.ts +++ b/core/keyboard_nav/ast_node.ts @@ -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; diff --git a/core/procedures.ts b/core/procedures.ts index e472fcd8a..7fad925e6 100644 --- a/core/procedures.ts +++ b/core/procedures.ts @@ -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;