Merge branch 'main' into monorepo13

This commit is contained in:
Aaron Dodson
2026-01-20 15:48:27 -08:00
1014 changed files with 14724 additions and 5256 deletions
+1
View File
@@ -20,6 +20,7 @@ jobs:
- name: Prepare demo files
# Install all dependencies, then copy all the files needed for demos.
run: |
cd packages/blockly
npm install
npm run prepareDemos
+4
View File
@@ -25,6 +25,10 @@ jobs:
# See supported Node.js release schedule at
# https://nodejs.org/en/about/releases/
defaults:
run:
working-directory: ./packages/blockly
steps:
- uses: actions/checkout@v5
with:
+1
View File
@@ -43,6 +43,7 @@ jobs:
- name: Linux Test Setup
if: runner.os == 'Linux'
run: source ./tests/scripts/setup_linux_env.sh
working-directory: ./packages/blockly
- name: Run
run: npm run test
+3 -3
View File
@@ -21,7 +21,7 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
os: [ubuntu-latest]
steps:
- name: Checkout core Blockly
@@ -52,11 +52,11 @@ jobs:
- name: Link latest core main with plugin
run: |
cd core-blockly
cd core-blockly/packages/blockly
npm run package
cd dist
npm link
cd ../../blockly-keyboard-experimentation
cd ../../../../blockly-keyboard-experimentation
npm link blockly
cd ..
-37
View File
@@ -1,37 +0,0 @@
# For new pull requests against the goog_module branch, adds the 'type: cleanup'
# label and sets the milestone to q3 2021 release.
name: Tag module cleanup
# Trigger on pull requests against goog_module branch only
# Uses pull_request_target to get write permissions so that it can write labels.
on:
pull_request_target:
branches:
- goog_module
jobs:
tag-module-cleanup:
# Add the type: cleanup label
runs-on: ubuntu-latest
steps:
- uses: actions/github-script@v8
with:
script: |
// Note that pull requests are considered issues and "shared"
// actions for both features, like manipulating labels and
// milestones are provided within the issues API.
await github.issues.update({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
// 2021 q3 release milestone.
// https://github.com/google/blockly/milestone/18
milestone: 18
})
await github.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
labels: ['type: cleanup']
})
-11
View File
@@ -9,14 +9,3 @@ build-debug.log
*.komodoproject
/nbproject/private/
tsdoc-metadata.json
tests/compile/main_compressed.js
tests/compile/main_compressed.js.map
tests/compile/*compiler*.jar
tests/screenshot/outputs/*
local_build/*compiler*.jar
local_build/local_*_compressed.js
chromedriver
build/
dist/
temp/
-171
View File
@@ -1,171 +0,0 @@
/**
* @license
* Copyright 2017 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* Object in charge of loading, storing, and playing audio for a
* workspace.
*
* @class
*/
// Former goog.module ID: Blockly.WorkspaceAudio
import * as userAgent from './utils/useragent.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Prevent a sound from playing if another sound preceded it within this many
* milliseconds.
*/
const SOUND_LIMIT = 100;
/**
* Class for loading, storing, and playing audio for a workspace.
*/
export class WorkspaceAudio {
/** Database of pre-loaded sounds. */
private sounds = new Map<string, HTMLAudioElement>();
/** Time that the last sound was played. */
private lastSound: Date | null = null;
/** Whether the audio is muted or not. */
private muted: boolean = false;
/**
* @param parentWorkspace The parent of the workspace this audio object
* belongs to, or null.
*/
constructor(private parentWorkspace: WorkspaceSvg) {}
/**
* Dispose of this audio manager.
*
* @internal
*/
dispose() {
this.sounds.clear();
}
/**
* Load an audio file. Cache it, ready for instantaneous playing.
*
* @param filenames List of file types in decreasing order of preference (i.e.
* increasing size). E.g. ['media/go.mp3', 'media/go.wav'] Filenames
* include path from Blockly's root. File extensions matter.
* @param name Name of sound.
*/
load(filenames: string[], name: string) {
if (!filenames.length) {
return;
}
let audioTest;
try {
audioTest = new globalThis['Audio']();
} catch {
// No browser support for Audio.
// IE can throw an error even if the Audio object exists.
return;
}
let sound;
for (let i = 0; i < filenames.length; i++) {
const filename = filenames[i];
const ext = filename.match(/\.(\w+)$/);
if (ext && audioTest.canPlayType('audio/' + ext[1])) {
// Found an audio format we can play.
sound = new globalThis['Audio'](filename);
break;
}
}
if (sound) {
this.sounds.set(name, sound);
}
}
/**
* Preload all the audio files so that they play quickly when asked for.
*
* @internal
*/
preload() {
for (const sound of this.sounds.values()) {
sound.volume = 0.01;
const playPromise = sound.play();
// Edge does not return a promise, so we need to check.
if (playPromise !== undefined) {
// If we don't wait for the play request to complete before calling
// pause() we will get an exception: (DOMException: The play() request
// was interrupted) See more:
// https://developers.google.com/web/updates/2017/06/play-request-was-interrupted
playPromise.then(sound.pause).catch(
// Play without user interaction was prevented.
function () {},
);
} else {
sound.pause();
}
// iOS can only process one sound at a time. Trying to load more than one
// corrupts the earlier ones. Just load one and leave the others
// uncached.
if (userAgent.IPAD || userAgent.IPHONE) {
break;
}
}
}
/**
* Play a named sound at specified volume. If volume is not specified,
* use full volume (1).
*
* @param name Name of sound.
* @param opt_volume Volume of sound (0-1).
*/
play(name: string, opt_volume?: number) {
if (this.muted) {
return;
}
const sound = this.sounds.get(name);
if (sound) {
// Don't play one sound on top of another.
const now = new Date();
if (
this.lastSound !== null &&
now.getTime() - this.lastSound.getTime() < SOUND_LIMIT
) {
return;
}
this.lastSound = now;
let mySound;
if (userAgent.IPAD || userAgent.ANDROID) {
// Creating a new audio node causes lag in Android and iPad. Android
// refetches the file from the server, iPad uses a singleton audio
// node which must be deleted and recreated for each new audio tag.
mySound = sound;
} else {
mySound = sound.cloneNode() as HTMLAudioElement;
}
mySound.volume = opt_volume === undefined ? 1 : opt_volume;
mySound.play();
} else if (this.parentWorkspace) {
// Maybe a workspace on a lower level knows about this sound.
this.parentWorkspace.getAudioManager().play(name, opt_volume);
}
}
/**
* @param muted If true, mute sounds. Otherwise, play them.
*/
setMuted(muted: boolean) {
this.muted = muted;
}
/**
* @returns Whether the audio is currently muted or not.
*/
getMuted(): boolean {
return this.muted;
}
}
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2086 -3275
View File
File diff suppressed because it is too large Load Diff
+19 -149
View File
@@ -1,158 +1,28 @@
{
"name": "blockly",
"version": "12.3.1",
"description": "Blockly is a library for building visual programming editors.",
"name": "blockly-repo",
"version": "0.0.0",
"description": "Monorepo for blockly and related packages",
"keywords": [
"blockly"
],
"homepage": "https://blockly.com",
"bugs": {
"url": "https://github.com/RaspberryPiFoundation/blockly/issues"
},
"repository": {
"type": "git",
"url": "git+https://github.com/google/blockly.git"
},
"bugs": {
"url": "https://github.com/google/blockly/issues"
},
"homepage": "https://developers.google.com/blockly/",
"author": {
"name": "Neil Fraser"
},
"scripts": {
"build": "gulp build",
"build-debug": "gulp build --verbose --debug",
"build-debug-log": "npm run build:debug > build-debug.log 2>&1 && tail -3 build-debug.log",
"build-strict": "gulp build --verbose --strict",
"build-strict-log": "npm run build:strict > build-debug.log 2>&1 && tail -3 build-debug.log",
"clean": "gulp clean",
"deployDemos": "npm ci && gulp deployDemos",
"deployDemos:beta": "npm ci && gulp deployDemosBeta",
"docs": "gulp docs",
"format": "prettier --write .",
"format:check": "prettier --check .",
"messages": "gulp messages",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"langfiles": "gulp langfiles",
"minify": "gulp minify",
"package": "gulp pack",
"postinstall": "patch-package",
"prepareDemos": "gulp prepareDemos",
"publish": "npm ci && gulp publish",
"publish:beta": "npm ci && gulp publishBeta",
"recompile": "gulp recompile",
"release": "gulp gitCreateRC",
"start": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"http-server ./ -s -o /tests/playground.html -c-1\"",
"tsc": "gulp tsc",
"test": "gulp test",
"test:browser": "cd tests/browser && npx mocha",
"test:generators": "gulp testGenerators",
"test:mocha:interactive": "npm run build && concurrently -n tsc,server \"tsc --watch --preserveWatchOutput --outDir \"build/src\" --declarationDir \"build/declarations\"\" \"gulp interactiveMocha\"",
"test:compile:advanced": "gulp buildAdvancedCompilationTest --debug",
"updateGithubPages": "npm ci && gulp gitUpdateGithubPages"
},
"exports": {
".": {
"types": "./index.d.ts",
"import": "./index.mjs",
"umd": "./blockly.min.js",
"default": "./index.js"
},
"./core": {
"types": "./core.d.ts",
"node": "./core-node.js",
"import": "./blockly.mjs",
"default": "./blockly_compressed.js"
},
"./blocks": {
"types": "./blocks.d.ts",
"import": "./blocks.mjs",
"default": "./blocks_compressed.js"
},
"./dart": {
"types": "./dart.d.ts",
"import": "./dart.mjs",
"default": "./dart_compressed.js"
},
"./lua": {
"types": "./lua.d.ts",
"import": "./lua.mjs",
"default": "./lua_compressed.js"
},
"./javascript": {
"types": "./javascript.d.ts",
"import": "./javascript.mjs",
"default": "./javascript_compressed.js"
},
"./php": {
"types": "./php.d.ts",
"import": "./php.mjs",
"default": "./php_compressed.js"
},
"./python": {
"types": "./python.d.ts",
"import": "./python.mjs",
"default": "./python_compressed.js"
},
"./msg/*": {
"types": "./msg/*.d.ts",
"import": "./msg/*.mjs",
"default": "./msg/*.js"
}
"url": "git+https://github.com/RaspberryPiFoundation/blockly.git"
},
"license": "Apache-2.0",
"devDependencies": {
"@blockly/block-test": "^7.0.2",
"@blockly/dev-tools": "^9.0.2",
"@blockly/keyboard-navigation": "^3.0.1",
"@blockly/theme-modern": "^7.0.1",
"@commitlint/cli": "^20.1.0",
"@commitlint/config-conventional": "^20.0.0",
"@hyperjump/browser": "^1.1.4",
"@hyperjump/json-schema": "^1.5.0",
"@microsoft/api-documenter": "7.22.4",
"@microsoft/api-extractor": "^7.29.5",
"ajv": "^8.17.1",
"async-done": "^2.0.0",
"chai": "^6.0.1",
"concurrently": "^9.0.1",
"eslint": "^9.15.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^10.1.1",
"eslint-plugin-jsdoc": "^52.0.2",
"eslint-plugin-mocha": "^11.1.0",
"eslint-plugin-prettier": "^5.2.1",
"glob": "^11.0.1",
"globals": "^16.0.0",
"google-closure-compiler": "^20251015.0.0",
"gulp": "^5.0.0",
"gulp-concat": "^2.6.1",
"gulp-gzip": "^1.4.2",
"gulp-header": "^2.0.9",
"gulp-insert": "^0.5.0",
"gulp-rename": "^2.0.0",
"gulp-replace": "^1.0.0",
"gulp-series": "^1.0.2",
"gulp-shell": "^0.8.0",
"gulp-sourcemaps": "^3.0.0",
"gulp-umd": "^2.0.0",
"http-server": "^14.0.0",
"json5": "^2.2.0",
"markdown-tables-to-json": "^0.1.7",
"mocha": "^11.3.0",
"patch-package": "^8.0.0",
"prettier": "^3.3.3",
"prettier-plugin-organize-imports": "^4.0.0",
"puppeteer-core": "^24.17.0",
"readline-sync": "^1.4.10",
"rimraf": "^6.1.2",
"typescript": "^5.3.3",
"typescript-eslint": "^8.16.0",
"webdriverio": "^9.0.7",
"yargs": "^17.2.1"
},
"dependencies": {
"jsdom": "26.1.0"
},
"engines": {
"node": ">=18"
"author": "Raspberry Pi Foundation",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"test": "npm run test --ws --if-present",
"lint": "npm run lint --ws --if-present",
"build": "npm run build --ws --if-present",
"format:check": "npm run format:check --ws --if-present"
}
}
}
+10
View File
@@ -0,0 +1,10 @@
tests/compile/main_compressed.js
tests/compile/main_compressed.js.map
tests/compile/*compiler*.jar
tests/screenshot/outputs/*
local_build/*compiler*.jar
local_build/local_*_compressed.js
chromedriver
build/
dist/
temp/

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Before

Width:  |  Height:  |  Size: 5.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

@@ -309,7 +309,9 @@ const PROCEDURE_DEF_COMMON = {
while (paramBlock && !paramBlock.isInsertionMarker()) {
const varName = paramBlock.getFieldValue('NAME');
this.arguments_.push(varName);
const variable = this.workspace.getVariable(varName, '')!;
const variable = this.workspace
.getVariableMap()
.getVariable(varName, '')!;
this.argumentVarModels_.push(variable);
this.paramIds_.push(paramBlock.id);
@@ -383,13 +385,13 @@ const PROCEDURE_DEF_COMMON = {
oldId: string,
newId: string,
) {
const oldVariable = this.workspace.getVariableById(oldId)!;
const oldVariable = this.workspace.getVariableMap().getVariableById(oldId)!;
if (oldVariable.getType() !== '') {
// Procedure arguments always have the empty type.
return;
}
const oldName = oldVariable.getName();
const newVar = this.workspace.getVariableById(newId)!;
const newVar = this.workspace.getVariableMap().getVariableById(newId)!;
let change = false;
for (let i = 0; i < this.argumentVarModels_.length; i++) {
@@ -471,7 +473,7 @@ const PROCEDURE_DEF_COMMON = {
// Add option to create caller.
const name = this.getFieldValue('NAME');
const callProcedureBlockState = {
type: (this as AnyDuringMigration).callType_,
type: this.callType_,
extraState: {name: name, params: this.arguments_},
};
options.push({
@@ -1168,9 +1168,9 @@ export class Block {
const vars = [];
for (const field of this.getFields()) {
if (field.referencesVariables()) {
const model = this.workspace.getVariableById(
field.getValue() as string,
);
const model = this.workspace
.getVariableMap()
.getVariableById(field.getValue() as string);
// Check if the variable actually exists (and isn't just a potential
// variable).
if (model) {
@@ -539,12 +539,22 @@ export class BlockSvg
* @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()) {
const next = this.getNextBlock();
const excluded = next ? new Set(next.getDescendants(false)) : null;
const descendants = this.getDescendants(false);
for (const descendant of descendants) {
if (descendant === this) {
continue;
}
if (excluded?.has(descendant)) {
continue;
}
if (descendant.getIcon(WarningIcon.TYPE)) {
return true;
}
}
return false;
}
@@ -46,6 +46,9 @@ const PAGE_MODE_MULTIPLIER = 125;
* @param opt_noCaptureIdentifier True if triggering on this event should not
* block execution of other event handlers on this touch or other
* simultaneous touches. False by default.
* @param options An object with options controlling the behavior of the event
* listener. Passed through directly as the third argument to
* `addEventListener`.
* @returns Opaque data that can be passed to unbindEvent_.
*/
export function conditionalBind(
@@ -54,6 +57,7 @@ export function conditionalBind(
thisObject: object | null,
func: Function,
opt_noCaptureIdentifier?: boolean,
options?: AddEventListenerOptions,
): Data {
/**
*
@@ -75,11 +79,11 @@ export function conditionalBind(
if (name in Touch.TOUCH_MAP) {
for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {
const type = Touch.TOUCH_MAP[name][i];
node.addEventListener(type, wrapFunc, false);
node.addEventListener(type, wrapFunc, {capture: false, ...options});
bindData.push([node, type, wrapFunc]);
}
} else {
node.addEventListener(name, wrapFunc, false);
node.addEventListener(name, wrapFunc, {capture: false, ...options});
bindData.push([node, name, wrapFunc]);
}
return bindData;
@@ -95,6 +99,9 @@ export function conditionalBind(
* @param name Event name to listen to (e.g. 'mousedown').
* @param thisObject The value of 'this' in the function.
* @param func Function to call when event is triggered.
* @param options An object with options controlling the behavior of the event
* listener. Passed through directly as the third argument to
* `addEventListener`.
* @returns Opaque data that can be passed to unbindEvent_.
*/
export function bind(
@@ -102,6 +109,7 @@ export function bind(
name: string,
thisObject: object | null,
func: Function,
options?: AddEventListenerOptions,
): Data {
/**
*
@@ -119,11 +127,11 @@ export function bind(
if (name in Touch.TOUCH_MAP) {
for (let i = 0; i < Touch.TOUCH_MAP[name].length; i++) {
const type = Touch.TOUCH_MAP[name][i];
node.addEventListener(type, wrapFunc, false);
node.addEventListener(type, wrapFunc, {capture: false, ...options});
bindData.push([node, type, wrapFunc]);
}
} else {
node.addEventListener(name, wrapFunc, false);
node.addEventListener(name, wrapFunc, {capture: false, ...options});
bindData.push([node, name, wrapFunc]);
}
return bindData;
@@ -95,9 +95,16 @@ export class CommentEditor implements IFocusableNode {
);
// Don't zoom with mousewheel; let it scroll instead.
browserEvents.conditionalBind(this.textArea, 'wheel', this, (e: Event) => {
e.stopPropagation();
});
browserEvents.conditionalBind(
this.textArea,
'wheel',
this,
(e: Event) => {
e.stopPropagation();
},
false,
{passive: true},
);
// Register listener for keydown events that would finish editing.
browserEvents.conditionalBind(
@@ -23,7 +23,6 @@ import {IFocusableNode} from '../interfaces/i_focusable_node.js';
import type {IFocusableTree} from '../interfaces/i_focusable_tree.js';
import {IRenderedElement} from '../interfaces/i_rendered_element.js';
import {ISelectable} from '../interfaces/i_selectable.js';
import * as layers from '../layers.js';
import * as commentSerialization from '../serialization/workspace_comments.js';
import {Coordinate} from '../utils/coordinate.js';
import * as dom from '../utils/dom.js';
@@ -346,7 +345,7 @@ export class RenderedWorkspaceComment
onNodeFocus(): void {
this.select();
// Ensure that the comment is always at the top when focused.
this.workspace.getLayerManager()?.append(this, layers.BLOCK);
this.getSvgRoot().parentElement?.appendChild(this.getSvgRoot());
this.workspace.scrollBoundsIntoView(this.getBoundingRectangle());
}
@@ -291,7 +291,10 @@ export class Connection {
}
let event;
if (eventUtils.isEnabled()) {
if (
eventUtils.isEnabled() &&
!childConnection.getSourceBlock().isDeadOrDying()
) {
event = new (eventUtils.get(EventType.BLOCK_MOVE))(
childConnection.getSourceBlock(),
) as BlockMove;
@@ -104,15 +104,15 @@ export class ContextMenuRegistry {
weight: item.weight,
};
const precondition = item.preconditionFn?.(scope, menuOpenEvent);
if (precondition === 'hidden') continue;
if (item.separator) {
menuOption = {
...menuOption,
separator: true,
};
} else {
const precondition = item.preconditionFn(scope, menuOpenEvent);
if (precondition === 'hidden') continue;
const displayText =
typeof item.displayText === 'function'
? item.displayText(scope)
@@ -165,6 +165,7 @@ export namespace ContextMenuRegistry {
scopeType?: ScopeType;
weight: number;
id: string;
preconditionFn?: (p1: Scope, menuOpenEvent: Event) => string;
}
/**
@@ -185,8 +186,8 @@ export namespace ContextMenuRegistry {
location: Coordinate,
) => void;
displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement;
preconditionFn: (p1: Scope, menuOpenEvent: Event) => string;
separator?: never;
preconditionFn: (p1: Scope, menuOpenEvent: Event) => string;
}
/**
@@ -196,7 +197,6 @@ export namespace ContextMenuRegistry {
separator: true;
callback?: never;
displayText?: never;
preconditionFn?: never;
}
/**
@@ -14,8 +14,10 @@ import {ConnectionType} from '../connection_type.js';
import type {BlockMove} from '../events/events_block_move.js';
import {EventType} from '../events/type.js';
import * as eventUtils from '../events/utils.js';
import type {IBubble} from '../interfaces/i_bubble.js';
import {IConnectionPreviewer} from '../interfaces/i_connection_previewer.js';
import {IDragStrategy} from '../interfaces/i_draggable.js';
import {IHasBubble, hasBubble} from '../interfaces/i_has_bubble.js';
import * as layers from '../layers.js';
import * as registry from '../registry.js';
import {finishQueuedRenders} from '../render_management.js';
@@ -120,6 +122,34 @@ export class BlockDragStrategy implements IDragStrategy {
}
this.block.setDragging(true);
this.workspace.getLayerManager()?.moveToDragLayer(this.block);
this.getVisibleBubbles(this.block).forEach((bubble) => {
this.workspace.getLayerManager()?.moveToDragLayer(bubble, false);
});
}
/**
* Returns an array of visible bubbles attached to the given block or its
* descendants.
*
* @param block The block to identify open bubbles on.
* @returns An array of all currently visible bubbles on the given block or
* its descendants.
*/
private getVisibleBubbles(block: BlockSvg): IBubble[] {
return block
.getDescendants(false)
.flatMap((block) => block.getIcons())
.filter((icon) => hasBubble(icon) && icon.bubbleIsVisible())
.map((icon) => (icon as unknown as IHasBubble).getBubble())
.filter((bubble) => !!bubble) // Convince TS they're non-null.
.sort((a, b) => {
// Sort the bubbles by their position in the DOM in order to maintain
// their relative z-ordering when moving between layers.
const position = a.getSvgRoot().compareDocumentPosition(b.getSvgRoot());
if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
return 0;
});
}
/**
@@ -393,6 +423,13 @@ export class BlockDragStrategy implements IDragStrategy {
this.workspace
.getLayerManager()
?.moveOffDragLayer(this.block, layers.BLOCK);
this.getVisibleBubbles(this.block).forEach((bubble) =>
this.workspace
.getLayerManager()
?.moveOffDragLayer(bubble, layers.BUBBLE, false),
);
this.block.setDragging(false);
}
@@ -462,6 +499,12 @@ export class BlockDragStrategy implements IDragStrategy {
this.workspace
.getLayerManager()
?.moveOffDragLayer(this.block, layers.BLOCK);
this.getVisibleBubbles(this.block).forEach((bubble) =>
this.workspace
.getLayerManager()
?.moveOffDragLayer(bubble, layers.BUBBLE, false),
);
// Blocks dragged directly from a flyout may need to be bumped into
// bounds.
bumpObjects.bumpIntoBounds(
@@ -193,7 +193,7 @@ export class BlockChange extends BlockBase {
break;
}
case 'comment':
block.setCommentText((value as string) || null);
block.setCommentText((value as string) ?? null);
break;
case 'collapsed':
block.setCollapsed(!!value);

Some files were not shown because too many files have changed in this diff Show More