mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
feat: Parse newlines in JSON message as row separators. (#6944)
* feat: Parse message newlines as endOfRow dummies. * Fix the multilineinput field test. * Addressing PR feedback. * Addressing PR feedback. * Newline parsing now uses a new custom input. * npm run format * Added input_end_row to block factory. * Addres feedback, fix endrow after external value.
This commit is contained in:
@@ -48,6 +48,7 @@ import {Size} from './utils/size.js';
|
||||
import type {VariableModel} from './variable_model.js';
|
||||
import type {Workspace} from './workspace.js';
|
||||
import {DummyInput} from './inputs/dummy_input.js';
|
||||
import {EndRowInput} from './inputs/end_row_input.js';
|
||||
import {ValueInput} from './inputs/value_input.js';
|
||||
import {StatementInput} from './inputs/statement_input.js';
|
||||
import {IconType} from './icons/icon_types.js';
|
||||
@@ -1339,6 +1340,12 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.inputList.length; i++) {
|
||||
if (this.inputList[i] instanceof EndRowInput) {
|
||||
// A row-end input is present. Inline value inputs.
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1560,6 +1567,17 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
return this.appendInput(new DummyInput(name, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends an input that ends the row.
|
||||
*
|
||||
* @param name Optional language-neutral identifier which may used to find
|
||||
* this input again. Should be unique to this block.
|
||||
* @returns The input object created.
|
||||
*/
|
||||
appendEndRowInput(name = ''): Input {
|
||||
return this.appendInput(new EndRowInput(name, this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends the given input row.
|
||||
*
|
||||
@@ -1628,7 +1646,8 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
this.interpolate_(
|
||||
json['message' + i],
|
||||
json['args' + i] || [],
|
||||
json['lastDummyAlign' + i],
|
||||
// Backwards compatibility: lastDummyAlign aliases implicitAlign.
|
||||
json['implicitAlign' + i] || json['lastDummyAlign' + i],
|
||||
warningPrefix,
|
||||
);
|
||||
i++;
|
||||
@@ -1765,19 +1784,19 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
* @param message Text contains interpolation tokens (%1, %2, ...) that match
|
||||
* with fields or inputs defined in the args array.
|
||||
* @param args Array of arguments to be interpolated.
|
||||
* @param lastDummyAlign If a dummy input is added at the end, how should it
|
||||
* be aligned?
|
||||
* @param implicitAlign If an implicit input is added at the end or in place
|
||||
* of newline tokens, how should it be aligned?
|
||||
* @param warningPrefix Warning prefix string identifying block.
|
||||
*/
|
||||
private interpolate_(
|
||||
message: string,
|
||||
args: AnyDuringMigration[],
|
||||
lastDummyAlign: string | undefined,
|
||||
implicitAlign: string | undefined,
|
||||
warningPrefix: string,
|
||||
) {
|
||||
const tokens = parsing.tokenizeInterpolation(message);
|
||||
this.validateTokens_(tokens, args.length);
|
||||
const elements = this.interpolateArguments_(tokens, args, lastDummyAlign);
|
||||
const elements = this.interpolateArguments_(tokens, args, implicitAlign);
|
||||
|
||||
// An array of [field, fieldName] tuples.
|
||||
const fieldStack = [];
|
||||
@@ -1855,19 +1874,20 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
|
||||
/**
|
||||
* Inserts args in place of numerical tokens. String args are converted to
|
||||
* JSON that defines a label field. If necessary an extra dummy input is added
|
||||
* to the end of the elements.
|
||||
* JSON that defines a label field. Newline characters are converted to
|
||||
* end-row inputs, and if necessary an extra dummy input is added to the end
|
||||
* of the elements.
|
||||
*
|
||||
* @param tokens The tokens to interpolate
|
||||
* @param args The arguments to insert.
|
||||
* @param lastDummyAlign The alignment the added dummy input should have, if
|
||||
* we are required to add one.
|
||||
* @param implicitAlign The alignment to use for any implicitly added end-row
|
||||
* or dummy inputs, if necessary.
|
||||
* @returns The JSON definitions of field and inputs to add to the block.
|
||||
*/
|
||||
private interpolateArguments_(
|
||||
tokens: Array<string | number>,
|
||||
args: Array<AnyDuringMigration | string>,
|
||||
lastDummyAlign: string | undefined,
|
||||
implicitAlign: string | undefined,
|
||||
): AnyDuringMigration[] {
|
||||
const elements = [];
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
@@ -1877,11 +1897,20 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
}
|
||||
// Args can be strings, which is why this isn't elseif.
|
||||
if (typeof element === 'string') {
|
||||
// AnyDuringMigration because: Type '{ text: string; type: string; } |
|
||||
// null' is not assignable to type 'string | number'.
|
||||
element = this.stringToFieldJson_(element) as AnyDuringMigration;
|
||||
if (!element) {
|
||||
continue;
|
||||
if (element === '\n') {
|
||||
// Convert newline tokens to end-row inputs.
|
||||
const newlineInput = {'type': 'input_end_row'};
|
||||
if (implicitAlign) {
|
||||
(newlineInput as AnyDuringMigration)['align'] = implicitAlign;
|
||||
}
|
||||
element = newlineInput as AnyDuringMigration;
|
||||
} else {
|
||||
// AnyDuringMigration because: Type '{ text: string; type: string; }
|
||||
// | null' is not assignable to type 'string | number'.
|
||||
element = this.stringToFieldJson_(element) as AnyDuringMigration;
|
||||
if (!element) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
elements.push(element);
|
||||
@@ -1895,8 +1924,8 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
)
|
||||
) {
|
||||
const dummyInput = {'type': 'input_dummy'};
|
||||
if (lastDummyAlign) {
|
||||
(dummyInput as AnyDuringMigration)['align'] = lastDummyAlign;
|
||||
if (implicitAlign) {
|
||||
(dummyInput as AnyDuringMigration)['align'] = implicitAlign;
|
||||
}
|
||||
elements.push(dummyInput);
|
||||
}
|
||||
@@ -1960,6 +1989,9 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
case 'input_dummy':
|
||||
input = this.appendDummyInput(element['name']);
|
||||
break;
|
||||
case 'input_end_row':
|
||||
input = this.appendEndRowInput(element['name']);
|
||||
break;
|
||||
default: {
|
||||
input = this.appendInputFromRegistry(element['type'], element['name']);
|
||||
break;
|
||||
@@ -1998,6 +2030,7 @@ export class Block implements IASTNodeLocation, IDeletable {
|
||||
str === 'input_value' ||
|
||||
str === 'input_statement' ||
|
||||
str === 'input_dummy' ||
|
||||
str === 'input_end_row' ||
|
||||
registry.hasItem(registry.Type.INPUT, str)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -7,8 +7,17 @@
|
||||
import {Align} from './inputs/align.js';
|
||||
import {Input} from './inputs/input.js';
|
||||
import {DummyInput} from './inputs/dummy_input.js';
|
||||
import {EndRowInput} from './inputs/end_row_input.js';
|
||||
import {StatementInput} from './inputs/statement_input.js';
|
||||
import {ValueInput} from './inputs/value_input.js';
|
||||
import {inputTypes} from './inputs/input_types.js';
|
||||
|
||||
export {Align, Input, DummyInput, StatementInput, ValueInput, inputTypes};
|
||||
export {
|
||||
Align,
|
||||
Input,
|
||||
DummyInput,
|
||||
EndRowInput,
|
||||
StatementInput,
|
||||
ValueInput,
|
||||
inputTypes,
|
||||
};
|
||||
|
||||
31
core/inputs/end_row_input.ts
Normal file
31
core/inputs/end_row_input.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2023 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type {Block} from '../block.js';
|
||||
import {Input} from './input.js';
|
||||
import {inputTypes} from './input_types.js';
|
||||
|
||||
/**
|
||||
* Represents an input on a block that is always the last input in the row. Any
|
||||
* following input will be rendered on the next row even if the block has inline
|
||||
* inputs. Any newline character in a JSON block definition's message will
|
||||
* automatically be parsed as an end-row input.
|
||||
*/
|
||||
export class EndRowInput extends Input {
|
||||
readonly type = inputTypes.END_ROW;
|
||||
|
||||
/**
|
||||
* @param name Language-neutral identifier which may used to find this input
|
||||
* again.
|
||||
* @param block The block containing this input.
|
||||
*/
|
||||
constructor(
|
||||
public name: string,
|
||||
block: Block,
|
||||
) {
|
||||
super(name, block);
|
||||
}
|
||||
}
|
||||
@@ -21,4 +21,8 @@ export enum inputTypes {
|
||||
DUMMY = 5,
|
||||
// An unknown type of input defined by an external developer.
|
||||
CUSTOM = 6,
|
||||
// An input with no connections that is always the last input of a row. Any
|
||||
// subsequent input will be rendered on the next row. Any newline character in
|
||||
// a JSON block definition's message will be parsed as an end-row input.
|
||||
END_ROW = 7,
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type {RenderedConnection} from '../../rendered_connection.js';
|
||||
import type {Measurable} from '../measurables/base.js';
|
||||
import {BottomRow} from '../measurables/bottom_row.js';
|
||||
import {DummyInput} from '../../inputs/dummy_input.js';
|
||||
import {EndRowInput} from '../../inputs/end_row_input.js';
|
||||
import {ExternalValueInput} from '../measurables/external_value_input.js';
|
||||
import {Field} from '../measurables/field.js';
|
||||
import {Hat} from '../measurables/hat.js';
|
||||
@@ -326,9 +327,9 @@ export class RenderInfo {
|
||||
} else if (input instanceof ValueInput) {
|
||||
activeRow.elements.push(new ExternalValueInput(this.constants_, input));
|
||||
activeRow.hasExternalInput = true;
|
||||
} else if (input instanceof DummyInput) {
|
||||
// Dummy inputs have no visual representation, but the information is
|
||||
// still important.
|
||||
} else if (input instanceof DummyInput || input instanceof EndRowInput) {
|
||||
// Dummy and end-row inputs have no visual representation, but the
|
||||
// information is still important.
|
||||
activeRow.minHeight = Math.max(
|
||||
activeRow.minHeight,
|
||||
input.getSourceBlock() && input.getSourceBlock()!.isShadow()
|
||||
@@ -355,6 +356,11 @@ export class RenderInfo {
|
||||
if (!lastInput) {
|
||||
return false;
|
||||
}
|
||||
// If the previous input was an end-row input, then any following input
|
||||
// should always be rendered on the next row.
|
||||
if (lastInput instanceof EndRowInput) {
|
||||
return true;
|
||||
}
|
||||
// A statement input or an input following one always gets a new row.
|
||||
if (
|
||||
input instanceof StatementInput ||
|
||||
@@ -362,8 +368,13 @@ export class RenderInfo {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Value and dummy inputs get new row if inputs are not inlined.
|
||||
if (input instanceof ValueInput || input instanceof DummyInput) {
|
||||
// Value inputs, dummy inputs, and any input following an external value
|
||||
// input get a new row if inputs are not inlined.
|
||||
if (
|
||||
input instanceof ValueInput ||
|
||||
input instanceof DummyInput ||
|
||||
lastInput instanceof ValueInput
|
||||
) {
|
||||
return !this.isInline;
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -13,6 +13,7 @@ import {RenderInfo as BaseRenderInfo} from '../common/info.js';
|
||||
import type {Measurable} from '../measurables/base.js';
|
||||
import type {BottomRow} from '../measurables/bottom_row.js';
|
||||
import {DummyInput} from '../../inputs/dummy_input.js';
|
||||
import {EndRowInput} from '../../inputs/end_row_input.js';
|
||||
import {ExternalValueInput} from '../measurables/external_value_input.js';
|
||||
import type {Field} from '../measurables/field.js';
|
||||
import {InRowSpacer} from '../measurables/in_row_spacer.js';
|
||||
@@ -90,9 +91,9 @@ export class RenderInfo extends BaseRenderInfo {
|
||||
} else if (input instanceof ValueInput) {
|
||||
activeRow.elements.push(new ExternalValueInput(this.constants_, input));
|
||||
activeRow.hasExternalInput = true;
|
||||
} else if (input instanceof DummyInput) {
|
||||
// Dummy inputs have no visual representation, but the information is
|
||||
// still important.
|
||||
} else if (input instanceof DummyInput || input instanceof EndRowInput) {
|
||||
// Dummy and end-row inputs have no visual representation, but the
|
||||
// information is still important.
|
||||
activeRow.minHeight = Math.max(
|
||||
activeRow.minHeight,
|
||||
this.constants_.DUMMY_INPUT_MIN_HEIGHT,
|
||||
@@ -379,8 +380,12 @@ export class RenderInfo extends BaseRenderInfo {
|
||||
row.width < prevInput.width
|
||||
) {
|
||||
rowNextRightEdges.set(row, prevInput.width);
|
||||
} else {
|
||||
} else if (row.hasStatement) {
|
||||
nextRightEdge = row.width;
|
||||
} else {
|
||||
// To keep right edges of consecutive non-statement rows aligned, use
|
||||
// the maximum width.
|
||||
nextRightEdge = Math.max(nextRightEdge, row.width);
|
||||
}
|
||||
prevInput = row;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ export class Row {
|
||||
hasInlineInput = false;
|
||||
|
||||
/**
|
||||
* Whether the row has any dummy inputs.
|
||||
* Whether the row has any dummy inputs or end-row inputs.
|
||||
*/
|
||||
hasDummyInput = false;
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ goog.declareModuleId('Blockly.zelos.RenderInfo');
|
||||
|
||||
import type {BlockSvg} from '../../block_svg.js';
|
||||
import {DummyInput} from '../../inputs/dummy_input.js';
|
||||
import {EndRowInput} from '../../inputs/end_row_input.js';
|
||||
import {FieldImage} from '../../field_image.js';
|
||||
import {FieldLabel} from '../../field_label.js';
|
||||
import {FieldTextInput} from '../../field_textinput.js';
|
||||
@@ -124,6 +125,11 @@ export class RenderInfo extends BaseRenderInfo {
|
||||
if (!lastInput) {
|
||||
return false;
|
||||
}
|
||||
// If the previous input was an end-row input, then any following input
|
||||
// should always be rendered on the next row.
|
||||
if (lastInput instanceof EndRowInput) {
|
||||
return true;
|
||||
}
|
||||
// A statement input or an input following one always gets a new row.
|
||||
if (
|
||||
input instanceof StatementInput ||
|
||||
@@ -131,8 +137,12 @@ export class RenderInfo extends BaseRenderInfo {
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// Value and dummy inputs get new row if inputs are not inlined.
|
||||
if (input instanceof ValueInput || input instanceof DummyInput) {
|
||||
// Value, dummy, and end-row inputs get new row if inputs are not inlined.
|
||||
if (
|
||||
input instanceof ValueInput ||
|
||||
input instanceof DummyInput ||
|
||||
input instanceof EndRowInput
|
||||
) {
|
||||
return !this.isInline || this.isMultiRow;
|
||||
}
|
||||
return false;
|
||||
@@ -267,9 +277,9 @@ export class RenderInfo extends BaseRenderInfo {
|
||||
override addInput_(input: Input, activeRow: Row) {
|
||||
// If we have two dummy inputs on the same row, one aligned left and the
|
||||
// other right, keep track of the right aligned dummy input so we can add
|
||||
// padding later.
|
||||
// padding later. An end-row input after a dummy input also counts.
|
||||
if (
|
||||
input instanceof DummyInput &&
|
||||
(input instanceof DummyInput || input instanceof EndRowInput) &&
|
||||
activeRow.hasDummyInput &&
|
||||
activeRow.align === Align.LEFT &&
|
||||
input.align === Align.RIGHT
|
||||
@@ -502,7 +512,7 @@ export class RenderInfo extends BaseRenderInfo {
|
||||
const connectionWidth = this.outputConnection.width;
|
||||
const outerShape = this.outputConnection.shape.type;
|
||||
const constants = this.constants_;
|
||||
if (this.isMultiRow && this.inputRows.length > 1) {
|
||||
if (this.inputRows.length > 1) {
|
||||
switch (outerShape) {
|
||||
case constants.SHAPES.ROUND: {
|
||||
// Special case for multi-row round reporter blocks.
|
||||
|
||||
@@ -17,13 +17,16 @@ import * as colourUtils from './colour.js';
|
||||
*
|
||||
* @param message Text which might contain string table references and
|
||||
* interpolation tokens.
|
||||
* @param parseInterpolationTokens Option to parse numeric
|
||||
* interpolation tokens (%1, %2, ...) when true.
|
||||
* @param parseInterpolationTokens Option to parse numeric interpolation
|
||||
* tokens (%1, %2, ...) when true.
|
||||
* @param tokenizeNewlines Split individual newline characters into separate
|
||||
* tokens when true.
|
||||
* @returns Array of strings and numbers.
|
||||
*/
|
||||
function tokenizeInterpolationInternal(
|
||||
message: string,
|
||||
parseInterpolationTokens: boolean,
|
||||
tokenizeNewlines: boolean,
|
||||
): (string | number)[] {
|
||||
const tokens = [];
|
||||
const chars = message.split('');
|
||||
@@ -47,6 +50,15 @@ function tokenizeInterpolationInternal(
|
||||
}
|
||||
buffer.length = 0;
|
||||
state = 1;
|
||||
} else if (tokenizeNewlines && c === '\n') {
|
||||
// Output newline characters as single-character tokens, to be replaced
|
||||
// with endOfRow dummies during interpolation.
|
||||
const text = buffer.join('');
|
||||
if (text) {
|
||||
tokens.push(text);
|
||||
}
|
||||
buffer.length = 0;
|
||||
tokens.push(c);
|
||||
} else {
|
||||
buffer.push(c); // Regular char.
|
||||
}
|
||||
@@ -108,6 +120,7 @@ function tokenizeInterpolationInternal(
|
||||
tokenizeInterpolationInternal(
|
||||
rawValue,
|
||||
parseInterpolationTokens,
|
||||
tokenizeNewlines,
|
||||
),
|
||||
);
|
||||
} else if (parseInterpolationTokens) {
|
||||
@@ -137,11 +150,15 @@ function tokenizeInterpolationInternal(
|
||||
tokens.push(text);
|
||||
}
|
||||
|
||||
// Merge adjacent text tokens into a single string.
|
||||
// Merge adjacent text tokens into a single string (but if newlines should be
|
||||
// tokenized, don't merge those with adjacent text).
|
||||
const mergedTokens = [];
|
||||
buffer.length = 0;
|
||||
for (let i = 0; i < tokens.length; i++) {
|
||||
if (typeof tokens[i] === 'string') {
|
||||
if (
|
||||
typeof tokens[i] === 'string' &&
|
||||
!(tokenizeNewlines && tokens[i] === '\n')
|
||||
) {
|
||||
buffer.push(tokens[i] as string);
|
||||
} else {
|
||||
text = buffer.join('');
|
||||
@@ -166,14 +183,15 @@ function tokenizeInterpolationInternal(
|
||||
* It will also replace string table references (e.g., %{bky_my_msg} and
|
||||
* %{BKY_MY_MSG} will both be replaced with the value in
|
||||
* Msg['MY_MSG']). Percentage sign characters '%' may be self-escaped
|
||||
* (e.g., '%%').
|
||||
* (e.g., '%%'). Newline characters will also be output as string tokens
|
||||
* containing a single newline character.
|
||||
*
|
||||
* @param message Text which might contain string table references and
|
||||
* interpolation tokens.
|
||||
* @returns Array of strings and numbers.
|
||||
*/
|
||||
export function tokenizeInterpolation(message: string): (string | number)[] {
|
||||
return tokenizeInterpolationInternal(message, true);
|
||||
return tokenizeInterpolationInternal(message, true, true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -189,9 +207,13 @@ export function replaceMessageReferences(message: string | any): string {
|
||||
if (typeof message !== 'string') {
|
||||
return message;
|
||||
}
|
||||
const interpolatedResult = tokenizeInterpolationInternal(message, false);
|
||||
// When parseInterpolationTokens === false, interpolatedResult should be at
|
||||
// most length 1.
|
||||
const interpolatedResult = tokenizeInterpolationInternal(
|
||||
message,
|
||||
false,
|
||||
false,
|
||||
);
|
||||
// When parseInterpolationTokens and tokenizeNewlines are false,
|
||||
// interpolatedResult should be at most length 1.
|
||||
return interpolatedResult.length ? String(interpolatedResult[0]) : '';
|
||||
}
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ export function blockToDom(
|
||||
const input = block.inputList[i];
|
||||
let container: Element;
|
||||
let empty = true;
|
||||
if (input.type === inputTypes.DUMMY) {
|
||||
if (input.type === inputTypes.DUMMY || input.type === inputTypes.END_ROW) {
|
||||
continue;
|
||||
} else {
|
||||
const childBlock = input.connection!.targetBlock();
|
||||
|
||||
@@ -287,14 +287,16 @@ BlockDefinitionExtractor.parseInputs_ = function(block) {
|
||||
* @private
|
||||
*/
|
||||
BlockDefinitionExtractor.input_ = function(input, align) {
|
||||
var isDummy = (input.type === Blockly.DUMMY_INPUT);
|
||||
var hasConnector = (input.type === Blockly.inputs.inputTypes.VALUE || input.type === Blockly.inputs.inputTypes.STATEMENT);
|
||||
var inputTypeAttr =
|
||||
isDummy ? 'input_dummy' :
|
||||
(input.type === Blockly.INPUT_VALUE) ? 'input_value' : 'input_statement';
|
||||
input.type === Blockly.inputs.inputTypes.DUMMY ? 'input_dummy' :
|
||||
input.type === Blockly.inputs.inputTypes.END_ROW ? 'input_end_row' :
|
||||
input.type === Blockly.inputs.inputTypes.VALUE ? 'input_value' :
|
||||
'input_statement';
|
||||
var inputDefBlock =
|
||||
BlockDefinitionExtractor.newDomElement_('block', {type: inputTypeAttr});
|
||||
|
||||
if (!isDummy) {
|
||||
if (hasConnector) {
|
||||
inputDefBlock.append(BlockDefinitionExtractor.newDomElement_(
|
||||
'field', {name: 'INPUTNAME'}, input.name));
|
||||
}
|
||||
@@ -307,7 +309,7 @@ BlockDefinitionExtractor.input_ = function(input, align) {
|
||||
fieldsDef.append(fieldsXml);
|
||||
inputDefBlock.append(fieldsDef);
|
||||
|
||||
if (!isDummy) {
|
||||
if (hasConnector) {
|
||||
var typeValue = BlockDefinitionExtractor.newDomElement_(
|
||||
'value', {name: 'TYPE'});
|
||||
typeValue.append(
|
||||
|
||||
@@ -220,14 +220,33 @@ Blockly.Blocks['input_dummy'] = {
|
||||
"previousStatement": "Input",
|
||||
"nextStatement": "Input",
|
||||
"colour": 210,
|
||||
"tooltip": "For adding fields on a separate row with no " +
|
||||
"connections. Alignment options (left, right, centre) " +
|
||||
"apply only to multi-line fields.",
|
||||
"tooltip": "For adding fields without any block connections." +
|
||||
"Alignment options (left, right, centre) only affect " +
|
||||
"multi-row blocks.",
|
||||
"helpUrl": "https://www.youtube.com/watch?v=s2_xaEvcVI0#t=293"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['input_end_row'] = {
|
||||
// End-row input.
|
||||
init: function() {
|
||||
this.jsonInit({
|
||||
"message0": "end-row input",
|
||||
"message1": FIELD_MESSAGE,
|
||||
"args1": FIELD_ARGS,
|
||||
"previousStatement": "Input",
|
||||
"nextStatement": "Input",
|
||||
"colour": 210,
|
||||
"tooltip": "For adding fields without any block connections that will " +
|
||||
"be rendered on a separate row from any following inputs. " +
|
||||
"Alignment options (left, right, centre) only affect " +
|
||||
"multi-row blocks.",
|
||||
"helpUrl": "https://developers.google.com/blockly/guides/create-custom-blocks/define-blocks#block_inputs"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
Blockly.Blocks['field_static'] = {
|
||||
// Text value.
|
||||
init: function() {
|
||||
|
||||
@@ -177,7 +177,8 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) {
|
||||
|
||||
var input = {type: contentsBlock.type};
|
||||
// Dummy inputs don't have names. Other inputs do.
|
||||
if (contentsBlock.type !== 'input_dummy') {
|
||||
if (contentsBlock.type !== 'input_dummy' &&
|
||||
contentsBlock.type !== 'input_end_row') {
|
||||
input.name = contentsBlock.getFieldValue('INPUTNAME');
|
||||
}
|
||||
var check = JSON.parse(
|
||||
@@ -202,7 +203,7 @@ FactoryUtils.formatJson_ = function(blockType, rootBlock) {
|
||||
if (fields && FactoryUtils.getFieldsJson_(fields).join('').trim() !== '') {
|
||||
var align = lastInput.getFieldValue('ALIGN');
|
||||
if (align !== 'LEFT') {
|
||||
JS.lastDummyAlign0 = align;
|
||||
JS.implicitAlign0 = align;
|
||||
}
|
||||
args.pop();
|
||||
message.pop();
|
||||
@@ -272,13 +273,15 @@ FactoryUtils.formatJavaScript_ = function(blockType, rootBlock, workspace) {
|
||||
// Generate inputs.
|
||||
var TYPES = {'input_value': 'appendValueInput',
|
||||
'input_statement': 'appendStatementInput',
|
||||
'input_dummy': 'appendDummyInput'};
|
||||
'input_dummy': 'appendDummyInput',
|
||||
'input_end_row': 'appendEndRowInput'};
|
||||
var contentsBlock = rootBlock.getInputTargetBlock('INPUTS');
|
||||
while (contentsBlock) {
|
||||
if (!contentsBlock.disabled && !contentsBlock.getInheritedDisabled()) {
|
||||
var name = '';
|
||||
// Dummy inputs don't have names. Other inputs do.
|
||||
if (contentsBlock.type !== 'input_dummy') {
|
||||
if (contentsBlock.type !== 'input_dummy' &&
|
||||
contentsBlock.type !== 'input_end_row') {
|
||||
name =
|
||||
JSON.stringify(contentsBlock.getFieldValue('INPUTNAME'));
|
||||
}
|
||||
|
||||
@@ -422,6 +422,7 @@
|
||||
</value>
|
||||
</block>
|
||||
<block type="input_dummy"></block>
|
||||
<block type="input_end_row"></block>
|
||||
</category>
|
||||
<category name="Field">
|
||||
<block type="field_static"></block>
|
||||
|
||||
@@ -92,6 +92,10 @@ suite('Block JSON initialization', function () {
|
||||
'Block "test": Message index %2 out of range.',
|
||||
);
|
||||
});
|
||||
|
||||
test('Newline tokens are valid', function () {
|
||||
this.assertNoError(['test', '\n', 'test'], 0);
|
||||
});
|
||||
});
|
||||
|
||||
suite('interpolateArguments_', function () {
|
||||
@@ -312,6 +316,70 @@ suite('Block JSON initialization', function () {
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('interpolation output includes end-row inputs', function () {
|
||||
this.assertInterpolation(
|
||||
['test1', {'type': 'input_end_row'}, 'test2'],
|
||||
[],
|
||||
undefined,
|
||||
[
|
||||
{
|
||||
'type': 'field_label',
|
||||
'text': 'test1',
|
||||
},
|
||||
{
|
||||
'type': 'input_end_row',
|
||||
},
|
||||
{
|
||||
'type': 'field_label',
|
||||
'text': 'test2',
|
||||
},
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('Newline is converted to end-row input', function () {
|
||||
this.assertInterpolation(['test1', '\n', 'test2'], [], undefined, [
|
||||
{
|
||||
'type': 'field_label',
|
||||
'text': 'test1',
|
||||
},
|
||||
{
|
||||
'type': 'input_end_row',
|
||||
},
|
||||
{
|
||||
'type': 'field_label',
|
||||
'text': 'test2',
|
||||
},
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test('Newline converted to end-row aligned like last dummy', function () {
|
||||
this.assertInterpolation(['test1', '\n', 'test2'], [], 'CENTER', [
|
||||
{
|
||||
'type': 'field_label',
|
||||
'text': 'test1',
|
||||
},
|
||||
{
|
||||
'type': 'input_end_row',
|
||||
'align': 'CENTER',
|
||||
},
|
||||
{
|
||||
'type': 'field_label',
|
||||
'text': 'test2',
|
||||
},
|
||||
{
|
||||
'type': 'input_dummy',
|
||||
'align': 'CENTER',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
suite('fieldFromJson_', function () {
|
||||
|
||||
@@ -10,6 +10,7 @@ import {ConnectionType} from '../../build/src/core/connection_type.js';
|
||||
import {createDeprecationWarningStub} from './test_helpers/warnings.js';
|
||||
import {createRenderedBlock} from './test_helpers/block_definitions.js';
|
||||
import * as eventUtils from '../../build/src/core/events/utils.js';
|
||||
import {EndRowInput} from '../../build/src/core/inputs/end_row_input.js';
|
||||
import {
|
||||
sharedTestSetup,
|
||||
sharedTestTeardown,
|
||||
@@ -2494,4 +2495,25 @@ suite('Blocks', function () {
|
||||
chai.assert.isTrue(initCalled, 'expected init function to be called');
|
||||
});
|
||||
});
|
||||
|
||||
suite('EndOfRow', function () {
|
||||
setup(function () {
|
||||
Blockly.defineBlocksWithJsonArray([
|
||||
{
|
||||
'type': 'end_row_test_block',
|
||||
'message0': 'Row1\nRow2',
|
||||
'inputsInline': true,
|
||||
},
|
||||
]);
|
||||
});
|
||||
test('Newline is converted to an end-row input', function () {
|
||||
const block = this.workspace.newBlock('end_row_test_block');
|
||||
chai.assert.equal(block.inputList[0].fieldRow[0].getValue(), 'Row1');
|
||||
chai.assert.isTrue(
|
||||
block.inputList[0] instanceof EndRowInput,
|
||||
'newline should be converted to an end-row input',
|
||||
);
|
||||
chai.assert.equal(block.inputList[1].fieldRow[0].getValue(), 'Row2');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,6 +58,13 @@ suite('Utils', function () {
|
||||
['Hello%World'],
|
||||
);
|
||||
});
|
||||
|
||||
test('Newlines are tokenized', function () {
|
||||
chai.assert.deepEqual(
|
||||
Blockly.utils.parsing.tokenizeInterpolation('Hello\nWorld'),
|
||||
['Hello', '\n', 'World'],
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
suite('Number interpolation', function () {
|
||||
@@ -231,6 +238,14 @@ suite('Utils', function () {
|
||||
'Unrecognized % escape code treated as literal',
|
||||
);
|
||||
|
||||
resultString =
|
||||
Blockly.utils.parsing.replaceMessageReferences('Hello\nWorld');
|
||||
chai.assert.equal(
|
||||
resultString,
|
||||
'Hello\nWorld',
|
||||
'Newlines are not tokenized',
|
||||
);
|
||||
|
||||
resultString = Blockly.utils.parsing.replaceMessageReferences('%1');
|
||||
chai.assert.equal(resultString, '%1', 'Interpolation tokens ignored.');
|
||||
resultString = Blockly.utils.parsing.replaceMessageReferences('%1 %2');
|
||||
|
||||
Reference in New Issue
Block a user