mirror of
https://github.com/google/blockly.git
synced 2026-01-07 17:10:11 +01:00
* feat: Invalid Blocks * Rename the new json property from invalid to invalidReasons. * Merged isValid into isEnabled. * Minor fixes. * More minor fixes. * Reverting some stuff that didn't need to change. * Addressing PR feedback. * Update the BlockInfo interface to match State. * Make BlockChange.disabledReason private.
836 lines
24 KiB
TypeScript
836 lines
24 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2021 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
// Former goog.module ID: Blockly.serialization.blocks
|
|
|
|
import type {Block} from '../block.js';
|
|
import type {BlockSvg} from '../block_svg.js';
|
|
import type {Connection} from '../connection.js';
|
|
import {MANUALLY_DISABLED} from '../constants.js';
|
|
import * as deprecation from '../utils/deprecation.js';
|
|
import * as eventUtils from '../events/utils.js';
|
|
import {inputTypes} from '../inputs/input_types.js';
|
|
import {isSerializable} from '../interfaces/i_serializable.js';
|
|
import type {ISerializer} from '../interfaces/i_serializer.js';
|
|
import * as registry from '../registry.js';
|
|
import * as utilsXml from '../utils/xml.js';
|
|
import type {Workspace} from '../workspace.js';
|
|
import * as Xml from '../xml.js';
|
|
import * as renderManagement from '../render_management.js';
|
|
|
|
import {
|
|
BadConnectionCheck,
|
|
MissingBlockType,
|
|
MissingConnection,
|
|
RealChildOfShadow,
|
|
UnregisteredIcon,
|
|
} from './exceptions.js';
|
|
import * as priorities from './priorities.js';
|
|
import * as serializationRegistry from './registry.js';
|
|
|
|
// TODO(#5160): Remove this once lint is fixed.
|
|
/* eslint-disable no-use-before-define */
|
|
|
|
/**
|
|
* Represents the state of a connection.
|
|
*/
|
|
export interface ConnectionState {
|
|
shadow?: State;
|
|
block?: State;
|
|
}
|
|
|
|
/**
|
|
* Represents the state of a given block.
|
|
*/
|
|
export interface State {
|
|
type: string;
|
|
id?: string;
|
|
x?: number;
|
|
y?: number;
|
|
collapsed?: boolean;
|
|
deletable?: boolean;
|
|
movable?: boolean;
|
|
editable?: boolean;
|
|
enabled?: boolean;
|
|
disabledReasons?: string[];
|
|
inline?: boolean;
|
|
data?: string;
|
|
extraState?: AnyDuringMigration;
|
|
icons?: {[key: string]: AnyDuringMigration};
|
|
fields?: {[key: string]: AnyDuringMigration};
|
|
inputs?: {[key: string]: ConnectionState};
|
|
next?: ConnectionState;
|
|
}
|
|
|
|
/**
|
|
* Returns the state of the given block as a plain JavaScript object.
|
|
*
|
|
* @param block The block to serialize.
|
|
* @param param1 addCoordinates: If true, the coordinates of the block are added
|
|
* to the serialized state. False by default. addinputBlocks: If true,
|
|
* children of the block which are connected to inputs will be serialized.
|
|
* True by default. addNextBlocks: If true, children of the block which are
|
|
* connected to the block's next connection (if it exists) will be
|
|
* serialized. True by default. doFullSerialization: If true, fields that
|
|
* normally just save a reference to some external state (eg variables) will
|
|
* instead serialize all of the info about that state. This supports
|
|
* deserializing the block into a workspace where that state doesn't yet
|
|
* exist. True by default.
|
|
* @returns The serialized state of the block, or null if the block could not be
|
|
* serialied (eg it was an insertion marker).
|
|
*/
|
|
export function save(
|
|
block: Block,
|
|
{
|
|
addCoordinates = false,
|
|
addInputBlocks = true,
|
|
addNextBlocks = true,
|
|
doFullSerialization = true,
|
|
saveIds = true,
|
|
}: {
|
|
addCoordinates?: boolean;
|
|
addInputBlocks?: boolean;
|
|
addNextBlocks?: boolean;
|
|
doFullSerialization?: boolean;
|
|
saveIds?: boolean;
|
|
} = {},
|
|
): State | null {
|
|
if (block.isInsertionMarker()) {
|
|
return null;
|
|
}
|
|
const state = {
|
|
'type': block.type,
|
|
'id': saveIds ? block.id : undefined,
|
|
};
|
|
|
|
if (addCoordinates) {
|
|
// AnyDuringMigration because: Argument of type '{ type: string; id:
|
|
// string; }' is not assignable to parameter of type 'State'.
|
|
saveCoords(block, state as AnyDuringMigration);
|
|
}
|
|
// AnyDuringMigration because: Argument of type '{ type: string; id: string;
|
|
// }' is not assignable to parameter of type 'State'.
|
|
saveAttributes(block, state as AnyDuringMigration);
|
|
// AnyDuringMigration because: Argument of type '{ type: string; id: string;
|
|
// }' is not assignable to parameter of type 'State'.
|
|
saveExtraState(block, state as AnyDuringMigration, doFullSerialization);
|
|
// AnyDuringMigration because: Argument of type '{ type: string; id: string;
|
|
// }' is not assignable to parameter of type 'State'.
|
|
saveIcons(block, state as AnyDuringMigration, doFullSerialization);
|
|
// AnyDuringMigration because: Argument of type '{ type: string; id: string;
|
|
// }' is not assignable to parameter of type 'State'.
|
|
saveFields(block, state as AnyDuringMigration, doFullSerialization);
|
|
if (addInputBlocks) {
|
|
// AnyDuringMigration because: Argument of type '{ type: string; id:
|
|
// string; }' is not assignable to parameter of type 'State'.
|
|
saveInputBlocks(
|
|
block,
|
|
state as AnyDuringMigration,
|
|
doFullSerialization,
|
|
saveIds,
|
|
);
|
|
}
|
|
if (addNextBlocks) {
|
|
// AnyDuringMigration because: Argument of type '{ type: string; id:
|
|
// string; }' is not assignable to parameter of type 'State'.
|
|
saveNextBlocks(
|
|
block,
|
|
state as AnyDuringMigration,
|
|
doFullSerialization,
|
|
saveIds,
|
|
);
|
|
}
|
|
|
|
// AnyDuringMigration because: Type '{ type: string; id: string; }' is not
|
|
// assignable to type 'State'.
|
|
return state as AnyDuringMigration;
|
|
}
|
|
|
|
/**
|
|
* Adds attributes to the given state object based on the state of the block.
|
|
* Eg collapsed, disabled, inline, etc.
|
|
*
|
|
* @param block The block to base the attributes on.
|
|
* @param state The state object to append to.
|
|
*/
|
|
function saveAttributes(block: Block, state: State) {
|
|
if (block.isCollapsed()) {
|
|
state['collapsed'] = true;
|
|
}
|
|
if (!block.isEnabled()) {
|
|
state['disabledReasons'] = Array.from(block.getDisabledReasons());
|
|
}
|
|
if (!block.isOwnDeletable()) {
|
|
state['deletable'] = false;
|
|
}
|
|
if (!block.isOwnMovable()) {
|
|
state['movable'] = false;
|
|
}
|
|
if (!block.isOwnEditable()) {
|
|
state['editable'] = false;
|
|
}
|
|
if (
|
|
block.inputsInline !== undefined &&
|
|
block.inputsInline !== block.inputsInlineDefault
|
|
) {
|
|
state['inline'] = block.inputsInline;
|
|
}
|
|
// Data is a nullable string, so we don't need to worry about falsy values.
|
|
if (block.data) {
|
|
state['data'] = block.data;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the coordinates of the given block to the given state object.
|
|
*
|
|
* @param block The block to base the coordinates on.
|
|
* @param state The state object to append to.
|
|
*/
|
|
function saveCoords(block: Block, state: State) {
|
|
const workspace = block.workspace;
|
|
const xy = block.getRelativeToSurfaceXY();
|
|
state['x'] = Math.round(workspace.RTL ? workspace.getWidth() - xy.x : xy.x);
|
|
state['y'] = Math.round(xy.y);
|
|
}
|
|
|
|
/**
|
|
* Adds any extra state the block may provide to the given state object.
|
|
*
|
|
* @param block The block to serialize the extra state of.
|
|
* @param state The state object to append to.
|
|
* @param doFullSerialization Whether or not to serialize the full state of the
|
|
* extra state (rather than possibly saving a reference to some state).
|
|
*/
|
|
function saveExtraState(
|
|
block: Block,
|
|
state: State,
|
|
doFullSerialization: boolean,
|
|
) {
|
|
if (block.saveExtraState) {
|
|
const extraState = block.saveExtraState(doFullSerialization);
|
|
if (extraState !== null) {
|
|
state['extraState'] = extraState;
|
|
}
|
|
} else if (block.mutationToDom) {
|
|
const extraState = block.mutationToDom();
|
|
if (extraState !== null) {
|
|
state['extraState'] = Xml.domToText(extraState).replace(
|
|
' xmlns="https://developers.google.com/blockly/xml"',
|
|
'',
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the state of all of the icons on the block to the given state object.
|
|
*
|
|
* @param block The block to serialize the icon state of.
|
|
* @param state The state object to append to.
|
|
* @param doFullSerialization Whether or not to serialize the full state of the
|
|
* icon (rather than possibly saving a reference to some state).
|
|
*/
|
|
function saveIcons(block: Block, state: State, doFullSerialization: boolean) {
|
|
const icons = Object.create(null);
|
|
for (const icon of block.getIcons()) {
|
|
if (isSerializable(icon)) {
|
|
const state = icon.saveState(doFullSerialization);
|
|
if (state) icons[icon.getType().toString()] = state;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(icons).length) {
|
|
state['icons'] = icons;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the state of all of the fields on the block to the given state object.
|
|
*
|
|
* @param block The block to serialize the field state of.
|
|
* @param state The state object to append to.
|
|
* @param doFullSerialization Whether or not to serialize the full state of the
|
|
* field (rather than possibly saving a reference to some state).
|
|
*/
|
|
function saveFields(block: Block, state: State, doFullSerialization: boolean) {
|
|
const fields = Object.create(null);
|
|
for (let i = 0; i < block.inputList.length; i++) {
|
|
const input = block.inputList[i];
|
|
for (let j = 0; j < input.fieldRow.length; j++) {
|
|
const field = input.fieldRow[j];
|
|
if (field.isSerializable()) {
|
|
fields[field.name!] = field.saveState(doFullSerialization);
|
|
}
|
|
}
|
|
}
|
|
if (Object.keys(fields).length) {
|
|
state['fields'] = fields;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the state of all of the child blocks of the given block (which are
|
|
* connected to inputs) to the given state object.
|
|
*
|
|
* @param block The block to serialize the input blocks of.
|
|
* @param state The state object to append to.
|
|
* @param doFullSerialization Whether or not to do full serialization.
|
|
*/
|
|
function saveInputBlocks(
|
|
block: Block,
|
|
state: State,
|
|
doFullSerialization: boolean,
|
|
saveIds: boolean,
|
|
) {
|
|
const inputs = Object.create(null);
|
|
for (let i = 0; i < block.inputList.length; i++) {
|
|
const input = block.inputList[i];
|
|
if (!input.connection) continue;
|
|
const connectionState = saveConnection(
|
|
input.connection as Connection,
|
|
doFullSerialization,
|
|
saveIds,
|
|
);
|
|
if (connectionState) {
|
|
inputs[input.name] = connectionState;
|
|
}
|
|
}
|
|
|
|
if (Object.keys(inputs).length) {
|
|
state['inputs'] = inputs;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the state of all of the next blocks of the given block to the given
|
|
* state object.
|
|
*
|
|
* @param block The block to serialize the next blocks of.
|
|
* @param state The state object to append to.
|
|
* @param doFullSerialization Whether or not to do full serialization.
|
|
*/
|
|
function saveNextBlocks(
|
|
block: Block,
|
|
state: State,
|
|
doFullSerialization: boolean,
|
|
saveIds: boolean,
|
|
) {
|
|
if (!block.nextConnection) {
|
|
return;
|
|
}
|
|
const connectionState = saveConnection(
|
|
block.nextConnection,
|
|
doFullSerialization,
|
|
saveIds,
|
|
);
|
|
if (connectionState) {
|
|
state['next'] = connectionState;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the state of the given connection (ie the state of any connected
|
|
* shadow or real blocks).
|
|
*
|
|
* @param connection The connection to serialize the connected blocks of.
|
|
* @returns An object containing the state of any connected shadow block, or any
|
|
* connected real block.
|
|
* @param doFullSerialization Whether or not to do full serialization.
|
|
*/
|
|
function saveConnection(
|
|
connection: Connection,
|
|
doFullSerialization: boolean,
|
|
saveIds: boolean,
|
|
): ConnectionState | null {
|
|
const shadow = connection.getShadowState(true);
|
|
const child = connection.targetBlock();
|
|
if (!shadow && !child) {
|
|
return null;
|
|
}
|
|
const state = Object.create(null);
|
|
if (shadow) {
|
|
state['shadow'] = shadow;
|
|
}
|
|
if (child && !child.isShadow()) {
|
|
state['block'] = save(child, {doFullSerialization, saveIds});
|
|
}
|
|
return state;
|
|
}
|
|
|
|
/**
|
|
* Loads the block represented by the given state into the given workspace.
|
|
*
|
|
* @param state The state of a block to deserialize into the workspace.
|
|
* @param workspace The workspace to add the block to.
|
|
* @param param1 recordUndo: If true, events triggered by this function will be
|
|
* undo-able by the user. False by default.
|
|
* @returns The block that was just loaded.
|
|
*/
|
|
export function append(
|
|
state: State,
|
|
workspace: Workspace,
|
|
{recordUndo = false}: {recordUndo?: boolean} = {},
|
|
): Block {
|
|
const block = appendInternal(state, workspace, {recordUndo});
|
|
if (workspace.rendered) renderManagement.triggerQueuedRenders();
|
|
return block;
|
|
}
|
|
|
|
/**
|
|
* Loads the block represented by the given state into the given workspace.
|
|
* This is defined internally so that the extra parameters don't clutter our
|
|
* external API.
|
|
* But it is exported so that other places within Blockly can call it directly
|
|
* with the extra parameters.
|
|
*
|
|
* @param state The state of a block to deserialize into the workspace.
|
|
* @param workspace The workspace to add the block to.
|
|
* @param param1 parentConnection: If provided, the system will attempt to
|
|
* connect the block to this connection after it is created. Undefined by
|
|
* default. isShadow: If true, the block will be set to a shadow block after
|
|
* it is created. False by default. recordUndo: If true, events triggered by
|
|
* this function will be undo-able by the user. False by default.
|
|
* @returns The block that was just appended.
|
|
* @internal
|
|
*/
|
|
export function appendInternal(
|
|
state: State,
|
|
workspace: Workspace,
|
|
{
|
|
parentConnection = undefined,
|
|
isShadow = false,
|
|
recordUndo = false,
|
|
}: {
|
|
parentConnection?: Connection;
|
|
isShadow?: boolean;
|
|
recordUndo?: boolean;
|
|
} = {},
|
|
): Block {
|
|
const prevRecordUndo = eventUtils.getRecordUndo();
|
|
eventUtils.setRecordUndo(recordUndo);
|
|
const existingGroup = eventUtils.getGroup();
|
|
if (!existingGroup) {
|
|
eventUtils.setGroup(true);
|
|
}
|
|
eventUtils.disable();
|
|
|
|
let block;
|
|
try {
|
|
block = appendPrivate(state, workspace, {parentConnection, isShadow});
|
|
} finally {
|
|
eventUtils.enable();
|
|
}
|
|
|
|
if (eventUtils.isEnabled()) {
|
|
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(block));
|
|
}
|
|
eventUtils.setGroup(existingGroup);
|
|
eventUtils.setRecordUndo(prevRecordUndo);
|
|
|
|
// Adding connections to the connection db is expensive. This defers that
|
|
// operation to decrease load time.
|
|
if (workspace.rendered) {
|
|
const blockSvg = block as BlockSvg;
|
|
setTimeout(() => {
|
|
if (!blockSvg.disposed) {
|
|
blockSvg.setConnectionTracking(true);
|
|
}
|
|
}, 1);
|
|
}
|
|
|
|
return block;
|
|
}
|
|
|
|
/**
|
|
* Loads the block represented by the given state into the given workspace.
|
|
* This is defined privately so that it can be called recursively without firing
|
|
* eroneous events. Events (and other things we only want to occur on the top
|
|
* block) are handled by appendInternal.
|
|
*
|
|
* @param state The state of a block to deserialize into the workspace.
|
|
* @param workspace The workspace to add the block to.
|
|
* @param param1 parentConnection: If provided, the system will attempt to
|
|
* connect the block to this connection after it is created. Undefined by
|
|
* default. isShadow: The block will be set to a shadow block after it is
|
|
* created. False by default.
|
|
* @returns The block that was just appended.
|
|
*/
|
|
function appendPrivate(
|
|
state: State,
|
|
workspace: Workspace,
|
|
{
|
|
parentConnection = undefined,
|
|
isShadow = false,
|
|
}: {parentConnection?: Connection; isShadow?: boolean} = {},
|
|
): Block {
|
|
if (!state['type']) {
|
|
throw new MissingBlockType(state);
|
|
}
|
|
|
|
const block = workspace.newBlock(state['type'], state['id']);
|
|
block.setShadow(isShadow);
|
|
loadCoords(block, state);
|
|
loadAttributes(block, state);
|
|
loadExtraState(block, state);
|
|
tryToConnectParent(parentConnection, block, state);
|
|
loadIcons(block, state);
|
|
loadFields(block, state);
|
|
loadInputBlocks(block, state);
|
|
loadNextBlocks(block, state);
|
|
initBlock(block, workspace.rendered);
|
|
|
|
return block;
|
|
}
|
|
|
|
/**
|
|
* Applies any coordinate information available on the state object to the
|
|
* block.
|
|
*
|
|
* @param block The block to set the position of.
|
|
* @param state The state object to reference.
|
|
*/
|
|
function loadCoords(block: Block, state: State) {
|
|
let x = state['x'] === undefined ? 0 : state['x'];
|
|
const y = state['y'] === undefined ? 0 : state['y'];
|
|
|
|
const workspace = block.workspace;
|
|
x = workspace.RTL ? workspace.getWidth() - x : x;
|
|
|
|
block.moveBy(x, y);
|
|
}
|
|
|
|
/**
|
|
* Applies any attribute information available on the state object to the block.
|
|
*
|
|
* @param block The block to set the attributes of.
|
|
* @param state The state object to reference.
|
|
*/
|
|
function loadAttributes(block: Block, state: State) {
|
|
if (state['collapsed']) {
|
|
block.setCollapsed(true);
|
|
}
|
|
if (state['deletable'] === false) {
|
|
block.setDeletable(false);
|
|
}
|
|
if (state['movable'] === false) {
|
|
block.setMovable(false);
|
|
}
|
|
if (state['editable'] === false) {
|
|
block.setEditable(false);
|
|
}
|
|
if (state['enabled'] === false) {
|
|
deprecation.warn(
|
|
'enabled',
|
|
'v11',
|
|
'v12',
|
|
'disabledReasons with the value ["' + MANUALLY_DISABLED + '"]',
|
|
);
|
|
block.setDisabledReason(true, MANUALLY_DISABLED);
|
|
}
|
|
if (Array.isArray(state['disabledReasons'])) {
|
|
for (const reason of state['disabledReasons']) {
|
|
block.setDisabledReason(true, reason);
|
|
}
|
|
}
|
|
if (state['inline'] !== undefined) {
|
|
block.setInputsInline(state['inline']);
|
|
}
|
|
if (state['data'] !== undefined) {
|
|
block.data = state['data'];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies any extra state information available on the state object to the
|
|
* block.
|
|
*
|
|
* @param block The block to set the extra state of.
|
|
* @param state The state object to reference.
|
|
*/
|
|
function loadExtraState(block: Block, state: State) {
|
|
if (!state['extraState']) {
|
|
return;
|
|
}
|
|
if (block.loadExtraState) {
|
|
block.loadExtraState(state['extraState']);
|
|
} else if (block.domToMutation) {
|
|
block.domToMutation(utilsXml.textToDom(state['extraState']));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Attempts to connect the block to the parent connection, if it exists.
|
|
*
|
|
* @param parentConnection The parent connection to try to connect the block to.
|
|
* @param child The block to try to connect to the parent.
|
|
* @param state The state which defines the given block
|
|
*/
|
|
function tryToConnectParent(
|
|
parentConnection: Connection | undefined,
|
|
child: Block,
|
|
state: State,
|
|
) {
|
|
if (!parentConnection) {
|
|
return;
|
|
}
|
|
|
|
if (parentConnection.getSourceBlock().isShadow() && !child.isShadow()) {
|
|
throw new RealChildOfShadow(state);
|
|
}
|
|
|
|
let connected = false;
|
|
let childConnection;
|
|
if (parentConnection.type === inputTypes.VALUE) {
|
|
childConnection = child.outputConnection;
|
|
if (!childConnection) {
|
|
throw new MissingConnection('output', child, state);
|
|
}
|
|
connected = parentConnection.connect(childConnection);
|
|
} else {
|
|
// Statement type.
|
|
childConnection = child.previousConnection;
|
|
if (!childConnection) {
|
|
throw new MissingConnection('previous', child, state);
|
|
}
|
|
connected = parentConnection.connect(childConnection);
|
|
}
|
|
|
|
if (!connected) {
|
|
const checker = child.workspace.connectionChecker;
|
|
throw new BadConnectionCheck(
|
|
checker.getErrorMessage(
|
|
checker.canConnectWithReason(childConnection, parentConnection, false),
|
|
childConnection,
|
|
parentConnection,
|
|
),
|
|
parentConnection.type === inputTypes.VALUE
|
|
? 'output connection'
|
|
: 'previous connection',
|
|
child,
|
|
state,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies icon state to the icons on the block, based on the given state
|
|
* object.
|
|
*
|
|
* @param block The block to set the icon state of.
|
|
* @param state The state object to reference.
|
|
*/
|
|
function loadIcons(block: Block, state: State) {
|
|
if (!state['icons']) return;
|
|
|
|
const iconTypes = Object.keys(state['icons']);
|
|
for (const iconType of iconTypes) {
|
|
const iconState = state['icons'][iconType];
|
|
let icon = block.getIcon(iconType);
|
|
if (!icon) {
|
|
const constructor = registry.getClass(
|
|
registry.Type.ICON,
|
|
iconType,
|
|
false,
|
|
);
|
|
if (!constructor) throw new UnregisteredIcon(iconType, block, state);
|
|
icon = new constructor(block);
|
|
block.addIcon(icon);
|
|
}
|
|
if (isSerializable(icon)) icon.loadState(iconState);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Applies any field information available on the state object to the block.
|
|
*
|
|
* @param block The block to set the field state of.
|
|
* @param state The state object to reference.
|
|
*/
|
|
function loadFields(block: Block, state: State) {
|
|
if (!state['fields']) {
|
|
return;
|
|
}
|
|
const keys = Object.keys(state['fields']);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const fieldName = keys[i];
|
|
const fieldState = state['fields'][fieldName];
|
|
const field = block.getField(fieldName);
|
|
if (!field) {
|
|
console.warn(
|
|
`Ignoring non-existant field ${fieldName} in block ${block.type}`,
|
|
);
|
|
continue;
|
|
}
|
|
field.loadState(fieldState);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates any child blocks (attached to inputs) defined by the given state
|
|
* and attaches them to the given block.
|
|
*
|
|
* @param block The block to attach input blocks to.
|
|
* @param state The state object to reference.
|
|
*/
|
|
function loadInputBlocks(block: Block, state: State) {
|
|
if (!state['inputs']) {
|
|
return;
|
|
}
|
|
const keys = Object.keys(state['inputs']);
|
|
for (let i = 0; i < keys.length; i++) {
|
|
const inputName = keys[i];
|
|
const input = block.getInput(inputName);
|
|
if (!input || !input.connection) {
|
|
throw new MissingConnection(inputName, block, state);
|
|
}
|
|
loadConnection(input.connection, state['inputs'][inputName]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates any next blocks defined by the given state and attaches them to the
|
|
* given block.
|
|
*
|
|
* @param block The block to attach next blocks to.
|
|
* @param state The state object to reference.
|
|
*/
|
|
function loadNextBlocks(block: Block, state: State) {
|
|
if (!state['next']) {
|
|
return;
|
|
}
|
|
if (!block.nextConnection) {
|
|
throw new MissingConnection('next', block, state);
|
|
}
|
|
loadConnection(block.nextConnection, state['next']);
|
|
}
|
|
/**
|
|
* Applies the state defined by connectionState to the given connection, ie
|
|
* assigns shadows and attaches child blocks.
|
|
*
|
|
* @param connection The connection to deserialize the connected blocks of.
|
|
* @param connectionState The object containing the state of any connected
|
|
* shadow block, or any connected real block.
|
|
*/
|
|
function loadConnection(
|
|
connection: Connection,
|
|
connectionState: ConnectionState,
|
|
) {
|
|
if (connectionState['shadow']) {
|
|
connection.setShadowState(connectionState['shadow']);
|
|
}
|
|
if (connectionState['block']) {
|
|
appendPrivate(
|
|
connectionState['block'],
|
|
connection.getSourceBlock().workspace,
|
|
{parentConnection: connection},
|
|
);
|
|
}
|
|
}
|
|
|
|
// TODO(#5146): Remove this from the serialization system.
|
|
/**
|
|
* Initializes the give block, eg init the model, inits the svg, renders, etc.
|
|
*
|
|
* @param block The block to initialize.
|
|
* @param rendered Whether the block is a rendered or headless block.
|
|
*/
|
|
function initBlock(block: Block, rendered: boolean) {
|
|
if (rendered) {
|
|
const blockSvg = block as BlockSvg;
|
|
// Adding connections to the connection db is expensive. This defers that
|
|
// operation to decrease load time.
|
|
blockSvg.setConnectionTracking(false);
|
|
|
|
blockSvg.initSvg();
|
|
blockSvg.queueRender();
|
|
|
|
// fixes #6076 JSO deserialization doesn't
|
|
// set .iconXY_ property so here it will be set
|
|
for (const icon of blockSvg.getIcons()) {
|
|
icon.onLocationChange(blockSvg.getRelativeToSurfaceXY());
|
|
}
|
|
} else {
|
|
block.initModel();
|
|
}
|
|
}
|
|
|
|
// Alias to disambiguate saving within the serializer.
|
|
const saveBlock = save;
|
|
|
|
/**
|
|
* Serializer for saving and loading block state.
|
|
*/
|
|
export class BlockSerializer implements ISerializer {
|
|
priority: number;
|
|
|
|
/* eslint-disable-next-line require-jsdoc */
|
|
constructor() {
|
|
/** The priority for deserializing blocks. */
|
|
this.priority = priorities.BLOCKS;
|
|
}
|
|
|
|
/**
|
|
* Serializes the blocks of the given workspace.
|
|
*
|
|
* @param workspace The workspace to save the blocks of.
|
|
* @returns The state of the workspace's blocks, or null if there are no
|
|
* blocks.
|
|
*/
|
|
save(
|
|
workspace: Workspace,
|
|
): {languageVersion: number; blocks: State[]} | null {
|
|
const blockStates = [];
|
|
for (const block of workspace.getTopBlocks(false)) {
|
|
const state = saveBlock(block, {
|
|
addCoordinates: true,
|
|
doFullSerialization: false,
|
|
});
|
|
if (state) {
|
|
blockStates.push(state);
|
|
}
|
|
}
|
|
if (blockStates.length) {
|
|
return {
|
|
'languageVersion': 0, // Currently unused.
|
|
'blocks': blockStates,
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Deserializes the blocks defined by the given state into the given
|
|
* workspace.
|
|
*
|
|
* @param state The state of the blocks to deserialize.
|
|
* @param workspace The workspace to deserialize into.
|
|
*/
|
|
load(
|
|
state: {languageVersion: number; blocks: State[]},
|
|
workspace: Workspace,
|
|
) {
|
|
const blockStates = state['blocks'];
|
|
for (const state of blockStates) {
|
|
append(state, workspace, {recordUndo: eventUtils.getRecordUndo()});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Disposes of any blocks that exist on the workspace.
|
|
*
|
|
* @param workspace The workspace to clear the blocks of.
|
|
*/
|
|
clear(workspace: Workspace) {
|
|
// Cannot use workspace.clear() because that also removes variables.
|
|
for (const block of workspace.getTopBlocks(false)) {
|
|
block.dispose(false);
|
|
}
|
|
}
|
|
}
|
|
|
|
serializationRegistry.register('blocks', new BlockSerializer());
|