mirror of
https://github.com/google/blockly.git
synced 2026-01-07 09:00:11 +01:00
chore: Merge branch 'develop' into rc/v12.0.0
This commit is contained in:
3
.github/workflows/conventional-label.yml
vendored
3
.github/workflows/conventional-label.yml
vendored
@@ -7,6 +7,9 @@ name: conventional-release-labels
|
|||||||
jobs:
|
jobs:
|
||||||
label:
|
label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: bcoe/conventional-release-labels@v1
|
- uses: bcoe/conventional-release-labels@v1
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -521,6 +521,21 @@ export class BlockSvg
|
|||||||
this.updateCollapsed();
|
this.updateCollapsed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traverses child blocks to see if any of them have a warning.
|
||||||
|
*
|
||||||
|
* @returns true if any child has a warning, false otherwise.
|
||||||
|
*/
|
||||||
|
private childHasWarning(): boolean {
|
||||||
|
const children = this.getChildren(false);
|
||||||
|
for (const child of children) {
|
||||||
|
if (child.getIcon(WarningIcon.TYPE) || child.childHasWarning()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes sure that when the block is collapsed, it is rendered correctly
|
* Makes sure that when the block is collapsed, it is rendered correctly
|
||||||
* for that state.
|
* for that state.
|
||||||
@@ -544,10 +559,17 @@ export class BlockSvg
|
|||||||
this.updateDisabled();
|
this.updateDisabled();
|
||||||
this.removeInput(collapsedInputName);
|
this.removeInput(collapsedInputName);
|
||||||
dom.removeClass(this.svgGroup, 'blocklyCollapsed');
|
dom.removeClass(this.svgGroup, 'blocklyCollapsed');
|
||||||
|
this.setWarningText(null, BlockSvg.COLLAPSED_WARNING_ID);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
dom.addClass(this.svgGroup, 'blocklyCollapsed');
|
dom.addClass(this.svgGroup, 'blocklyCollapsed');
|
||||||
|
if (this.childHasWarning()) {
|
||||||
|
this.setWarningText(
|
||||||
|
Msg['COLLAPSED_WARNINGS_WARNING'],
|
||||||
|
BlockSvg.COLLAPSED_WARNING_ID,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const text = this.toString(internalConstants.COLLAPSE_CHARS);
|
const text = this.toString(internalConstants.COLLAPSE_CHARS);
|
||||||
const field = this.getField(collapsedFieldName);
|
const field = this.getField(collapsedFieldName);
|
||||||
|
|||||||
@@ -129,26 +129,11 @@ export class FieldDropdown extends Field<string> {
|
|||||||
// If we pass SKIP_SETUP, don't do *anything* with the menu generator.
|
// If we pass SKIP_SETUP, don't do *anything* with the menu generator.
|
||||||
if (menuGenerator === Field.SKIP_SETUP) return;
|
if (menuGenerator === Field.SKIP_SETUP) return;
|
||||||
|
|
||||||
if (Array.isArray(menuGenerator)) {
|
this.setOptions(menuGenerator);
|
||||||
this.validateOptions(menuGenerator);
|
|
||||||
const trimmed = this.trimOptions(menuGenerator);
|
|
||||||
this.menuGenerator_ = trimmed.options;
|
|
||||||
this.prefixField = trimmed.prefix || null;
|
|
||||||
this.suffixField = trimmed.suffix || null;
|
|
||||||
} else {
|
|
||||||
this.menuGenerator_ = menuGenerator;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The currently selected option. The field is initialized with the
|
|
||||||
* first option selected.
|
|
||||||
*/
|
|
||||||
this.selectedOption = this.getOptions(false)[0];
|
|
||||||
|
|
||||||
if (config) {
|
if (config) {
|
||||||
this.configure_(config);
|
this.configure_(config);
|
||||||
}
|
}
|
||||||
this.setValue(this.selectedOption[1]);
|
|
||||||
if (validator) {
|
if (validator) {
|
||||||
this.setValidator(validator);
|
this.setValidator(validator);
|
||||||
}
|
}
|
||||||
@@ -417,6 +402,28 @@ export class FieldDropdown extends Field<string> {
|
|||||||
return this.generatedOptions;
|
return this.generatedOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the options on this dropdown. This will reset the selected item to
|
||||||
|
* the first item in the list.
|
||||||
|
*
|
||||||
|
* @param menuGenerator The array of options or a generator function.
|
||||||
|
*/
|
||||||
|
setOptions(menuGenerator: MenuGenerator) {
|
||||||
|
if (Array.isArray(menuGenerator)) {
|
||||||
|
this.validateOptions(menuGenerator);
|
||||||
|
const trimmed = this.trimOptions(menuGenerator);
|
||||||
|
this.menuGenerator_ = trimmed.options;
|
||||||
|
this.prefixField = trimmed.prefix || null;
|
||||||
|
this.suffixField = trimmed.suffix || null;
|
||||||
|
} else {
|
||||||
|
this.menuGenerator_ = menuGenerator;
|
||||||
|
}
|
||||||
|
// The currently selected option. The field is initialized with the
|
||||||
|
// first option selected.
|
||||||
|
this.selectedOption = this.getOptions(false)[0];
|
||||||
|
this.setValue(this.selectedOption[1]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the input value is a valid language-neutral option.
|
* Ensure that the input value is a valid language-neutral option.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
// Unused import preserved for side-effects. Remove if unneeded.
|
// Unused import preserved for side-effects. Remove if unneeded.
|
||||||
import {BlockSvg} from '../block_svg.js';
|
import {BlockSvg} from '../block_svg.js';
|
||||||
import type {BlocklyOptions} from '../blockly_options.js';
|
|
||||||
import * as browserEvents from '../browser_events.js';
|
import * as browserEvents from '../browser_events.js';
|
||||||
import * as common from '../common.js';
|
import * as common from '../common.js';
|
||||||
import {ComponentManager} from '../component_manager.js';
|
import {ComponentManager} from '../component_manager.js';
|
||||||
@@ -36,7 +35,6 @@ import {isSelectableToolboxItem} from '../interfaces/i_selectable_toolbox_item.j
|
|||||||
import type {IStyleable} from '../interfaces/i_styleable.js';
|
import type {IStyleable} from '../interfaces/i_styleable.js';
|
||||||
import type {IToolbox} from '../interfaces/i_toolbox.js';
|
import type {IToolbox} from '../interfaces/i_toolbox.js';
|
||||||
import type {IToolboxItem} from '../interfaces/i_toolbox_item.js';
|
import type {IToolboxItem} from '../interfaces/i_toolbox_item.js';
|
||||||
import {Options} from '../options.js';
|
|
||||||
import * as registry from '../registry.js';
|
import * as registry from '../registry.js';
|
||||||
import type {KeyboardShortcut} from '../shortcut_registry.js';
|
import type {KeyboardShortcut} from '../shortcut_registry.js';
|
||||||
import * as Touch from '../touch.js';
|
import * as Touch from '../touch.js';
|
||||||
@@ -333,18 +331,7 @@ export class Toolbox
|
|||||||
*/
|
*/
|
||||||
protected createFlyout_(): IFlyout {
|
protected createFlyout_(): IFlyout {
|
||||||
const workspace = this.workspace_;
|
const workspace = this.workspace_;
|
||||||
// TODO (#4247): Look into adding a makeFlyout method to Blockly Options.
|
const workspaceOptions = workspace.copyOptionsForFlyout();
|
||||||
const workspaceOptions = new Options({
|
|
||||||
'parentWorkspace': workspace,
|
|
||||||
'rtl': workspace.RTL,
|
|
||||||
'oneBasedIndex': workspace.options.oneBasedIndex,
|
|
||||||
'horizontalLayout': workspace.horizontalLayout,
|
|
||||||
'renderer': workspace.options.renderer,
|
|
||||||
'rendererOverrides': workspace.options.rendererOverrides,
|
|
||||||
'move': {
|
|
||||||
'scrollbars': true,
|
|
||||||
},
|
|
||||||
} as BlocklyOptions);
|
|
||||||
// Options takes in either 'end' or 'start'. This has already been parsed to
|
// Options takes in either 'end' or 'start'. This has already been parsed to
|
||||||
// be either 0 or 1, so set it after.
|
// be either 0 or 1, so set it after.
|
||||||
workspaceOptions.toolboxPosition = workspace.options.toolboxPosition;
|
workspaceOptions.toolboxPosition = workspace.options.toolboxPosition;
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
// Former goog.module ID: Blockly.Trashcan
|
// Former goog.module ID: Blockly.Trashcan
|
||||||
|
|
||||||
// Unused import preserved for side-effects. Remove if unneeded.
|
// Unused import preserved for side-effects. Remove if unneeded.
|
||||||
import type {BlocklyOptions} from './blockly_options.js';
|
|
||||||
import * as browserEvents from './browser_events.js';
|
import * as browserEvents from './browser_events.js';
|
||||||
import {ComponentManager} from './component_manager.js';
|
import {ComponentManager} from './component_manager.js';
|
||||||
import {DeleteArea} from './delete_area.js';
|
import {DeleteArea} from './delete_area.js';
|
||||||
@@ -26,7 +25,6 @@ import type {IDraggable} from './interfaces/i_draggable.js';
|
|||||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||||
import type {IPositionable} from './interfaces/i_positionable.js';
|
import type {IPositionable} from './interfaces/i_positionable.js';
|
||||||
import type {UiMetrics} from './metrics_manager.js';
|
import type {UiMetrics} from './metrics_manager.js';
|
||||||
import {Options} from './options.js';
|
|
||||||
import * as uiPosition from './positionable_helpers.js';
|
import * as uiPosition from './positionable_helpers.js';
|
||||||
import * as registry from './registry.js';
|
import * as registry from './registry.js';
|
||||||
import type * as blocks from './serialization/blocks.js';
|
import type * as blocks from './serialization/blocks.js';
|
||||||
@@ -103,17 +101,7 @@ export class Trashcan
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create flyout options.
|
// Create flyout options.
|
||||||
const flyoutWorkspaceOptions = new Options({
|
const flyoutWorkspaceOptions = this.workspace.copyOptionsForFlyout();
|
||||||
'scrollbars': true,
|
|
||||||
'parentWorkspace': this.workspace,
|
|
||||||
'rtl': this.workspace.RTL,
|
|
||||||
'oneBasedIndex': this.workspace.options.oneBasedIndex,
|
|
||||||
'renderer': this.workspace.options.renderer,
|
|
||||||
'rendererOverrides': this.workspace.options.rendererOverrides,
|
|
||||||
'move': {
|
|
||||||
'scrollbars': true,
|
|
||||||
},
|
|
||||||
} as BlocklyOptions);
|
|
||||||
// Create vertical or horizontal flyout.
|
// Create vertical or horizontal flyout.
|
||||||
if (this.workspace.horizontalLayout) {
|
if (this.workspace.horizontalLayout) {
|
||||||
flyoutWorkspaceOptions.toolboxPosition =
|
flyoutWorkspaceOptions.toolboxPosition =
|
||||||
|
|||||||
@@ -985,6 +985,28 @@ export class WorkspaceSvg
|
|||||||
this.svgGroup_.appendChild(svgZoomControls);
|
this.svgGroup_.appendChild(svgZoomControls);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new set of options from this workspace's options with just the
|
||||||
|
* values that are relevant to a flyout.
|
||||||
|
*
|
||||||
|
* @returns A subset of this workspace's options.
|
||||||
|
*/
|
||||||
|
copyOptionsForFlyout(): Options {
|
||||||
|
return new Options({
|
||||||
|
'parentWorkspace': this,
|
||||||
|
'rtl': this.RTL,
|
||||||
|
'oneBasedIndex': this.options.oneBasedIndex,
|
||||||
|
'horizontalLayout': this.horizontalLayout,
|
||||||
|
'renderer': this.options.renderer,
|
||||||
|
'rendererOverrides': this.options.rendererOverrides,
|
||||||
|
'plugins': this.options.plugins,
|
||||||
|
'modalInputs': this.options.modalInputs,
|
||||||
|
'move': {
|
||||||
|
'scrollbars': true,
|
||||||
|
},
|
||||||
|
} as BlocklyOptions);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a flyout element in an element with the given tag name.
|
* Add a flyout element in an element with the given tag name.
|
||||||
*
|
*
|
||||||
@@ -993,17 +1015,7 @@ export class WorkspaceSvg
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
addFlyout(tagName: string | Svg<SVGSVGElement> | Svg<SVGGElement>): Element {
|
addFlyout(tagName: string | Svg<SVGSVGElement> | Svg<SVGGElement>): Element {
|
||||||
const workspaceOptions = new Options({
|
const workspaceOptions = this.copyOptionsForFlyout();
|
||||||
'parentWorkspace': this,
|
|
||||||
'rtl': this.RTL,
|
|
||||||
'oneBasedIndex': this.options.oneBasedIndex,
|
|
||||||
'horizontalLayout': this.horizontalLayout,
|
|
||||||
'renderer': this.options.renderer,
|
|
||||||
'rendererOverrides': this.options.rendererOverrides,
|
|
||||||
'move': {
|
|
||||||
'scrollbars': true,
|
|
||||||
},
|
|
||||||
} as BlocklyOptions);
|
|
||||||
workspaceOptions.toolboxPosition = this.options.toolboxPosition;
|
workspaceOptions.toolboxPosition = this.options.toolboxPosition;
|
||||||
if (this.horizontalLayout) {
|
if (this.horizontalLayout) {
|
||||||
const HorizontalFlyout = registry.getClassFromOptions(
|
const HorizontalFlyout = registry.getClassFromOptions(
|
||||||
|
|||||||
931
package-lock.json
generated
931
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -112,11 +112,11 @@
|
|||||||
"concurrently": "^9.0.1",
|
"concurrently": "^9.0.1",
|
||||||
"eslint": "^9.15.0",
|
"eslint": "^9.15.0",
|
||||||
"eslint-config-google": "^0.14.0",
|
"eslint-config-google": "^0.14.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^10.1.1",
|
||||||
"eslint-plugin-jsdoc": "^50.5.0",
|
"eslint-plugin-jsdoc": "^50.5.0",
|
||||||
"eslint-plugin-prettier": "^5.2.1",
|
"eslint-plugin-prettier": "^5.2.1",
|
||||||
"glob": "^11.0.1",
|
"glob": "^11.0.1",
|
||||||
"globals": "^15.12.0",
|
"globals": "^16.0.0",
|
||||||
"google-closure-compiler": "^20240317.0.0",
|
"google-closure-compiler": "^20240317.0.0",
|
||||||
"gulp": "^5.0.0",
|
"gulp": "^5.0.0",
|
||||||
"gulp-concat": "^2.6.1",
|
"gulp-concat": "^2.6.1",
|
||||||
@@ -144,7 +144,7 @@
|
|||||||
"yargs": "^17.2.1"
|
"yargs": "^17.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"jsdom": "25.0.1"
|
"jsdom": "26.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
|||||||
@@ -1881,6 +1881,62 @@ suite('Blocks', function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
suite('Warning icons and collapsing', function () {
|
||||||
|
setup(function () {
|
||||||
|
this.workspace = Blockly.inject('blocklyDiv');
|
||||||
|
this.parentBlock = Blockly.serialization.blocks.append(
|
||||||
|
{
|
||||||
|
'type': 'statement_block',
|
||||||
|
'inputs': {
|
||||||
|
'STATEMENT': {
|
||||||
|
'block': {
|
||||||
|
'type': 'statement_block',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
this.workspace,
|
||||||
|
);
|
||||||
|
this.parentBlock.initSvg();
|
||||||
|
this.parentBlock.render();
|
||||||
|
|
||||||
|
this.childBlock = this.parentBlock.getInputTargetBlock('STATEMENT');
|
||||||
|
this.childBlock.initSvg();
|
||||||
|
this.childBlock.render();
|
||||||
|
});
|
||||||
|
|
||||||
|
teardown(function () {
|
||||||
|
workspaceTeardown.call(this, this.workspace);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Adding a warning to a child block does not affect the parent', function () {
|
||||||
|
const text = 'Warning Text';
|
||||||
|
this.childBlock.setWarningText(text);
|
||||||
|
const icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE);
|
||||||
|
assert.isUndefined(
|
||||||
|
icon,
|
||||||
|
"Setting a child block's warning should not add a warning to the parent",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Warnings are added and removed when collapsing a stack with warnings', function () {
|
||||||
|
const text = 'Warning Text';
|
||||||
|
|
||||||
|
this.childBlock.setWarningText(text);
|
||||||
|
|
||||||
|
this.parentBlock.setCollapsed(true);
|
||||||
|
let icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE);
|
||||||
|
assert.exists(icon?.getText(), 'Expected warning icon text to be set');
|
||||||
|
|
||||||
|
this.parentBlock.setCollapsed(false);
|
||||||
|
icon = this.parentBlock.getIcon(Blockly.icons.WarningIcon.TYPE);
|
||||||
|
assert.isUndefined(
|
||||||
|
icon,
|
||||||
|
'Warning should be removed from parent after expanding',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
suite('Bubbles and collapsing', function () {
|
suite('Bubbles and collapsing', function () {
|
||||||
setup(function () {
|
setup(function () {
|
||||||
this.workspace = Blockly.inject('blocklyDiv');
|
this.workspace = Blockly.inject('blocklyDiv');
|
||||||
|
|||||||
@@ -195,6 +195,52 @@ suite('Dropdown Fields', function () {
|
|||||||
assertFieldValue(this.field, 'B', 'b');
|
assertFieldValue(this.field, 'B', 'b');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
suite('setOptions', function () {
|
||||||
|
setup(function () {
|
||||||
|
this.field = new Blockly.FieldDropdown([
|
||||||
|
['a', 'A'],
|
||||||
|
['b', 'B'],
|
||||||
|
['c', 'C'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
test('With array updates options', function () {
|
||||||
|
this.field.setOptions([
|
||||||
|
['d', 'D'],
|
||||||
|
['e', 'E'],
|
||||||
|
['f', 'F'],
|
||||||
|
]);
|
||||||
|
assertFieldValue(this.field, 'D', 'd');
|
||||||
|
});
|
||||||
|
test('With generator updates options', function () {
|
||||||
|
this.field.setOptions(function () {
|
||||||
|
return [
|
||||||
|
['d', 'D'],
|
||||||
|
['e', 'E'],
|
||||||
|
['f', 'F'],
|
||||||
|
];
|
||||||
|
});
|
||||||
|
assertFieldValue(this.field, 'D', 'd');
|
||||||
|
});
|
||||||
|
test('With trimmable options gets trimmed', function () {
|
||||||
|
this.field.setOptions([
|
||||||
|
['a d b', 'D'],
|
||||||
|
['a e b', 'E'],
|
||||||
|
['a f b', 'F'],
|
||||||
|
]);
|
||||||
|
assert.deepEqual(this.field.prefixField, 'a');
|
||||||
|
assert.deepEqual(this.field.suffixField, 'b');
|
||||||
|
assert.deepEqual(this.field.getOptions(), [
|
||||||
|
['d', 'D'],
|
||||||
|
['e', 'E'],
|
||||||
|
['f', 'F'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
test('With an empty array of options throws', function () {
|
||||||
|
assert.throws(function () {
|
||||||
|
this.field.setOptions([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
suite('Validators', function () {
|
suite('Validators', function () {
|
||||||
setup(function () {
|
setup(function () {
|
||||||
|
|||||||
Reference in New Issue
Block a user