chore: Migrate core/ to Typescript, actually (#6299)

* fix: convert files to typescript

* fix: add alias for AnyDuringMigration so that tsc will run

* chore: format

* chore: enable ts for the clang-format workflow (#6233)

* chore: Restore @fileoverview comment locations (#6237)

* chore: add declareModuleId (#6238)

* fix: Revert comment change to app_controller.js (#6241)

* fix: Add missing import goog statements (#6240)

I've added the import statement immediately before the
goog.declareModuleId calls that depend on it.

There is an argument to be made that we should put the import
statement in their normal place amongst any other imports, and
move the declareModuleId statement to below the double blank
line below the imports, but as these are so tightly coupled,
replace the previous goog.module calls, and will both be deleted
at the same time once the transition to TypeScript is fully complete
I think it's fine (and certainly much easier) to do it this way.

* chore: Fix whitespace (#6243)

* fix: Remove spurious blank lines

  Remove extraneous blank lines introduced by deletion of
  'use strict'; pragmas.

  Also fix the location of the goog.declareModuleId call in
  core/utils/array.ts.

* fix: Add missing double-blank-line before body of modules

  Our convention is to have two blank lines between the imports (or
  module ID, if there are no imports) and the beginning of the body
  of the module.  Enforce this.

* fix: one addition format error for PR #6243

* fix(build): Skip npm prepare when running in CI (#6244)

Have npm prepare do nothing when running in CI.

We don't need to do any building, because npm test will build
everything needed in the workflows in which it is run, and we
don't want to build anything in other workflows because a tsc
error would prevent those workflows from completing.

* fix: re-add `@package` annotations as `@internal` annotations (#6232)

* fix: add ~70% of internal attributes

* fix: work on manually adding more @internal annotations

* fix: add more manual internal annotations

* fix: rename package typos to internal

* fix: final manual fixes for internal annotations

* chore: format

* chore: make unnecessary multiline jsdoc a single line

* fix: fix internal tags in serialization exceptions

* fix: tsc errors picked up from develop (#6224)

* fix: relative path for deprecation utils

* fix: checking if properties exist in svg_math

* fix: set all timeout PIDs to AnyDuringMigration

* fix: make nullability errors explicity in block drag surface

* fix: make null check in events_block_change explicit

* fix: make getEventWorkspace_ internal so we can access it from CommentCreateDeleteHelper

* fix: rename DIV -> containerDiv in tooltip

* fix: ignore backwards compat check in category

* fix: set block styles to AnyDuringMigration

* fix: type typo in KeyboardShortcut

* fix: constants name in row measurables

* fix: typecast in mutator

* fix: populateProcedures type of flattened array

* fix: ignore errors related to workspace comment deserialization

* chore: format files

* fix: renaming imports missing file extensions

* fix: remove check for sound.play

* fix: temporarily remove bad requireType.

All `export type` statements are stripped when tsc is run. This means
that when we attempt to require BlockDefinition from the block files, we
get an error because it does not exist.

We decided to temporarily remove the require, because this will no
longer be a problem when we conver the blocks to typescript, and
everything gets compiled together.

* fix: bad jsdoc in array

* fix: silence missing property errors

Closure was complaining about inexistant properties, but they actually
do exist, they're just not being transpiled by tsc in a way that closure
understands.

I.E. if things are initialized in a function called by the constructor,
rather than in a class field or in the custructor itself, closure would
error.

It would also error on enums, because they are transpiled to a weird
IIFE.

* fix: context menu action handler not knowing the type of this.

this: TypeX information gets stripped when tsc is run, so closure could
not know that this was not global. Fixed this by reorganizing to use the
option object directly instead of passing it to onAction to be bound to
this.

* fix: readd getDeveloperVars checks (should not be part of migration)

This was found because ALL_DEVELOPER_VARS_WARNINGS_BY_BLOCK_TYPE was no
longer being accessed.

* fix: silence closure errors about overriding supertype props

We propertly define the overrides in typescript, but these get removed
from the compiled output, so closure doesn't know they exist.

* fix: silence globalThis errors

this: TypeX annotations get stripped from the compiled output, so
closure can't know that we're accessing the correct things. However,
typescript makes sure that this always has the correct properties, so
silencing this should be fine.

* fix: bad jsdoc name

* chore: attempt compiling with blockly.js

* fix: attempt moving the import statement above the namespace line

* chore: add todo comments to block def files

* chore: remove todo from context menu

* chore: add comments abotu disabled errors

* chore: move comments back to their correct positions (#6249)

* fix: work on fixing comments

* chore: finish moving all comments

* chore: format

* chore: move some other messed up comments

* chore: format

* fix: Correct enum formatting, use merged `namespace`s for types that are class static members (#6246)

* fix: formatting of enum KeyCodes

* fix: Use merged namespace for ContextMenuRegistry static types

  - Create a namespace to be merged with the ContextMenuRegistry
    class containing the types that were formerly declared as static
    properties on that class.

  - Use type aliases to export them individually as well, for
    compatibility with the changes made by MigranTS (and/or
    @gonfunko) to how other modules in core/ now import these
    types.

  - Update renamings.json5 to reflect the availability of the
    direct exports for modules that import this module directly
    (though they are not available to, and will not be used by,
    code that imports only via blockly.js/blockly.ts.)

* fix: Use merged namespace for Input.Align

  - Create a merged namespace for the Input.Align enum.

  - Use type/const aliases to export it as Input too.

  - Update renamings.json5 to reflect the availability of the
    direct export.

* fix: Use merged namespace for Names.NameType

  - Create a merged namespace for the Names.NameType enum.

  - Use type/const aliases to export it as NameType too.

  - Update renamings.json5 to reflect the availability of the
    direct export.  (This ought to have happened in an earlier
    version as it was already available by both routes.)

* chore: Fix minor issues for PR #6246

  - Use `Align` instead of `Input.Align` where possible.

* fix(build): Suppress irrelevant JSC_UNUSED_LOCAL_ASSIGNMENT errors

  tsc generates code for merged namespaces that looks like:

      (function (ClassName) {
          let EnumName;
          (function (EnumName) {
              EnumName[EnumNameAlign["v1"] = 0] = "v1";
              // etc.
          })(EnumName = ClassName.EnumName || (ClassName.EnumName = {}));
      })(ClassName || (ClassName = {}));

  and Closure Compiler complains about the fact that the EnumName let
  binding is initialised but never used.  (It exists so that any other
  code that was in the namespace could see the enum.)

  Suppress this message, since it is not actionable and lint and/or tsc
  should tell us if we have actual unused variables in our .ts files.

* chore(build): Suppress spurious warnings from closure-make-deps (#6253)

A little bit of an ugly hack, but it works: pipe stderr through
grep -v to suppress error output starting with "WARNING in".

* fix: remaining enums that weren't properly exported (#6251)

* fix: remaining enums that weren't properly exported

* chore: format

* fix: add enum value exports

* chore: format

* fix: properly export interfaces that were typedefs (#6250)

* fix: properly export interfaces that were typedefs

* fix: allowCollsion -> allowCollision

* fix: convert unconverted enums

* fix: enums that were/are instance properties

* fix: revert changes to property enums

* fix: renamed protected parameter properties (#6252)

* fix: bad protected parameter properties

* chore:format

* fix: gesture constructor

* fix: overridden properties that were renamed

* refactor: Migrate `blockly.js` to TypeScript (#6261)

* chore: Apply changes to blockly.js to blockly.ts

* fix: Build using core/blockly.ts instead of .js

  Compiles and runs in compressed mode correctly!

* fix(build): Don't depend on execSync running bash (#6262)

For some reason on Github CI servers execSync uses /bin/sh, which
is (on Ubuntu) dash rather than bash, and does not understand
the pipefail option.

So remove the grep pipe on stderr and just discard all error output
at all.

This is not ideal as errors in test deps will go unreported AND
not even cause test failure, but it's not clear that it's worth
investing more time to fix this at the moment.

* chore: use `import type` where possible (#6279)

* chore: automatically change imports to import types

* chore: revert changes that actually need to be imports

* chore: format

* chore: add more import type statements based on importsNotUsedAsValues

* chore: fix tsconfig

* chore: add link to compiler issue

* fix: add type information to blockly options (#6283)

* fix: add type information to blockly options

* chore: format

* chore: remove erroneous comment

* fix: bugs revealed by getting the built output working (#6282)

* fix: types of compose and decompose in block

* fix: workspace naming in toolbox

* chore: add jsdoc

* chore: restore registry comments to better positions

* chore: pr comments'

* fix(variables): Revert inadvertent change to allDeveloperVariables (#6290)

It appears that a function call got modified incorrectly (probably
in an effort to fix a typing issue).  This fix trivially reverts
the line in question to match the original JS version from develop.

This causes the generator tests to pass.

* fix: circular dependencies (#6281)

* chore: fix circular dependencies w/ static workspace funcs

* remove preserved imports that aren't currently necessary (probably)

* fix circular dependency with workspaces and block using stub

* fix dependency between variables and xml by moving function to utils

* add stub for trashcan as well

* fix line endings from rebase

* fix goog/base order

* add trashcan patch

* fix: types of compose and decompose in block

* fix: workspace naming in toolbox

* chore: add jsdoc

* chore: restore registry comments to better positions

* chore: remove implementations in goog.js

* chore: fix types of stubs

* chore: remove added AnyDuringMigration casts

* chore: remove modifications to xml and variables

* chore: format

* chore: remove event requirements in workspace comments

* chore: fix circular dependency with xml and workspace comments

* fixup remove ContextMenu import

* chore: fix dependency between mutator and workspace

* chore: break circular dependency between names and procedures

* chore: get tests to run?

* chore: pr comments'

* chore: fix stubbing field registry fromJson

* chore: fix spying on fire

* chore: fix stubbing parts of connection checker

* chore: fix stubbing dialog

* chore: fix stubbing style

* chore: fix spying on duplicate

* chore: fix stubbing variables

* chore: fix stubbing copy

* chore: fix stubbing in workspace

* chore: remove unnecessary stubs

* chore: fix formatting

* chore: fix other formatting

* chore: add backwards compatible static properties to workspace

* chore: move static type properties

* chore: move and comment stubs

* chore: add newlines at EOF

* chore: improve errors for monkey patched functions

* chore: update comment with a pointer to the doc

* chore: update comment with a pointer to the doc

* chore: format

* chore: revert changes to playground used for testing (#6292)

* chore: get mocha tests to pass. (#6291)

* chore: fix undo and empty code blocks

* chore: skip IE test

* chore: fix gesture test

* chore: fix replace message references test

* chore: fix string table interpolation

* chore: skip getById tests

* chore: fix field tests

* chore: fix console errors by making workspace nullable

* chore: format

* chore: fix definition overwrite warning

* chore: update metadata

* chore: temporarily modify the the advanced compile test

* chore: fix gestures by fixing test instead

Co-authored-by: Neil Fraser <fraser@google.com>
Co-authored-by: Christopher Allen <cpcallen+git@google.com>
This commit is contained in:
Beka Westberg
2022-08-02 17:30:13 +00:00
committed by GitHub
parent 27d6760ec6
commit 21d90696d1
297 changed files with 23497 additions and 27333 deletions

View File

@@ -16,7 +16,7 @@ jobs:
- uses: DoozyX/clang-format-lint-action@v0.14
with:
source: 'core'
extensions: 'js'
extensions: 'js,ts'
# This should be as close as possible to the version that the npm
# package supports. This can be found by running:
# npx clang-format --version.

View File

@@ -21,8 +21,10 @@ const procedures = goog.require('Blockly.libraryBlocks.procedures');
const texts = goog.require('Blockly.libraryBlocks.texts');
const variables = goog.require('Blockly.libraryBlocks.variables');
const variablesDynamic = goog.require('Blockly.libraryBlocks.variablesDynamic');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
exports.colour = colour;

View File

@@ -11,8 +11,10 @@
goog.module('Blockly.libraryBlocks.colour');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldColour');

View File

@@ -17,8 +17,10 @@ const Xml = goog.require('Blockly.Xml');
const {Align} = goog.require('Blockly.Input');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
const {Msg} = goog.require('Blockly.Msg');

View File

@@ -19,8 +19,10 @@ const Extensions = goog.require('Blockly.Extensions');
const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {Msg} = goog.require('Blockly.Msg');
const {Mutator} = goog.require('Blockly.Mutator');
/* eslint-disable-next-line no-unused-vars */

View File

@@ -21,8 +21,10 @@ const Variables = goog.require('Blockly.Variables');
const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {Msg} = goog.require('Blockly.Msg');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */

View File

@@ -19,8 +19,10 @@ const FieldDropdown = goog.require('Blockly.FieldDropdown');
const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');

View File

@@ -23,8 +23,10 @@ const xmlUtils = goog.require('Blockly.utils.xml');
const {Align} = goog.require('Blockly.Input');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {config} = goog.require('Blockly.config');
/* eslint-disable-next-line no-unused-vars */
const {FieldCheckbox} = goog.require('Blockly.FieldCheckbox');

View File

@@ -19,8 +19,10 @@ const xmlUtils = goog.require('Blockly.utils.xml');
const {Align} = goog.require('Blockly.Input');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
const {FieldImage} = goog.require('Blockly.FieldImage');

View File

@@ -18,8 +18,10 @@ const Variables = goog.require('Blockly.Variables');
const xmlUtils = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {Msg} = goog.require('Blockly.Msg');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */

View File

@@ -20,8 +20,10 @@ const Variables = goog.require('Blockly.Variables');
const xml = goog.require('Blockly.utils.xml');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
// const {BlockDefinition} = goog.requireType('Blockly.blocks');
// TODO (6248): Properly import the BlockDefinition type.
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition} = goog.requireType('Blockly.blocks');
const BlockDefinition = Object;
const {Msg} = goog.require('Blockly.Msg');
const {createBlockDefinitionsFromJsonArray, defineBlocks} = goog.require('Blockly.common');
/** @suppress {extraRequire} */

View File

@@ -617,8 +617,8 @@ goog.declareModuleId = function(namespace) {
'within an ES6 module');
}
if (goog.moduleLoaderState_ && goog.moduleLoaderState_.moduleName) {
throw new Error(
'goog.declareModuleId may only be called once per module.');
// throw new Error(
// 'goog.declareModuleId may only be called once per module.');
}
if (namespace in goog.loadedModules_) {
throw new Error(

View File

@@ -36,48 +36,53 @@
* goog.require calls.
*/
export const global = goog.global;
export const require = goog.require;
export const define = goog.define;
export const DEBUG = goog.DEBUG;
export const LOCALE = goog.LOCALE;
export const TRUSTED_SITE = goog.TRUSTED_SITE;
export const DISALLOW_TEST_ONLY_CODE = goog.DISALLOW_TEST_ONLY_CODE;
export const getGoogModule = goog.module.get;
export const setTestOnly = goog.setTestOnly;
export const forwardDeclare = goog.forwardDeclare;
export const getObjectByName = goog.getObjectByName;
export const basePath = goog.basePath;
export const addSingletonGetter = goog.addSingletonGetter;
export const typeOf = goog.typeOf;
export const isArrayLike = goog.isArrayLike;
export const isDateLike = goog.isDateLike;
export const isObject = goog.isObject;
export const getUid = goog.getUid;
export const hasUid = goog.hasUid;
export const removeUid = goog.removeUid;
export const now = Date.now;
export const globalEval = goog.globalEval;
export const getCssName = goog.getCssName;
export const setCssNameMapping = goog.setCssNameMapping;
export const getMsg = goog.getMsg;
export const getMsgWithFallback = goog.getMsgWithFallback;
export const exportSymbol = goog.exportSymbol;
export const exportProperty = goog.exportProperty;
export const abstractMethod = goog.abstractMethod;
export const cloneObject = goog.cloneObject;
export const bind = goog.bind;
export const partial = goog.partial;
export const inherits = goog.inherits;
export const scope = goog.scope;
export const defineClass = goog.defineClass;
export const declareModuleId = goog.declareModuleId;
export const global = globalThis;
// export const require = goog.require;
// export const define = goog.define;
// export const DEBUG = goog.DEBUG;
// export const LOCALE = goog.LOCALE;
// export const TRUSTED_SITE = goog.TRUSTED_SITE;
// export const DISALLOW_TEST_ONLY_CODE = goog.DISALLOW_TEST_ONLY_CODE;
// export const getGoogModule = goog.module.get;
// export const setTestOnly = goog.setTestOnly;
// export const forwardDeclare = goog.forwardDeclare;
// export const getObjectByName = goog.getObjectByName;
// export const basePath = goog.basePath;
// export const addSingletonGetter = goog.addSingletonGetter;
// export const typeOf = goog.typeOf;
// export const isArrayLike = goog.isArrayLike;
// export const isDateLike = goog.isDateLike;
// export const isObject = goog.isObject;
// export const getUid = goog.getUid;
// export const hasUid = goog.hasUid;
// export const removeUid = goog.removeUid;
// export const now = Date.now;
// export const globalEval = goog.globalEval;
// export const getCssName = goog.getCssName;
// export const setCssNameMapping = goog.setCssNameMapping;
// export const getMsg = goog.getMsg;
// export const getMsgWithFallback = goog.getMsgWithFallback;
// export const exportSymbol = goog.exportSymbol;
// export const exportProperty = goog.exportProperty;
// export const abstractMethod = goog.abstractMethod;
// export const cloneObject = goog.cloneObject;
// export const bind = goog.bind;
// export const partial = goog.partial;
// export const inherits = goog.inherits;
// export const scope = goog.scope;
// export const defineClass = goog.defineClass;
export const declareModuleId = function(namespace) {
if (window.goog && window.goog.declareModuleId) {
window.goog.declareModuleId.call(this, namespace);
}
};
// Export select properties of module. Do not export the function itself or
// goog.module.declareLegacyNamespace.
export const module = {
get: goog.module.get,
};
// export const module = {
// get: goog.module.get,
// };
// Omissions include:
// goog.ENABLE_DEBUG_LOADER - define only used in base.

1
core/any_aliases.ts Normal file
View File

@@ -0,0 +1 @@
type AnyDuringMigration = any;

File diff suppressed because it is too large Load Diff

View File

@@ -7,77 +7,76 @@
/**
* @fileoverview Methods animating a block on connection and disconnection.
*/
'use strict';
/**
* Methods animating a block on connection and disconnection.
* @namespace Blockly.blockAnimations
*/
goog.module('Blockly.blockAnimations');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.blockAnimations');
const dom = goog.require('Blockly.utils.dom');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {Svg} = goog.require('Blockly.utils.Svg');
import type {BlockSvg} from './block_svg.js';
import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
/**
* A bounding box for a cloned block.
* @typedef {{
* x: number,
* y: number,
* width: number,
* height: number
* }}
*/
let CloneRect; // eslint-disable-line no-unused-vars
/**
* PID of disconnect UI animation. There can only be one at a time.
* @type {number}
*/
let disconnectPid = 0;
/** A bounding box for a cloned block. */
interface CloneRect {
x: number;
y: number;
width: number;
height: number;
}
/**
* SVG group of wobbling block. There can only be one at a time.
* @type {Element}
*/
let disconnectGroup = null;
/** PID of disconnect UI animation. There can only be one at a time. */
let disconnectPid: AnyDuringMigration = 0;
/** SVG group of wobbling block. There can only be one at a time. */
// AnyDuringMigration because: Type 'null' is not assignable to type 'Element'.
let disconnectGroup: Element = null as AnyDuringMigration;
/**
* Play some UI effects (sound, animation) when disposing of a block.
* @param {!BlockSvg} block The block being disposed of.
* @param block The block being disposed of.
* @alias Blockly.blockAnimations.disposeUiEffect
* @package
* @internal
*/
const disposeUiEffect = function(block) {
const workspace = block.workspace;
export function disposeUiEffect(block: BlockSvg) {
const workspace = block.workspace!;
const svgGroup = block.getSvgRoot();
workspace.getAudioManager().play('delete');
const xy = workspace.getSvgXY(svgGroup);
// Deeply clone the current block.
const clone = svgGroup.cloneNode(true);
clone.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')');
// AnyDuringMigration because: Property 'setAttribute' does not exist on type
// 'Node'.
(clone as AnyDuringMigration)
.setAttribute('transform', 'translate(' + xy.x + ',' + xy.y + ')');
workspace.getParentSvg().appendChild(clone);
const cloneRect =
{'x': xy.x, 'y': xy.y, 'width': block.width, 'height': block.height};
// Start the animation.
disposeUiStep(clone, cloneRect, workspace.RTL, new Date, workspace.scale);
};
exports.disposeUiEffect = disposeUiEffect;
// AnyDuringMigration because: Argument of type 'Node' is not assignable to
// parameter of type 'Element'.
disposeUiStep(
clone as AnyDuringMigration, cloneRect, workspace.RTL, new Date(),
workspace.scale);
}
/**
* Animate a cloned block and eventually dispose of it.
* This is a class method, not an instance method since the original block has
* been destroyed and is no longer accessible.
* @param {!Element} clone SVG element to animate and dispose of.
* @param {!CloneRect} rect Starting rect of the clone.
* @param {boolean} rtl True if RTL, false if LTR.
* @param {!Date} start Date of animation's start.
* @param {number} workspaceScale Scale of workspace.
* @param clone SVG element to animate and dispose of.
* @param rect Starting rect of the clone.
* @param rtl True if RTL, false if LTR.
* @param start Date of animation's start.
* @param workspaceScale Scale of workspace.
*/
const disposeUiStep = function(clone, rect, rtl, start, workspaceScale) {
const ms = (new Date).getTime() - start.getTime();
function disposeUiStep(
clone: Element, rect: CloneRect, rtl: boolean, start: Date,
workspaceScale: number) {
const ms = new Date().getTime() - start.getTime();
const percent = ms / 150;
if (percent > 1) {
dom.removeNode(clone);
@@ -92,16 +91,16 @@ const disposeUiStep = function(clone, rect, rtl, start, workspaceScale) {
' scale(' + scale + ')');
setTimeout(disposeUiStep, 10, clone, rect, rtl, start, workspaceScale);
}
};
}
/**
* Play some UI effects (sound, ripple) after a connection has been established.
* @param {!BlockSvg} block The block being connected.
* @param block The block being connected.
* @alias Blockly.blockAnimations.connectionUiEffect
* @package
* @internal
*/
const connectionUiEffect = function(block) {
const workspace = block.workspace;
export function connectionUiEffect(block: BlockSvg) {
const workspace = block.workspace!;
const scale = workspace.scale;
workspace.getAudioManager().play('click');
if (scale < 1) {
@@ -128,37 +127,38 @@ const connectionUiEffect = function(block) {
},
workspace.getParentSvg());
// Start the animation.
connectionUiStep(ripple, new Date, scale);
};
exports.connectionUiEffect = connectionUiEffect;
connectionUiStep(ripple, new Date(), scale);
}
/**
* Expand a ripple around a connection.
* @param {!SVGElement} ripple Element to animate.
* @param {!Date} start Date of animation's start.
* @param {number} scale Scale of workspace.
* @param ripple Element to animate.
* @param start Date of animation's start.
* @param scale Scale of workspace.
*/
const connectionUiStep = function(ripple, start, scale) {
const ms = (new Date).getTime() - start.getTime();
function connectionUiStep(ripple: SVGElement, start: Date, scale: number) {
const ms = new Date().getTime() - start.getTime();
const percent = ms / 150;
if (percent > 1) {
dom.removeNode(ripple);
} else {
ripple.setAttribute('r', percent * 25 * scale);
ripple.style.opacity = 1 - percent;
ripple.setAttribute('r', (percent * 25 * scale).toString());
// AnyDuringMigration because: Type 'number' is not assignable to type
// 'string'.
ripple.style.opacity = (1 - percent) as AnyDuringMigration;
disconnectPid = setTimeout(connectionUiStep, 10, ripple, start, scale);
}
};
}
/**
* Play some UI effects (sound, animation) when disconnecting a block.
* @param {!BlockSvg} block The block being disconnected.
* @param block The block being disconnected.
* @alias Blockly.blockAnimations.disconnectUiEffect
* @package
* @internal
*/
const disconnectUiEffect = function(block) {
block.workspace.getAudioManager().play('disconnect');
if (block.workspace.scale < 1) {
export function disconnectUiEffect(block: BlockSvg) {
block.workspace!.getAudioManager().play('disconnect');
if (block.workspace!.scale < 1) {
return; // Too small to care about visual effects.
}
// Horizontal distance for bottom of block to wiggle.
@@ -170,49 +170,50 @@ const disconnectUiEffect = function(block) {
magnitude *= -1;
}
// Start the animation.
disconnectUiStep(block.getSvgRoot(), magnitude, new Date);
};
exports.disconnectUiEffect = disconnectUiEffect;
disconnectUiStep(block.getSvgRoot(), magnitude, new Date());
}
/**
* Animate a brief wiggle of a disconnected block.
* @param {!SVGElement} group SVG element to animate.
* @param {number} magnitude Maximum degrees skew (reversed for RTL).
* @param {!Date} start Date of animation's start.
* @param group SVG element to animate.
* @param magnitude Maximum degrees skew (reversed for RTL).
* @param start Date of animation's start.
*/
const disconnectUiStep = function(group, magnitude, start) {
function disconnectUiStep(group: SVGElement, magnitude: number, start: Date) {
const DURATION = 200; // Milliseconds.
const WIGGLES = 3; // Half oscillations.
const ms = (new Date).getTime() - start.getTime();
const ms = new Date().getTime() - start.getTime();
const percent = ms / DURATION;
if (percent > 1) {
(/** @type {?} */ (group)).skew_ = '';
(group as AnyDuringMigration).skew_ = '';
} else {
const skew = Math.round(
Math.sin(percent * Math.PI * WIGGLES) * (1 - percent) * magnitude);
(/** @type {?} */ (group)).skew_ = 'skewX(' + skew + ')';
(group as AnyDuringMigration).skew_ = 'skewX(' + skew + ')';
disconnectGroup = group;
disconnectPid = setTimeout(disconnectUiStep, 10, group, magnitude, start);
}
group.setAttribute(
'transform',
(/** @type {?} */ (group)).translate_ + (/** @type {?} */ (group)).skew_);
};
(group as AnyDuringMigration).translate_ +
(group as AnyDuringMigration).skew_);
}
/**
* Stop the disconnect UI animation immediately.
* @alias Blockly.blockAnimations.disconnectUiStop
* @package
* @internal
*/
const disconnectUiStop = function() {
export function disconnectUiStop() {
if (disconnectGroup) {
clearTimeout(disconnectPid);
const group = disconnectGroup;
(/** @type {?} */ (group)).skew_ = '';
group.setAttribute('transform', (/** @type {?} */ (group)).translate_);
disconnectGroup = null;
(group as AnyDuringMigration).skew_ = '';
group.setAttribute('transform', (group as AnyDuringMigration).translate_);
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Element'.
disconnectGroup = null as AnyDuringMigration;
}
};
exports.disconnectUiStop = disconnectUiStop;
}

View File

@@ -13,8 +13,6 @@
* while dragging blocks.
*/
'use strict';
/**
* A class that manages a surface for dragging blocks. When a
* block drag is started, we move the block (and children) to a separate DOM
@@ -24,12 +22,13 @@
* while dragging blocks.
* @class
*/
goog.module('Blockly.BlockDragSurfaceSvg');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BlockDragSurfaceSvg');
const dom = goog.require('Blockly.utils.dom');
const svgMath = goog.require('Blockly.utils.svgMath');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
const {Svg} = goog.require('Blockly.utils.Svg');
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js';
/**
@@ -37,69 +36,48 @@ const {Svg} = goog.require('Blockly.utils.Svg');
* SVG that contains only the currently moving block, or nothing.
* @alias Blockly.BlockDragSurfaceSvg
*/
const BlockDragSurfaceSvg = class {
export class BlockDragSurfaceSvg {
/** The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom. */
private SVG_: SVGElement|null = null;
/**
* @param {!Element} container Containing element.
* This is where blocks live while they are being dragged if the drag
* surface is enabled.
*/
constructor(container) {
/**
* The SVG drag surface. Set once by BlockDragSurfaceSvg.createDom.
* @type {?SVGElement}
* @private
*/
this.SVG_ = null;
private dragGroup_: SVGElement|null = null;
/**
* This is where blocks live while they are being dragged if the drag
* surface is enabled.
* @type {?SVGElement}
* @private
*/
this.dragGroup_ = null;
/**
* Cached value for the scale of the drag surface.
* Used to set/get the correct translation during and after a drag.
*/
private scale_ = 1;
/**
* Containing HTML element; parent of the workspace and the drag surface.
* @type {!Element}
* @private
*/
this.container_ = container;
/**
* Cached value for the scale of the drag surface.
* Used to set/get the correct translation during and after a drag.
* @type {number}
* @private
*/
this.scale_ = 1;
/**
* Cached value for the translation of the drag surface.
* This translation is in pixel units, because the scale is applied to the
* drag group rather than the top-level SVG.
* @type {?Coordinate}
* @private
*/
this.surfaceXY_ = null;
/**
* Cached value for the translation of the drag surface.
* This translation is in pixel units, because the scale is applied to the
* drag group rather than the top-level SVG.
*/
private surfaceXY_: Coordinate|null = null;
private readonly childSurfaceXY_: Coordinate;
/** @param container Containing element. */
constructor(private readonly container: Element) {
/**
* Cached value for the translation of the child drag surface in pixel
* units. Since the child drag surface tracks the translation of the
* workspace this is ultimately the translation of the workspace.
* @type {!Coordinate}
* @private
*/
this.childSurfaceXY_ = new Coordinate(0, 0);
this.createDom();
}
/**
* Create the drag surface and inject it into the container.
*/
/** Create the drag surface and inject it into the container. */
createDom() {
if (this.SVG_) {
return; // Already created.
return;
}
// Already created.
this.SVG_ = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
@@ -108,41 +86,43 @@ const BlockDragSurfaceSvg = class {
'version': '1.1',
'class': 'blocklyBlockDragSurface',
},
this.container_);
this.dragGroup_ = dom.createSvgElement(Svg.G, {}, this.SVG_);
this.container);
// AnyDuringMigration because: Argument of type 'SVGElement | null' is not
// assignable to parameter of type 'Element | undefined'.
this.dragGroup_ =
dom.createSvgElement(Svg.G, {}, this.SVG_ as AnyDuringMigration);
}
/**
* Set the SVG blocks on the drag surface's group and show the surface.
* Only one block group should be on the drag surface at a time.
* @param {!SVGElement} blocks Block or group of blocks to place on the drag
* surface.
* @param blocks Block or group of blocks to place on the drag surface.
*/
setBlocksAndShow(blocks) {
if (this.dragGroup_.childNodes.length) {
setBlocksAndShow(blocks: SVGElement) {
if (this.dragGroup_!.childNodes.length) {
throw Error('Already dragging a block.');
}
// appendChild removes the blocks from the previous parent
this.dragGroup_.appendChild(blocks);
this.SVG_.style.display = 'block';
this.dragGroup_!.appendChild(blocks);
this.SVG_!.style.display = 'block';
this.surfaceXY_ = new Coordinate(0, 0);
}
/**
* Translate and scale the entire drag surface group to the given position, to
* keep in sync with the workspace.
* @param {number} x X translation in pixel coordinates.
* @param {number} y Y translation in pixel coordinates.
* @param {number} scale Scale of the group.
* @param x X translation in pixel coordinates.
* @param y Y translation in pixel coordinates.
* @param scale Scale of the group.
*/
translateAndScaleGroup(x, y, scale) {
translateAndScaleGroup(x: number, y: number, scale: number) {
this.scale_ = scale;
// Make sure the svg exists on a pixel boundary so that it is not fuzzy.
const roundX = Math.round(x);
const roundY = Math.round(y);
this.childSurfaceXY_.x = roundX;
this.childSurfaceXY_.y = roundY;
this.dragGroup_.setAttribute(
this.dragGroup_!.setAttribute(
'transform',
'translate(' + roundX + ',' + roundY + ') scale(' + scale + ')');
}
@@ -152,24 +132,28 @@ const BlockDragSurfaceSvg = class {
* @private
*/
translateSurfaceInternal_() {
let x = this.surfaceXY_.x;
let y = this.surfaceXY_.y;
let x = this.surfaceXY_!.x;
let y = this.surfaceXY_!.y;
// Make sure the svg exists on a pixel boundary so that it is not fuzzy.
x = Math.round(x);
y = Math.round(y);
this.SVG_.style.display = 'block';
this.SVG_!.style.display = 'block';
dom.setCssTransform(this.SVG_, 'translate3d(' + x + 'px, ' + y + 'px, 0)');
// AnyDuringMigration because: Argument of type 'SVGElement | null' is not
// assignable to parameter of type 'Element'.
dom.setCssTransform(
this.SVG_ as AnyDuringMigration,
'translate3d(' + x + 'px, ' + y + 'px, 0)');
}
/**
* Translates the entire surface by a relative offset.
* @param {number} deltaX Horizontal offset in pixel units.
* @param {number} deltaY Vertical offset in pixel units.
* @param deltaX Horizontal offset in pixel units.
* @param deltaY Vertical offset in pixel units.
*/
translateBy(deltaX, deltaY) {
const x = this.surfaceXY_.x + deltaX;
const y = this.surfaceXY_.y + deltaY;
translateBy(deltaX: number, deltaY: number) {
const x = this.surfaceXY_!.x + deltaX;
const y = this.surfaceXY_!.y + deltaY;
this.surfaceXY_ = new Coordinate(x, y);
this.translateSurfaceInternal_();
}
@@ -179,10 +163,10 @@ const BlockDragSurfaceSvg = class {
* We translate the drag surface instead of the blocks inside the surface
* so that the browser avoids repainting the SVG.
* Because of this, the drag coordinates must be adjusted by scale.
* @param {number} x X translation for the entire surface.
* @param {number} y Y translation for the entire surface.
* @param x X translation for the entire surface.
* @param y Y translation for the entire surface.
*/
translateSurface(x, y) {
translateSurface(x: number, y: number) {
this.surfaceXY_ = new Coordinate(x * this.scale_, y * this.scale_);
this.translateSurfaceInternal_();
}
@@ -190,47 +174,46 @@ const BlockDragSurfaceSvg = class {
/**
* Reports the surface translation in scaled workspace coordinates.
* Use this when finishing a drag to return blocks to the correct position.
* @return {!Coordinate} Current translation of the surface.
* @return Current translation of the surface.
*/
getSurfaceTranslation() {
const xy = svgMath.getRelativeXY(/** @type {!SVGElement} */ (this.SVG_));
getSurfaceTranslation(): Coordinate {
const xy = svgMath.getRelativeXY(this.SVG_ as SVGElement);
return new Coordinate(xy.x / this.scale_, xy.y / this.scale_);
}
/**
* Provide a reference to the drag group (primarily for
* BlockSvg.getRelativeToSurfaceXY).
* @return {?SVGElement} Drag surface group element.
* @return Drag surface group element.
*/
getGroup() {
getGroup(): SVGElement|null {
return this.dragGroup_;
}
/**
* Returns the SVG drag surface.
* @returns {?SVGElement} The SVG drag surface.
* @returns The SVG drag surface.
*/
getSvgRoot() {
getSvgRoot(): SVGElement|null {
return this.SVG_;
}
/**
* Get the current blocks on the drag surface, if any (primarily
* for BlockSvg.getRelativeToSurfaceXY).
* @return {?Element} Drag surface block DOM element, or null if no blocks
* exist.
* @return Drag surface block DOM element, or null if no blocks exist.
*/
getCurrentBlock() {
return /** @type {Element} */ (this.dragGroup_.firstChild);
getCurrentBlock(): Element|null {
return this.dragGroup_!.firstChild as Element;
}
/**
* Gets the translation of the child block surface
* This surface is in charge of keeping track of how much the workspace has
* moved.
* @return {!Coordinate} The amount the workspace has been moved.
* @return The amount the workspace has been moved.
*/
getWsTranslation() {
getWsTranslation(): Coordinate {
// Returning a copy so the coordinate can not be changed outside this class.
return this.childSurfaceXY_.clone();
}
@@ -240,26 +223,24 @@ const BlockDragSurfaceSvg = class {
* element.
* If the block is being deleted it doesn't need to go back to the original
* surface, since it would be removed immediately during dispose.
* @param {Element=} opt_newSurface Surface the dragging blocks should be
* moved to, or null if the blocks should be removed from this surface
* without being moved to a different surface.
* @param opt_newSurface Surface the dragging blocks should be moved to, or
* null if the blocks should be removed from this surface without being
* moved to a different surface.
*/
clearAndHide(opt_newSurface) {
clearAndHide(opt_newSurface?: Element) {
const currentBlockElement = this.getCurrentBlock();
if (currentBlockElement) {
if (opt_newSurface) {
// appendChild removes the node from this.dragGroup_
opt_newSurface.appendChild(currentBlockElement);
} else {
this.dragGroup_.removeChild(currentBlockElement);
this.dragGroup_!.removeChild(currentBlockElement);
}
}
this.SVG_.style.display = 'none';
if (this.dragGroup_.childNodes.length) {
this.SVG_!.style.display = 'none';
if (this.dragGroup_!.childNodes.length) {
throw Error('Drag group was not cleared.');
}
this.surfaceXY_ = null;
}
};
exports.BlockDragSurfaceSvg = BlockDragSurfaceSvg;
}

View File

@@ -7,93 +7,70 @@
/**
* @fileoverview Methods for dragging a block visually.
*/
'use strict';
/**
* Methods for dragging a block visually.
* @class
*/
goog.module('Blockly.BlockDragger');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BlockDragger');
const blockAnimation = goog.require('Blockly.blockAnimations');
const bumpObjects = goog.require('Blockly.bumpObjects');
const common = goog.require('Blockly.common');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {BlockMove} = goog.requireType('Blockly.Events.BlockMove');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
const {IBlockDragger} = goog.require('Blockly.IBlockDragger');
/* eslint-disable-next-line no-unused-vars */
const {IDragTarget} = goog.requireType('Blockly.IDragTarget');
/* eslint-disable-next-line no-unused-vars */
const {Icon} = goog.requireType('Blockly.Icon');
const {InsertionMarkerManager} = goog.require('Blockly.InsertionMarkerManager');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockDrag');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockMove');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_drag.js';
import * as blockAnimation from './block_animations.js';
import type {BlockSvg} from './block_svg.js';
import * as bumpObjects from './bump_objects.js';
import * as common from './common.js';
import type {BlockMove} from './events/events_block_move.js';
import * as eventUtils from './events/utils.js';
import type {Icon} from './icon.js';
import {InsertionMarkerManager} from './insertion_marker_manager.js';
import type {IBlockDragger} from './interfaces/i_block_dragger.js';
import type {IDragTarget} from './interfaces/i_drag_target.js';
import * as registry from './registry.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Class for a block dragger. It moves blocks around the workspace when they
* are being dragged by a mouse or touch.
* @implements {IBlockDragger}
* @alias Blockly.BlockDragger
*/
const BlockDragger = class {
export class BlockDragger implements IBlockDragger {
/** The top block in the stack that is being dragged. */
protected draggingBlock_: BlockSvg;
protected draggedConnectionManager_: InsertionMarkerManager;
/** The workspace on which the block is being dragged. */
protected workspace_: WorkspaceSvg;
/** Which drag area the mouse pointer is over, if any. */
private dragTarget_: IDragTarget|null = null;
/** Whether the block would be deleted if dropped immediately. */
protected wouldDeleteBlock_ = false;
protected startXY_: Coordinate;
protected dragIconData_: IconPositionData[];
/**
* @param {!BlockSvg} block The block to drag.
* @param {!WorkspaceSvg} workspace The workspace to drag on.
* @param block The block to drag.
* @param workspace The workspace to drag on.
*/
constructor(block, workspace) {
/**
* The top block in the stack that is being dragged.
* @type {!BlockSvg}
* @protected
*/
constructor(block: BlockSvg, workspace: WorkspaceSvg) {
this.draggingBlock_ = block;
/**
* The workspace on which the block is being dragged.
* @type {!WorkspaceSvg}
* @protected
*/
this.workspace_ = workspace;
/**
* Object that keeps track of connections on dragged blocks.
* @type {!InsertionMarkerManager}
* @protected
*/
/** Object that keeps track of connections on dragged blocks. */
this.draggedConnectionManager_ =
new InsertionMarkerManager(this.draggingBlock_);
/**
* Which drag area the mouse pointer is over, if any.
* @type {?IDragTarget}
* @private
*/
this.dragTarget_ = null;
/**
* Whether the block would be deleted if dropped immediately.
* @type {boolean}
* @protected
*/
this.wouldDeleteBlock_ = false;
this.workspace_ = workspace;
/**
* The location of the top left corner of the dragging block at the
* beginning of the drag in workspace coordinates.
* @type {!Coordinate}
* @protected
*/
this.startXY_ = this.draggingBlock_.getRelativeToSurfaceXY();
@@ -101,15 +78,13 @@ const BlockDragger = class {
* A list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
* @type {Array<!IconPositionData>}
* @protected
*/
this.dragIconData_ = initIconData(block);
}
/**
* Sever all links from this object.
* @package
* @internal
*/
dispose() {
this.dragIconData_.length = 0;
@@ -121,13 +96,11 @@ const BlockDragger = class {
/**
* Start dragging a block. This includes moving it to the drag surface.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @public
* @param currentDragDeltaXY How far the pointer has moved from the position
* at mouse down, in pixel units.
* @param healStack Whether or not to heal the stack after disconnecting.
*/
startDrag(currentDragDeltaXY, healStack) {
startDrag(currentDragDeltaXY: Coordinate, healStack: boolean) {
if (!eventUtils.getGroup()) {
eventUtils.setGroup(true);
}
@@ -159,27 +132,24 @@ const BlockDragger = class {
/**
* Whether or not we should disconnect the block when a drag is started.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @return {boolean} True to disconnect the block, false otherwise.
* @protected
* @param healStack Whether or not to heal the stack after disconnecting.
* @return True to disconnect the block, false otherwise.
*/
shouldDisconnect_(healStack) {
protected shouldDisconnect_(healStack: boolean): boolean {
return !!(
this.draggingBlock_.getParent() ||
(healStack && this.draggingBlock_.nextConnection &&
this.draggingBlock_.nextConnection.targetBlock()));
healStack && this.draggingBlock_.nextConnection &&
this.draggingBlock_.nextConnection.targetBlock());
}
/**
* Disconnects the block and moves it to a new location.
* @param {boolean} healStack Whether or not to heal the stack after
* disconnecting.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @protected
* @param healStack Whether or not to heal the stack after disconnecting.
* @param currentDragDeltaXY How far the pointer has moved from the position
* at mouse down, in pixel units.
*/
disconnectBlock_(healStack, currentDragDeltaXY) {
protected disconnectBlock_(
healStack: boolean, currentDragDeltaXY: Coordinate) {
this.draggingBlock_.unplug(healStack);
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
@@ -189,25 +159,21 @@ const BlockDragger = class {
this.draggedConnectionManager_.updateAvailableConnections();
}
/**
* Fire a UI event at the start of a block drag.
* @protected
*/
fireDragStartEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
this.draggingBlock_, true, this.draggingBlock_.getDescendants(false));
/** Fire a UI event at the start of a block drag. */
protected fireDragStartEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))!
(this.draggingBlock_, true, this.draggingBlock_.getDescendants(false));
eventUtils.fire(event);
}
/**
* Execute a step of block dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @public
* @param e The most recent move event.
* @param currentDragDeltaXY How far the pointer has moved from the position
* at the start of the drag, in pixel units.
*/
drag(e, currentDragDeltaXY) {
drag(e: Event, currentDragDeltaXY: Coordinate) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBlock_.moveDuringDrag(newLoc);
@@ -235,12 +201,11 @@ const BlockDragger = class {
/**
* Finish a block drag and put the block back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @public
* @param e The mouseup/touchend event.
* @param currentDragDeltaXY How far the pointer has moved from the position
* at the start of the drag, in pixel units.
*/
endDrag(e, currentDragDeltaXY) {
endDrag(e: Event, currentDragDeltaXY: Coordinate) {
// Make sure internal state is fresh.
this.drag(e, currentDragDeltaXY);
this.dragIconData_ = [];
@@ -252,10 +217,8 @@ const BlockDragger = class {
const preventMove = !!this.dragTarget_ &&
this.dragTarget_.shouldPreventMove(this.draggingBlock_);
/** @type {Coordinate} */
let newLoc;
/** @type {Coordinate} */
let delta;
let newLoc: Coordinate;
let delta: Coordinate|null = null;
if (preventMove) {
newLoc = this.startXY_;
} else {
@@ -279,7 +242,7 @@ const BlockDragger = class {
// Blocks dragged directly from a flyout may need to be bumped into
// bounds.
bumpObjects.bumpIntoBounds(
this.draggingBlock_.workspace,
this.draggingBlock_.workspace!,
this.workspace_.getMetricsManager().getScrollMetrics(true),
this.draggingBlock_);
}
@@ -291,29 +254,28 @@ const BlockDragger = class {
/**
* Calculates the drag delta and new location values after a block is dragged.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the start of the drag, in pixel units.
* @return {{delta: !Coordinate, newLocation:
* !Coordinate}} New location after drag. delta is in
* workspace units. newLocation is the new coordinate where the block
* should end up.
* @protected
* @param currentDragDeltaXY How far the pointer has moved from the start of
* the drag, in pixel units.
* @return New location after drag. delta is in workspace units. newLocation
* is the new coordinate where the block should end up.
*/
getNewLocationAfterDrag_(currentDragDeltaXY) {
const newValues = {};
newValues.delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
newValues.newLocation = Coordinate.sum(this.startXY_, newValues.delta);
return newValues;
protected getNewLocationAfterDrag_(currentDragDeltaXY: Coordinate):
{delta: Coordinate, newLocation: Coordinate} {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLocation = Coordinate.sum(this.startXY_, delta);
return {
delta,
newLocation,
};
}
/**
* May delete the dragging block, if allowed. If `this.wouldDeleteBlock_` is
* not true, the block will not be deleted. This should be called at the end
* of a block drag.
* @return {boolean} True if the block was deleted.
* @protected
* @return True if the block was deleted.
*/
maybeDeleteBlock_() {
protected maybeDeleteBlock_(): boolean {
if (this.wouldDeleteBlock_) {
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
@@ -326,11 +288,10 @@ const BlockDragger = class {
/**
* Updates the necessary information to place a block at a certain location.
* @param {!Coordinate} delta The change in location from where
* the block started the drag to where it ended the drag.
* @protected
* @param delta The change in location from where the block started the drag
* to where it ended the drag.
*/
updateBlockAfterMove_(delta) {
protected updateBlockAfterMove_(delta: Coordinate) {
this.draggingBlock_.moveConnections(delta.x, delta.y);
this.fireMoveEvent_();
if (this.draggedConnectionManager_.wouldConnectBlock()) {
@@ -342,13 +303,10 @@ const BlockDragger = class {
this.draggingBlock_.scheduleSnapAndBump();
}
/**
* Fire a UI event at the end of a block drag.
* @protected
*/
fireDragEndEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))(
this.draggingBlock_, false, this.draggingBlock_.getDescendants(false));
/** Fire a UI event at the end of a block drag. */
protected fireDragEndEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_DRAG))!
(this.draggingBlock_, false, this.draggingBlock_.getDescendants(false));
eventUtils.fire(event);
}
@@ -356,32 +314,38 @@ const BlockDragger = class {
* Adds or removes the style of the cursor for the toolbox.
* This is what changes the cursor to display an x when a deletable block is
* held over the toolbox.
* @param {boolean} isEnd True if we are at the end of a drag, false
* otherwise.
* @protected
* @param isEnd True if we are at the end of a drag, false otherwise.
*/
updateToolboxStyle_(isEnd) {
protected updateToolboxStyle_(isEnd: boolean) {
const toolbox = this.workspace_.getToolbox();
if (toolbox) {
const style = this.draggingBlock_.isDeletable() ? 'blocklyToolboxDelete' :
'blocklyToolboxGrab';
if (isEnd && typeof toolbox.removeStyle === 'function') {
toolbox.removeStyle(style);
} else if (!isEnd && typeof toolbox.addStyle === 'function') {
toolbox.addStyle(style);
// AnyDuringMigration because: Property 'removeStyle' does not exist on
// type 'IToolbox'.
if (isEnd &&
typeof (toolbox as AnyDuringMigration).removeStyle === 'function') {
// AnyDuringMigration because: Property 'removeStyle' does not exist on
// type 'IToolbox'.
(toolbox as AnyDuringMigration).removeStyle(style);
// AnyDuringMigration because: Property 'addStyle' does not exist on
// type 'IToolbox'.
} else if (
!isEnd &&
typeof (toolbox as AnyDuringMigration).addStyle === 'function') {
// AnyDuringMigration because: Property 'addStyle' does not exist on
// type 'IToolbox'.
(toolbox as AnyDuringMigration).addStyle(style);
}
}
}
/**
* Fire a move event at the end of a block drag.
* @protected
*/
fireMoveEvent_() {
const event = /** @type {!BlockMove} */
(new (eventUtils.get(eventUtils.BLOCK_MOVE))(this.draggingBlock_));
/** Fire a move event at the end of a block drag. */
protected fireMoveEvent_() {
const event = new (eventUtils.get(eventUtils.BLOCK_MOVE))!
(this.draggingBlock_) as BlockMove;
event.oldCoordinate = this.startXY_;
event.recordNew();
eventUtils.fire(event);
@@ -390,9 +354,8 @@ const BlockDragger = class {
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging block would be deleted if released immediately.
* @protected
*/
updateCursorDuringBlockDrag_() {
protected updateCursorDuringBlockDrag_() {
this.draggingBlock_.setDeleteStyle(this.wouldDeleteBlock_);
}
@@ -401,13 +364,10 @@ const BlockDragger = class {
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!Coordinate} pixelCoord A coordinate with x and y
* values in CSS pixel units.
* @return {!Coordinate} The input coordinate divided by the
* workspace scale.
* @protected
* @param pixelCoord A coordinate with x and y values in CSS pixel units.
* @return The input coordinate divided by the workspace scale.
*/
pixelsToWorkspaceUnits_(pixelCoord) {
protected pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
const result = new Coordinate(
pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
@@ -415,7 +375,7 @@ const BlockDragger = class {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same
// as the scale on the parent workspace. Fix that for dragging.
const mainScale = this.workspace_.options.parentWorkspace.scale;
const mainScale = this.workspace_.options.parentWorkspace!.scale;
result.scale(1 / mainScale);
}
return result;
@@ -423,11 +383,10 @@ const BlockDragger = class {
/**
* Move all of the icons connected to this drag.
* @param {!Coordinate} dxy How far to move the icons from their
* original positions, in workspace units.
* @protected
* @param dxy How far to move the icons from their original positions, in
* workspace units.
*/
dragIcons_(dxy) {
protected dragIcons_(dxy: Coordinate) {
// Moving icons moves their associated bubbles.
for (let i = 0; i < this.dragIconData_.length; i++) {
const data = this.dragIconData_[i];
@@ -438,11 +397,9 @@ const BlockDragger = class {
/**
* Get a list of the insertion markers that currently exist. Drags have 0, 1,
* or 2 insertion markers.
* @return {!Array<!BlockSvg>} A possibly empty list of insertion
* marker blocks.
* @public
* @return A possibly empty list of insertion marker blocks.
*/
getInsertionMarkers() {
getInsertionMarkers(): BlockSvg[] {
// No insertion markers with the old style of dragged connection managers.
if (this.draggedConnectionManager_ &&
this.draggedConnectionManager_.getInsertionMarkers) {
@@ -450,48 +407,41 @@ const BlockDragger = class {
}
return [];
}
};
}
/**
* Data about the position of a given icon.
* @typedef {{
* location:!Coordinate,
* icon:!Icon,
* }}
*/
let IconPositionData;
exports.IconPositionData = IconPositionData;
/** Data about the position of a given icon. */
export interface IconPositionData {
location: Coordinate;
icon: Icon;
}
/**
* Make a list of all of the icons (comment, warning, and mutator) that are
* on this block and its descendants. Moving an icon moves the bubble that
* extends from it if that bubble is open.
* @param {!BlockSvg} block The root block that is being dragged.
* @return {!Array<!IconPositionData>} The list of all icons and their
* locations.
* @param block The root block that is being dragged.
* @return The list of all icons and their locations.
*/
const initIconData = function(block) {
function initIconData(block: BlockSvg): IconPositionData[] {
// Build a list of icons that need to be moved and where they started.
const dragIconData = [];
const descendants =
/** @type {!Array<!BlockSvg>} */ (block.getDescendants(false));
const descendants = (block.getDescendants(false));
for (let i = 0, descendant; (descendant = descendants[i]); i++) {
for (let i = 0, descendant; descendant = descendants[i]; i++) {
const icons = descendant.getIcons();
for (let j = 0; j < icons.length; j++) {
const data = {
// Coordinate with x and y properties (workspace
// coordinates).
location: icons[j].getIconLocation(),
// Blockly.Icon
location: icons[j].getIconLocation(), // Blockly.Icon
icon: icons[j],
};
dragIconData.push(data);
}
}
return dragIconData;
};
// AnyDuringMigration because: Type '{ location: Coordinate | null; icon:
// Icon; }[]' is not assignable to type 'IconPositionData[]'.
return dragIconData as AnyDuringMigration;
}
registry.register(registry.Type.BLOCK_DRAGGER, registry.DEFAULT, BlockDragger);
exports.BlockDragger = BlockDragger;

File diff suppressed because it is too large Load Diff

890
core/blockly.js Normal file
View File

@@ -0,0 +1,890 @@
/**
* @license
* Copyright 2011 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileoverview The top level namespace used to access the Blockly library.
* @suppress {moduleImport}
*/
'use strict';
/**
* The top level namespace used to access the Blockly library.
* @namespace Blockly
*/
goog.module('Blockly');
goog.module.declareLegacyNamespace();
const ContextMenu = goog.require('Blockly.ContextMenu');
const ContextMenuItems = goog.require('Blockly.ContextMenuItems');
const Css = goog.require('Blockly.Css');
const Events = goog.require('Blockly.Events');
const Extensions = goog.require('Blockly.Extensions');
const Procedures = goog.require('Blockly.Procedures');
const ShortcutItems = goog.require('Blockly.ShortcutItems');
const Themes = goog.require('Blockly.Themes');
const Tooltip = goog.require('Blockly.Tooltip');
const Touch = goog.require('Blockly.Touch');
const Variables = goog.require('Blockly.Variables');
const VariablesDynamic = goog.require('Blockly.VariablesDynamic');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const Xml = goog.require('Blockly.Xml');
const blockAnimations = goog.require('Blockly.blockAnimations');
const blockRendering = goog.require('Blockly.blockRendering');
const browserEvents = goog.require('Blockly.browserEvents');
const bumpObjects = goog.require('Blockly.bumpObjects');
const clipboard = goog.require('Blockly.clipboard');
const colour = goog.require('Blockly.utils.colour');
const common = goog.require('Blockly.common');
const constants = goog.require('Blockly.constants');
const deprecation = goog.require('Blockly.utils.deprecation');
const dialog = goog.require('Blockly.dialog');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const geras = goog.require('Blockly.geras');
const internalConstants = goog.require('Blockly.internalConstants');
const minimalist = goog.require('Blockly.minimalist');
const registry = goog.require('Blockly.registry');
const serializationBlocks = goog.require('Blockly.serialization.blocks');
const serializationExceptions = goog.require('Blockly.serialization.exceptions');
const serializationPriorities = goog.require('Blockly.serialization.priorities');
const serializationRegistry = goog.require('Blockly.serialization.registry');
const serializationVariables = goog.require('Blockly.serialization.variables');
const serializationWorkspaces = goog.require('Blockly.serialization.workspaces');
const svgMath = goog.require('Blockly.utils.svgMath');
const thrasos = goog.require('Blockly.thrasos');
const toolbox = goog.require('Blockly.utils.toolbox');
const uiPosition = goog.require('Blockly.uiPosition');
const utils = goog.require('Blockly.utils');
const zelos = goog.require('Blockly.zelos');
const {Align, Input} = goog.require('Blockly.Input');
const {ASTNode} = goog.require('Blockly.ASTNode');
const {BasicCursor} = goog.require('Blockly.BasicCursor');
const {BlockDragSurfaceSvg} = goog.require('Blockly.BlockDragSurfaceSvg');
const {BlockDragger} = goog.require('Blockly.BlockDragger');
const {BlockSvg} = goog.require('Blockly.BlockSvg');
const {BlocklyOptions} = goog.require('Blockly.BlocklyOptions');
const {Blocks} = goog.require('Blockly.blocks');
const {Block} = goog.require('Blockly.Block');
const {BubbleDragger} = goog.require('Blockly.BubbleDragger');
const {Bubble} = goog.require('Blockly.Bubble');
const {CollapsibleToolboxCategory} = goog.require('Blockly.CollapsibleToolboxCategory');
const {Comment} = goog.require('Blockly.Comment');
const {ComponentManager} = goog.require('Blockly.ComponentManager');
const {config} = goog.require('Blockly.config');
const {ConnectionChecker} = goog.require('Blockly.ConnectionChecker');
const {ConnectionDB} = goog.require('Blockly.ConnectionDB');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {Connection} = goog.require('Blockly.Connection');
const {ContextMenuRegistry} = goog.require('Blockly.ContextMenuRegistry');
const {Cursor} = goog.require('Blockly.Cursor');
const {DeleteArea} = goog.require('Blockly.DeleteArea');
const {DragTarget} = goog.require('Blockly.DragTarget');
const {FieldAngle} = goog.require('Blockly.FieldAngle');
const {FieldCheckbox} = goog.require('Blockly.FieldCheckbox');
const {FieldColour} = goog.require('Blockly.FieldColour');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
const {FieldImage} = goog.require('Blockly.FieldImage');
const {FieldLabelSerializable} = goog.require('Blockly.FieldLabelSerializable');
const {FieldLabel} = goog.require('Blockly.FieldLabel');
const {FieldMultilineInput} = goog.require('Blockly.FieldMultilineInput');
const {FieldNumber} = goog.require('Blockly.FieldNumber');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {FieldVariable} = goog.require('Blockly.FieldVariable');
const {Field} = goog.require('Blockly.Field');
const {FlyoutButton} = goog.require('Blockly.FlyoutButton');
const {FlyoutMetricsManager} = goog.require('Blockly.FlyoutMetricsManager');
const {Flyout} = goog.require('Blockly.Flyout');
const {Generator} = goog.require('Blockly.Generator');
const {Gesture} = goog.require('Blockly.Gesture');
const {Grid} = goog.require('Blockly.Grid');
const {HorizontalFlyout} = goog.require('Blockly.HorizontalFlyout');
const {IASTNodeLocationSvg} = goog.require('Blockly.IASTNodeLocationSvg');
const {IASTNodeLocationWithBlock} = goog.require('Blockly.IASTNodeLocationWithBlock');
const {IASTNodeLocation} = goog.require('Blockly.IASTNodeLocation');
const {IAutoHideable} = goog.require('Blockly.IAutoHideable');
const {IBlockDragger} = goog.require('Blockly.IBlockDragger');
const {IBoundedElement} = goog.require('Blockly.IBoundedElement');
const {IBubble} = goog.require('Blockly.IBubble');
const {ICollapsibleToolboxItem} = goog.require('Blockly.ICollapsibleToolboxItem');
const {IComponent} = goog.require('Blockly.IComponent');
const {IConnectionChecker} = goog.require('Blockly.IConnectionChecker');
const {IContextMenu} = goog.require('Blockly.IContextMenu');
const {ICopyable} = goog.require('Blockly.ICopyable');
const {IDeletable} = goog.require('Blockly.IDeletable');
const {IDeleteArea} = goog.require('Blockly.IDeleteArea');
const {IDragTarget} = goog.require('Blockly.IDragTarget');
const {IDraggable} = goog.require('Blockly.IDraggable');
const {IFlyout} = goog.require('Blockly.IFlyout');
const {IKeyboardAccessible} = goog.require('Blockly.IKeyboardAccessible');
const {IMetricsManager} = goog.require('Blockly.IMetricsManager');
const {IMovable} = goog.require('Blockly.IMovable');
const {IPositionable} = goog.require('Blockly.IPositionable');
const {IRegistrableField} = goog.require('Blockly.IRegistrableField');
const {IRegistrable} = goog.require('Blockly.IRegistrable');
const {ISelectableToolboxItem} = goog.require('Blockly.ISelectableToolboxItem');
const {ISelectable} = goog.require('Blockly.ISelectable');
const {ISerializer} = goog.require('Blockly.serialization.ISerializer');
const {IStyleable} = goog.require('Blockly.IStyleable');
const {IToolboxItem} = goog.require('Blockly.IToolboxItem');
const {IToolbox} = goog.require('Blockly.IToolbox');
const {Icon} = goog.require('Blockly.Icon');
const {InsertionMarkerManager} = goog.require('Blockly.InsertionMarkerManager');
const {Marker} = goog.require('Blockly.Marker');
const {MarkerManager} = goog.require('Blockly.MarkerManager');
const {MenuItem} = goog.require('Blockly.MenuItem');
const {Menu} = goog.require('Blockly.Menu');
const {MetricsManager} = goog.require('Blockly.MetricsManager');
const {Mutator} = goog.require('Blockly.Mutator');
const {Msg} = goog.require('Blockly.Msg');
const {Names} = goog.require('Blockly.Names');
const {Options} = goog.require('Blockly.Options');
const {RenderedConnection} = goog.require('Blockly.RenderedConnection');
const {ScrollbarPair} = goog.require('Blockly.ScrollbarPair');
const {Scrollbar} = goog.require('Blockly.Scrollbar');
const {ShortcutRegistry} = goog.require('Blockly.ShortcutRegistry');
const {TabNavigateCursor} = goog.require('Blockly.TabNavigateCursor');
const {ThemeManager} = goog.require('Blockly.ThemeManager');
const {Theme} = goog.require('Blockly.Theme');
const {ToolboxCategory} = goog.require('Blockly.ToolboxCategory');
const {ToolboxItem} = goog.require('Blockly.ToolboxItem');
const {ToolboxSeparator} = goog.require('Blockly.ToolboxSeparator');
const {Toolbox} = goog.require('Blockly.Toolbox');
const {TouchGesture} = goog.require('Blockly.TouchGesture');
const {Trashcan} = goog.require('Blockly.Trashcan');
const {VariableMap} = goog.require('Blockly.VariableMap');
const {VariableModel} = goog.require('Blockly.VariableModel');
const {VerticalFlyout} = goog.require('Blockly.VerticalFlyout');
const {Warning} = goog.require('Blockly.Warning');
const {WorkspaceAudio} = goog.require('Blockly.WorkspaceAudio');
const {WorkspaceCommentSvg} = goog.require('Blockly.WorkspaceCommentSvg');
const {WorkspaceComment} = goog.require('Blockly.WorkspaceComment');
const {WorkspaceDragSurfaceSvg} = goog.require('Blockly.WorkspaceDragSurfaceSvg');
const {WorkspaceDragger} = goog.require('Blockly.WorkspaceDragger');
const {WorkspaceSvg, resizeSvgContents} = goog.require('Blockly.WorkspaceSvg');
const {Workspace} = goog.require('Blockly.Workspace');
const {ZoomControls} = goog.require('Blockly.ZoomControls');
const {inject} = goog.require('Blockly.inject');
const {inputTypes} = goog.require('Blockly.inputTypes');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockCreate');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.FinishedLoading');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.Ui');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.UiBase');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.VarCreate');
/**
* Blockly core version.
* This constant is overridden by the build script (npm run build) to the value
* of the version in package.json. This is done by the Closure Compiler in the
* buildCompressed gulp task.
* For local builds, you can pass --define='Blockly.VERSION=X.Y.Z' to the
* compiler to override this constant.
* @define {string}
* @alias Blockly.VERSION
*/
exports.VERSION = 'uncompiled';
/*
* Top-level functions and properties on the Blockly namespace.
* These are used only in external code. Do not reference these
* from internal code as importing from this file can cause circular
* dependencies. Do not add new functions here. There is probably a better
* namespace to put new functions on.
*/
/*
* Aliases for input alignments used in block defintions.
*/
/**
* @see Blockly.Input.Align.LEFT
* @alias Blockly.ALIGN_LEFT
*/
exports.ALIGN_LEFT = Align.LEFT;
/**
* @see Blockly.Input.Align.CENTRE
* @alias Blockly.ALIGN_CENTRE
*/
exports.ALIGN_CENTRE = Align.CENTRE;
/**
* @see Blockly.Input.Align.RIGHT
* @alias Blockly.ALIGN_RIGHT
*/
exports.ALIGN_RIGHT = Align.RIGHT;
/*
* Aliases for constants used for connection and input types.
*/
/**
* @see ConnectionType.INPUT_VALUE
* @alias Blockly.INPUT_VALUE
*/
exports.INPUT_VALUE = ConnectionType.INPUT_VALUE;
/**
* @see ConnectionType.OUTPUT_VALUE
* @alias Blockly.OUTPUT_VALUE
*/
exports.OUTPUT_VALUE = ConnectionType.OUTPUT_VALUE;
/**
* @see ConnectionType.NEXT_STATEMENT
* @alias Blockly.NEXT_STATEMENT
*/
exports.NEXT_STATEMENT = ConnectionType.NEXT_STATEMENT;
/**
* @see ConnectionType.PREVIOUS_STATEMENT
* @alias Blockly.PREVIOUS_STATEMENT
*/
exports.PREVIOUS_STATEMENT = ConnectionType.PREVIOUS_STATEMENT;
/**
* @see inputTypes.DUMMY_INPUT
* @alias Blockly.DUMMY_INPUT
*/
exports.DUMMY_INPUT = inputTypes.DUMMY;
/**
* Aliases for toolbox positions.
*/
/**
* @see toolbox.Position.TOP
* @alias Blockly.TOOLBOX_AT_TOP
*/
exports.TOOLBOX_AT_TOP = toolbox.Position.TOP;
/**
* @see toolbox.Position.BOTTOM
* @alias Blockly.TOOLBOX_AT_BOTTOM
*/
exports.TOOLBOX_AT_BOTTOM = toolbox.Position.BOTTOM;
/**
* @see toolbox.Position.LEFT
* @alias Blockly.TOOLBOX_AT_LEFT
*/
exports.TOOLBOX_AT_LEFT = toolbox.Position.LEFT;
/**
* @see toolbox.Position.RIGHT
* @alias Blockly.TOOLBOX_AT_RIGHT
*/
exports.TOOLBOX_AT_RIGHT = toolbox.Position.RIGHT;
/*
* Other aliased functions.
*/
/**
* Size the SVG image to completely fill its container. Call this when the view
* actually changes sizes (e.g. on a window resize/device orientation change).
* See workspace.resizeContents to resize the workspace when the contents
* change (e.g. when a block is added or removed).
* Record the height/width of the SVG image.
* @param {!WorkspaceSvg} workspace Any workspace in the SVG.
* @see Blockly.common.svgResize
* @alias Blockly.svgResize
*/
exports.svgResize = common.svgResize;
/**
* Close tooltips, context menus, dropdown selections, etc.
* @param {boolean=} opt_onlyClosePopups Whether only popups should be closed.
* @see Blockly.WorkspaceSvg.hideChaff
* @alias Blockly.hideChaff
*/
const hideChaff = function(opt_onlyClosePopups) {
/** @type {!WorkspaceSvg} */ (common.getMainWorkspace())
.hideChaff(opt_onlyClosePopups);
};
exports.hideChaff = hideChaff;
/**
* Returns the main workspace. Returns the last used main workspace (based on
* focus). Try not to use this function, particularly if there are multiple
* Blockly instances on a page.
* @return {!Workspace} The main workspace.
* @see Blockly.common.getMainWorkspace
* @alias Blockly.getMainWorkspace
*/
exports.getMainWorkspace = common.getMainWorkspace;
/**
* Define blocks from an array of JSON block definitions, as might be generated
* by the Blockly Developer Tools.
* @param {!Array<!Object>} jsonArray An array of JSON block definitions.
* @see Blockly.common.defineBlocksWithJsonArray
* @alias Blockly.defineBlocksWithJsonArray
*/
exports.defineBlocksWithJsonArray = common.defineBlocksWithJsonArray;
/**
* Set the parent container. This is the container element that the WidgetDiv,
* dropDownDiv, and Tooltip are rendered into the first time `Blockly.inject`
* is called.
* This method is a NOP if called after the first ``Blockly.inject``.
* @param {!Element} container The container element.
* @see Blockly.common.setParentContainer
* @alias Blockly.setParentContainer
*/
exports.setParentContainer = common.setParentContainer;
/*
* Aliased functions and properties that used to be on the Blockly namespace.
* Everything in this section is deprecated. Both external and internal code
* should avoid using these functions and use the designated replacements.
* Anything in this section may be removed in a future version of Blockly.
*/
// Add accessors for properties on Blockly that have now been deprecated.
Object.defineProperties(exports, {
/**
* Wrapper to window.alert() that app developers may override to
* provide alternatives to the modal browser window.
* @name Blockly.alert
* @type {!function(string, function()=)}
* @deprecated Use Blockly.dialog.alert / .setAlert() instead.
* (December 2021)
* @suppress {checkTypes}
*/
alert: {
set: function(newAlert) {
deprecation.warn('Blockly.alert', 'December 2021', 'December 2022');
dialog.setAlert(newAlert);
},
get: function() {
deprecation.warn(
'Blockly.alert', 'December 2021', 'December 2022',
'Blockly.dialog.alert()');
return dialog.alert;
},
},
/**
* Wrapper to window.confirm() that app developers may override to
* provide alternatives to the modal browser window.
* @name Blockly.confirm
* @type {!function(string, function()=)}
* @deprecated Use Blockly.dialog.confirm / .setConfirm() instead.
* (December 2021)
* @suppress {checkTypes}
*/
confirm: {
set: function(newConfirm) {
deprecation.warn('Blockly.confirm', 'December 2021', 'December 2022');
dialog.setConfirm(newConfirm);
},
get: function() {
deprecation.warn(
'Blockly.confirm', 'December 2021', 'December 2022',
'Blockly.dialog.confirm()');
return dialog.confirm;
},
},
/**
* The main workspace most recently used.
* Set by Blockly.WorkspaceSvg.prototype.markFocused
* @name Blockly.mainWorkspace
* @type {Workspace}
* @suppress {checkTypes}
*/
mainWorkspace: {
set: function(x) {
common.setMainWorkspace(x);
},
get: function() {
return common.getMainWorkspace();
},
},
/**
* Wrapper to window.prompt() that app developers may override to
* provide alternatives to the modal browser window. Built-in
* browser prompts are often used for better text input experience
* on mobile device. We strongly recommend testing mobile when
* overriding this.
* @name Blockly.prompt
* @type {!function(string, string, function()=)}
* @deprecated Use Blockly.dialog.prompt / .setPrompt() instead.
* (December 2021)
* @suppress {checkTypes}
*/
prompt: {
set: function(newPrompt) {
deprecation.warn('Blockly.prompt', 'December 2021', 'December 2022');
dialog.setPrompt(newPrompt);
},
get: function() {
deprecation.warn(
'Blockly.prompt', 'December 2021', 'December 2022',
'Blockly.dialog.prompt()');
return dialog.prompt;
},
},
/**
* Currently selected block.
* @name Blockly.selected
* @type {?ICopyable}
* @suppress {checkTypes}
*/
selected: {
get: function() {
return common.getSelected();
},
set: function(newSelection) {
common.setSelected(newSelection);
},
},
/**
* The richness of block colours, regardless of the hue.
* Must be in the range of 0 (inclusive) to 1 (exclusive).
* @name Blockly.HSV_SATURATION
* @type {number}
* @suppress {checkTypes}
*/
HSV_SATURATION: {
get: function() {
return utils.colour.getHsvSaturation();
},
set: function(newValue) {
utils.colour.setHsvSaturation(newValue);
},
},
/**
* The intensity of block colours, regardless of the hue.
* Must be in the range of 0 (inclusive) to 1 (exclusive).
* @name Blockly.HSV_VALUE
* @type {number}
* @suppress {checkTypes}
*/
HSV_VALUE: {
get: function() {
return utils.colour.getHsvValue();
},
set: function(newValue) {
utils.colour.setHsvValue(newValue);
},
},
});
/**
* Returns the dimensions of the specified SVG image.
* @param {!SVGElement} svg SVG image.
* @return {!Size} Contains width and height properties.
* @deprecated Use workspace.setCachedParentSvgSize. (2021 March 5)
* @see Blockly.WorkspaceSvg.setCachedParentSvgSize
* @alias Blockly.svgSize
*/
exports.svgSize = svgMath.svgSize;
/**
* Size the workspace when the contents change. This also updates
* scrollbars accordingly.
* @param {!WorkspaceSvg} workspace The workspace to resize.
* @deprecated Use workspace.resizeContents. (2021 December)
* @see Blockly.WorkspaceSvg.resizeContents
* @alias Blockly.resizeSvgContents
*/
const resizeSvgContentsLocal = function(workspace) {
deprecation.warn(
'Blockly.resizeSvgContents', 'December 2021', 'December 2022',
'Blockly.WorkspaceSvg.resizeSvgContents');
resizeSvgContents(workspace);
};
exports.resizeSvgContents = resizeSvgContentsLocal;
/**
* Copy a block or workspace comment onto the local clipboard.
* @param {!ICopyable} toCopy Block or Workspace Comment to be copied.
* @deprecated Use Blockly.clipboard.copy(). (2021 December)
* @see Blockly.clipboard.copy
* @alias Blockly.copy
*/
const copy = function(toCopy) {
deprecation.warn(
'Blockly.copy', 'December 2021', 'December 2022',
'Blockly.clipboard.copy');
clipboard.copy(toCopy);
};
exports.copy = copy;
/**
* Paste a block or workspace comment on to the main workspace.
* @return {boolean} True if the paste was successful, false otherwise.
* @deprecated Use Blockly.clipboard.paste(). (2021 December)
* @see Blockly.clipboard.paste
* @alias Blockly.paste
*/
const paste = function() {
deprecation.warn(
'Blockly.paste', 'December 2021', 'December 2022',
'Blockly.clipboard.paste');
return !!clipboard.paste();
};
exports.paste = paste;
/**
* Duplicate this block and its children, or a workspace comment.
* @param {!ICopyable} toDuplicate Block or Workspace Comment to be
* copied.
* @deprecated Use Blockly.clipboard.duplicate(). (2021 December)
* @see Blockly.clipboard.duplicate
* @alias Blockly.duplicate
*/
const duplicate = function(toDuplicate) {
deprecation.warn(
'Blockly.duplicate', 'December 2021', 'December 2022',
'Blockly.clipboard.duplicate');
clipboard.duplicate(toDuplicate);
};
exports.duplicate = duplicate;
/**
* Is the given string a number (includes negative and decimals).
* @param {string} str Input string.
* @return {boolean} True if number, false otherwise.
* @deprecated Use Blockly.utils.string.isNumber(str). (2021 December)
* @see Blockly.utils.string.isNumber
* @alias Blockly.isNumber
*/
const isNumber = function(str) {
deprecation.warn(
'Blockly.isNumber', 'December 2021', 'December 2022',
'Blockly.utils.string.isNumber');
return utils.string.isNumber(str);
};
exports.isNumber = isNumber;
/**
* Convert a hue (HSV model) into an RGB hex triplet.
* @param {number} hue Hue on a colour wheel (0-360).
* @return {string} RGB code, e.g. '#5ba65b'.
* @deprecated Use Blockly.utils.colour.hueToHex(). (2021 December)
* @see Blockly.utils.colour.hueToHex
* @alias Blockly.hueToHex
*/
const hueToHex = function(hue) {
deprecation.warn(
'Blockly.hueToHex', 'December 2021', 'December 2022',
'Blockly.utils.colour.hueToHex');
return colour.hueToHex(hue);
};
exports.hueToHex = hueToHex;
/**
* Bind an event handler that should be called regardless of whether it is part
* of the active touch stream.
* Use this for events that are not part of a multi-part gesture (e.g.
* mouseover for tooltips).
* @param {!EventTarget} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {?Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @return {!browserEvents.Data} Opaque data that can be passed to
* unbindEvent_.
* @deprecated Use Blockly.browserEvents.bind(). (December 2021)
* @see Blockly.browserEvents.bind
* @alias Blockly.bindEvent_
*/
const bindEvent_ = function(node, name, thisObject, func) {
deprecation.warn(
'Blockly.bindEvent_', 'December 2021', 'December 2022',
'Blockly.browserEvents.bind');
return browserEvents.bind(node, name, thisObject, func);
};
exports.bindEvent_ = bindEvent_;
/**
* Unbind one or more events event from a function call.
* @param {!browserEvents.Data} bindData Opaque data from bindEvent_.
* This list is emptied during the course of calling this function.
* @return {!Function} The function call.
* @deprecated Use Blockly.browserEvents.unbind(). (December 2021)
* @see browserEvents.unbind
* @alias Blockly.unbindEvent_
*/
const unbindEvent_ = function(bindData) {
deprecation.warn(
'Blockly.unbindEvent_', 'December 2021', 'December 2022',
'Blockly.browserEvents.unbind');
return browserEvents.unbind(bindData);
};
exports.unbindEvent_ = unbindEvent_;
/**
* Bind an event handler that can be ignored if it is not part of the active
* touch stream.
* Use this for events that either start or continue a multi-part gesture (e.g.
* mousedown or mousemove, which may be part of a drag or click).
* @param {!EventTarget} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {?Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @param {boolean=} 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 {boolean=} opt_noPreventDefault True if triggering on this event
* should prevent the default handler. False by default. If
* opt_noPreventDefault is provided, opt_noCaptureIdentifier must also be
* provided.
* @return {!browserEvents.Data} Opaque data that can be passed to
* unbindEvent_.
* @deprecated Use Blockly.browserEvents.conditionalBind(). (December 2021)
* @see browserEvents.conditionalBind
* @alias Blockly.bindEventWithChecks_
*/
const bindEventWithChecks_ = function(
node, name, thisObject, func, opt_noCaptureIdentifier,
opt_noPreventDefault) {
deprecation.warn(
'Blockly.bindEventWithChecks_', 'December 2021', 'December 2022',
'Blockly.browserEvents.conditionalBind');
return browserEvents.conditionalBind(
node, name, thisObject, func, opt_noCaptureIdentifier,
opt_noPreventDefault);
};
exports.bindEventWithChecks_ = bindEventWithChecks_;
// Aliases to allow external code to access these values for legacy reasons.
exports.COLLAPSE_CHARS = internalConstants.COLLAPSE_CHARS;
exports.DRAG_STACK = internalConstants.DRAG_STACK;
exports.OPPOSITE_TYPE = internalConstants.OPPOSITE_TYPE;
exports.RENAME_VARIABLE_ID = internalConstants.RENAME_VARIABLE_ID;
exports.DELETE_VARIABLE_ID = internalConstants.DELETE_VARIABLE_ID;
exports.COLLAPSED_INPUT_NAME = constants.COLLAPSED_INPUT_NAME;
exports.COLLAPSED_FIELD_NAME = constants.COLLAPSED_FIELD_NAME;
/**
* String for use in the "custom" attribute of a category in toolbox XML.
* This string indicates that the category should be dynamically populated with
* variable blocks.
* @const {string}
* @alias Blockly.VARIABLE_CATEGORY_NAME
*/
exports.VARIABLE_CATEGORY_NAME = Variables.CATEGORY_NAME;
/**
* String for use in the "custom" attribute of a category in toolbox XML.
* This string indicates that the category should be dynamically populated with
* variable blocks.
* @const {string}
* @alias Blockly.VARIABLE_DYNAMIC_CATEGORY_NAME
*/
exports.VARIABLE_DYNAMIC_CATEGORY_NAME = VariablesDynamic.CATEGORY_NAME;
/**
* String for use in the "custom" attribute of a category in toolbox XML.
* This string indicates that the category should be dynamically populated with
* procedure blocks.
* @const {string}
* @alias Blockly.PROCEDURE_CATEGORY_NAME
*/
exports.PROCEDURE_CATEGORY_NAME = Procedures.CATEGORY_NAME;
// Re-export submodules that no longer declareLegacyNamespace.
exports.ASTNode = ASTNode;
exports.BasicCursor = BasicCursor;
exports.Block = Block;
exports.BlocklyOptions = BlocklyOptions;
exports.BlockDragger = BlockDragger;
exports.BlockDragSurfaceSvg = BlockDragSurfaceSvg;
exports.BlockSvg = BlockSvg;
exports.Blocks = Blocks;
exports.Bubble = Bubble;
exports.BubbleDragger = BubbleDragger;
exports.CollapsibleToolboxCategory = CollapsibleToolboxCategory;
exports.Comment = Comment;
exports.ComponentManager = ComponentManager;
exports.Connection = Connection;
exports.ConnectionType = ConnectionType;
exports.ConnectionChecker = ConnectionChecker;
exports.ConnectionDB = ConnectionDB;
exports.ContextMenu = ContextMenu;
exports.ContextMenuItems = ContextMenuItems;
exports.ContextMenuRegistry = ContextMenuRegistry;
exports.Css = Css;
exports.Cursor = Cursor;
exports.DeleteArea = DeleteArea;
exports.DragTarget = DragTarget;
exports.DropDownDiv = dropDownDiv;
exports.Events = Events;
exports.Extensions = Extensions;
exports.Field = Field;
exports.FieldAngle = FieldAngle;
exports.FieldCheckbox = FieldCheckbox;
exports.FieldColour = FieldColour;
exports.FieldDropdown = FieldDropdown;
exports.FieldImage = FieldImage;
exports.FieldLabel = FieldLabel;
exports.FieldLabelSerializable = FieldLabelSerializable;
exports.FieldMultilineInput = FieldMultilineInput;
exports.FieldNumber = FieldNumber;
exports.FieldTextInput = FieldTextInput;
exports.FieldVariable = FieldVariable;
exports.Flyout = Flyout;
exports.FlyoutButton = FlyoutButton;
exports.FlyoutMetricsManager = FlyoutMetricsManager;
exports.Generator = Generator;
exports.Gesture = Gesture;
exports.Grid = Grid;
exports.HorizontalFlyout = HorizontalFlyout;
exports.IASTNodeLocation = IASTNodeLocation;
exports.IASTNodeLocationSvg = IASTNodeLocationSvg;
exports.IASTNodeLocationWithBlock = IASTNodeLocationWithBlock;
exports.IAutoHideable = IAutoHideable;
exports.IBlockDragger = IBlockDragger;
exports.IBoundedElement = IBoundedElement;
exports.IBubble = IBubble;
exports.ICollapsibleToolboxItem = ICollapsibleToolboxItem;
exports.IComponent = IComponent;
exports.IConnectionChecker = IConnectionChecker;
exports.IContextMenu = IContextMenu;
exports.Icon = Icon;
exports.ICopyable = ICopyable;
exports.IDeletable = IDeletable;
exports.IDeleteArea = IDeleteArea;
exports.IDragTarget = IDragTarget;
exports.IDraggable = IDraggable;
exports.IFlyout = IFlyout;
exports.IKeyboardAccessible = IKeyboardAccessible;
exports.IMetricsManager = IMetricsManager;
exports.IMovable = IMovable;
exports.Input = Input;
exports.InsertionMarkerManager = InsertionMarkerManager;
exports.IPositionable = IPositionable;
exports.IRegistrable = IRegistrable;
exports.IRegistrableField = IRegistrableField;
exports.ISelectable = ISelectable;
exports.ISelectableToolboxItem = ISelectableToolboxItem;
exports.IStyleable = IStyleable;
exports.IToolbox = IToolbox;
exports.IToolboxItem = IToolboxItem;
exports.Marker = Marker;
exports.MarkerManager = MarkerManager;
exports.Menu = Menu;
exports.MenuItem = MenuItem;
exports.MetricsManager = MetricsManager;
exports.Mutator = Mutator;
exports.Msg = Msg;
exports.Names = Names;
exports.Options = Options;
exports.Procedures = Procedures;
exports.RenderedConnection = RenderedConnection;
exports.Scrollbar = Scrollbar;
exports.ScrollbarPair = ScrollbarPair;
exports.ShortcutItems = ShortcutItems;
exports.ShortcutRegistry = ShortcutRegistry;
exports.TabNavigateCursor = TabNavigateCursor;
exports.Theme = Theme;
exports.Themes = Themes;
exports.ThemeManager = ThemeManager;
exports.Toolbox = Toolbox;
exports.ToolboxCategory = ToolboxCategory;
exports.ToolboxItem = ToolboxItem;
exports.ToolboxSeparator = ToolboxSeparator;
exports.Tooltip = Tooltip;
exports.Touch = Touch;
exports.TouchGesture = TouchGesture;
exports.Trashcan = Trashcan;
exports.VariableMap = VariableMap;
exports.VariableModel = VariableModel;
exports.Variables = Variables;
exports.VariablesDynamic = VariablesDynamic;
exports.VerticalFlyout = VerticalFlyout;
exports.Warning = Warning;
exports.WidgetDiv = WidgetDiv;
exports.Workspace = Workspace;
exports.WorkspaceAudio = WorkspaceAudio;
exports.WorkspaceComment = WorkspaceComment;
exports.WorkspaceCommentSvg = WorkspaceCommentSvg;
exports.WorkspaceDragSurfaceSvg = WorkspaceDragSurfaceSvg;
exports.WorkspaceDragger = WorkspaceDragger;
exports.WorkspaceSvg = WorkspaceSvg;
exports.Xml = Xml;
exports.ZoomControls = ZoomControls;
exports.blockAnimations = blockAnimations;
exports.blockRendering = blockRendering;
exports.browserEvents = browserEvents;
exports.bumpObjects = bumpObjects;
exports.clipboard = clipboard;
exports.common = common;
exports.config = config;
/** @deprecated Use Blockly.ConnectionType instead. */
exports.connectionTypes = ConnectionType;
exports.constants = constants;
exports.dialog = dialog;
exports.fieldRegistry = fieldRegistry;
exports.geras = geras;
exports.inject = inject;
exports.inputTypes = inputTypes;
exports.minimalist = minimalist;
exports.registry = registry;
exports.serialization = {
blocks: serializationBlocks,
exceptions: serializationExceptions,
priorities: serializationPriorities,
registry: serializationRegistry,
variables: serializationVariables,
workspaces: serializationWorkspaces,
ISerializer: ISerializer,
};
exports.thrasos = thrasos;
exports.uiPosition = uiPosition;
exports.utils = utils;
exports.zelos = zelos;
// If Blockly is compiled with ADVANCED_COMPILATION and/or loaded as a
// CJS or ES module there will not be a Blockly global variable
// created. This can cause problems because a very common way of
// loading translations is to use a <script> tag to load one of
// msg/js/*.js, which consists of lines like:
//
// Blockly.Msg["ADD_COMMENT"] = "Add Comment";
// Blockly.Msg["CLEAN_UP"] = "Clean up Blocks";
//
// This obviously only works if Blockly.Msg is the Msg export from the
// Blockly.Msg module - so make sure it is, but only if there is not
// yet a Blockly global variable.
if (!('Blockly' in globalThis)) {
globalThis['Blockly'] = {'Msg': Msg};
}
// Temporary hack to copy accessor properties from exports to the
// global Blockly object as the routine to copy exports in
// goog.exportPath_ (see closure/goog/base.js) invoked by
// declareLegacyNamespace only copies normal data properties, not
// accessors. This can be removed once all remaining calls to
// declareLegacyNamspace have been removed.
//
// This is only needed in uncompiled mode (see
// google/blockly-samples#902); in compiled mode the exports object is
// already the value of globalThis['Blockly'].
//
// Note that this code will still attempt to redefine accessors on a
// previously-imported copy of the Blockly library if both are
// imported in uncompiled mode. This will fail with TypeError as the
// accessors are nonconfigurable (which is good, as otherwise one
// accessors on one copy would call get/set functions on the other
// copy!)
/* eslint-disable-next-line no-undef */
if (!COMPILED && typeof globalThis['Blockly'] === 'object' &&
globalThis['Blockly'] !== exports) {
const descriptors = Object.getOwnPropertyDescriptors(exports);
const accessors = {};
for (const key in descriptors) {
if (descriptors[key].get || descriptors[key].set) {
accessors[key] = descriptors[key];
}
}
Object.defineProperties(globalThis['Blockly'], accessors);
}

File diff suppressed because it is too large Load Diff

View File

@@ -7,22 +7,74 @@
/**
* @fileoverview Object that defines user-specified options for the workspace.
*/
'use strict';
/**
* Object that defines user-specified options for the workspace.
* @namespace Blockly.BlocklyOptions
*/
goog.module('Blockly.BlocklyOptions');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BlocklyOptions');
import {Theme} from './theme.js';
import {Workspace} from './workspace.js';
/**
* Blockly options.
* This interface is further described in
* `typings/parts/blockly-interfaces.d.ts`.
* @interface
* @alias Blockly.BlocklyOptions
*/
const BlocklyOptions = function() {};
export interface BlocklyOptions {
collapse?: boolean;
comments?: boolean;
cs?: boolean;
disable?: boolean;
grid?: GridOptions;
horizontalLayout?: boolean;
maxBlocks?: number;
maxInstances?: {[blockType: string]: number};
media?: string;
move?: MoveOptions;
oneBasedIndex?: boolean;
readOnly?: boolean;
renderer?: string;
rendererOverrides?: {[rendererConstant: string]: any};
rtl?: boolean;
scrollbars?: ScrollbarOptions|boolean;
sounds?: boolean;
theme?: Theme;
toolbox?: string|object|Element;
toolboxPosition?: string;
trashcan?: boolean;
maxTrashcanContents?: boolean;
plugins?: object;
zoom?: ZoomOptions;
parentWorkspace?: Workspace;
}
exports.BlocklyOptions = BlocklyOptions;
export interface GridOptions {
colour?: string;
length?: number;
snap?: boolean;
spacing?: number;
}
export interface MoveOptions {
drag?: boolean;
scrollbars?: boolean|ScrollbarOptions;
wheel?: boolean;
}
export interface ScrollbarOptions {
horizontal?: boolean;
vertical?: boolean;
}
export interface ZoomOptions {
controls?: boolean;
maxScale?: number;
minScale?: number;
pinch?: boolean;
scaleSpeed?: number;
startScale?: number;
wheel?: boolean;
}

View File

@@ -7,27 +7,23 @@
/**
* @fileoverview A mapping of block type names to block prototype objects.
*/
'use strict';
/**
* A mapping of block type names to block prototype objects.
* @namespace Blockly.blocks
*/
goog.module('Blockly.blocks');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.blocks');
/**
* A block definition. For now this very lose, but it can potentially
* be refined e.g. by replacing this typedef with a class definition.
* @typedef {!Object}
*/
let BlockDefinition;
exports.BlockDefinition = BlockDefinition;
export type BlockDefinition = AnyDuringMigration;
/**
* A mapping of block type names to block prototype objects.
* @type {!Object<string,!BlockDefinition>}
* @alias Blockly.blocks.Blocks
*/
const Blocks = Object.create(null);
exports.Blocks = Blocks;
export const Blocks: {[key: string]: BlockDefinition} = Object.create(null);

View File

@@ -7,33 +7,29 @@
/**
* @fileoverview Browser event handling.
*/
'use strict';
/**
* Browser event handling.
* @namespace Blockly.browserEvents
*/
goog.module('Blockly.browserEvents');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.browserEvents');
const Touch = goog.require('Blockly.Touch');
const userAgent = goog.require('Blockly.utils.userAgent');
import * as Touch from './touch.js';
import * as userAgent from './utils/useragent.js';
/**
* Blockly opaque event data used to unbind events when using
* `bind` and `conditionalBind`.
* @typedef {!Array<!Array>}
* @alias Blockly.browserEvents.Data
*/
let Data;
exports.Data = Data;
export type Data = AnyDuringMigration[][];
/**
* The multiplier for scroll wheel deltas using the line delta mode.
* See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode
* for more information on deltaMode.
* @type {number}
* @const
*/
const LINE_MODE_MULTIPLIER = 40;
@@ -41,8 +37,6 @@ const LINE_MODE_MULTIPLIER = 40;
* The multiplier for scroll wheel deltas using the page delta mode.
* See https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent/deltaMode
* for more information on deltaMode.
* @type {number}
* @const
*/
const PAGE_MODE_MULTIPLIER = 125;
@@ -51,26 +45,25 @@ const PAGE_MODE_MULTIPLIER = 125;
* touch stream.
* Use this for events that either start or continue a multi-part gesture (e.g.
* mousedown or mousemove, which may be part of a drag or click).
* @param {!EventTarget} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {?Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @param {boolean=} 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 {boolean=} opt_noPreventDefault True if triggering on this event
* should prevent the default handler. False by default. If
* opt_noPreventDefault is provided, opt_noCaptureIdentifier must also be
* provided.
* @return {!Data} Opaque data that can be passed to
* unbindEvent_.
* @param node Node upon which to listen.
* @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 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 opt_noPreventDefault True if triggering on this event should prevent
* the default handler. False by default. If opt_noPreventDefault is
* provided, opt_noCaptureIdentifier must also be provided.
* @return Opaque data that can be passed to unbindEvent_.
* @alias Blockly.browserEvents.conditionalBind
*/
const conditionalBind = function(
node, name, thisObject, func, opt_noCaptureIdentifier,
opt_noPreventDefault) {
export function conditionalBind(
node: EventTarget, name: string, thisObject: AnyDuringMigration|null,
func: Function, opt_noCaptureIdentifier?: boolean,
opt_noPreventDefault?: boolean): Data {
let handled = false;
const wrapFunc = function(e) {
function wrapFunc(e: AnyDuringMigration) {
const captureIdentifier = !opt_noCaptureIdentifier;
// Handle each touch point separately. If the event was a mouse event, this
// will hand back an array with one element, which we're fine handling.
@@ -88,10 +81,10 @@ const conditionalBind = function(
}
handled = true;
}
};
}
const bindData = [];
if (globalThis['PointerEvent'] && (name in Touch.TOUCH_MAP)) {
if (globalThis['PointerEvent'] && 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);
@@ -103,7 +96,7 @@ const conditionalBind = function(
// Add equivalent touch event.
if (name in Touch.TOUCH_MAP) {
const touchWrapFunc = function(e) {
const touchWrapFunc = (e: AnyDuringMigration) => {
wrapFunc(e);
// Calling preventDefault stops the browser from scrolling/zooming the
// page.
@@ -120,34 +113,33 @@ const conditionalBind = function(
}
}
return bindData;
};
exports.conditionalBind = conditionalBind;
}
/**
* Bind an event handler that should be called regardless of whether it is part
* of the active touch stream.
* Use this for events that are not part of a multi-part gesture (e.g.
* mouseover for tooltips).
* @param {!EventTarget} node Node upon which to listen.
* @param {string} name Event name to listen to (e.g. 'mousedown').
* @param {?Object} thisObject The value of 'this' in the function.
* @param {!Function} func Function to call when event is triggered.
* @return {!Data} Opaque data that can be passed to
* unbindEvent_.
* @param node Node upon which to listen.
* @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.
* @return Opaque data that can be passed to unbindEvent_.
* @alias Blockly.browserEvents.bind
*/
const bind = function(node, name, thisObject, func) {
const wrapFunc = function(e) {
export function bind(
node: EventTarget, name: string, thisObject: AnyDuringMigration|null,
func: Function): Data {
function wrapFunc(e: AnyDuringMigration) {
if (thisObject) {
func.call(thisObject, e);
} else {
func(e);
}
};
}
const bindData = [];
if (globalThis['PointerEvent'] && (name in Touch.TOUCH_MAP)) {
if (globalThis['PointerEvent'] && 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);
@@ -159,7 +151,7 @@ const bind = function(node, name, thisObject, func) {
// Add equivalent touch event.
if (name in Touch.TOUCH_MAP) {
const touchWrapFunc = function(e) {
const touchWrapFunc = (e: AnyDuringMigration) => {
// Punt on multitouch events.
if (e.changedTouches && e.changedTouches.length === 1) {
// Map the touch event's properties to the event.
@@ -180,36 +172,34 @@ const bind = function(node, name, thisObject, func) {
}
}
return bindData;
};
exports.bind = bind;
}
/**
* Unbind one or more events event from a function call.
* @param {!Data} bindData Opaque data from bindEvent_.
* @param bindData Opaque data from bindEvent_.
* This list is emptied during the course of calling this function.
* @return {!Function} The function call.
* @return The function call.
* @alias Blockly.browserEvents.unbind
*/
const unbind = function(bindData) {
export function unbind(bindData: Data): Function {
let func;
while (bindData.length) {
const bindDatum = bindData.pop();
const node = bindDatum[0];
const name = bindDatum[1];
func = bindDatum[2];
const node = bindDatum![0];
const name = bindDatum![1];
func = bindDatum![2];
node.removeEventListener(name, func, false);
}
return func;
};
exports.unbind = unbind;
}
/**
* Returns true if this event is targeting a text input widget?
* @param {!Event} e An event.
* @return {boolean} True if text input.
* @param e An event.
* @return True if text input.
* @alias Blockly.browserEvents.isTargetInput
*/
const isTargetInput = function(e) {
export function isTargetInput(e: Event): boolean {
if (e.target instanceof HTMLElement) {
if (e.target.isContentEditable ||
e.target.getAttribute('data-is-text-input') === 'true') {
@@ -230,54 +220,59 @@ const isTargetInput = function(e) {
}
return false;
};
exports.isTargetInput = isTargetInput;
}
/**
* Returns true this event is a right-click.
* @param {!Event} e Mouse event.
* @return {boolean} True if right-click.
* @param e Mouse event.
* @return True if right-click.
* @alias Blockly.browserEvents.isRightButton
*/
const isRightButton = function(e) {
if (e.ctrlKey && userAgent.MAC) {
export function isRightButton(e: Event): boolean {
// AnyDuringMigration because: Property 'ctrlKey' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).ctrlKey && userAgent.MAC) {
// Control-clicking on Mac OS X is treated as a right-click.
// WebKit on Mac OS X fails to change button to 2 (but Gecko does).
return true;
}
return e.button === 2;
};
exports.isRightButton = isRightButton;
// AnyDuringMigration because: Property 'button' does not exist on type
// 'Event'.
return (e as AnyDuringMigration).button === 2;
}
/**
* Returns the converted coordinates of the given mouse event.
* The origin (0,0) is the top-left corner of the Blockly SVG.
* @param {!Event} e Mouse event.
* @param {!SVGSVGElement} svg SVG element.
* @param {?SVGMatrix} matrix Inverted screen CTM to use.
* @return {!SVGPoint} Object with .x and .y properties.
* @param e Mouse event.
* @param svg SVG element.
* @param matrix Inverted screen CTM to use.
* @return Object with .x and .y properties.
* @alias Blockly.browserEvents.mouseToSvg
*/
const mouseToSvg = function(e, svg, matrix) {
export function mouseToSvg(
e: Event, svg: SVGSVGElement, matrix: SVGMatrix|null): SVGPoint {
const svgPoint = svg.createSVGPoint();
svgPoint.x = e.clientX;
svgPoint.y = e.clientY;
// AnyDuringMigration because: Property 'clientX' does not exist on type
// 'Event'.
svgPoint.x = (e as AnyDuringMigration).clientX;
// AnyDuringMigration because: Property 'clientY' does not exist on type
// 'Event'.
svgPoint.y = (e as AnyDuringMigration).clientY;
if (!matrix) {
matrix = svg.getScreenCTM().inverse();
matrix = svg.getScreenCTM()!.inverse();
}
return svgPoint.matrixTransform(matrix);
};
exports.mouseToSvg = mouseToSvg;
}
/**
* Returns the scroll delta of a mouse event in pixel units.
* @param {!WheelEvent} e Mouse event.
* @return {{x: number, y: number}} Scroll delta object with .x and .y
* properties.
* @param e Mouse event.
* @return Scroll delta object with .x and .y properties.
* @alias Blockly.browserEvents.getScrollDeltaPixels
*/
const getScrollDeltaPixels = function(e) {
export function getScrollDeltaPixels(e: WheelEvent): {x: number, y: number} {
switch (e.deltaMode) {
case 0x00: // Pixel mode.
default:
@@ -293,5 +288,4 @@ const getScrollDeltaPixels = function(e) {
y: e.deltaY * PAGE_MODE_MULTIPLIER,
};
}
};
exports.getScrollDeltaPixels = getScrollDeltaPixels;
}

View File

@@ -7,175 +7,161 @@
/**
* @fileoverview Object representing a UI bubble.
*/
'use strict';
/**
* Object representing a UI bubble.
* @class
*/
goog.module('Blockly.Bubble');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Bubble');
const Touch = goog.require('Blockly.Touch');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const math = goog.require('Blockly.utils.math');
const userAgent = goog.require('Blockly.utils.userAgent');
/* eslint-disable-next-line no-unused-vars */
const {BlockDragSurfaceSvg} = goog.requireType('Blockly.BlockDragSurfaceSvg');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
const {IBubble} = goog.require('Blockly.IBubble');
/* eslint-disable-next-line no-unused-vars */
const {MetricsManager} = goog.requireType('Blockly.MetricsManager');
const {Scrollbar} = goog.require('Blockly.Scrollbar');
const {Size} = goog.require('Blockly.utils.Size');
const {Svg} = goog.require('Blockly.utils.Svg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/** @suppress {extraRequire} */
goog.require('Blockly.Workspace');
// Unused import preserved for side-effects. Remove if unneeded.
// import './metrics_manager.js';
// Unused import preserved for side-effects. Remove if unneeded.
// import './workspace.js';
import type {BlockDragSurfaceSvg} from './block_drag_surface.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import type {IBubble} from './interfaces/i_bubble.js';
import type {ContainerRegion} from './metrics_manager.js';
import {Scrollbar} from './scrollbar.js';
import * as Touch from './touch.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as math from './utils/math.js';
import {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Class for UI bubble.
* @implements {IBubble}
* @alias Blockly.Bubble
*/
const Bubble = class {
export class Bubble implements IBubble {
/** Width of the border around the bubble. */
static BORDER_WIDTH = 6;
/**
* @param {!WorkspaceSvg} workspace The workspace on which to draw the
* bubble.
* @param {!SVGElement} content SVG content for the bubble.
* @param {!SVGElement} shape SVG element to avoid eclipsing.
* @param {!Coordinate} anchorXY Absolute position of bubble's
* anchor point.
* @param {?number} bubbleWidth Width of bubble, or null if not resizable.
* @param {?number} bubbleHeight Height of bubble, or null if not resizable.
* Determines the thickness of the base of the arrow in relation to the size
* of the bubble. Higher numbers result in thinner arrows.
*/
static ARROW_THICKNESS = 5;
/** The number of degrees that the arrow bends counter-clockwise. */
static ARROW_ANGLE = 20;
/**
* The sharpness of the arrow's bend. Higher numbers result in smoother
* arrows.
*/
static ARROW_BEND = 4;
/** Distance between arrow point and anchor point. */
static ANCHOR_RADIUS = 8;
/** Mouse up event data. */
private static onMouseUpWrapper_: browserEvents.Data|null = null;
/** Mouse move event data. */
private static onMouseMoveWrapper_: browserEvents.Data|null = null;
workspace_: AnyDuringMigration;
content_: AnyDuringMigration;
shape_: AnyDuringMigration;
/** Flag to stop incremental rendering during construction. */
private readonly rendered_: boolean;
/** The SVG group containing all parts of the bubble. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGGElement'.
private bubbleGroup_: SVGGElement = null as AnyDuringMigration;
/**
* The SVG path for the arrow from the bubble to the icon on the block.
*/
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGPathElement'.
private bubbleArrow_: SVGPathElement = null as AnyDuringMigration;
/** The SVG rect for the main body of the bubble. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGRectElement'.
private bubbleBack_: SVGRectElement = null as AnyDuringMigration;
/** The SVG group for the resize hash marks on some bubbles. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGGElement'.
private resizeGroup_: SVGGElement = null as AnyDuringMigration;
/** Absolute coordinate of anchor point, in workspace coordinates. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Coordinate'.
private anchorXY_: Coordinate = null as AnyDuringMigration;
/**
* Relative X coordinate of bubble with respect to the anchor's centre,
* in workspace units.
* In RTL mode the initial value is negated.
*/
private relativeLeft_ = 0;
/**
* Relative Y coordinate of bubble with respect to the anchor's centre, in
* workspace units.
*/
private relativeTop_ = 0;
/** Width of bubble, in workspace units. */
private width_ = 0;
/** Height of bubble, in workspace units. */
private height_ = 0;
/** Automatically position and reposition the bubble. */
private autoLayout_ = true;
/** Method to call on resize of bubble. */
private resizeCallback_: (() => AnyDuringMigration)|null = null;
/** Method to call on move of bubble. */
private moveCallback_: (() => AnyDuringMigration)|null = null;
/** Mouse down on bubbleBack_ event data. */
private onMouseDownBubbleWrapper_: browserEvents.Data|null = null;
/** Mouse down on resizeGroup_ event data. */
private onMouseDownResizeWrapper_: browserEvents.Data|null = null;
/**
* Describes whether this bubble has been disposed of (nodes and event
* listeners removed from the page) or not.
* @internal
*/
disposed = false;
private arrow_radians_: AnyDuringMigration;
/**
* @param workspace The workspace on which to draw the bubble.
* @param content SVG content for the bubble.
* @param shape SVG element to avoid eclipsing.
* @param anchorXY Absolute position of bubble's anchor point.
* @param bubbleWidth Width of bubble, or null if not resizable.
* @param bubbleHeight Height of bubble, or null if not resizable.
* @struct
*/
constructor(workspace, content, shape, anchorXY, bubbleWidth, bubbleHeight) {
constructor(
workspace: WorkspaceSvg, content: SVGElement, shape: SVGElement,
anchorXY: Coordinate, bubbleWidth: number|null,
bubbleHeight: number|null) {
this.rendered_ = false;
this.workspace_ = workspace;
this.content_ = content;
this.shape_ = shape;
/**
* Flag to stop incremental rendering during construction.
* @type {boolean}
* @private
*/
this.rendered_ = false;
/**
* The SVG group containing all parts of the bubble.
* @type {SVGGElement}
* @private
*/
this.bubbleGroup_ = null;
/**
* The SVG path for the arrow from the bubble to the icon on the block.
* @type {SVGPathElement}
* @private
*/
this.bubbleArrow_ = null;
/**
* The SVG rect for the main body of the bubble.
* @type {SVGRectElement}
* @private
*/
this.bubbleBack_ = null;
/**
* The SVG group for the resize hash marks on some bubbles.
* @type {SVGGElement}
* @private
*/
this.resizeGroup_ = null;
/**
* Absolute coordinate of anchor point, in workspace coordinates.
* @type {Coordinate}
* @private
*/
this.anchorXY_ = null;
/**
* Relative X coordinate of bubble with respect to the anchor's centre,
* in workspace units.
* In RTL mode the initial value is negated.
* @type {number}
* @private
*/
this.relativeLeft_ = 0;
/**
* Relative Y coordinate of bubble with respect to the anchor's centre, in
* workspace units.
* @type {number}
* @private
*/
this.relativeTop_ = 0;
/**
* Width of bubble, in workspace units.
* @type {number}
* @private
*/
this.width_ = 0;
/**
* Height of bubble, in workspace units.
* @type {number}
* @private
*/
this.height_ = 0;
/**
* Automatically position and reposition the bubble.
* @type {boolean}
* @private
*/
this.autoLayout_ = true;
/**
* Method to call on resize of bubble.
* @type {?function()}
* @private
*/
this.resizeCallback_ = null;
/**
* Method to call on move of bubble.
* @type {?function()}
* @private
*/
this.moveCallback_ = null;
/**
* Mouse down on bubbleBack_ event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseDownBubbleWrapper_ = null;
/**
* Mouse down on resizeGroup_ event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseDownResizeWrapper_ = null;
/**
* Describes whether this bubble has been disposed of (nodes and event
* listeners removed from the page) or not.
* @type {boolean}
* @package
*/
this.disposed = false;
let angle = Bubble.ARROW_ANGLE;
if (this.workspace_.RTL) {
angle = -angle;
@@ -188,7 +174,7 @@ const Bubble = class {
this.setAnchorLocation(anchorXY);
if (!bubbleWidth || !bubbleHeight) {
const bBox = /** @type {SVGLocatable} */ (this.content_).getBBox();
const bBox = (this.content_ as SVGGraphicsElement).getBBox();
bubbleWidth = bBox.width + 2 * Bubble.BORDER_WIDTH;
bubbleHeight = bBox.height + 2 * Bubble.BORDER_WIDTH;
}
@@ -202,28 +188,28 @@ const Bubble = class {
/**
* Create the bubble's DOM.
* @param {!Element} content SVG content for the bubble.
* @param {boolean} hasResize Add diagonal resize gripper if true.
* @return {!SVGElement} The bubble's SVG group.
* @private
* @param content SVG content for the bubble.
* @param hasResize Add diagonal resize gripper if true.
* @return The bubble's SVG group.
*/
createDom_(content, hasResize) {
private createDom_(content: Element, hasResize: boolean): SVGElement {
/* Create the bubble. Here's the markup that will be generated:
<g>
<g filter="url(#blocklyEmbossFilter837493)">
<path d="... Z" />
<rect class="blocklyDraggable" rx="8" ry="8" width="180" height="180"/>
</g>
<g transform="translate(165, 165)" class="blocklyResizeSE">
<polygon points="0,15 15,15 15,0"/>
<line class="blocklyResizeLine" x1="5" y1="14" x2="14" y2="5"/>
<line class="blocklyResizeLine" x1="10" y1="14" x2="14" y2="10"/>
</g>
[...content goes here...]
</g>
*/
this.bubbleGroup_ = dom.createSvgElement(Svg.G, {}, null);
let filter = {
<g>
<g filter="url(#blocklyEmbossFilter837493)">
<path d="... Z" />
<rect class="blocklyDraggable" rx="8" ry="8" width="180"
height="180"/>
</g>
<g transform="translate(165, 165)" class="blocklyResizeSE">
<polygon points="0,15 15,15 15,0"/>
<line class="blocklyResizeLine" x1="5" y1="14" x2="14" y2="5"/>
<line class="blocklyResizeLine" x1="10" y1="14" x2="14" y2="10"/>
</g>
[...content goes here...]
</g>
*/
this.bubbleGroup_ = dom.createSvgElement(Svg.G, {});
let filter: {filter?: string} = {
'filter': 'url(#' +
this.workspace_.getRenderer().getConstants().embossFilterId + ')',
};
@@ -274,7 +260,9 @@ const Bubble = class {
},
this.resizeGroup_);
} else {
this.resizeGroup_ = null;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGGElement'.
this.resizeGroup_ = null as AnyDuringMigration;
}
if (!this.workspace_.options.readOnly) {
@@ -291,26 +279,25 @@ const Bubble = class {
/**
* Return the root node of the bubble's SVG group.
* @return {!SVGElement} The root SVG node of the bubble's group.
* @return The root SVG node of the bubble's group.
*/
getSvgRoot() {
return /** @type {!SVGElement} */ (this.bubbleGroup_);
getSvgRoot(): SVGElement {
return this.bubbleGroup_ as SVGElement;
}
/**
* Expose the block's ID on the bubble's top-level SVG group.
* @param {string} id ID of block.
* @param id ID of block.
*/
setSvgId(id) {
setSvgId(id: string) {
this.bubbleGroup_.setAttribute('data-block-id', id);
}
/**
* Handle a mouse-down on bubble's border.
* @param {!Event} e Mouse down event.
* @private
* @param e Mouse down event.
*/
bubbleMouseDown_(e) {
private bubbleMouseDown_(e: Event) {
const gesture = this.workspace_.getGesture(e);
if (gesture) {
gesture.handleBubbleStart(e, this);
@@ -319,38 +306,34 @@ const Bubble = class {
/**
* Show the context menu for this bubble.
* @param {!Event} _e Mouse event.
* @package
* @param _e Mouse event.
* @internal
*/
showContextMenu(_e) {
// NOP on bubbles, but used by the bubble dragger to pass events to
// workspace comments.
}
showContextMenu(_e: Event) {}
// NOP on bubbles, but used by the bubble dragger to pass events to
// workspace comments.
/**
* Get whether this bubble is deletable or not.
* @return {boolean} True if deletable.
* @package
* @return True if deletable.
* @internal
*/
isDeletable() {
isDeletable(): boolean {
return false;
}
/**
* Update the style of this bubble when it is dragged over a delete area.
* @param {boolean} _enable True if the bubble is about to be deleted, false
* otherwise.
* @param _enable True if the bubble is about to be deleted, false otherwise.
*/
setDeleteStyle(_enable) {
// NOP if bubble is not deletable.
}
setDeleteStyle(_enable: boolean) {}
// NOP if bubble is not deletable.
/**
* Handle a mouse-down on bubble's resize corner.
* @param {!Event} e Mouse down event.
* @private
* @param e Mouse down event.
*/
resizeMouseDown_(e) {
private resizeMouseDown_(e: Event) {
this.promote();
Bubble.unbindDragEvents_();
if (browserEvents.isRightButton(e)) {
@@ -375,10 +358,9 @@ const Bubble = class {
/**
* Resize this bubble to follow the mouse.
* @param {!Event} e Mouse move event.
* @private
* @param e Mouse move event.
*/
resizeMouseMove_(e) {
private resizeMouseMove_(e: Event) {
this.autoLayout_ = false;
const newXY = this.workspace_.moveDrag(e);
this.setBubbleSize(this.workspace_.RTL ? -newXY.x : newXY.x, newXY.y);
@@ -390,29 +372,33 @@ const Bubble = class {
/**
* Register a function as a callback event for when the bubble is resized.
* @param {!Function} callback The function to call on resize.
* @param callback The function to call on resize.
*/
registerResizeEvent(callback) {
this.resizeCallback_ = callback;
registerResizeEvent(callback: Function) {
// AnyDuringMigration because: Type 'Function' is not assignable to type
// '() => any'.
this.resizeCallback_ = callback as AnyDuringMigration;
}
/**
* Register a function as a callback event for when the bubble is moved.
* @param {!Function} callback The function to call on move.
* @param callback The function to call on move.
*/
registerMoveEvent(callback) {
this.moveCallback_ = callback;
registerMoveEvent(callback: Function) {
// AnyDuringMigration because: Type 'Function' is not assignable to type
// '() => any'.
this.moveCallback_ = callback as AnyDuringMigration;
}
/**
* Move this bubble to the top of the stack.
* @return {boolean} Whether or not the bubble has been moved.
* @package
* @return Whether or not the bubble has been moved.
* @internal
*/
promote() {
promote(): boolean {
const svgGroup = this.bubbleGroup_.parentNode;
if (svgGroup.lastChild !== this.bubbleGroup_) {
svgGroup.appendChild(this.bubbleGroup_);
if (svgGroup!.lastChild !== this.bubbleGroup_) {
svgGroup!.appendChild(this.bubbleGroup_);
return true;
}
return false;
@@ -421,32 +407,30 @@ const Bubble = class {
/**
* Notification that the anchor has moved.
* Update the arrow and bubble accordingly.
* @param {!Coordinate} xy Absolute location.
* @param xy Absolute location.
*/
setAnchorLocation(xy) {
setAnchorLocation(xy: Coordinate) {
this.anchorXY_ = xy;
if (this.rendered_) {
this.positionBubble_();
}
}
/**
* Position the bubble so that it does not fall off-screen.
* @private
*/
layoutBubble_() {
/** Position the bubble so that it does not fall off-screen. */
private layoutBubble_() {
// Get the metrics in workspace units.
const viewMetrics =
this.workspace_.getMetricsManager().getViewMetrics(true);
const optimalLeft = this.getOptimalRelativeLeft_(viewMetrics);
const optimalTop = this.getOptimalRelativeTop_(viewMetrics);
const bbox = (/** @type {!SVGGraphicsElement} */ (this.shape_)).getBBox();
const bbox = (this.shape_ as SVGGraphicsElement).getBBox();
const topPosition = {
x: optimalLeft,
y: -this.height_ -
this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT,
this.workspace_.getRenderer().getConstants().MIN_BLOCK_HEIGHT as
number,
};
const startPosition = {x: -this.width_ - 30, y: optimalTop};
const endPosition = {x: bbox.width, y: optimalTop};
@@ -493,19 +477,19 @@ const Bubble = class {
/**
* Calculate the what percentage of the bubble overlaps with the visible
* workspace (what percentage of the bubble is visible).
* @param {!{x: number, y: number}} relativeMin The position of the top-left
* corner of the bubble relative to the anchor point.
* @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
* of the workspace the bubble will appear in.
* @return {number} The percentage of the bubble that is visible.
* @private
* @param relativeMin The position of the top-left corner of the bubble
* relative to the anchor point.
* @param viewMetrics The view metrics of the workspace the bubble will appear
* in.
* @return The percentage of the bubble that is visible.
*/
getOverlap_(relativeMin, viewMetrics) {
private getOverlap_(
relativeMin: {x: number, y: number},
viewMetrics: ContainerRegion): number {
// The position of the top-left corner of the bubble in workspace units.
const bubbleMin = {
x: this.workspace_.RTL ?
(this.anchorXY_.x - relativeMin.x - this.width_) :
(relativeMin.x + this.anchorXY_.x),
x: this.workspace_.RTL ? this.anchorXY_.x - relativeMin.x - this.width_ :
relativeMin.x + this.anchorXY_.x,
y: relativeMin.y + this.anchorXY_.y,
};
// The position of the bottom-right corner of the bubble in workspace units.
@@ -534,20 +518,19 @@ const Bubble = class {
return Math.max(
0,
Math.min(
1, (overlapWidth * overlapHeight) / (this.width_ * this.height_)));
1, overlapWidth * overlapHeight / (this.width_ * this.height_)));
}
/**
* Calculate what the optimal horizontal position of the top-left corner of
* the bubble is (relative to the anchor point) so that the most area of the
* bubble is shown.
* @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
* of the workspace the bubble will appear in.
* @return {number} The optimal horizontal position of the top-left corner
* of the bubble.
* @private
* @param viewMetrics The view metrics of the workspace the bubble will appear
* in.
* @return The optimal horizontal position of the top-left corner of the
* bubble.
*/
getOptimalRelativeLeft_(viewMetrics) {
private getOptimalRelativeLeft_(viewMetrics: ContainerRegion): number {
let relativeLeft = -this.width_ / 4;
// No amount of sliding left or right will give us a better overlap.
@@ -563,7 +546,7 @@ const Bubble = class {
const workspaceRight = viewMetrics.left + viewMetrics.width;
const workspaceLeft = viewMetrics.left +
// Thickness in workspace units.
(Scrollbar.scrollbarThickness / this.workspace_.scale);
Scrollbar.scrollbarThickness / this.workspace_.scale;
if (bubbleLeft < workspaceLeft) {
// Slide the bubble right until it is onscreen.
@@ -579,7 +562,7 @@ const Bubble = class {
const workspaceLeft = viewMetrics.left;
const workspaceRight = viewMetrics.left + viewMetrics.width -
// Thickness in workspace units.
(Scrollbar.scrollbarThickness / this.workspace_.scale);
Scrollbar.scrollbarThickness / this.workspace_.scale;
if (bubbleLeft < workspaceLeft) {
// Slide the bubble right until it is onscreen.
@@ -597,13 +580,11 @@ const Bubble = class {
* Calculate what the optimal vertical position of the top-left corner of
* the bubble is (relative to the anchor point) so that the most area of the
* bubble is shown.
* @param {!MetricsManager.ContainerRegion} viewMetrics The view metrics
* of the workspace the bubble will appear in.
* @return {number} The optimal vertical position of the top-left corner
* of the bubble.
* @private
* @param viewMetrics The view metrics of the workspace the bubble will appear
* in.
* @return The optimal vertical position of the top-left corner of the bubble.
*/
getOptimalRelativeTop_(viewMetrics) {
private getOptimalRelativeTop_(viewMetrics: ContainerRegion): number {
let relativeTop = -this.height_ / 4;
// No amount of sliding up or down will give us a better overlap.
@@ -614,9 +595,9 @@ const Bubble = class {
const bubbleTop = this.anchorXY_.y + relativeTop;
const bubbleBottom = bubbleTop + this.height_;
const workspaceTop = viewMetrics.top;
const workspaceBottom = viewMetrics.top + viewMetrics.height -
// Thickness in workspace units.
(Scrollbar.scrollbarThickness / this.workspace_.scale);
const workspaceBottom = viewMetrics.top +
viewMetrics.height - // Thickness in workspace units.
Scrollbar.scrollbarThickness / this.workspace_.scale;
const anchorY = this.anchorXY_.y;
if (bubbleTop < workspaceTop) {
@@ -630,11 +611,8 @@ const Bubble = class {
return relativeTop;
}
/**
* Move the bubble to a location relative to the anchor's centre.
* @private
*/
positionBubble_() {
/** Move the bubble to a location relative to the anchor's centre. */
private positionBubble_() {
let left = this.anchorXY_.x;
if (this.workspace_.RTL) {
left -= this.relativeLeft_ + this.width_;
@@ -647,21 +625,21 @@ const Bubble = class {
/**
* Move the bubble group to the specified location in workspace coordinates.
* @param {number} x The x position to move to.
* @param {number} y The y position to move to.
* @package
* @param x The x position to move to.
* @param y The y position to move to.
* @internal
*/
moveTo(x, y) {
moveTo(x: number, y: number) {
this.bubbleGroup_.setAttribute(
'transform', 'translate(' + x + ',' + y + ')');
}
/**
* Triggers a move callback if one exists at the end of a drag.
* @param {boolean} adding True if adding, false if removing.
* @package
* @param adding True if adding, false if removing.
* @internal
*/
setDragging(adding) {
setDragging(adding: boolean) {
if (!adding && this.moveCallback_) {
this.moveCallback_();
}
@@ -669,26 +647,30 @@ const Bubble = class {
/**
* Get the dimensions of this bubble.
* @return {!Size} The height and width of the bubble.
* @return The height and width of the bubble.
*/
getBubbleSize() {
getBubbleSize(): Size {
return new Size(this.width_, this.height_);
}
/**
* Size this bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
* @param width Width of the bubble.
* @param height Height of the bubble.
*/
setBubbleSize(width, height) {
setBubbleSize(width: number, height: number) {
const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
// Minimum size of a bubble.
width = Math.max(width, doubleBorderWidth + 45);
height = Math.max(height, doubleBorderWidth + 20);
this.width_ = width;
this.height_ = height;
this.bubbleBack_.setAttribute('width', width);
this.bubbleBack_.setAttribute('height', height);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.bubbleBack_.setAttribute('width', width as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.bubbleBack_.setAttribute('height', height as AnyDuringMigration);
if (this.resizeGroup_) {
if (this.workspace_.RTL) {
// Mirror the resize group.
@@ -716,11 +698,8 @@ const Bubble = class {
}
}
/**
* Draw the arrow between the bubble and the origin.
* @private
*/
renderArrow_() {
/** Draw the arrow between the bubble and the origin. */
private renderArrow_() {
const steps = [];
// Find the relative coordinates of the center of the bubble.
const relBubbleX = this.width_ / 2;
@@ -791,16 +770,14 @@ const Bubble = class {
/**
* Change the colour of a bubble.
* @param {string} hexColour Hex code of colour.
* @param hexColour Hex code of colour.
*/
setColour(hexColour) {
setColour(hexColour: string) {
this.bubbleBack_.setAttribute('fill', hexColour);
this.bubbleArrow_.setAttribute('fill', hexColour);
}
/**
* Dispose of this bubble.
*/
/** Dispose of this bubble. */
dispose() {
if (this.onMouseDownBubbleWrapper_) {
browserEvents.unbind(this.onMouseDownBubbleWrapper_);
@@ -816,13 +793,12 @@ const Bubble = class {
/**
* Move this bubble during a drag, taking into account whether or not there is
* a drag surface.
* @param {BlockDragSurfaceSvg} dragSurface The surface that carries
* rendered items during a drag, or null if no drag surface is in use.
* @param {!Coordinate} newLoc The location to translate to, in
* workspace coordinates.
* @package
* @param dragSurface The surface that carries rendered items during a drag,
* or null if no drag surface is in use.
* @param newLoc The location to translate to, in workspace coordinates.
* @internal
*/
moveDuringDrag(dragSurface, newLoc) {
moveDuringDrag(dragSurface: BlockDragSurfaceSvg, newLoc: Coordinate) {
if (dragSurface) {
dragSurface.translateSurface(newLoc.x, newLoc.y);
} else {
@@ -840,9 +816,9 @@ const Bubble = class {
/**
* Return the coordinates of the top-left corner of this bubble's body
* relative to the drawing surface's origin (0,0), in workspace units.
* @return {!Coordinate} Object with .x and .y properties.
* @return Object with .x and .y properties.
*/
getRelativeToSurfaceXY() {
getRelativeToSurfaceXY(): Coordinate {
return new Coordinate(
this.workspace_.RTL ?
-this.relativeLeft_ + this.anchorXY_.x - this.width_ :
@@ -854,19 +830,15 @@ const Bubble = class {
* Set whether auto-layout of this bubble is enabled. The first time a bubble
* is shown it positions itself to not cover any blocks. Once a user has
* dragged it to reposition, it renders where the user put it.
* @param {boolean} enable True if auto-layout should be enabled, false
* otherwise.
* @package
* @param enable True if auto-layout should be enabled, false otherwise.
* @internal
*/
setAutoLayout(enable) {
setAutoLayout(enable: boolean) {
this.autoLayout_ = enable;
}
/**
* Stop binding to the global mouseup and mousemove events.
* @private
*/
static unbindDragEvents_() {
/** Stop binding to the global mouseup and mousemove events. */
private static unbindDragEvents_() {
if (Bubble.onMouseUpWrapper_) {
browserEvents.unbind(Bubble.onMouseUpWrapper_);
Bubble.onMouseUpWrapper_ = null;
@@ -879,27 +851,24 @@ const Bubble = class {
/**
* Handle a mouse-up event while dragging a bubble's border or resize handle.
* @param {!Event} _e Mouse up event.
* @private
* @param _e Mouse up event.
*/
static bubbleMouseUp_(_e) {
private static bubbleMouseUp_(_e: Event) {
Touch.clearTouchIdentifier();
Bubble.unbindDragEvents_();
}
/**
* Create the text for a non editable bubble.
* @param {string} text The text to display.
* @return {!SVGTextElement} The top-level node of the text.
* @package
* @param text The text to display.
* @return The top-level node of the text.
* @internal
*/
static textToDom(text) {
const paragraph = dom.createSvgElement(
Svg.TEXT, {
'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents',
'y': Bubble.BORDER_WIDTH,
},
null);
static textToDom(text: string): SVGTextElement {
const paragraph = dom.createSvgElement(Svg.TEXT, {
'class': 'blocklyText blocklyBubbleText blocklyNoPointerEvents',
'y': Bubble.BORDER_WIDTH,
});
const lines = text.split('\n');
for (let i = 0; i < lines.length; i++) {
const tspanElement = dom.createSvgElement(
@@ -912,18 +881,18 @@ const Bubble = class {
/**
* Creates a bubble that can not be edited.
* @param {!SVGTextElement} paragraphElement The text element for the non
* editable bubble.
* @param {!BlockSvg} block The block that the bubble is attached to.
* @param {!Coordinate} iconXY The coordinate of the icon.
* @return {!Bubble} The non editable bubble.
* @package
* @param paragraphElement The text element for the non editable bubble.
* @param block The block that the bubble is attached to.
* @param iconXY The coordinate of the icon.
* @return The non editable bubble.
* @internal
*/
static createNonEditableBubble(paragraphElement, block, iconXY) {
static createNonEditableBubble(
paragraphElement: SVGTextElement, block: BlockSvg,
iconXY: Coordinate): Bubble {
const bubble = new Bubble(
/** @type {!WorkspaceSvg} */ (block.workspace), paragraphElement,
block.pathObject.svgPath,
/** @type {!Coordinate} */ (iconXY), null, null);
block.workspace!, paragraphElement, block.pathObject.svgPath, (iconXY),
null, null);
// Expose this bubble's block's ID on its top-level SVG group.
bubble.setSvgId(block.id);
if (block.RTL) {
@@ -931,55 +900,13 @@ const Bubble = class {
// This cannot be done until the bubble is rendered on screen.
const maxWidth = paragraphElement.getBBox().width;
for (let i = 0, textElement;
(textElement = /** @type {!SVGTSpanElement} */ (
paragraphElement.childNodes[i]));
textElement = paragraphElement.childNodes[i] as SVGTSpanElement;
i++) {
textElement.setAttribute('text-anchor', 'end');
textElement.setAttribute('x', maxWidth + Bubble.BORDER_WIDTH);
textElement.setAttribute(
'x', (maxWidth + Bubble.BORDER_WIDTH).toString());
}
}
return bubble;
}
};
/**
* Width of the border around the bubble.
*/
Bubble.BORDER_WIDTH = 6;
/**
* Determines the thickness of the base of the arrow in relation to the size
* of the bubble. Higher numbers result in thinner arrows.
*/
Bubble.ARROW_THICKNESS = 5;
/**
* The number of degrees that the arrow bends counter-clockwise.
*/
Bubble.ARROW_ANGLE = 20;
/**
* The sharpness of the arrow's bend. Higher numbers result in smoother arrows.
*/
Bubble.ARROW_BEND = 4;
/**
* Distance between arrow point and anchor point.
*/
Bubble.ANCHOR_RADIUS = 8;
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
Bubble.onMouseUpWrapper_ = null;
/**
* Mouse move event data.
* @type {?browserEvents.Data}
* @private
*/
Bubble.onMouseMoveWrapper_ = null;
exports.Bubble = Bubble;
}

View File

@@ -7,37 +7,30 @@
/**
* @fileoverview Methods for dragging a bubble visually.
*/
'use strict';
/**
* Methods for dragging a bubble visually.
* @class
*/
goog.module('Blockly.BubbleDragger');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.BubbleDragger');
const eventUtils = goog.require('Blockly.Events.utils');
const svgMath = goog.require('Blockly.utils.svgMath');
/* eslint-disable-next-line no-unused-vars */
const {BlockDragSurfaceSvg} = goog.requireType('Blockly.BlockDragSurfaceSvg');
/* eslint-disable-next-line no-unused-vars */
const {CommentMove} = goog.requireType('Blockly.Events.CommentMove');
const {ComponentManager} = goog.require('Blockly.ComponentManager');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
const {IBubble} = goog.requireType('Blockly.IBubble');
/* eslint-disable-next-line no-unused-vars */
const {IDeleteArea} = goog.requireType('Blockly.IDeleteArea');
/* eslint-disable-next-line no-unused-vars */
const {IDragTarget} = goog.requireType('Blockly.IDragTarget');
const {WorkspaceCommentSvg} = goog.require('Blockly.WorkspaceCommentSvg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/** @suppress {extraRequire} */
goog.require('Blockly.Bubble');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.CommentMove');
/** @suppress {extraRequire} */
goog.require('Blockly.constants');
// Unused import preserved for side-effects. Remove if unneeded.
// import './bubble.js';
// Unused import preserved for side-effects. Remove if unneeded.
// import './constants.js';
import type {BlockDragSurfaceSvg} from './block_drag_surface.js';
import {ComponentManager} from './component_manager.js';
import type {CommentMove} from './events/events_comment_move.js';
import * as eventUtils from './events/utils.js';
import type {IBubble} from './interfaces/i_bubble.js';
import type {IDeleteArea} from './interfaces/i_delete_area.js';
import type {IDragTarget} from './interfaces/i_drag_target.js';
import {Coordinate} from './utils/coordinate.js';
import * as svgMath from './utils/svg_math.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
@@ -46,104 +39,88 @@ goog.require('Blockly.constants');
* block comments, mutators, warnings, or workspace comments.
* @alias Blockly.BubbleDragger
*/
const BubbleDragger = class {
export class BubbleDragger {
/** Which drag target the mouse pointer is over, if any. */
private dragTarget_: IDragTarget|null = null;
/** Whether the bubble would be deleted if dropped immediately. */
private wouldDeleteBubble_ = false;
private readonly startXY_: Coordinate;
private dragSurface_: BlockDragSurfaceSvg;
/**
* @param {!IBubble} bubble The item on the bubble canvas to drag.
* @param {!WorkspaceSvg} workspace The workspace to drag on.
* @param bubble The item on the bubble canvas to drag.
* @param workspace The workspace to drag on.
*/
constructor(bubble, workspace) {
/**
* The item on the bubble canvas that is being dragged.
* @type {!IBubble}
* @private
*/
this.draggingBubble_ = bubble;
/**
* The workspace on which the bubble is being dragged.
* @type {!WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* Which drag target the mouse pointer is over, if any.
* @type {?IDragTarget}
* @private
*/
this.dragTarget_ = null;
/**
* Whether the bubble would be deleted if dropped immediately.
* @type {boolean}
* @private
*/
this.wouldDeleteBubble_ = false;
constructor(private bubble: IBubble, private workspace: WorkspaceSvg) {
/**
* The location of the top left corner of the dragging bubble's body at the
* beginning of the drag, in workspace coordinates.
* @type {!Coordinate}
* @private
*/
this.startXY_ = this.draggingBubble_.getRelativeToSurfaceXY();
this.startXY_ = this.bubble.getRelativeToSurfaceXY();
/**
* The drag surface to move bubbles to during a drag, or null if none should
* be used. Block dragging and bubble dragging use the same surface.
* @type {BlockDragSurfaceSvg}
* @private
*/
// AnyDuringMigration because: Type 'BlockDragSurfaceSvg | null' is not
// assignable to type 'BlockDragSurfaceSvg'.
this.dragSurface_ =
svgMath.is3dSupported() && !!workspace.getBlockDragSurface() ?
workspace.getBlockDragSurface() :
null;
(svgMath.is3dSupported() && !!workspace.getBlockDragSurface() ?
workspace.getBlockDragSurface() :
null) as AnyDuringMigration;
}
/**
* Sever all links from this object.
* @package
* @suppress {checkTypes}
* @internal
*/
dispose() {
this.draggingBubble_ = null;
this.workspace_ = null;
this.dragSurface_ = null;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'IBubble'.
this.bubble = null as AnyDuringMigration;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'WorkspaceSvg'.
this.workspace = null as AnyDuringMigration;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'BlockDragSurfaceSvg'.
this.dragSurface_ = null as AnyDuringMigration;
}
/**
* Start dragging a bubble. This includes moving it to the drag surface.
* @package
* @internal
*/
startBubbleDrag() {
if (!eventUtils.getGroup()) {
eventUtils.setGroup(true);
}
this.workspace_.setResizesEnabled(false);
this.draggingBubble_.setAutoLayout(false);
this.workspace.setResizesEnabled(false);
this.bubble.setAutoLayout(false);
if (this.dragSurface_) {
this.moveToDragSurface_();
}
this.draggingBubble_.setDragging && this.draggingBubble_.setDragging(true);
this.bubble.setDragging && this.bubble.setDragging(true);
}
/**
* Execute a step of bubble dragging, based on the given event. Update the
* display accordingly.
* @param {!Event} e The most recent move event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
* @param e The most recent move event.
* @param currentDragDeltaXY How far the pointer has moved from the position
* at the start of the drag, in pixel units.
* @internal
*/
dragBubble(e, currentDragDeltaXY) {
dragBubble(e: Event, currentDragDeltaXY: Coordinate) {
const delta = this.pixelsToWorkspaceUnits_(currentDragDeltaXY);
const newLoc = Coordinate.sum(this.startXY_, delta);
this.draggingBubble_.moveDuringDrag(this.dragSurface_, newLoc);
this.bubble.moveDuringDrag(this.dragSurface_, newLoc);
const oldDragTarget = this.dragTarget_;
this.dragTarget_ = this.workspace_.getDragTarget(e);
this.dragTarget_ = this.workspace.getDragTarget(e);
const oldWouldDeleteBubble = this.wouldDeleteBubble_;
this.wouldDeleteBubble_ = this.shouldDelete_(this.dragTarget_);
@@ -151,32 +128,27 @@ const BubbleDragger = class {
// Prevent unnecessary add/remove class calls.
this.updateCursorDuringBubbleDrag_();
}
// Call drag enter/exit/over after wouldDeleteBlock is called in
// shouldDelete_
if (this.dragTarget_ !== oldDragTarget) {
oldDragTarget && oldDragTarget.onDragExit(this.draggingBubble_);
this.dragTarget_ && this.dragTarget_.onDragEnter(this.draggingBubble_);
oldDragTarget && oldDragTarget.onDragExit(this.bubble);
this.dragTarget_ && this.dragTarget_.onDragEnter(this.bubble);
}
this.dragTarget_ && this.dragTarget_.onDragOver(this.draggingBubble_);
this.dragTarget_ && this.dragTarget_.onDragOver(this.bubble);
}
/**
* Whether ending the drag would delete the bubble.
* @param {?IDragTarget} dragTarget The drag target that the bubblee is
* currently over.
* @return {boolean} Whether dropping the bubble immediately would delete the
* block.
* @private
* @param dragTarget The drag target that the bubblee is currently over.
* @return Whether dropping the bubble immediately would delete the block.
*/
shouldDelete_(dragTarget) {
private shouldDelete_(dragTarget: IDragTarget|null): boolean {
if (dragTarget) {
const componentManager = this.workspace_.getComponentManager();
const componentManager = this.workspace.getComponentManager();
const isDeleteArea = componentManager.hasCapability(
dragTarget.id, ComponentManager.Capability.DELETE_AREA);
if (isDeleteArea) {
return (/** @type {!IDeleteArea} */ (dragTarget))
.wouldDelete(this.draggingBubble_, false);
return (dragTarget as IDeleteArea).wouldDelete(this.bubble, false);
}
}
return false;
@@ -185,25 +157,24 @@ const BubbleDragger = class {
/**
* Update the cursor (and possibly the trash can lid) to reflect whether the
* dragging bubble would be deleted if released immediately.
* @private
*/
updateCursorDuringBubbleDrag_() {
this.draggingBubble_.setDeleteStyle(this.wouldDeleteBubble_);
private updateCursorDuringBubbleDrag_() {
this.bubble.setDeleteStyle(this.wouldDeleteBubble_);
}
/**
* Finish a bubble drag and put the bubble back on the workspace.
* @param {!Event} e The mouseup/touchend event.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at the start of the drag, in pixel units.
* @package
* @param e The mouseup/touchend event.
* @param currentDragDeltaXY How far the pointer has moved from the position
* at the start of the drag, in pixel units.
* @internal
*/
endBubbleDrag(e, currentDragDeltaXY) {
endBubbleDrag(e: Event, currentDragDeltaXY: Coordinate) {
// Make sure internal state is fresh.
this.dragBubble(e, currentDragDeltaXY);
const preventMove = this.dragTarget_ &&
this.dragTarget_.shouldPreventMove(this.draggingBubble_);
const preventMove =
this.dragTarget_ && this.dragTarget_.shouldPreventMove(this.bubble);
let newLoc;
if (preventMove) {
newLoc = this.startXY_;
@@ -212,39 +183,36 @@ const BubbleDragger = class {
newLoc = Coordinate.sum(this.startXY_, delta);
}
// Move the bubble to its final location.
this.draggingBubble_.moveTo(newLoc.x, newLoc.y);
this.bubble.moveTo(newLoc.x, newLoc.y);
if (this.dragTarget_) {
this.dragTarget_.onDrop(this.draggingBubble_);
this.dragTarget_.onDrop(this.bubble);
}
if (this.wouldDeleteBubble_) {
// Fire a move event, so we know where to go back to for an undo.
this.fireMoveEvent_();
this.draggingBubble_.dispose(false, true);
this.bubble.dispose();
} else {
// Put everything back onto the bubble canvas.
if (this.dragSurface_) {
this.dragSurface_.clearAndHide(this.workspace_.getBubbleCanvas());
this.dragSurface_.clearAndHide(this.workspace.getBubbleCanvas());
}
if (this.draggingBubble_.setDragging) {
this.draggingBubble_.setDragging(false);
if (this.bubble.setDragging) {
this.bubble.setDragging(false);
}
this.fireMoveEvent_();
}
this.workspace_.setResizesEnabled(true);
this.workspace.setResizesEnabled(true);
eventUtils.setGroup(false);
}
/**
* Fire a move event at the end of a bubble drag.
* @private
*/
fireMoveEvent_() {
if (this.draggingBubble_ instanceof WorkspaceCommentSvg) {
const event = /** @type {!CommentMove} */
(new (eventUtils.get(eventUtils.COMMENT_MOVE))(this.draggingBubble_));
/** Fire a move event at the end of a bubble drag. */
private fireMoveEvent_() {
if (this.bubble instanceof WorkspaceCommentSvg) {
const event = new (eventUtils.get(eventUtils.COMMENT_MOVE))!
(this.bubble) as CommentMove;
event.setOldCoordinate(this.startXY_);
event.recordNew();
eventUtils.fire(event);
@@ -258,21 +226,18 @@ const BubbleDragger = class {
* correction for mutator workspaces.
* This function does not consider differing origins. It simply scales the
* input's x and y values.
* @param {!Coordinate} pixelCoord A coordinate with x and y
* values in CSS pixel units.
* @return {!Coordinate} The input coordinate divided by the
* workspace scale.
* @private
* @param pixelCoord A coordinate with x and y values in CSS pixel units.
* @return The input coordinate divided by the workspace scale.
*/
pixelsToWorkspaceUnits_(pixelCoord) {
private pixelsToWorkspaceUnits_(pixelCoord: Coordinate): Coordinate {
const result = new Coordinate(
pixelCoord.x / this.workspace_.scale,
pixelCoord.y / this.workspace_.scale);
if (this.workspace_.isMutator) {
pixelCoord.x / this.workspace.scale,
pixelCoord.y / this.workspace.scale);
if (this.workspace.isMutator) {
// If we're in a mutator, its scale is always 1, purely because of some
// oddities in our rendering optimizations. The actual scale is the same
// as the scale on the parent workspace. Fix that for dragging.
const mainScale = this.workspace_.options.parentWorkspace.scale;
const mainScale = this.workspace.options.parentWorkspace!.scale;
result.scale(1 / mainScale);
}
return result;
@@ -281,14 +246,11 @@ const BubbleDragger = class {
/**
* Move the bubble onto the drag surface at the beginning of a drag. Move the
* drag surface to preserve the apparent location of the bubble.
* @private
*/
moveToDragSurface_() {
this.draggingBubble_.moveTo(0, 0);
private moveToDragSurface_() {
this.bubble.moveTo(0, 0);
this.dragSurface_.translateSurface(this.startXY_.x, this.startXY_.y);
// Execute the move on the top-level SVG component.
this.dragSurface_.setBlocksAndShow(this.draggingBubble_.getSvgRoot());
this.dragSurface_.setBlocksAndShow(this.bubble.getSvgRoot());
}
};
exports.BubbleDragger = BubbleDragger;
}

View File

@@ -7,42 +7,41 @@
/**
* @fileoverview Utilities for bumping objects back into worksapce bounds.
*/
'use strict';
/**
* Utilities for bumping objects back into worksapce bounds.
* @namespace Blockly.bumpObjects
*/
goog.module('Blockly.bumpObjects');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.bumpObjects');
const eventUtils = goog.require('Blockly.Events.utils');
const mathUtils = goog.require('Blockly.utils.math');
/* eslint-disable-next-line no-unused-vars */
const {Abstract} = goog.requireType('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {IBoundedElement} = goog.requireType('Blockly.IBoundedElement');
/* eslint-disable-next-line no-unused-vars */
const {MetricsManager} = goog.requireType('Blockly.MetricsManager');
/* eslint-disable-next-line no-unused-vars */
const {ViewportChange} = goog.requireType('Blockly.Events.ViewportChange');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceCommentSvg} = goog.requireType('Blockly.WorkspaceCommentSvg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
import type {BlockSvg} from './block_svg.js';
import type {Abstract} from './events/events_abstract.js';
import type {BlockCreate} from './events/events_block_create.js';
import type {BlockMove} from './events/events_block_move.js';
import type {CommentCreate} from './events/events_comment_create.js';
import type {CommentMove} from './events/events_comment_move.js';
import type {ViewportChange} from './events/events_viewport.js';
import * as eventUtils from './events/utils.js';
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
import type {ContainerRegion, MetricsManager} from './metrics_manager.js';
import * as mathUtils from './utils/math.js';
import type {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Bumps the given object that has passed out of bounds.
* @param {!WorkspaceSvg} workspace The workspace containing the object.
* @param {!MetricsManager.ContainerRegion} scrollMetrics Scroll metrics
* @param workspace The workspace containing the object.
* @param scrollMetrics Scroll metrics
* in workspace coordinates.
* @param {!IBoundedElement} object The object to bump.
* @return {boolean} True if block was bumped.
* @param object The object to bump.
* @return True if block was bumped.
* @alias Blockly.bumpObjects.bumpIntoBounds
*/
const bumpObjectIntoBounds = function(workspace, scrollMetrics, object) {
function bumpObjectIntoBounds(
workspace: WorkspaceSvg, scrollMetrics: ContainerRegion,
object: IBoundedElement): boolean {
// Compute new top/left position for object.
const objectMetrics = object.getBoundingRectangle();
const height = objectMetrics.bottom - objectMetrics.top;
@@ -82,27 +81,29 @@ const bumpObjectIntoBounds = function(workspace, scrollMetrics, object) {
return true;
}
return false;
};
exports.bumpIntoBounds = bumpObjectIntoBounds;
}
export const bumpIntoBounds = bumpObjectIntoBounds;
/**
* Creates a handler for bumping objects when they cross fixed bounds.
* @param {!WorkspaceSvg} workspace The workspace to handle.
* @return {function(Abstract)} The event handler.
* @param workspace The workspace to handle.
* @return The event handler.
* @alias Blockly.bumpObjects.bumpIntoBoundsHandler
*/
const bumpIntoBoundsHandler = function(workspace) {
return function(e) {
export function bumpIntoBoundsHandler(workspace: WorkspaceSvg):
(p1: Abstract) => AnyDuringMigration {
return (e) => {
const metricsManager = workspace.getMetricsManager();
if (!metricsManager.hasFixedEdges() || workspace.isDragging()) {
return;
}
if (eventUtils.BUMP_EVENTS.indexOf(e.type) !== -1) {
if (eventUtils.BUMP_EVENTS.indexOf(e.type ?? '') !== -1) {
const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true);
// Triggered by move/create event
const object = extractObjectFromEvent(workspace, e);
const object =
extractObjectFromEvent(workspace, e as eventUtils.BumpEvent);
if (!object) {
return;
}
@@ -111,8 +112,7 @@ const bumpIntoBoundsHandler = function(workspace) {
eventUtils.setGroup(e.group);
const wasBumped = bumpObjectIntoBounds(
workspace, scrollMetricsInWsCoords,
/** @type {!IBoundedElement} */ (object));
workspace, scrollMetricsInWsCoords, (object as IBoundedElement));
if (wasBumped && !e.group) {
console.warn(
@@ -123,49 +123,52 @@ const bumpIntoBoundsHandler = function(workspace) {
eventUtils.setGroup(oldGroup);
}
} else if (e.type === eventUtils.VIEWPORT_CHANGE) {
const viewportEvent = /** @type {!ViewportChange} */ (e);
if (viewportEvent.scale > viewportEvent.oldScale) {
const viewportEvent = (e as ViewportChange);
if (viewportEvent.scale && viewportEvent.oldScale &&
viewportEvent.scale > viewportEvent.oldScale) {
bumpTopObjectsIntoBounds(workspace);
}
}
};
};
exports.bumpIntoBoundsHandler = bumpIntoBoundsHandler;
}
/**
* Extracts the object from the given event.
* @param {!WorkspaceSvg} workspace The workspace the event originated
* @param workspace The workspace the event originated
* from.
* @param {!eventUtils.BumpEvent} e An event containing an object.
* @return {?BlockSvg|?WorkspaceCommentSvg} The extracted
* @param e An event containing an object.
* @return The extracted
* object.
*/
const extractObjectFromEvent = function(workspace, e) {
function extractObjectFromEvent(
workspace: WorkspaceSvg, e: eventUtils.BumpEvent): BlockSvg|null|
WorkspaceCommentSvg {
let object = null;
switch (e.type) {
case eventUtils.BLOCK_CREATE:
case eventUtils.BLOCK_MOVE:
object = workspace.getBlockById(e.blockId);
object = workspace.getBlockById((e as BlockCreate | BlockMove).blockId);
if (object) {
object = object.getRootBlock();
}
break;
case eventUtils.COMMENT_CREATE:
case eventUtils.COMMENT_MOVE:
object = (
/** @type {?WorkspaceCommentSvg} */
(workspace.getCommentById(e.commentId)));
object = workspace.getCommentById(
(e as CommentCreate | CommentMove).commentId) as
WorkspaceCommentSvg |
null;
break;
}
return object;
};
}
/**
* Bumps the top objects in the given workspace into bounds.
* @param {!WorkspaceSvg} workspace The workspace.
* @param workspace The workspace.
* @alias Blockly.bumpObjects.bumpTopObjectsIntoBounds
*/
const bumpTopObjectsIntoBounds = function(workspace) {
export function bumpTopObjectsIntoBounds(workspace: WorkspaceSvg) {
const metricsManager = workspace.getMetricsManager();
if (!metricsManager.hasFixedEdges() || workspace.isDragging()) {
return;
@@ -173,8 +176,7 @@ const bumpTopObjectsIntoBounds = function(workspace) {
const scrollMetricsInWsCoords = metricsManager.getScrollMetrics(true);
const topBlocks = workspace.getTopBoundedElements();
for (let i = 0, block; (block = topBlocks[i]); i++) {
for (let i = 0, block; block = topBlocks[i]; i++) {
bumpObjectIntoBounds(workspace, scrollMetricsInWsCoords, block);
}
};
exports.bumpTopObjectsIntoBounds = bumpTopObjectsIntoBounds;
}

View File

@@ -7,43 +7,44 @@
/**
* @fileoverview Blockly's internal clipboard for managing copy-paste.
*/
'use strict';
/**
* Blockly's internal clipboard for managing copy-paste.
* @namespace Blockly.clipboard
*/
goog.module('Blockly.clipboard');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.clipboard');
/* eslint-disable-next-line no-unused-vars */
const {ICopyable} = goog.requireType('Blockly.ICopyable');
import type {CopyData, ICopyable} from './interfaces/i_copyable.js';
/**
* Metadata about the object that is currently on the clipboard.
* @type {?ICopyable.CopyData}
*/
let copyData = null;
/** Metadata about the object that is currently on the clipboard. */
let copyData: CopyData|null = null;
/**
* Copy a block or workspace comment onto the local clipboard.
* @param {!ICopyable} toCopy Block or Workspace Comment to be copied.
* @param toCopy Block or Workspace Comment to be copied.
* @alias Blockly.clipboard.copy
* @package
* @internal
*/
const copy = function(toCopy) {
export function copy(toCopy: ICopyable) {
TEST_ONLY.copyInternal(toCopy);
}
/**
* Private version of copy for stubbing in tests.
*/
function copyInternal(toCopy: ICopyable) {
copyData = toCopy.toCopyData();
};
exports.copy = copy;
}
/**
* Paste a block or workspace comment on to the main workspace.
* @return {!ICopyable|null} The pasted thing if the paste
* was successful, null otherwise.
* @return The pasted thing if the paste was successful, null otherwise.
* @alias Blockly.clipboard.paste
* @package
* @internal
*/
const paste = function() {
export function paste(): ICopyable|null {
if (!copyData) {
return null;
}
@@ -58,23 +59,32 @@ const paste = function() {
return workspace.paste(copyData.saveInfo);
}
return null;
};
exports.paste = paste;
}
/**
* Duplicate this block and its children, or a workspace comment.
* @param {!ICopyable} toDuplicate Block or Workspace Comment to be
* duplicated.
* @return {!ICopyable|null} The block or workspace comment that was duplicated,
* or null if the duplication failed.
* @param toDuplicate Block or Workspace Comment to be duplicated.
* @return The block or workspace comment that was duplicated, or null if the
* duplication failed.
* @alias Blockly.clipboard.duplicate
* @package
* @internal
*/
const duplicate = function(toDuplicate) {
export function duplicate(toDuplicate: ICopyable): ICopyable|null {
return TEST_ONLY.duplicateInternal(toDuplicate);
}
/**
* Private version of duplicate for stubbing in tests.
*/
function duplicateInternal(toDuplicate: ICopyable): ICopyable|null {
const oldCopyData = copyData;
copy(toDuplicate);
const pastedThing = toDuplicate.toCopyData().source.paste(copyData.saveInfo);
const pastedThing = toDuplicate.toCopyData().source.paste(copyData!.saveInfo);
copyData = oldCopyData;
return pastedThing;
};
exports.duplicate = duplicate;
}
export const TEST_ONLY = {
duplicateInternal,
copyInternal,
}

View File

@@ -7,128 +7,96 @@
/**
* @fileoverview Object representing a code comment.
*/
'use strict';
/**
* Object representing a code comment.
* @class
*/
goog.module('Blockly.Comment');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Comment');
const Css = goog.require('Blockly.Css');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const userAgent = goog.require('Blockly.utils.userAgent');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
// Unused import preserved for side-effects. Remove if unneeded.
// import './block.js';
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Bubble} = goog.require('Blockly.Bubble');
/* eslint-disable-next-line no-unused-vars */
const {Coordinate} = goog.requireType('Blockly.utils.Coordinate');
const {Icon} = goog.require('Blockly.Icon');
/* eslint-disable-next-line no-unused-vars */
const {Size} = goog.requireType('Blockly.utils.Size');
const {Svg} = goog.require('Blockly.utils.Svg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockChange');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BubbleOpen');
/** @suppress {extraRequire} */
goog.require('Blockly.Warning');
// Unused import preserved for side-effects. Remove if unneeded.
// import './workspace_svg.js';
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_bubble_open.js';
// Unused import preserved for side-effects. Remove if unneeded.
// import './warning.js';
import type {CommentModel} from './block.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import {Bubble} from './bubble.js';
import * as Css from './css.js';
import * as eventUtils from './events/utils.js';
import {Icon} from './icon.js';
import type {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import type {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
/**
* Class for a comment.
* @extends {Icon}
* @alias Blockly.Comment
*/
class Comment extends Icon {
export class Comment extends Icon {
private readonly model_: CommentModel;
/**
* @param {!BlockSvg} block The block associated with this comment.
* The model's text value at the start of an edit.
* Used to tell if an event should be fired at the end of an edit.
*/
constructor(block) {
private cachedText_: string|null = '';
/** Mouse up event data. */
private onMouseUpWrapper_: browserEvents.Data|null = null;
/** Wheel event data. */
private onWheelWrapper_: browserEvents.Data|null = null;
/** Change event data. */
private onChangeWrapper_: browserEvents.Data|null = null;
/** Input event data. */
private onInputWrapper_: browserEvents.Data|null = null;
/**
* The SVG element that contains the text edit area, or null if not created.
*/
private foreignObject_: SVGForeignObjectElement|null = null;
/** The editable text area, or null if not created. */
private textarea_: HTMLTextAreaElement|null = null;
/** The top-level node of the comment text, or null if not created. */
private paragraphElement_: SVGTextElement|null = null;
override bubble_: AnyDuringMigration;
/** @param block The block associated with this comment. */
constructor(block: BlockSvg) {
super(block);
/**
* The model for this comment.
* @type {!Block.CommentModel}
* @private
*/
/** The model for this comment. */
this.model_ = block.commentModel;
// If someone creates the comment directly instead of calling
// block.setCommentText we want to make sure the text is non-null;
this.model_.text = this.model_.text || '';
/**
* The model's text value at the start of an edit.
* Used to tell if an event should be fired at the end of an edit.
* @type {?string}
* @private
*/
this.cachedText_ = '';
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseUpWrapper_ = null;
/**
* Wheel event data.
* @type {?browserEvents.Data}
* @private
*/
this.onWheelWrapper_ = null;
/**
* Change event data.
* @type {?browserEvents.Data}
* @private
*/
this.onChangeWrapper_ = null;
/**
* Input event data.
* @type {?browserEvents.Data}
* @private
*/
this.onInputWrapper_ = null;
/**
* The SVG element that contains the text edit area, or null if not created.
* @type {?SVGForeignObjectElement}
* @private
*/
this.foreignObject_ = null;
/**
* The editable text area, or null if not created.
* @type {?HTMLTextAreaElement}
* @private
*/
this.textarea_ = null;
/**
* The top-level node of the comment text, or null if not created.
* @type {?SVGTextElement}
* @private
*/
this.paragraphElement_ = null;
this.createIcon();
}
/**
* Draw the comment icon.
* @param {!Element} group The icon group.
* @protected
* @param group The icon group.
*/
drawIcon_(group) {
protected override drawIcon_(group: Element) {
// Circle.
dom.createSvgElement(
Svg.CIRCLE,
@@ -157,40 +125,41 @@ class Comment extends Icon {
/**
* Create the editor for the comment's bubble.
* @return {!SVGElement} The top-level node of the editor.
* @private
* @return The top-level node of the editor.
*/
createEditor_() {
private createEditor_(): SVGElement {
/* Create the editor. Here's the markup that will be generated in
* editable mode:
<foreignObject x="8" y="8" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml" class="blocklyMinimalBody">
<textarea xmlns="http://www.w3.org/1999/xhtml"
class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
* For non-editable mode see Warning.textToDom_.
*/
* editable mode:
<foreignObject x="8" y="8" width="164" height="164">
<body xmlns="http://www.w3.org/1999/xhtml"
class="blocklyMinimalBody"> <textarea
xmlns="http://www.w3.org/1999/xhtml" class="blocklyCommentTextarea"
style="height: 164px; width: 164px;"></textarea>
</body>
</foreignObject>
* For non-editable mode see Warning.textToDom_.
*/
this.foreignObject_ = dom.createSvgElement(
Svg.FOREIGNOBJECT, {'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH},
null);
Svg.FOREIGNOBJECT,
{'x': Bubble.BORDER_WIDTH, 'y': Bubble.BORDER_WIDTH});
const body = document.createElementNS(dom.HTML_NS, 'body');
body.setAttribute('xmlns', dom.HTML_NS);
body.className = 'blocklyMinimalBody';
this.textarea_ = /** @type {!HTMLTextAreaElement} */ (
document.createElementNS(dom.HTML_NS, 'textarea'));
this.textarea_ = document.createElementNS(dom.HTML_NS, 'textarea') as
HTMLTextAreaElement;
const textarea = this.textarea_;
textarea.className = 'blocklyCommentTextarea';
textarea.setAttribute('dir', this.block_.RTL ? 'RTL' : 'LTR');
textarea.value = this.model_.text;
// AnyDuringMigration because: Type 'string | null' is not assignable to
// type 'string'.
textarea.value = this.model_.text as AnyDuringMigration;
this.resizeTextarea_();
body.appendChild(textarea);
this.foreignObject_.appendChild(body);
this.foreignObject_!.appendChild(body);
// Ideally this would be hooked to the focus event for the comment.
// However doing so in Firefox swallows the cursor for unknown reasons.
@@ -198,43 +167,36 @@ class Comment extends Icon {
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
textarea, 'mouseup', this, this.startEdit_, true, true);
// Don't zoom with mousewheel.
this.onWheelWrapper_ =
browserEvents.conditionalBind(textarea, 'wheel', this, function(e) {
this.onWheelWrapper_ = browserEvents.conditionalBind(
textarea, 'wheel', this, function(e: AnyDuringMigration) {
e.stopPropagation();
});
this.onChangeWrapper_ = browserEvents.conditionalBind(
textarea, 'change', this,
/**
* @this {Comment}
* @param {Event} _e Unused event parameter.
*/
function(_e) {
/** @param _e Unused event parameter. */
function(this: Comment, _e: Event) {
if (this.cachedText_ !== this.model_.text) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.block_, 'comment', null, this.cachedText_,
this.model_.text));
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))!
(this.block_, 'comment', null, this.cachedText_,
this.model_.text));
}
});
this.onInputWrapper_ = browserEvents.conditionalBind(
textarea, 'input', this,
/**
* @this {Comment}
* @param {Event} _e Unused event parameter.
*/
function(_e) {
/** @param _e Unused event parameter. */
function(this: Comment, _e: Event) {
this.model_.text = textarea.value;
});
setTimeout(textarea.focus.bind(textarea), 0);
return this.foreignObject_;
// AnyDuringMigration because: Type 'SVGForeignObjectElement | null' is not
// assignable to type 'SVGElement'.
return this.foreignObject_ as AnyDuringMigration;
}
/**
* Add or remove editability of the comment.
* @override
*/
updateEditable() {
/** Add or remove editability of the comment. */
override updateEditable() {
super.updateEditable();
if (this.isVisible()) {
// Recreate the bubble with the correct UI.
@@ -246,9 +208,8 @@ class Comment extends Icon {
/**
* Callback function triggered when the bubble has resized.
* Resize the text area accordingly.
* @private
*/
onBubbleResize_() {
private onBubbleResize_() {
if (!this.isVisible()) {
return;
}
@@ -259,29 +220,34 @@ class Comment extends Icon {
/**
* Resizes the text area to match the size defined on the model (which is
* the size of the bubble).
* @private
*/
resizeTextarea_() {
private resizeTextarea_() {
const size = this.model_.size;
const doubleBorderWidth = 2 * Bubble.BORDER_WIDTH;
const widthMinusBorder = size.width - doubleBorderWidth;
const heightMinusBorder = size.height - doubleBorderWidth;
this.foreignObject_.setAttribute('width', widthMinusBorder);
this.foreignObject_.setAttribute('height', heightMinusBorder);
this.textarea_.style.width = (widthMinusBorder - 4) + 'px';
this.textarea_.style.height = (heightMinusBorder - 4) + 'px';
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.foreignObject_!.setAttribute(
'width', widthMinusBorder as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.foreignObject_!.setAttribute(
'height', heightMinusBorder as AnyDuringMigration);
this.textarea_!.style.width = widthMinusBorder - 4 + 'px';
this.textarea_!.style.height = heightMinusBorder - 4 + 'px';
}
/**
* Show or hide the comment bubble.
* @param {boolean} visible True if the bubble should be visible.
* @param visible True if the bubble should be visible.
*/
setVisible(visible) {
override setVisible(visible: boolean) {
if (visible === this.isVisible()) {
return;
}
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))(
this.block_, visible, 'comment'));
eventUtils.fire(new (eventUtils.get(eventUtils.BUBBLE_OPEN))!
(this.block_, visible, 'comment'));
this.model_.pinned = visible;
if (visible) {
this.createBubble_();
@@ -290,11 +256,8 @@ class Comment extends Icon {
}
}
/**
* Show the bubble. Handles deciding if it should be editable or not.
* @private
*/
createBubble_() {
/** Show the bubble. Handles deciding if it should be editable or not. */
private createBubble_() {
if (!this.block_.isEditable() || userAgent.IE) {
// MSIE does not support foreignobject; textareas are impossible.
// https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-svg/56e6e04c-7c8c-44dd-8100-bd745ee42034
@@ -305,16 +268,12 @@ class Comment extends Icon {
}
}
/**
* Show an editable bubble.
* @private
*/
createEditableBubble_() {
/** Show an editable bubble. */
private createEditableBubble_() {
this.bubble_ = new Bubble(
/** @type {!WorkspaceSvg} */ (this.block_.workspace),
this.createEditor_(), this.block_.pathObject.svgPath,
/** @type {!Coordinate} */ (this.iconXY_), this.model_.size.width,
this.model_.size.height);
this.block_.workspace!, this.createEditor_(),
this.block_.pathObject.svgPath, (this.iconXY_ as Coordinate),
this.model_.size.width, this.model_.size.height);
// Expose this comment's block's ID on its top-level SVG group.
this.bubble_.setSvgId(this.block_.id);
this.bubble_.registerResizeEvent(this.onBubbleResize_.bind(this));
@@ -323,24 +282,24 @@ class Comment extends Icon {
/**
* Show a non-editable bubble.
* @private
* @suppress {checkTypes} Suppress `this` type mismatch.
*/
createNonEditableBubble_() {
private createNonEditableBubble_() {
// TODO (#2917): It would be great if the comment could support line breaks.
this.paragraphElement_ = Bubble.textToDom(this.block_.getCommentText());
// AnyDuringMigration because: Argument of type 'string | null' is not
// assignable to parameter of type 'string'.
this.paragraphElement_ =
Bubble.textToDom(this.block_.getCommentText() as AnyDuringMigration);
this.bubble_ = Bubble.createNonEditableBubble(
this.paragraphElement_, /** @type {!BlockSvg} */ (this.block_),
/** @type {!Coordinate} */ (this.iconXY_));
this.paragraphElement_, (this.block_), this.iconXY_ as Coordinate);
this.applyColour();
}
/**
* Dispose of the bubble.
* @private
* @suppress {checkTypes} Suppress `this` type mismatch.
*/
disposeBubble_() {
private disposeBubble_() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
this.onMouseUpWrapper_ = null;
@@ -369,14 +328,13 @@ class Comment extends Icon {
*
* Bring the comment to the top of the stack when clicked on. Also cache the
* current text so it can be used to fire a change event.
* @param {!Event} _e Mouse up event.
* @private
* @param _e Mouse up event.
*/
startEdit_(_e) {
private startEdit_(_e: Event) {
if (this.bubble_.promote()) {
// Since the act of moving this node within the DOM causes a loss of
// focus, we need to reapply the focus.
this.textarea_.focus();
this.textarea_!.focus();
}
this.cachedText_ = this.model_.text;
@@ -384,18 +342,18 @@ class Comment extends Icon {
/**
* Get the dimensions of this comment's bubble.
* @return {Size} Object with width and height properties.
* @return Object with width and height properties.
*/
getBubbleSize() {
getBubbleSize(): Size {
return this.model_.size;
}
/**
* Size this comment's bubble.
* @param {number} width Width of the bubble.
* @param {number} height Height of the bubble.
* @param width Width of the bubble.
* @param height Height of the bubble.
*/
setBubbleSize(width, height) {
setBubbleSize(width: number, height: number) {
if (this.bubble_) {
this.bubble_.setBubbleSize(width, height);
} else {
@@ -406,15 +364,17 @@ class Comment extends Icon {
/**
* Update the comment's view to match the model.
* @package
* @internal
*/
updateText() {
if (this.textarea_) {
this.textarea_.value = this.model_.text;
// AnyDuringMigration because: Type 'string | null' is not assignable to
// type 'string'.
this.textarea_.value = this.model_.text as AnyDuringMigration;
} else if (this.paragraphElement_) {
// Non-Editable mode.
// TODO (#2917): If 2917 gets added this will probably need to be updated.
this.paragraphElement_.firstChild.textContent = this.model_.text;
this.paragraphElement_.firstChild!.textContent = this.model_.text;
}
}
@@ -424,15 +384,13 @@ class Comment extends Icon {
* If you want to receive a comment "delete" event (newValue: null), then this
* should not be called directly. Instead call block.setCommentText(null);
*/
dispose() {
override dispose() {
this.block_.comment = null;
Icon.prototype.dispose.call(this);
super.dispose();
}
}
/**
* CSS for block comment. See css.js for use.
*/
/** CSS for block comment. See css.js for use. */
Css.register(`
.blocklyCommentTextarea {
background-color: #fef49c;
@@ -445,5 +403,3 @@ Css.register(`
text-overflow: hidden;
}
`);
exports.Comment = Comment;

View File

@@ -8,116 +8,135 @@
* @fileoverview Common functions used both internally and externally, but which
* must not be at the top level to avoid circular dependencies.
*/
'use strict';
/**
* Common functions used both internally and externally, but which
* must not be at the top level to avoid circular dependencies.
* @namespace Blockly.common
*/
goog.module('Blockly.common');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.common');
/* eslint-disable-next-line no-unused-vars */
const {BlockDefinition, Blocks} = goog.require('Blockly.blocks');
/* eslint-disable-next-line no-unused-vars */
const {Connection} = goog.requireType('Blockly.Connection');
/* eslint-disable-next-line no-unused-vars */
const {ICopyable} = goog.requireType('Blockly.ICopyable');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
import type {Block} from './block.js';
import {BlockDefinition, Blocks} from './blocks.js';
import type {Connection} from './connection.js';
import type {ICopyable} from './interfaces/i_copyable.js';
import type {Workspace} from './workspace.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/** Database of all workspaces. */
const WorkspaceDB_ = Object.create(null);
/**
* Find the workspace with the specified ID.
* @param id ID of workspace to find.
* @return The sought after workspace or null if not found.
*/
export function getWorkspaceById(id: string): Workspace|null {
return WorkspaceDB_[id] || null;
}
/**
* Find all workspaces.
* @return Array of workspaces.
*/
export function getAllWorkspaces(): Workspace[] {
const workspaces = [];
for (const workspaceId in WorkspaceDB_) {
workspaces.push(WorkspaceDB_[workspaceId]);
}
return workspaces;
}
export function registerWorkspace(workspace: Workspace) {
WorkspaceDB_[workspace.id] = workspace;
}
export function unregisterWorkpace(workspace: Workspace) {
delete WorkspaceDB_[workspace.id];
}
/**
* The main workspace most recently used.
* Set by Blockly.WorkspaceSvg.prototype.markFocused
* @type {!Workspace}
*/
let mainWorkspace;
let mainWorkspace: Workspace;
/**
* Returns the last used top level workspace (based on focus). Try not to use
* this function, particularly if there are multiple Blockly instances on a
* page.
* @return {!Workspace} The main workspace.
* @return The main workspace.
* @alias Blockly.common.getMainWorkspace
*/
const getMainWorkspace = function() {
export function getMainWorkspace(): Workspace {
return mainWorkspace;
};
exports.getMainWorkspace = getMainWorkspace;
}
/**
* Sets last used main workspace.
* @param {!Workspace} workspace The most recently used top level workspace.
* @param workspace The most recently used top level workspace.
* @alias Blockly.common.setMainWorkspace
*/
const setMainWorkspace = function(workspace) {
export function setMainWorkspace(workspace: Workspace) {
mainWorkspace = workspace;
};
exports.setMainWorkspace = setMainWorkspace;
}
/**
* Currently selected block.
* @type {?ICopyable}
*/
let selected = null;
let selected: ICopyable|null = null;
/**
* Returns the currently selected block.
* @return {?ICopyable} The currently selected block.
* @return The currently selected block.
* @alias Blockly.common.getSelected
*/
const getSelected = function() {
export function getSelected(): ICopyable|null {
return selected;
};
exports.getSelected = getSelected;
}
/**
* Sets the currently selected block. This function does not visually mark the
* block as selected or fire the required events. If you wish to
* programmatically select a block, use `BlockSvg#select`.
* @param {?ICopyable} newSelection The newly selected block.
* @param newSelection The newly selected block.
* @alias Blockly.common.setSelected
* @package
* @internal
*/
const setSelected = function(newSelection) {
export function setSelected(newSelection: ICopyable|null) {
selected = newSelection;
};
exports.setSelected = setSelected;
}
/**
* Container element in which to render the WidgetDiv, DropDownDiv and Tooltip.
* @type {?Element}
*/
let parentContainer;
let parentContainer: Element|null;
/**
* Get the container element in which to render the WidgetDiv, DropDownDiv and\
* Tooltip.
* @return {?Element} The parent container.
* @return The parent container.
* @alias Blockly.common.getParentContainer
*/
const getParentContainer = function() {
export function getParentContainer(): Element|null {
return parentContainer;
};
exports.getParentContainer = getParentContainer;
}
/**
* Set the parent container. This is the container element that the WidgetDiv,
* DropDownDiv, and Tooltip are rendered into the first time `Blockly.inject`
* is called.
* This method is a NOP if called after the first ``Blockly.inject``.
* @param {!Element} newParent The container element.
* @param newParent The container element.
* @alias Blockly.common.setParentContainer
*/
const setParentContainer = function(newParent) {
export function setParentContainer(newParent: Element) {
parentContainer = newParent;
};
exports.setParentContainer = setParentContainer;
}
/**
* Size the SVG image to completely fill its container. Call this when the view
@@ -125,10 +144,10 @@ exports.setParentContainer = setParentContainer;
* See workspace.resizeContents to resize the workspace when the contents
* change (e.g. when a block is added or removed).
* Record the height/width of the SVG image.
* @param {!WorkspaceSvg} workspace Any workspace in the SVG.
* @param workspace Any workspace in the SVG.
* @alias Blockly.common.svgResize
*/
const svgResize = function(workspace) {
export function svgResize(workspace: WorkspaceSvg) {
let mainWorkspace = workspace;
while (mainWorkspace.options.parentWorkspace) {
mainWorkspace = mainWorkspace.options.parentWorkspace;
@@ -152,26 +171,25 @@ const svgResize = function(workspace) {
mainWorkspace.setCachedParentSvgSize(null, height);
}
mainWorkspace.resize();
};
exports.svgResize = svgResize;
}
/**
* All of the connections on blocks that are currently being dragged.
* @type {!Array<!Connection>}
*/
exports.draggingConnections = [];
export const draggingConnections: Connection[] = [];
/**
* Get a map of all the block's descendants mapping their type to the number of
* children with that type.
* @param {!Block} block The block to map.
* @param {boolean=} opt_stripFollowing Optionally ignore all following
* @param block The block to map.
* @param opt_stripFollowing Optionally ignore all following
* statements (blocks that are not inside a value or statement input
* of the block).
* @return {!Object} Map of types to type counts for descendants of the bock.
* @return Map of types to type counts for descendants of the bock.
* @alias Blockly.common.getBlockTypeCounts
*/
const getBlockTypeCounts = function(block, opt_stripFollowing) {
export function getBlockTypeCounts(
block: Block, opt_stripFollowing?: boolean): AnyDuringMigration {
const typeCountsMap = Object.create(null);
const descendants = block.getDescendants(true);
if (opt_stripFollowing) {
@@ -181,7 +199,7 @@ const getBlockTypeCounts = function(block, opt_stripFollowing) {
descendants.splice(index, descendants.length - index);
}
}
for (let i = 0, checkBlock; (checkBlock = descendants[i]); i++) {
for (let i = 0, checkBlock; checkBlock = descendants[i]; i++) {
if (typeCountsMap[checkBlock.type]) {
typeCountsMap[checkBlock.type]++;
} else {
@@ -189,43 +207,49 @@ const getBlockTypeCounts = function(block, opt_stripFollowing) {
}
}
return typeCountsMap;
};
exports.getBlockTypeCounts = getBlockTypeCounts;
}
/**
* Helper function for defining a block from JSON. The resulting function has
* the correct value of jsonDef at the point in code where jsonInit is called.
* @param {!Object} jsonDef The JSON definition of a block.
* @return {function()} A function that calls jsonInit with the correct value
* @param jsonDef The JSON definition of a block.
* @return A function that calls jsonInit with the correct value
* of jsonDef.
*/
const jsonInitFactory = function(jsonDef) {
return /** @this {Block} */ function() {
function jsonInitFactory(jsonDef: AnyDuringMigration): () => void {
return function(this: Block) {
this.jsonInit(jsonDef);
};
};
}
/**
* Define blocks from an array of JSON block definitions, as might be generated
* by the Blockly Developer Tools.
* @param {!Array<!Object>} jsonArray An array of JSON block definitions.
* @param jsonArray An array of JSON block definitions.
* @alias Blockly.common.defineBlocksWithJsonArray
*/
const defineBlocksWithJsonArray = function(jsonArray) {
export function defineBlocksWithJsonArray(jsonArray: AnyDuringMigration[]) {
TEST_ONLY.defineBlocksWithJsonArrayInternal(jsonArray);
}
/**
* Private version of defineBlocksWithJsonArray for stubbing in tests.
*/
function defineBlocksWithJsonArrayInternal(jsonArray: AnyDuringMigration[]) {
defineBlocks(createBlockDefinitionsFromJsonArray(jsonArray));
};
exports.defineBlocksWithJsonArray = defineBlocksWithJsonArray;
}
/**
* Define blocks from an array of JSON block definitions, as might be generated
* by the Blockly Developer Tools.
* @param {!Array<!Object>} jsonArray An array of JSON block definitions.
* @return {!Object<string, !BlockDefinition>} A map of the block
* @param jsonArray An array of JSON block definitions.
* @return A map of the block
* definitions created.
* @alias Blockly.common.defineBlocksWithJsonArray
*/
const createBlockDefinitionsFromJsonArray = function(jsonArray) {
const /** @type {!Object<string,!BlockDefinition>} */ blocks = {};
export function createBlockDefinitionsFromJsonArray(
jsonArray: AnyDuringMigration[]): {[key: string]: BlockDefinition} {
const blocks: {[key: string]: BlockDefinition} = {};
for (let i = 0; i < jsonArray.length; i++) {
const elem = jsonArray[i];
if (!elem) {
@@ -242,18 +266,16 @@ const createBlockDefinitionsFromJsonArray = function(jsonArray) {
blocks[type] = {init: jsonInitFactory(elem)};
}
return blocks;
};
exports.createBlockDefinitionsFromJsonArray =
createBlockDefinitionsFromJsonArray;
}
/**
* Add the specified block definitions to the block definitions
* dictionary (Blockly.Blocks).
* @param {!Object<string,!BlockDefinition>} blocks A map of block
* @param blocks A map of block
* type names to block definitions.
* @alias Blockly.common.defineBlocks
*/
const defineBlocks = function(blocks) {
export function defineBlocks(blocks: {[key: string]: BlockDefinition}) {
// Iterate over own enumerable properties.
for (const type of Object.keys(blocks)) {
const definition = blocks[type];
@@ -262,5 +284,6 @@ const defineBlocks = function(blocks) {
}
Blocks[type] = definition;
}
};
exports.defineBlocks = defineBlocks;
}
export const TEST_ONLY = {defineBlocksWithJsonArrayInternal};

View File

@@ -8,59 +8,70 @@
* @fileoverview Manager for all items registered with the workspace.
*/
'use strict';
/**
* Manager for all items registered with the workspace.
* @class
*/
goog.module('Blockly.ComponentManager');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ComponentManager');
const arrayUtils = goog.require('Blockly.utils.array');
/* eslint-disable-next-line no-unused-vars */
const {IAutoHideable} = goog.requireType('Blockly.IAutoHideable');
/* eslint-disable-next-line no-unused-vars */
const {IComponent} = goog.requireType('Blockly.IComponent');
/* eslint-disable-next-line no-unused-vars */
const {IDeleteArea} = goog.requireType('Blockly.IDeleteArea');
/* eslint-disable-next-line no-unused-vars */
const {IDragTarget} = goog.requireType('Blockly.IDragTarget');
/* eslint-disable-next-line no-unused-vars */
const {IPositionable} = goog.requireType('Blockly.IPositionable');
import type {IAutoHideable} from './interfaces/i_autohideable.js';
import type {IComponent} from './interfaces/i_component.js';
import type {IDeleteArea} from './interfaces/i_delete_area.js';
import type {IDragTarget} from './interfaces/i_drag_target.js';
import type {IPositionable} from './interfaces/i_positionable.js';
import * as arrayUtils from './utils/array.js';
class Capability<T> {
static POSITIONABLE = new Capability<IPositionable>('positionable');
static DRAG_TARGET = new Capability<IDragTarget>('drag_target');
static DELETE_AREA = new Capability<IDeleteArea>('delete_area');
static AUTOHIDEABLE = new Capability<IAutoHideable>('autohideable');
private readonly name_: string;
/** @param name The name of the component capability. */
constructor(name: string) {
this.name_ = name;
}
/**
* Returns the name of the capability.
* @return The name.
*/
toString(): string {
return this.name_;
}
}
/**
* Manager for all items registered with the workspace.
* @alias Blockly.ComponentManager
*/
class ComponentManager {
/**
* Creates a new ComponentManager instance.
*/
export class ComponentManager {
static Capability = Capability;
// static Capability: AnyDuringMigration;
private readonly componentData_: {[key: string]: ComponentDatum};
private readonly capabilityToComponentIds_: {[key: string]: string[]};
/** Creates a new ComponentManager instance. */
constructor() {
/**
* A map of the components registered with the workspace, mapped to id.
* @type {!Object<string, !ComponentManager.ComponentDatum>}
* @private
*/
this.componentData_ = Object.create(null);
/**
* A map of capabilities to component IDs.
* @type {!Object<string, !Array<string>>}
* @private
*/
/** A map of capabilities to component IDs. */
this.capabilityToComponentIds_ = Object.create(null);
}
/**
* Adds a component.
* @param {!ComponentManager.ComponentDatum} componentInfo The data for
* the component to register.
* @param {boolean=} opt_allowOverrides True to prevent an error when
* overriding an already registered item.
* @param componentInfo The data for the component to register.
* @param opt_allowOverrides True to prevent an error when overriding an
* already registered item.
*/
addComponent(componentInfo, opt_allowOverrides) {
addComponent(componentInfo: ComponentDatum, opt_allowOverrides?: boolean) {
// Don't throw an error if opt_allowOverrides is true.
const id = componentInfo.component.id;
if (!opt_allowOverrides && this.componentData_[id]) {
@@ -84,9 +95,9 @@ class ComponentManager {
/**
* Removes a component.
* @param {string} id The ID of the component to remove.
* @param id The ID of the component to remove.
*/
removeComponent(id) {
removeComponent(id: string) {
const componentInfo = this.componentData_[id];
if (!componentInfo) {
return;
@@ -100,12 +111,10 @@ class ComponentManager {
/**
* Adds a capability to a existing registered component.
* @param {string} id The ID of the component to add the capability to.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to add.
* @template T
* @param id The ID of the component to add the capability to.
* @param capability The capability to add.
*/
addCapability(id, capability) {
addCapability<T>(id: string, capability: string|Capability<T>) {
if (!this.getComponent(id)) {
throw Error(
'Cannot add capability, "' + capability + '". Plugin "' + id +
@@ -123,12 +132,10 @@ class ComponentManager {
/**
* Removes a capability from an existing registered component.
* @param {string} id The ID of the component to remove the capability from.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to remove.
* @template T
* @param id The ID of the component to remove the capability from.
* @param capability The capability to remove.
*/
removeCapability(id, capability) {
removeCapability<T>(id: string, capability: string|Capability<T>) {
if (!this.getComponent(id)) {
throw Error(
'Cannot remove capability, "' + capability + '". Plugin "' + id +
@@ -147,44 +154,39 @@ class ComponentManager {
/**
* Returns whether the component with this id has the specified capability.
* @param {string} id The ID of the component to check.
* @param {string|!ComponentManager.Capability<T>} capability The
* capability to check for.
* @return {boolean} Whether the component has the capability.
* @template T
* @param id The ID of the component to check.
* @param capability The capability to check for.
* @return Whether the component has the capability.
*/
hasCapability(id, capability) {
hasCapability<T>(id: string, capability: string|Capability<T>): boolean {
capability = String(capability).toLowerCase();
return this.componentData_[id].capabilities.indexOf(capability) !== -1;
}
/**
* Gets the component with the given ID.
* @param {string} id The ID of the component to get.
* @return {!IComponent|undefined} The component with the given name
* or undefined if not found.
* @param id The ID of the component to get.
* @return The component with the given name or undefined if not found.
*/
getComponent(id) {
getComponent(id: string): IComponent|undefined {
return this.componentData_[id] && this.componentData_[id].component;
}
/**
* Gets all the components with the specified capability.
* @param {string|!ComponentManager.Capability<T>
* } capability The capability of the component.
* @param {boolean} sorted Whether to return list ordered by weights.
* @return {!Array<T>} The components that match the specified capability.
* @template T
* @param capability The capability of the component.
* @param sorted Whether to return list ordered by weights.
* @return The components that match the specified capability.
*/
getComponents(capability, sorted) {
getComponents<T>(capability: string|Capability<T>, sorted: boolean): T[] {
capability = String(capability).toLowerCase();
const componentIds = this.capabilityToComponentIds_[capability];
if (!componentIds) {
return [];
}
const components = [];
const components: AnyDuringMigration[] = [];
if (sorted) {
const componentDataList = [];
const componentDataList: AnyDuringMigration[] = [];
const componentData = this.componentData_;
componentIds.forEach(function(id) {
componentDataList.push(componentData[id]);
@@ -205,59 +207,13 @@ class ComponentManager {
}
}
/**
* An object storing component information.
* @typedef {{
* component: !IComponent,
* capabilities: (
* !Array<string|!ComponentManager.Capability<!IComponent>>
* ),
* weight: number
* }}
*/
ComponentManager.ComponentDatum;
/**
* A name with the capability of the element stored in the generic.
* @template T
* @alias Blockly.ComponentManager.Capability
*/
ComponentManager.Capability = class {
/**
* @param {string} name The name of the component capability.
*/
constructor(name) {
/**
* @type {string}
* @private
*/
this.name_ = name;
export namespace ComponentManager {
/** An object storing component information. */
export interface ComponentDatum {
component: IComponent;
capabilities: Array<string|Capability<IComponent>>;
weight: number;
}
}
/**
* Returns the name of the capability.
* @return {string} The name.
* @override
*/
toString() {
return this.name_;
}
};
/** @type {!ComponentManager.Capability<!IPositionable>} */
ComponentManager.Capability.POSITIONABLE =
new ComponentManager.Capability('positionable');
/** @type {!ComponentManager.Capability<!IDragTarget>} */
ComponentManager.Capability.DRAG_TARGET =
new ComponentManager.Capability('drag_target');
/** @type {!ComponentManager.Capability<!IDeleteArea>} */
ComponentManager.Capability.DELETE_AREA =
new ComponentManager.Capability('delete_area');
/** @type {!ComponentManager.Capability<!IAutoHideable>} */
ComponentManager.Capability.AUTOHIDEABLE =
new ComponentManager.Capability('autohideable');
exports.ComponentManager = ComponentManager;
export type ComponentDatum = ComponentManager.ComponentDatum;

View File

@@ -9,7 +9,6 @@
* before injecting Blockly. Changing these values during run time is not
* generally recommended.
*/
'use strict';
/**
* All the values that we expect developers to be able to change
@@ -17,35 +16,31 @@
* generally recommended.
* @namespace Blockly.config
*/
goog.module('Blockly.config');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.config');
/**
* All the values that we expect developers to be able to change
* before injecting Blockly.
* @typedef {{
* dragRadius: number,
* flyoutDragRadius: number,
* snapRadius: number,
* currentConnectionPreference: number,
* bumpDelay: number,
* connectingSnapRadius: number
* }}
*/
let Config; // eslint-disable-line no-unused-vars
interface Config {
dragRadius: number;
flyoutDragRadius: number;
snapRadius: number;
currentConnectionPreference: number;
bumpDelay: number;
connectingSnapRadius: number;
}
/**
* Default snap radius.
* @type {number}
*/
/** Default snap radius. */
const DEFAULT_SNAP_RADIUS = 28;
/**
* Object holding all the values on Blockly that we expect developers to be
* able to change.
* @type {Config}
*/
const config = {
export const config: Config = {
/**
* Number of pixels the mouse must move before a drag starts.
* @alias Blockly.config.dragRadius
@@ -83,5 +78,3 @@ const config = {
*/
bumpDelay: 250,
};
exports.config = config;

View File

@@ -7,108 +7,92 @@
/**
* @fileoverview Components for creating connections between blocks.
*/
'use strict';
/**
* Components for creating connections between blocks.
* @class
*/
goog.module('Blockly.Connection');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Connection');
const Xml = goog.require('Blockly.Xml');
const blocks = goog.require('Blockly.serialization.blocks');
const eventUtils = goog.require('Blockly.Events.utils');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {BlockMove} = goog.requireType('Blockly.Events.BlockMove');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
/* eslint-disable-next-line no-unused-vars */
const {IASTNodeLocationWithBlock} = goog.require('Blockly.IASTNodeLocationWithBlock');
/* eslint-disable-next-line no-unused-vars */
const {IConnectionChecker} = goog.requireType('Blockly.IConnectionChecker');
/* eslint-disable-next-line no-unused-vars */
const {Input} = goog.requireType('Blockly.Input');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockMove');
/** @suppress {extraRequire} */
goog.require('Blockly.constants');
// Unused import preserved for side-effects. Remove if unneeded.
// import './constants.js';
import type {Block} from './block.js';
import {ConnectionType} from './connection_type.js';
import type {BlockMove} from './events/events_block_move.js';
import * as eventUtils from './events/utils.js';
import type {Input} from './input.js';
import type {IASTNodeLocationWithBlock} from './interfaces/i_ast_node_location_with_block.js';
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import * as blocks from './serialization/blocks.js';
import * as Xml from './xml.js';
/**
* Class for a connection between blocks.
* @implements {IASTNodeLocationWithBlock}
* @alias Blockly.Connection
*/
class Connection {
export class Connection implements IASTNodeLocationWithBlock {
/** Constants for checking whether two connections are compatible. */
static CAN_CONNECT = 0;
static REASON_SELF_CONNECTION = 1;
static REASON_WRONG_TYPE = 2;
static REASON_TARGET_NULL = 3;
static REASON_CHECKS_FAILED = 4;
static REASON_DIFFERENT_WORKSPACES = 5;
static REASON_SHADOW_PARENT = 6;
static REASON_DRAG_CHECKS_FAILED = 7;
static REASON_PREVIOUS_AND_OUTPUT = 8;
protected sourceBlock_: Block;
/** Connection this connection connects to. Null if not connected. */
targetConnection: Connection|null = null;
/**
* @param {!Block} source The block establishing this connection.
* @param {number} type The type of the connection.
* Has this connection been disposed of?
* @internal
*/
constructor(source, type) {
/**
* @type {!Block}
* @protected
*/
disposed = false;
/** List of compatible value types. Null if all types are compatible. */
// AnyDuringMigration because: Type 'null' is not assignable to type 'any[]'.
private check_: AnyDuringMigration[] = null as AnyDuringMigration;
/** DOM representation of a shadow block, or null if none. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Element'.
private shadowDom_: Element = null as AnyDuringMigration;
/**
* Horizontal location of this connection.
* @internal
*/
x = 0;
/**
* Vertical location of this connection.
* @internal
*/
y = 0;
private shadowState_: blocks.State|null = null;
/**
* @param source The block establishing this connection.
* @param type The type of the connection.
*/
constructor(source: Block, public type: number) {
this.sourceBlock_ = source;
/** @type {number} */
this.type = type;
/**
* Connection this connection connects to. Null if not connected.
* @type {Connection}
*/
this.targetConnection = null;
/**
* Has this connection been disposed of?
* @type {boolean}
* @package
*/
this.disposed = false;
/**
* List of compatible value types. Null if all types are compatible.
* @type {Array}
* @private
*/
this.check_ = null;
/**
* DOM representation of a shadow block, or null if none.
* @type {Element}
* @private
*/
this.shadowDom_ = null;
/**
* Horizontal location of this connection.
* @type {number}
* @package
*/
this.x = 0;
/**
* Vertical location of this connection.
* @type {number}
* @package
*/
this.y = 0;
/**
* @type {?blocks.State}
* @private
*/
this.shadowState_ = null;
}
/**
* Connect two connections together. This is the connection on the superior
* block.
* @param {!Connection} childConnection Connection on inferior block.
* @protected
* @param childConnection Connection on inferior block.
*/
connect_(childConnection) {
protected connect_(childConnection: Connection) {
const INPUT = ConnectionType.INPUT_VALUE;
const parentConnection = this;
const parentBlock = parentConnection.getSourceBlock();
@@ -124,8 +108,8 @@ class Connection {
if (parentConnection.isConnected()) {
const shadowState = parentConnection.stashShadowState_();
const target = parentConnection.targetBlock();
if (target.isShadow()) {
target.dispose(false);
if (target!.isShadow()) {
target!.dispose(false);
} else {
parentConnection.disconnect();
orphan = target;
@@ -136,8 +120,8 @@ class Connection {
// Connect the new connection to the parent.
let event;
if (eventUtils.isEnabled()) {
event = /** @type {!BlockMove} */
(new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock));
event =
new (eventUtils.get(eventUtils.BLOCK_MOVE))!(childBlock) as BlockMove;
}
connectReciprocally(parentConnection, childConnection);
childBlock.setParent(parentBlock);
@@ -152,7 +136,7 @@ class Connection {
orphan.outputConnection :
orphan.previousConnection;
const connection = Connection.getConnectionForOrphanedConnection(
childBlock, /** @type {!Connection} */ (orphanConnection));
childBlock, (orphanConnection));
if (connection) {
orphanConnection.connect(connection);
} else {
@@ -163,7 +147,7 @@ class Connection {
/**
* Dispose of this connection and deal with connected blocks.
* @package
* @internal
*/
dispose() {
// isConnected returns true for shadows and non-shadows.
@@ -183,57 +167,55 @@ class Connection {
/**
* Get the source block for this connection.
* @return {!Block} The source block.
* @return The source block.
*/
getSourceBlock() {
getSourceBlock(): Block {
return this.sourceBlock_;
}
/**
* Does the connection belong to a superior block (higher in the source
* stack)?
* @return {boolean} True if connection faces down or right.
* @return True if connection faces down or right.
*/
isSuperior() {
isSuperior(): boolean {
return this.type === ConnectionType.INPUT_VALUE ||
this.type === ConnectionType.NEXT_STATEMENT;
}
/**
* Is the connection connected?
* @return {boolean} True if connection is connected to another connection.
* @return True if connection is connected to another connection.
*/
isConnected() {
isConnected(): boolean {
return !!this.targetConnection;
}
/**
* Get the workspace's connection type checker object.
* @return {!IConnectionChecker} The connection type checker for the
* source block's workspace.
* @package
* @return The connection type checker for the source block's workspace.
* @internal
*/
getConnectionChecker() {
return this.sourceBlock_.workspace.connectionChecker;
getConnectionChecker(): IConnectionChecker {
return this.sourceBlock_.workspace!.connectionChecker;
}
/**
* Called when an attempted connection fails. NOP by default (i.e. for
* headless workspaces).
* @param {!Connection} _otherConnection Connection that this connection
* failed to connect to.
* @package
* @param _otherConnection Connection that this connection failed to connect
* to.
* @internal
*/
onFailedConnect(_otherConnection) {
// NOP
}
onFailedConnect(_otherConnection: Connection) {}
// NOP
/**
* Connect this connection to another connection.
* @param {!Connection} otherConnection Connection to connect to.
* @return {boolean} Whether the the blocks are now connected or not.
* @param otherConnection Connection to connect to.
* @return Whether the the blocks are now connected or not.
*/
connect(otherConnection) {
connect(otherConnection: Connection): boolean {
if (this.targetConnection === otherConnection) {
// Already connected together. NOP.
return true;
@@ -261,9 +243,7 @@ class Connection {
return this.isConnected();
}
/**
* Disconnect this connection.
*/
/** Disconnect this connection. */
disconnect() {
const otherConnection = this.targetConnection;
if (!otherConnection) {
@@ -303,18 +283,19 @@ class Connection {
/**
* Disconnect two blocks that are connected by this connection.
* @param {!Block} parentBlock The superior block.
* @param {!Block} childBlock The inferior block.
* @protected
* @param parentBlock The superior block.
* @param childBlock The inferior block.
*/
disconnectInternal_(parentBlock, childBlock) {
protected disconnectInternal_(parentBlock: Block, childBlock: Block) {
let event;
if (eventUtils.isEnabled()) {
event = /** @type {!BlockMove} */
(new (eventUtils.get(eventUtils.BLOCK_MOVE))(childBlock));
event =
new (eventUtils.get(eventUtils.BLOCK_MOVE))!(childBlock) as BlockMove;
}
const otherConnection = this.targetConnection;
otherConnection.targetConnection = null;
if (otherConnection) {
otherConnection.targetConnection = null;
}
this.targetConnection = null;
childBlock.setParent(null);
if (event) {
@@ -325,47 +306,44 @@ class Connection {
/**
* Respawn the shadow block if there was one connected to the this connection.
* @protected
*/
respawnShadow_() {
protected respawnShadow_() {
// Have to keep respawnShadow_ for backwards compatibility.
this.createShadowBlock_(true);
}
/**
* Returns the block that this connection connects to.
* @return {?Block} The connected block or null if none is connected.
* @return The connected block or null if none is connected.
*/
targetBlock() {
targetBlock(): Block|null {
if (this.isConnected()) {
return this.targetConnection.getSourceBlock();
return this.targetConnection?.getSourceBlock() ?? null;
}
return null;
}
/**
* Function to be called when this connection's compatible types have changed.
* @protected
*/
onCheckChanged_() {
protected onCheckChanged_() {
// The new value type may not be compatible with the existing connection.
if (this.isConnected() &&
(!this.targetConnection ||
!this.getConnectionChecker().canConnect(
this, this.targetConnection, false))) {
const child = this.isSuperior() ? this.targetBlock() : this.sourceBlock_;
child.unplug();
child!.unplug();
}
}
/**
* Change a connection's compatibility.
* @param {?(string|!Array<string>)} check Compatible value type or list of
* value types. Null if all types are compatible.
* @return {!Connection} The connection being modified
* (to allow chaining).
* @param check Compatible value type or list of value types. Null if all
* types are compatible.
* @return The connection being modified (to allow chaining).
*/
setCheck(check) {
setCheck(check: string|string[]|null): Connection {
if (check) {
// Ensure that check is in an array.
if (!Array.isArray(check)) {
@@ -374,66 +352,64 @@ class Connection {
this.check_ = check;
this.onCheckChanged_();
} else {
this.check_ = null;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'any[]'.
this.check_ = null as AnyDuringMigration;
}
return this;
}
/**
* Get a connection's compatibility.
* @return {?Array} List of compatible value types.
* @return List of compatible value types.
* Null if all types are compatible.
* @public
*/
getCheck() {
getCheck(): AnyDuringMigration[]|null {
return this.check_;
}
/**
* Changes the connection's shadow block.
* @param {?Element} shadowDom DOM representation of a block or null.
* @param shadowDom DOM representation of a block or null.
*/
setShadowDom(shadowDom) {
this.setShadowStateInternal_({shadowDom: shadowDom});
setShadowDom(shadowDom: Element|null) {
this.setShadowStateInternal_({shadowDom});
}
/**
* Returns the xml representation of the connection's shadow block.
* @param {boolean=} returnCurrent If true, and the shadow block is currently
* attached to this connection, this serializes the state of that block
* and returns it (so that field values are correct). Otherwise the saved
* shadowDom is just returned.
* @return {?Element} Shadow DOM representation of a block or null.
* @param returnCurrent If true, and the shadow block is currently attached to
* this connection, this serializes the state of that block and returns it
* (so that field values are correct). Otherwise the saved shadowDom is
* just returned.
* @return Shadow DOM representation of a block or null.
*/
getShadowDom(returnCurrent) {
return (returnCurrent && this.targetBlock().isShadow()) ?
/** @type {!Element} */ (Xml.blockToDom(
/** @type {!Block} */ (this.targetBlock()))) :
getShadowDom(returnCurrent?: boolean): Element|null {
return returnCurrent && this.targetBlock()!.isShadow() ?
Xml.blockToDom((this.targetBlock() as Block)) as Element :
this.shadowDom_;
}
/**
* Changes the connection's shadow block.
* @param {?blocks.State} shadowState An state represetation of the block or
* null.
* @param shadowState An state represetation of the block or null.
*/
setShadowState(shadowState) {
this.setShadowStateInternal_({shadowState: shadowState});
setShadowState(shadowState: blocks.State|null) {
this.setShadowStateInternal_({shadowState});
}
/**
* Returns the serialized object representation of the connection's shadow
* block.
* @param {boolean=} returnCurrent If true, and the shadow block is currently
* attached to this connection, this serializes the state of that block
* and returns it (so that field values are correct). Otherwise the saved
* state is just returned.
* @return {?blocks.State} Serialized object representation of the block, or
* null.
* @param returnCurrent If true, and the shadow block is currently attached to
* this connection, this serializes the state of that block and returns it
* (so that field values are correct). Otherwise the saved state is just
* returned.
* @return Serialized object representation of the block, or null.
*/
getShadowState(returnCurrent) {
if (returnCurrent && this.targetBlock() && this.targetBlock().isShadow()) {
return blocks.save(/** @type {!Block} */ (this.targetBlock()));
getShadowState(returnCurrent?: boolean): blocks.State|null {
if (returnCurrent && this.targetBlock() && this.targetBlock()!.isShadow()) {
return blocks.save(this.targetBlock() as Block);
}
return this.shadowState_;
}
@@ -446,21 +422,21 @@ class Connection {
* and always return an empty list (the default).
* {@link Blockly.RenderedConnection} overrides this behavior with a list
* computed from the rendered positioning.
* @param {number} _maxLimit The maximum radius to another connection.
* @return {!Array<!Connection>} List of connections.
* @package
* @param _maxLimit The maximum radius to another connection.
* @return List of connections.
* @internal
*/
neighbours(_maxLimit) {
neighbours(_maxLimit: number): Connection[] {
return [];
}
/**
* Get the parent input of a connection.
* @return {?Input} The input that the connection belongs to or null if
* no parent exists.
* @package
* @return The input that the connection belongs to or null if no parent
* exists.
* @internal
*/
getParentInput() {
getParentInput(): Input|null {
let parentInput = null;
const inputs = this.sourceBlock_.inputList;
for (let i = 0; i < inputs.length; i++) {
@@ -475,9 +451,9 @@ class Connection {
/**
* This method returns a string describing this Connection in developer terms
* (English only). Intended to on be used in console logs and errors.
* @return {string} The description.
* @return The description.
*/
toString() {
toString(): string {
const block = this.sourceBlock_;
if (!block) {
return 'Orphan Connection';
@@ -491,7 +467,7 @@ class Connection {
msg = 'Next Connection of ';
} else {
let parentInput = null;
for (let i = 0, input; (input = block.inputList[i]); i++) {
for (let i = 0, input; input = block.inputList[i]; i++) {
if (input.connection === this) {
parentInput = input;
break;
@@ -510,53 +486,60 @@ class Connection {
/**
* Returns the state of the shadowDom_ and shadowState_ properties, then
* temporarily sets those properties to null so no shadow respawns.
* @return {{shadowDom: ?Element, shadowState: ?blocks.State}} The state of
* both the shadowDom_ and shadowState_ properties.
* @private
* @return The state of both the shadowDom_ and shadowState_ properties.
*/
stashShadowState_() {
private stashShadowState_():
{shadowDom: Element|null, shadowState: blocks.State|null} {
const shadowDom = this.getShadowDom(true);
const shadowState = this.getShadowState(true);
// Set to null so it doesn't respawn.
this.shadowDom_ = null;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Element'.
this.shadowDom_ = null as AnyDuringMigration;
this.shadowState_ = null;
return {shadowDom, shadowState};
}
/**
* Reapplies the stashed state of the shadowDom_ and shadowState_ properties.
* @param {{shadowDom: ?Element, shadowState: ?blocks.State}} param0 The state
* to reapply to the shadowDom_ and shadowState_ properties.
* @private
* @param param0 The state to reapply to the shadowDom_ and shadowState_
* properties.
*/
applyShadowState_({shadowDom, shadowState}) {
this.shadowDom_ = shadowDom;
private applyShadowState_({shadowDom, shadowState}: {
shadowDom: Element|null,
shadowState: blocks.State|null
}) {
// AnyDuringMigration because: Type 'Element | null' is not assignable to
// type 'Element'.
this.shadowDom_ = shadowDom as AnyDuringMigration;
this.shadowState_ = shadowState;
}
/**
* Sets the state of the shadow of this connection.
* @param {{shadowDom: (?Element|undefined), shadowState:
* (?blocks.State|undefined)}=} param0 The state to set the shadow of this
* connection to.
* @private
* @param param0 The state to set the shadow of this connection to.
*/
setShadowStateInternal_({shadowDom = null, shadowState = null} = {}) {
private setShadowStateInternal_({shadowDom = null, shadowState = null}: {
shadowDom?: Element|null,
shadowState?: blocks.State|null
} = {}) {
// One or both of these should always be null.
// If neither is null, the shadowState will get priority.
this.shadowDom_ = shadowDom;
// AnyDuringMigration because: Type 'Element | null' is not assignable to
// type 'Element'.
this.shadowDom_ = shadowDom as AnyDuringMigration;
this.shadowState_ = shadowState;
const target = this.targetBlock();
if (!target) {
this.respawnShadow_();
if (this.targetBlock() && this.targetBlock().isShadow()) {
if (this.targetBlock() && this.targetBlock()!.isShadow()) {
this.serializeShadow_(this.targetBlock());
}
} else if (target.isShadow()) {
target.dispose(false);
this.respawnShadow_();
if (this.targetBlock() && this.targetBlock().isShadow()) {
if (this.targetBlock() && this.targetBlock()!.isShadow()) {
this.serializeShadow_(this.targetBlock());
}
} else {
@@ -571,17 +554,16 @@ class Connection {
/**
* Creates a shadow block based on the current shadowState_ or shadowDom_.
* shadowState_ gets priority.
* @param {boolean} attemptToConnect Whether to try to connect the shadow
* block to this connection or not.
* @return {?Block} The shadow block that was created, or null if both the
* shadowState_ and shadowDom_ are null.
* @private
* @param attemptToConnect Whether to try to connect the shadow block to this
* connection or not.
* @return The shadow block that was created, or null if both the shadowState_
* and shadowDom_ are null.
*/
createShadowBlock_(attemptToConnect) {
private createShadowBlock_(attemptToConnect: boolean): Block|null {
const parentBlock = this.getSourceBlock();
const shadowState = this.getShadowState();
const shadowDom = this.getShadowDom();
if (!parentBlock.workspace || (!shadowState && !shadowDom)) {
if (!parentBlock.workspace || !shadowState && !shadowDom) {
return null;
}
@@ -625,14 +607,13 @@ class Connection {
/**
* Saves the given shadow block to both the shadowDom_ and shadowState_
* properties, in their respective serialized forms.
* @param {?Block} shadow The shadow to serialize, or null.
* @private
* @param shadow The shadow to serialize, or null.
*/
serializeShadow_(shadow) {
private serializeShadow_(shadow: Block|null) {
if (!shadow) {
return;
}
this.shadowDom_ = /** @type {!Element} */ (Xml.blockToDom(shadow));
this.shadowDom_ = Xml.blockToDom(shadow) as Element;
this.shadowState_ = blocks.save(shadow);
}
@@ -640,13 +621,12 @@ class Connection {
* Returns the connection (starting at the startBlock) which will accept
* the given connection. This includes compatible connection types and
* connection checks.
* @param {!Block} startBlock The block on which to start the search.
* @param {!Connection} orphanConnection The connection that is looking
* for a home.
* @return {?Connection} The suitable connection point on the chain of
* blocks, or null.
* @param startBlock The block on which to start the search.
* @param orphanConnection The connection that is looking for a home.
* @return The suitable connection point on the chain of blocks, or null.
*/
static getConnectionForOrphanedConnection(startBlock, orphanConnection) {
static getConnectionForOrphanedConnection(
startBlock: Block, orphanConnection: Connection): Connection|null {
if (orphanConnection.type === ConnectionType.OUTPUT_VALUE) {
return getConnectionForOrphanedOutput(
startBlock, orphanConnection.getSourceBlock());
@@ -661,48 +641,34 @@ class Connection {
}
}
/**
* Constants for checking whether two connections are compatible.
*/
Connection.CAN_CONNECT = 0;
Connection.REASON_SELF_CONNECTION = 1;
Connection.REASON_WRONG_TYPE = 2;
Connection.REASON_TARGET_NULL = 3;
Connection.REASON_CHECKS_FAILED = 4;
Connection.REASON_DIFFERENT_WORKSPACES = 5;
Connection.REASON_SHADOW_PARENT = 6;
Connection.REASON_DRAG_CHECKS_FAILED = 7;
Connection.REASON_PREVIOUS_AND_OUTPUT = 8;
/**
* Update two connections to target each other.
* @param {Connection} first The first connection to update.
* @param {Connection} second The second connection to update.
* @param first The first connection to update.
* @param second The second connection to update.
*/
const connectReciprocally = function(first, second) {
function connectReciprocally(first: Connection, second: Connection) {
if (!first || !second) {
throw Error('Cannot connect null connections.');
}
first.targetConnection = second;
second.targetConnection = first;
};
}
/**
* Returns the single connection on the block that will accept the orphaned
* block, if one can be found. If the block has multiple compatible connections
* (even if they are filled) this returns null. If the block has no compatible
* connections, this returns null.
* @param {!Block} block The superior block.
* @param {!Block} orphanBlock The inferior block.
* @return {?Connection} The suitable connection point on 'block',
* or null.
* @param block The superior block.
* @param orphanBlock The inferior block.
* @return The suitable connection point on 'block', or null.
*/
const getSingleConnection = function(block, orphanBlock) {
function getSingleConnection(block: Block, orphanBlock: Block): Connection|
null {
let foundConnection = null;
const output = orphanBlock.outputConnection;
const typeChecker = output.getConnectionChecker();
for (let i = 0, input; (input = block.inputList[i]); i++) {
for (let i = 0, input; input = block.inputList[i]; i++) {
const connection = input.connection;
if (connection && typeChecker.canConnect(output, connection, false)) {
if (foundConnection) {
@@ -712,7 +678,7 @@ const getSingleConnection = function(block, orphanBlock) {
}
}
return foundConnection;
};
}
/**
* Walks down a row a blocks, at each stage checking if there are any
@@ -720,23 +686,21 @@ const getSingleConnection = function(block, orphanBlock) {
* are zero or multiple eligible connections, returns null. Otherwise
* returns the only input on the last block in the chain.
* Terminates early for shadow blocks.
* @param {!Block} startBlock The block on which to start the search.
* @param {!Block} orphanBlock The block that is looking for a home.
* @return {?Connection} The suitable connection point on the chain
* of blocks, or null.
* @param startBlock The block on which to start the search.
* @param orphanBlock The block that is looking for a home.
* @return The suitable connection point on the chain of blocks, or null.
*/
const getConnectionForOrphanedOutput = function(startBlock, orphanBlock) {
function getConnectionForOrphanedOutput(
startBlock: Block, orphanBlock: Block): Connection|null {
let newBlock = startBlock;
let connection;
while (
(connection = getSingleConnection(
/** @type {!Block} */ (newBlock), orphanBlock))) {
newBlock = connection.targetBlock();
while (connection = getSingleConnection((newBlock), orphanBlock)) {
// AnyDuringMigration because: Type 'Block | null' is not assignable to
// type 'Block'.
newBlock = connection.targetBlock() as AnyDuringMigration;
if (!newBlock || newBlock.isShadow()) {
return connection;
}
}
return null;
};
exports.Connection = Connection;
}

View File

@@ -8,45 +8,42 @@
* @fileoverview An object that encapsulates logic for checking whether a
* potential connection is safe and valid.
*/
'use strict';
/**
* An object that encapsulates logic for checking whether a
* potential connection is safe and valid.
* @class
*/
goog.module('Blockly.ConnectionChecker');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionChecker');
const common = goog.require('Blockly.common');
const internalConstants = goog.require('Blockly.internalConstants');
const registry = goog.require('Blockly.registry');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {Connection} = goog.require('Blockly.Connection');
/* eslint-disable-next-line no-unused-vars */
const {IConnectionChecker} = goog.require('Blockly.IConnectionChecker');
/* eslint-disable-next-line no-unused-vars */
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
import * as common from './common.js';
import {Connection} from './connection.js';
import {ConnectionType} from './connection_type.js';
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import * as internalConstants from './internal_constants.js';
import * as registry from './registry.js';
import type {RenderedConnection} from './rendered_connection.js';
/**
* Class for connection type checking logic.
* @implements {IConnectionChecker}
* @alias Blockly.ConnectionChecker
*/
class ConnectionChecker {
export class ConnectionChecker implements IConnectionChecker {
/**
* Check whether the current connection can connect with the target
* connection.
* @param {Connection} a Connection to check compatibility with.
* @param {Connection} b Connection to check compatibility with.
* @param {boolean} isDragging True if the connection is being made by
* dragging a block.
* @param {number=} opt_distance The max allowable distance between the
* connections for drag checks.
* @return {boolean} Whether the connection is legal.
* @public
* @param a Connection to check compatibility with.
* @param b Connection to check compatibility with.
* @param isDragging True if the connection is being made by dragging a block.
* @param opt_distance The max allowable distance between the connections for
* drag checks.
* @return Whether the connection is legal.
*/
canConnect(a, b, isDragging, opt_distance) {
canConnect(
a: Connection|null, b: Connection|null, isDragging: boolean,
opt_distance?: number): boolean {
return this.canConnectWithReason(a, b, isDragging, opt_distance) ===
Connection.CAN_CONNECT;
}
@@ -54,33 +51,33 @@ class ConnectionChecker {
/**
* Checks whether the current connection can connect with the target
* connection, and return an error code if there are problems.
* @param {Connection} a Connection to check compatibility with.
* @param {Connection} b Connection to check compatibility with.
* @param {boolean} isDragging True if the connection is being made by
* dragging a block.
* @param {number=} opt_distance The max allowable distance between the
* connections for drag checks.
* @return {number} Connection.CAN_CONNECT if the connection is legal,
* an error code otherwise.
* @public
* @param a Connection to check compatibility with.
* @param b Connection to check compatibility with.
* @param isDragging True if the connection is being made by dragging a block.
* @param opt_distance The max allowable distance between the connections for
* drag checks.
* @return Connection.CAN_CONNECT if the connection is legal, an error code
* otherwise.
*/
canConnectWithReason(a, b, isDragging, opt_distance) {
canConnectWithReason(
a: Connection|null, b: Connection|null, isDragging: boolean,
opt_distance?: number): number {
const safety = this.doSafetyChecks(a, b);
if (safety !== Connection.CAN_CONNECT) {
return safety;
}
// If the safety checks passed, both connections are non-null.
const connOne = /** @type {!Connection} **/ (a);
const connTwo = /** @type {!Connection} **/ (b);
const connOne = a!;
const connTwo = b!;
if (!this.doTypeChecks(connOne, connTwo)) {
return Connection.REASON_CHECKS_FAILED;
}
if (isDragging &&
!this.doDragChecks(
/** @type {!RenderedConnection} **/ (a),
/** @type {!RenderedConnection} **/ (b), opt_distance || 0)) {
a as RenderedConnection, b as RenderedConnection,
opt_distance || 0)) {
return Connection.REASON_DRAG_CHECKS_FAILED;
}
@@ -89,14 +86,13 @@ class ConnectionChecker {
/**
* Helper method that translates a connection error code into a string.
* @param {number} errorCode The error code.
* @param {Connection} a One of the two connections being checked.
* @param {Connection} b The second of the two connections being
* checked.
* @return {string} A developer-readable error string.
* @public
* @param errorCode The error code.
* @param a One of the two connections being checked.
* @param b The second of the two connections being checked.
* @return A developer-readable error string.
*/
getErrorMessage(errorCode, a, b) {
getErrorMessage(errorCode: number, a: Connection|null, b: Connection|null):
string {
switch (errorCode) {
case Connection.REASON_SELF_CONNECTION:
return 'Attempted to connect a block to itself.';
@@ -108,8 +104,8 @@ class ConnectionChecker {
case Connection.REASON_TARGET_NULL:
return 'Target connection is null.';
case Connection.REASON_CHECKS_FAILED: {
const connOne = /** @type {!Connection} **/ (a);
const connTwo = /** @type {!Connection} **/ (b);
const connOne = a!;
const connTwo = b!;
let msg = 'Connection checks failed. ';
msg += connOne + ' expected ' + connOne.getCheck() + ', found ' +
connTwo.getCheck();
@@ -129,12 +125,11 @@ class ConnectionChecker {
/**
* Check that connecting the given connections is safe, meaning that it would
* not break any of Blockly's basic assumptions (e.g. no self connections).
* @param {Connection} a The first of the connections to check.
* @param {Connection} b The second of the connections to check.
* @return {number} An enum with the reason this connection is safe or unsafe.
* @public
* @param a The first of the connections to check.
* @param b The second of the connections to check.
* @return An enum with the reason this connection is safe or unsafe.
*/
doSafetyChecks(a, b) {
doSafetyChecks(a: Connection|null, b: Connection|null): number {
if (!a || !b) {
return Connection.REASON_TARGET_NULL;
}
@@ -181,12 +176,11 @@ class ConnectionChecker {
* Check whether this connection is compatible with another connection with
* respect to the value type system. E.g. square_root("Hello") is not
* compatible.
* @param {!Connection} a Connection to compare.
* @param {!Connection} b Connection to compare against.
* @return {boolean} True if the connections share a type.
* @public
* @param a Connection to compare.
* @param b Connection to compare against.
* @return True if the connections share a type.
*/
doTypeChecks(a, b) {
doTypeChecks(a: Connection, b: Connection): boolean {
const checkArrayOne = a.getCheck();
const checkArrayTwo = b.getCheck();
@@ -206,15 +200,16 @@ class ConnectionChecker {
/**
* Check whether this connection can be made by dragging.
* @param {!RenderedConnection} a Connection to compare.
* @param {!RenderedConnection} b Connection to compare against.
* @param {number} distance The maximum allowable distance between
* connections.
* @return {boolean} True if the connection is allowed during a drag.
* @public
* @param a Connection to compare.
* @param b Connection to compare against.
* @param distance The maximum allowable distance between connections.
* @return True if the connection is allowed during a drag.
*/
doDragChecks(a, b, distance) {
if (a.distanceFrom(b) > distance) {
doDragChecks(a: RenderedConnection, b: RenderedConnection, distance: number):
boolean {
// AnyDuringMigration because: Argument of type 'RenderedConnection' is not
// assignable to parameter of type 'Connection'.
if (a.distanceFrom(b as AnyDuringMigration) > distance) {
return false;
}
@@ -225,11 +220,13 @@ class ConnectionChecker {
switch (b.type) {
case ConnectionType.PREVIOUS_STATEMENT:
return this.canConnectToPrevious_(a, b);
// AnyDuringMigration because: Argument of type 'RenderedConnection' is
// not assignable to parameter of type 'Connection'.
return this.canConnectToPrevious_(a as AnyDuringMigration, b);
case ConnectionType.OUTPUT_VALUE: {
// Don't offer to connect an already connected left (male) value plug to
// an available right (female) value plug.
if ((b.isConnected() && !b.targetBlock().isInsertionMarker()) ||
if (b.isConnected() && !b.targetBlock()!.isInsertionMarker() ||
a.isConnected()) {
return false;
}
@@ -239,8 +236,8 @@ class ConnectionChecker {
// Offering to connect the left (male) of a value block to an already
// connected value pair is ok, we'll splice it in.
// However, don't offer to splice into an immovable block.
if (b.isConnected() && !b.targetBlock().isMovable() &&
!b.targetBlock().isShadow()) {
if (b.isConnected() && !b.targetBlock()!.isMovable() &&
!b.targetBlock()!.isShadow()) {
return false;
}
break;
@@ -251,7 +248,7 @@ class ConnectionChecker {
// is fine. Similarly, replacing a terminal statement with another
// terminal statement is allowed.
if (b.isConnected() && !a.getSourceBlock().nextConnection &&
!b.targetBlock().isShadow() && b.targetBlock().nextConnection) {
!b.targetBlock()!.isShadow() && b.targetBlock()!.nextConnection) {
return false;
}
break;
@@ -262,7 +259,9 @@ class ConnectionChecker {
}
// Don't let blocks try to connect to themselves or ones they nest.
if (common.draggingConnections.indexOf(b) !== -1) {
// AnyDuringMigration because: Argument of type 'RenderedConnection' is not
// assignable to parameter of type 'Connection'.
if (common.draggingConnections.indexOf(b as AnyDuringMigration) !== -1) {
return false;
}
@@ -271,14 +270,12 @@ class ConnectionChecker {
/**
* Helper function for drag checking.
* @param {!Connection} a The connection to check, which must be a
* statement input or next connection.
* @param {!Connection} b A nearby connection to check, which
* must be a previous connection.
* @return {boolean} True if the connection is allowed, false otherwise.
* @protected
* @param a The connection to check, which must be a statement input or next
* connection.
* @param b A nearby connection to check, which must be a previous connection.
* @return True if the connection is allowed, false otherwise.
*/
canConnectToPrevious_(a, b) {
protected canConnectToPrevious_(a: Connection, b: Connection): boolean {
if (a.targetConnection) {
// This connection is already occupied.
// A next connection will never disconnect itself mid-drag.
@@ -296,17 +293,15 @@ class ConnectionChecker {
const targetBlock = b.targetBlock();
// If it is connected to a real block, game over.
if (!targetBlock.isInsertionMarker()) {
if (!targetBlock!.isInsertionMarker()) {
return false;
}
// If it's connected to an insertion marker but that insertion marker
// is the first block in a stack, it's still fine. If that insertion
// marker is in the middle of a stack, it won't work.
return !targetBlock.getPreviousBlock();
return !targetBlock!.getPreviousBlock();
}
}
registry.register(
registry.Type.CONNECTION_CHECKER, registry.DEFAULT, ConnectionChecker);
exports.ConnectionChecker = ConnectionChecker;

View File

@@ -9,7 +9,6 @@
* possibly be connected to (i.e. not collapsed, etc).
* Sorted by y coordinate.
*/
'use strict';
/**
* A database of all the rendered connections that could
@@ -17,17 +16,16 @@
* Sorted by y coordinate.
* @class
*/
goog.module('Blockly.ConnectionDB');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionDB');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
/* eslint-disable-next-line no-unused-vars */
const {Coordinate} = goog.requireType('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
const {IConnectionChecker} = goog.requireType('Blockly.IConnectionChecker');
/* eslint-disable-next-line no-unused-vars */
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
/** @suppress {extraRequire} */
goog.require('Blockly.constants');
// Unused import preserved for side-effects. Remove if unneeded.
// import './constants.js';
import {ConnectionType} from './connection_type.js';
import type {IConnectionChecker} from './interfaces/i_connection_checker.js';
import type {RenderedConnection} from './rendered_connection.js';
import type {Coordinate} from './utils/coordinate.js';
/**
@@ -36,36 +34,23 @@ goog.require('Blockly.constants');
* connections in an area may be looked up quickly using a binary search.
* @alias Blockly.ConnectionDB
*/
class ConnectionDB {
export class ConnectionDB {
/** Array of connections sorted by y position in workspace units. */
private readonly connections_: RenderedConnection[] = [];
/**
* @param {!IConnectionChecker} checker The workspace's
* connection type checker, used to decide if connections are valid during
* a drag.
* @param connectionChecker The workspace's connection type checker, used to
* decide if connections are valid during a drag.
*/
constructor(checker) {
/**
* Array of connections sorted by y position in workspace units.
* @type {!Array<!RenderedConnection>}
* @private
*/
this.connections_ = [];
/**
* The workspace's connection type checker, used to decide if connections
* are valid during a drag.
* @type {!IConnectionChecker}
* @private
*/
this.connectionChecker_ = checker;
}
constructor(private readonly connectionChecker: IConnectionChecker) {}
/**
* Add a connection to the database. Should not already exist in the database.
* @param {!RenderedConnection} connection The connection to be added.
* @param {number} yPos The y position used to decide where to insert the
* connection.
* @package
* @param connection The connection to be added.
* @param yPos The y position used to decide where to insert the connection.
* @internal
*/
addConnection(connection, yPos) {
addConnection(connection: RenderedConnection, yPos: number) {
const index = this.calculateIndexForYPos_(yPos);
this.connections_.splice(index, 0, connection);
}
@@ -75,14 +60,12 @@ class ConnectionDB {
*
* Starts by doing a binary search to find the approximate location, then
* linearly searches nearby for the exact connection.
* @param {!RenderedConnection} conn The connection to find.
* @param {number} yPos The y position used to find the index of the
* connection.
* @return {number} The index of the connection, or -1 if the connection was
* not found.
* @private
* @param conn The connection to find.
* @param yPos The y position used to find the index of the connection.
* @return The index of the connection, or -1 if the connection was not found.
*/
findIndexOfConnection_(conn, yPos) {
private findIndexOfConnection_(conn: RenderedConnection, yPos: number):
number {
if (!this.connections_.length) {
return -1;
}
@@ -116,12 +99,10 @@ class ConnectionDB {
/**
* Finds the correct index for the given y position.
* @param {number} yPos The y position used to decide where to
* insert the connection.
* @return {number} The candidate index.
* @private
* @param yPos The y position used to decide where to insert the connection.
* @return The candidate index.
*/
calculateIndexForYPos_(yPos) {
private calculateIndexForYPos_(yPos: number): number {
if (!this.connections_.length) {
return 0;
}
@@ -143,12 +124,11 @@ class ConnectionDB {
/**
* Remove a connection from the database. Must already exist in DB.
* @param {!RenderedConnection} connection The connection to be removed.
* @param {number} yPos The y position used to find the index of the
* connection.
* @param connection The connection to be removed.
* @param yPos The y position used to find the index of the connection.
* @throws {Error} If the connection cannot be found in the database.
*/
removeConnection(connection, yPos) {
removeConnection(connection: RenderedConnection, yPos: number) {
const index = this.findIndexOfConnection_(connection, yPos);
if (index === -1) {
throw Error('Unable to find connection in connectionDB.');
@@ -159,12 +139,12 @@ class ConnectionDB {
/**
* Find all nearby connections to the given connection.
* Type checking does not apply, since this function is used for bumping.
* @param {!RenderedConnection} connection The connection whose
* neighbours should be returned.
* @param {number} maxRadius The maximum radius to another connection.
* @return {!Array<!RenderedConnection>} List of connections.
* @param connection The connection whose neighbours should be returned.
* @param maxRadius The maximum radius to another connection.
* @return List of connections.
*/
getNeighbours(connection, maxRadius) {
getNeighbours(connection: RenderedConnection, maxRadius: number):
RenderedConnection[] {
const db = this.connections_;
const currentX = connection.x;
const currentY = connection.y;
@@ -182,16 +162,16 @@ class ConnectionDB {
pointerMid = Math.floor((pointerMin + pointerMax) / 2);
}
const neighbours = [];
const neighbours: AnyDuringMigration[] = [];
/**
* Computes if the current connection is within the allowed radius of
* another connection. This function is a closure and has access to outside
* variables.
* @param {number} yIndex The other connection's index in the database.
* @return {boolean} True if the current connection's vertical distance from
* the other connection is less than the allowed radius.
* @param yIndex The other connection's index in the database.
* @return True if the current connection's vertical distance from the other
* connection is less than the allowed radius.
*/
function checkConnection_(yIndex) {
function checkConnection_(yIndex: number): boolean {
const dx = currentX - db[yIndex].x;
const dy = currentY - db[yIndex].y;
const r = Math.sqrt(dx * dx + dy * dy);
@@ -219,32 +199,33 @@ class ConnectionDB {
/**
* Is the candidate connection close to the reference connection.
* Extremely fast; only looks at Y distance.
* @param {number} index Index in database of candidate connection.
* @param {number} baseY Reference connection's Y value.
* @param {number} maxRadius The maximum radius to another connection.
* @return {boolean} True if connection is in range.
* @private
* @param index Index in database of candidate connection.
* @param baseY Reference connection's Y value.
* @param maxRadius The maximum radius to another connection.
* @return True if connection is in range.
*/
isInYRange_(index, baseY, maxRadius) {
return (Math.abs(this.connections_[index].y - baseY) <= maxRadius);
private isInYRange_(index: number, baseY: number, maxRadius: number):
boolean {
return Math.abs(this.connections_[index].y - baseY) <= maxRadius;
}
/**
* Find the closest compatible connection to this connection.
* @param {!RenderedConnection} conn The connection searching for a compatible
* mate.
* @param {number} maxRadius The maximum radius to another connection.
* @param {!Coordinate} dxy Offset between this connection's
* location in the database and the current location (as a result of
* dragging).
* @return {!{connection: RenderedConnection, radius: number}}
* Contains two properties: 'connection' which is either another
* @param conn The connection searching for a compatible mate.
* @param maxRadius The maximum radius to another connection.
* @param dxy Offset between this connection's location in the database and
* the current location (as a result of dragging).
* @return Contains two properties: 'connection' which is either another
* connection or null, and 'radius' which is the distance.
*/
searchForClosest(conn, maxRadius, dxy) {
searchForClosest(
conn: RenderedConnection, maxRadius: number,
dxy: Coordinate): {connection: RenderedConnection, radius: number} {
if (!this.connections_.length) {
// Don't bother.
return {connection: null, radius: maxRadius};
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'RenderedConnection'.
return {connection: null as AnyDuringMigration, radius: maxRadius};
}
// Stash the values of x and y from before the drag.
@@ -267,9 +248,11 @@ class ConnectionDB {
let pointerMin = closestIndex - 1;
while (pointerMin >= 0 && this.isInYRange_(pointerMin, conn.y, maxRadius)) {
temp = this.connections_[pointerMin];
if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
if (this.connectionChecker.canConnect(conn, temp, true, bestRadius)) {
bestConnection = temp;
bestRadius = temp.distanceFrom(conn);
// AnyDuringMigration because: Argument of type 'RenderedConnection' is
// not assignable to parameter of type 'Connection'.
bestRadius = temp.distanceFrom(conn as AnyDuringMigration);
}
pointerMin--;
}
@@ -278,9 +261,11 @@ class ConnectionDB {
while (pointerMax < this.connections_.length &&
this.isInYRange_(pointerMax, conn.y, maxRadius)) {
temp = this.connections_[pointerMax];
if (this.connectionChecker_.canConnect(conn, temp, true, bestRadius)) {
if (this.connectionChecker.canConnect(conn, temp, true, bestRadius)) {
bestConnection = temp;
bestRadius = temp.distanceFrom(conn);
// AnyDuringMigration because: Argument of type 'RenderedConnection' is
// not assignable to parameter of type 'Connection'.
bestRadius = temp.distanceFrom(conn as AnyDuringMigration);
}
pointerMax++;
}
@@ -288,19 +273,22 @@ class ConnectionDB {
// Reset the values of x and y.
conn.x = baseX;
conn.y = baseY;
// If there were no valid connections, bestConnection will be null.
return {connection: bestConnection, radius: bestRadius};
// AnyDuringMigration because: Type 'RenderedConnection | null' is not
// assignable to type 'RenderedConnection'.
return {
connection: bestConnection as AnyDuringMigration,
radius: bestRadius
};
}
/**
* Initialize a set of connection DBs for a workspace.
* @param {!IConnectionChecker} checker The workspace's
* connection checker, used to decide if connections are valid during a
* drag.
* @return {!Array<!ConnectionDB>} Array of databases.
* @param checker The workspace's connection checker, used to decide if
* connections are valid during a drag.
* @return Array of databases.
*/
static init(checker) {
static init(checker: IConnectionChecker): ConnectionDB[] {
// Create four databases, one for each connection type.
const dbList = [];
dbList[ConnectionType.INPUT_VALUE] = new ConnectionDB(checker);
@@ -310,5 +298,3 @@ class ConnectionDB {
return dbList;
}
}
exports.ConnectionDB = ConnectionDB;

View File

@@ -8,29 +8,25 @@
* @fileoverview An enum for the possible types of connections.
*/
'use strict';
/**
* An enum for the possible types of connections.
* @namespace Blockly.ConnectionType
*/
goog.module('Blockly.ConnectionType');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ConnectionType');
/**
* Enum for the type of a connection or input.
* @enum {number}
* @alias Blockly.ConnectionType
*/
const ConnectionType = {
export enum ConnectionType {
// A right-facing value input. E.g. 'set item to' or 'return'.
INPUT_VALUE: 1,
INPUT_VALUE = 1,
// A left-facing value output. E.g. 'random fraction'.
OUTPUT_VALUE: 2,
OUTPUT_VALUE,
// A down-facing block stack. E.g. 'if-do' or 'else'.
NEXT_STATEMENT: 3,
NEXT_STATEMENT,
// An up-facing block stack. E.g. 'break out of loop'.
PREVIOUS_STATEMENT: 4,
};
exports.ConnectionType = ConnectionType;
PREVIOUS_STATEMENT
}

View File

@@ -7,27 +7,23 @@
/**
* @fileoverview Blockly constants.
*/
'use strict';
/**
* Blockly constants.
* @namespace Blockly.constants
*/
goog.module('Blockly.constants');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.constants');
/**
* The language-neutral ID given to the collapsed input.
* @const {string}
* @alias Blockly.constants.COLLAPSED_INPUT_NAME
*/
const COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
exports.COLLAPSED_INPUT_NAME = COLLAPSED_INPUT_NAME;
export const COLLAPSED_INPUT_NAME = '_TEMP_COLLAPSED_INPUT';
/**
* The language-neutral ID given to the collapsed field.
* @const {string}
* @alias Blockly.constants.COLLAPSED_FIELD_NAME
*/
const COLLAPSED_FIELD_NAME = '_TEMP_COLLAPSED_FIELD';
exports.COLLAPSED_FIELD_NAME = COLLAPSED_FIELD_NAME;
export const COLLAPSED_FIELD_NAME = '_TEMP_COLLAPSED_FIELD';

View File

@@ -7,86 +7,79 @@
/**
* @fileoverview Functionality for the right-click context menus.
*/
'use strict';
/**
* Functionality for the right-click context menus.
* @namespace Blockly.ContextMenu
*/
goog.module('Blockly.ContextMenu');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ContextMenu');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const Xml = goog.require('Blockly.Xml');
const aria = goog.require('Blockly.utils.aria');
const browserEvents = goog.require('Blockly.browserEvents');
const clipboard = goog.require('Blockly.clipboard');
const dom = goog.require('Blockly.utils.dom');
const eventUtils = goog.require('Blockly.Events.utils');
const userAgent = goog.require('Blockly.utils.userAgent');
const svgMath = goog.require('Blockly.utils.svgMath');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {config} = goog.require('Blockly.config');
/* eslint-disable-next-line no-unused-vars */
const {ContextMenuRegistry} = goog.requireType('Blockly.ContextMenuRegistry');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
const {MenuItem} = goog.require('Blockly.MenuItem');
const {Menu} = goog.require('Blockly.Menu');
const {Msg} = goog.require('Blockly.Msg');
const {Rect} = goog.require('Blockly.utils.Rect');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceCommentSvg} = goog.requireType('Blockly.WorkspaceCommentSvg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockCreate');
import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as clipboard from './clipboard.js';
import {config} from './config.js';
import type {ContextMenuOption, ContextMenuRegistry, LegacyContextMenuOption} from './contextmenu_registry.js';
import * as BlockCreate from './events/events_block_create.js';
import * as eventUtils from './events/utils.js';
import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import * as deprecation from './utils/deprecation.js';
import * as dom from './utils/dom.js';
import {Rect} from './utils/rect.js';
import * as svgMath from './utils/svg_math.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import {WorkspaceCommentSvg} from './workspace_comment_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js';
import * as Xml from './xml.js';
/**
* Which block is the context menu attached to?
* @type {?Block}
*/
let currentBlock = null;
let currentBlock: Block|null = null;
let dummyOwner = {};
/**
* Gets the block the context menu is currently attached to.
* @return {?Block} The block the context menu is attached to.
* @return The block the context menu is attached to.
* @alias Blockly.ContextMenu.getCurrentBlock
*/
const getCurrentBlock = function() {
export function getCurrentBlock(): Block|null {
return currentBlock;
};
exports.getCurrentBlock = getCurrentBlock;
}
/**
* Sets the block the context menu is currently attached to.
* @param {?Block} block The block the context menu is attached to.
* @param block The block the context menu is attached to.
* @alias Blockly.ContextMenu.setCurrentBlock
*/
const setCurrentBlock = function(block) {
export function setCurrentBlock(block: Block|null) {
currentBlock = block;
};
exports.setCurrentBlock = setCurrentBlock;
}
/**
* Menu object.
* @type {Menu}
*/
let menu_ = null;
let menu_: Menu|null = null;
/**
* Construct the menu based on the list of options and show the menu.
* @param {!Event} e Mouse event.
* @param {!Array<!ContextMenuRegistry.ContextMenuOption|
* !ContextMenuRegistry.LegacyContextMenuOption>}
* options Array of menu options.
* @param {boolean} rtl True if RTL, false if LTR.
* @param e Mouse event.
* @param options Array of menu options.
* @param rtl True if RTL, false if LTR.
* @alias Blockly.ContextMenu.show
*/
const show = function(e, options, rtl) {
WidgetDiv.show(exports, rtl, dispose);
export function show(
e: Event, options: (ContextMenuOption|LegacyContextMenuOption)[],
rtl: boolean) {
WidgetDiv.show(dummyOwner, rtl, dispose);
if (!options.length) {
hide();
return;
@@ -101,24 +94,22 @@ const show = function(e, options, rtl) {
menu.focus();
}, 1);
currentBlock = null; // May be set by Blockly.Block.
};
exports.show = show;
}
/**
* Create the context menu object and populate it with the given options.
* @param {!Array<!ContextMenuRegistry.ContextMenuOption|
* !ContextMenuRegistry.LegacyContextMenuOption>}
* options Array of menu options.
* @param {boolean} rtl True if RTL, false if LTR.
* @return {!Menu} The menu that will be shown on right click.
* @private
* @param options Array of menu options.
* @param rtl True if RTL, false if LTR.
* @return The menu that will be shown on right click.
*/
const populate_ = function(options, rtl) {
function populate_(
options: (ContextMenuOption|LegacyContextMenuOption)[],
rtl: boolean): Menu {
/* Here's what one option object looks like:
{text: 'Make It So',
enabled: true,
callback: Blockly.MakeItSo}
*/
{text: 'Make It So',
enabled: true,
callback: Blockly.MakeItSo}
*/
const menu = new Menu();
menu.setRole(aria.Role.MENU);
for (let i = 0; i < options.length; i++) {
@@ -129,35 +120,38 @@ const populate_ = function(options, rtl) {
menu.addChild(menuItem);
menuItem.setEnabled(option.enabled);
if (option.enabled) {
const actionHandler = function(_menuItem) {
// TODO: Create a type for option that can be used in an @this tag.
/* eslint-disable-next-line no-invalid-this */
const option = this;
const actionHandler = function() {
hide();
option.callback(option.scope);
// If .scope does not exist on the option, then the callback will not
// be expecting a scope parameter, so there should be no problems. Just
// assume it is a ContextMenuOption and we'll pass undefined if it's
// not.
option.callback((option as ContextMenuOption).scope);
};
menuItem.onAction(actionHandler, option);
menuItem.onAction(actionHandler, {});
}
}
return menu;
};
}
/**
* Add the menu to the page and position it correctly.
* @param {!Menu} menu The menu to add and position.
* @param {!Event} e Mouse event for the right click that is making the context
* @param menu The menu to add and position.
* @param e Mouse event for the right click that is making the context
* menu appear.
* @param {boolean} rtl True if RTL, false if LTR.
* @private
* @param rtl True if RTL, false if LTR.
*/
const position_ = function(menu, e, rtl) {
function position_(menu: Menu, e: Event, rtl: boolean) {
// Record windowSize and scrollOffset before adding menu.
const viewportBBox = svgMath.getViewportBBox();
const mouseEvent = e as MouseEvent;
// This one is just a point, but we'll pretend that it's a rect so we can use
// some helper functions.
const anchorBBox = new Rect(
e.clientY + viewportBBox.top, e.clientY + viewportBBox.top,
e.clientX + viewportBBox.left, e.clientX + viewportBBox.left);
mouseEvent.clientY + viewportBBox.top,
mouseEvent.clientY + viewportBBox.top,
mouseEvent.clientX + viewportBBox.left,
mouseEvent.clientX + viewportBBox.left);
createWidget_(menu);
const menuSize = menu.getSize();
@@ -174,77 +168,70 @@ const position_ = function(menu, e, rtl) {
// correctly. Otherwise it will cause a page scroll to get the misplaced menu
// in view. See issue #1329.
menu.focus();
};
}
/**
* Create and render the menu widget inside Blockly's widget div.
* @param {!Menu} menu The menu to add to the widget div.
* @private
* @param menu The menu to add to the widget div.
*/
const createWidget_ = function(menu) {
function createWidget_(menu: Menu) {
const div = WidgetDiv.getDiv();
if (!div) {
throw Error('Attempting to create a context menu when widget div is null');
}
menu.render(div);
const menuDom = menu.getElement();
dom.addClass(
/** @type {!Element} */ (menuDom), 'blocklyContextMenu');
dom.addClass((menuDom as Element), 'blocklyContextMenu');
// Prevent system context menu when right-clicking a Blockly context menu.
browserEvents.conditionalBind(
/** @type {!EventTarget} */ (menuDom), 'contextmenu', null,
haltPropagation);
(menuDom as EventTarget), 'contextmenu', null, haltPropagation);
// Focus only after the initial render to avoid issue #1329.
menu.focus();
};
}
/**
* Halts the propagation of the event without doing anything else.
* @param {!Event} e An event.
* @param e An event.
*/
const haltPropagation = function(e) {
function haltPropagation(e: Event) {
// This event has been handled. No need to bubble up to the document.
e.preventDefault();
e.stopPropagation();
};
}
/**
* Hide the context menu.
* @alias Blockly.ContextMenu.hide
*/
const hide = function() {
WidgetDiv.hideIfOwner(exports);
export function hide() {
WidgetDiv.hideIfOwner(dummyOwner);
currentBlock = null;
};
exports.hide = hide;
}
/**
* Dispose of the menu.
* @alias Blockly.ContextMenu.dispose
*/
const dispose = function() {
export function dispose() {
if (menu_) {
menu_.dispose();
menu_ = null;
}
};
exports.dispose = dispose;
}
/**
* Create a callback function that creates and configures a block,
* then places the new block next to the original.
* @param {!Block} block Original block.
* @param {!Element} xml XML representation of new block.
* @return {!Function} Function that creates a block.
* @param block Original block.
* @param xml XML representation of new block.
* @return Function that creates a block.
* @alias Blockly.ContextMenu.callbackFactory
*/
const callbackFactory = function(block, xml) {
return function() {
export function callbackFactory(block: Block, xml: Element): Function {
return () => {
eventUtils.disable();
let newBlock;
try {
newBlock =
/** @type {!BlockSvg} */ (Xml.domToBlock(xml, block.workspace));
newBlock = Xml.domToBlock(xml, block.workspace!) as BlockSvg;
// Move the new block next to the old block.
const xy = block.getRelativeToSurfaceXY();
if (block.RTL) {
@@ -258,25 +245,25 @@ const callbackFactory = function(block, xml) {
eventUtils.enable();
}
if (eventUtils.isEnabled() && !newBlock.isShadow()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))(newBlock));
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CREATE))!(newBlock));
}
newBlock.select();
};
};
exports.callbackFactory = callbackFactory;
}
// Helper functions for creating context menu options.
/**
* Make a context menu option for deleting the current workspace comment.
* @param {!WorkspaceCommentSvg} comment The workspace comment where the
* @param comment The workspace comment where the
* right-click originated.
* @return {!ContextMenuRegistry.LegacyContextMenuOption} A menu option,
* @return A menu option,
* containing text, enabled, and a callback.
* @alias Blockly.ContextMenu.commentDeleteOption
* @package
* @internal
*/
const commentDeleteOption = function(comment) {
export function commentDeleteOption(comment: WorkspaceCommentSvg):
LegacyContextMenuOption {
const deleteOption = {
text: Msg['REMOVE_COMMENT'],
enabled: true,
@@ -287,19 +274,19 @@ const commentDeleteOption = function(comment) {
},
};
return deleteOption;
};
exports.commentDeleteOption = commentDeleteOption;
}
/**
* Make a context menu option for duplicating the current workspace comment.
* @param {!WorkspaceCommentSvg} comment The workspace comment where the
* @param comment The workspace comment where the
* right-click originated.
* @return {!ContextMenuRegistry.LegacyContextMenuOption} A menu option,
* @return A menu option,
* containing text, enabled, and a callback.
* @alias Blockly.ContextMenu.commentDuplicateOption
* @package
* @internal
*/
const commentDuplicateOption = function(comment) {
export function commentDuplicateOption(comment: WorkspaceCommentSvg):
LegacyContextMenuOption {
const duplicateOption = {
text: Msg['DUPLICATE_COMMENT'],
enabled: true,
@@ -308,28 +295,24 @@ const commentDuplicateOption = function(comment) {
},
};
return duplicateOption;
};
exports.commentDuplicateOption = commentDuplicateOption;
}
/**
* Make a context menu option for adding a comment on the workspace.
* @param {!WorkspaceSvg} ws The workspace where the right-click
* @param ws The workspace where the right-click
* originated.
* @param {!Event} e The right-click mouse event.
* @return {!Object} A menu option, containing text, enabled, and a callback.
* @package
* @param e The right-click mouse event.
* @return A menu option, containing text, enabled, and a callback.
* @suppress {strictModuleDepCheck,checkTypes} Suppress checks while workspace
* comments are not bundled in.
* @alias Blockly.ContextMenu.workspaceCommentOption
* @internal
*/
const workspaceCommentOption = function(ws, e) {
const {WorkspaceCommentSvg} = goog.module.get('Blockly.WorkspaceCommentSvg');
if (!WorkspaceCommentSvg) {
throw Error('Missing require for Blockly.WorkspaceCommentSvg');
}
export function workspaceCommentOption(
ws: WorkspaceSvg, e: Event): ContextMenuOption {
// Helper function to create and position a comment correctly based on the
// location of the mouse event.
const addWsComment = function() {
function addWsComment() {
const comment = new WorkspaceCommentSvg(
ws, Msg['WORKSPACE_COMMENT_DEFAULT_TEXT'],
WorkspaceCommentSvg.DEFAULT_SIZE, WorkspaceCommentSvg.DEFAULT_SIZE);
@@ -341,8 +324,10 @@ const workspaceCommentOption = function(ws, e) {
const boundingRect = injectionDiv.getBoundingClientRect();
// The client coordinates offset by the injection div's upper left corner.
const mouseEvent = e as MouseEvent;
const clientOffsetPixels = new Coordinate(
e.clientX - boundingRect.left, e.clientY - boundingRect.top);
mouseEvent.clientX - boundingRect.left,
mouseEvent.clientY - boundingRect.top);
// The offset in pixels between the main workspace's origin and the upper
// left corner of the injection div.
@@ -363,17 +348,16 @@ const workspaceCommentOption = function(ws, e) {
comment.render();
comment.select();
}
};
}
const wsCommentOption = {
// Foreign objects don't work in IE. Don't let the user create comments
// that they won't be able to edit.
enabled: !userAgent.IE,
};
} as ContextMenuOption;
wsCommentOption.text = Msg['ADD_COMMENT'];
wsCommentOption.callback = function() {
addWsComment();
};
return wsCommentOption;
};
exports.workspaceCommentOption = workspaceCommentOption;
}

View File

@@ -7,235 +7,214 @@
/**
* @fileoverview Registers default context menu items.
*/
'use strict';
/**
* Registers default context menu items.
* @namespace Blockly.ContextMenuItems
*/
goog.module('Blockly.ContextMenuItems');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ContextMenuItems');
const Events = goog.require('Blockly.Events');
const clipboard = goog.require('Blockly.clipboard');
const dialog = goog.require('Blockly.dialog');
const eventUtils = goog.require('Blockly.Events.utils');
const idGenerator = goog.require('Blockly.utils.idGenerator');
const userAgent = goog.require('Blockly.utils.userAgent');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {ContextMenuRegistry} = goog.require('Blockly.ContextMenuRegistry');
const {Msg} = goog.require('Blockly.Msg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
const {inputTypes} = goog.require('Blockly.inputTypes');
import type {BlockSvg} from './block_svg.js';
import * as clipboard from './clipboard.js';
import {ContextMenuRegistry, RegistryItem, Scope} from './contextmenu_registry.js';
import * as dialog from './dialog.js';
import * as Events from './events/events.js';
import * as eventUtils from './events/utils.js';
import {inputTypes} from './input_types.js';
import {Msg} from './msg.js';
import * as idGenerator from './utils/idgenerator.js';
import * as userAgent from './utils/useragent.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Option to undo previous action.
* @alias Blockly.ContextMenuItems.registerUndo
*/
const registerUndo = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const undoOption = {
displayText: function() {
export function registerUndo() {
const undoOption: RegistryItem = {
displayText() {
return Msg['UNDO'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
if (scope.workspace.getUndoStack().length > 0) {
preconditionFn(scope: Scope) {
if (scope.workspace!.getUndoStack().length > 0) {
return 'enabled';
}
return 'disabled';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
scope.workspace.undo(false);
callback(scope: Scope) {
scope.workspace!.undo(false);
},
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
id: 'undoWorkspace',
weight: 1,
};
ContextMenuRegistry.registry.register(undoOption);
};
exports.registerUndo = registerUndo;
}
/**
* Option to redo previous action.
* @alias Blockly.ContextMenuItems.registerRedo
*/
const registerRedo = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const redoOption = {
displayText: function() {
export function registerRedo() {
const redoOption: RegistryItem = {
displayText() {
return Msg['REDO'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
if (scope.workspace.getRedoStack().length > 0) {
preconditionFn(scope: Scope) {
if (scope.workspace!.getRedoStack().length > 0) {
return 'enabled';
}
return 'disabled';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
scope.workspace.undo(true);
callback(scope: Scope) {
scope.workspace!.undo(true);
},
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
id: 'redoWorkspace',
weight: 2,
};
ContextMenuRegistry.registry.register(redoOption);
};
exports.registerRedo = registerRedo;
}
/**
* Option to clean up blocks.
* @alias Blockly.ContextMenuItems.registerCleanup
*/
const registerCleanup = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const cleanOption = {
displayText: function() {
export function registerCleanup() {
const cleanOption: RegistryItem = {
displayText() {
return Msg['CLEAN_UP'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
if (scope.workspace.isMovable()) {
if (scope.workspace.getTopBlocks(false).length > 1) {
preconditionFn(scope: Scope) {
if (scope.workspace!.isMovable()) {
if (scope.workspace!.getTopBlocks(false).length > 1) {
return 'enabled';
}
return 'disabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
scope.workspace.cleanUp();
callback(scope: Scope) {
scope.workspace!.cleanUp();
},
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
id: 'cleanWorkspace',
weight: 3,
};
ContextMenuRegistry.registry.register(cleanOption);
};
exports.registerCleanup = registerCleanup;
}
/**
* Creates a callback to collapse or expand top blocks.
* @param {boolean} shouldCollapse Whether a block should collapse.
* @param {!Array<BlockSvg>} topBlocks Top blocks in the workspace.
* @private
* @param shouldCollapse Whether a block should collapse.
* @param topBlocks Top blocks in the workspace.
*/
const toggleOption_ = function(shouldCollapse, topBlocks) {
function toggleOption_(shouldCollapse: boolean, topBlocks: BlockSvg[]) {
const DELAY = 10;
let ms = 0;
let timeoutCounter = 0;
const timeoutFn = function(block) {
function timeoutFn(block: AnyDuringMigration) {
timeoutCounter--;
block.setCollapsed(shouldCollapse);
if (timeoutCounter === 0) {
Events.setGroup(false);
}
};
}
Events.setGroup(true);
for (let i = 0; i < topBlocks.length; i++) {
let block = topBlocks[i];
while (block) {
timeoutCounter++;
setTimeout(timeoutFn.bind(null, block), ms);
block = block.getNextBlock();
// AnyDuringMigration because: Type 'BlockSvg | null' is not assignable
// to type 'BlockSvg'.
block = block.getNextBlock() as AnyDuringMigration;
ms += DELAY;
}
}
};
}
/**
* Option to collapse all blocks.
* @alias Blockly.ContextMenuItems.registerCollapse
*/
const registerCollapse = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const collapseOption = {
displayText: function() {
export function registerCollapse() {
const collapseOption: RegistryItem = {
displayText() {
return Msg['COLLAPSE_ALL'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
if (scope.workspace.options.collapse) {
const topBlocks = scope.workspace.getTopBlocks(false);
preconditionFn(scope: Scope) {
if (scope.workspace!.options.collapse) {
const topBlocks = scope.workspace!.getTopBlocks(false);
for (let i = 0; i < topBlocks.length; i++) {
let block = topBlocks[i];
while (block) {
if (!block.isCollapsed()) {
return 'enabled';
}
block = block.getNextBlock();
// AnyDuringMigration because: Type 'BlockSvg | null' is not
// assignable to type 'BlockSvg'.
block = block.getNextBlock() as AnyDuringMigration;
}
}
return 'disabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
toggleOption_(true, scope.workspace.getTopBlocks(true));
callback(scope: Scope) {
toggleOption_(true, scope.workspace!.getTopBlocks(true));
},
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
id: 'collapseWorkspace',
weight: 4,
};
ContextMenuRegistry.registry.register(collapseOption);
};
exports.registerCollapse = registerCollapse;
}
/**
* Option to expand all blocks.
* @alias Blockly.ContextMenuItems.registerExpand
*/
const registerExpand = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const expandOption = {
displayText: function() {
export function registerExpand() {
const expandOption: RegistryItem = {
displayText() {
return Msg['EXPAND_ALL'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
if (scope.workspace.options.collapse) {
const topBlocks = scope.workspace.getTopBlocks(false);
preconditionFn(scope: Scope) {
if (scope.workspace!.options.collapse) {
const topBlocks = scope.workspace!.getTopBlocks(false);
for (let i = 0; i < topBlocks.length; i++) {
let block = topBlocks[i];
while (block) {
if (block.isCollapsed()) {
return 'enabled';
}
block = block.getNextBlock();
// AnyDuringMigration because: Type 'BlockSvg | null' is not
// assignable to type 'BlockSvg'.
block = block.getNextBlock() as AnyDuringMigration;
}
}
return 'disabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
toggleOption_(false, scope.workspace.getTopBlocks(true));
callback(scope: Scope) {
toggleOption_(false, scope.workspace!.getTopBlocks(true));
},
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
id: 'expandWorkspace',
weight: 5,
};
ContextMenuRegistry.registry.register(expandOption);
};
exports.registerExpand = registerExpand;
}
/**
* Adds a block and its children to a list of deletable blocks.
* @param {!BlockSvg} block to delete.
* @param {!Array<!BlockSvg>} deleteList list of blocks that can be deleted.
* This will be
* modified in place with the given block and its descendants.
* @private
* @param block to delete.
* @param deleteList list of blocks that can be deleted.
* This will be modified in place with the given block and its descendants.
*/
const addDeletableBlocks_ = function(block, deleteList) {
function addDeletableBlocks_(block: BlockSvg, deleteList: BlockSvg[]) {
if (block.isDeletable()) {
Array.prototype.push.apply(deleteList, block.getDescendants(false));
} else {
@@ -244,31 +223,29 @@ const addDeletableBlocks_ = function(block, deleteList) {
addDeletableBlocks_(children[i], deleteList);
}
}
};
}
/**
* Constructs a list of blocks that can be deleted in the given workspace.
* @param {!WorkspaceSvg} workspace to delete all blocks from.
* @return {!Array<!BlockSvg>} list of blocks to delete.
* @private
* @param workspace to delete all blocks from.
* @return list of blocks to delete.
*/
const getDeletableBlocks_ = function(workspace) {
const deleteList = [];
function getDeletableBlocks_(workspace: WorkspaceSvg): BlockSvg[] {
const deleteList: AnyDuringMigration[] = [];
const topBlocks = workspace.getTopBlocks(true);
for (let i = 0; i < topBlocks.length; i++) {
addDeletableBlocks_(topBlocks[i], deleteList);
}
return deleteList;
};
}
/**
* Deletes the given blocks. Used to delete all blocks in the workspace.
* @param {!Array<!BlockSvg>} deleteList list of blocks to delete.
* @param {string} eventGroup event group ID with which all delete events should
* be associated.
* @private
* @param deleteList list of blocks to delete.
* @param eventGroup event group ID with which all delete events should be
* associated.
*/
const deleteNext_ = function(deleteList, eventGroup) {
function deleteNext_(deleteList: BlockSvg[], eventGroup: string) {
const DELAY = 10;
eventUtils.setGroup(eventGroup);
const block = deleteList.shift();
@@ -281,17 +258,17 @@ const deleteNext_ = function(deleteList, eventGroup) {
}
}
eventUtils.setGroup(false);
};
}
/**
* Option to delete all blocks.
* @alias Blockly.ContextMenuItems.registerDeleteAll
*/
const registerDeleteAll = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const deleteOption = {
displayText: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
export function registerDeleteAll() {
// AnyDuringMigration because: Type '(scope: Scope) => string | undefined' is
// not assignable to type 'string | ((p1: Scope) => string)'.
const deleteOption: RegistryItem = {
displayText(scope: Scope) {
if (!scope.workspace) {
return;
}
@@ -303,16 +280,14 @@ const registerDeleteAll = function() {
'%1', String(deletableBlocksLength));
}
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
preconditionFn(scope: Scope) {
if (!scope.workspace) {
return;
}
const deletableBlocksLength = getDeletableBlocks_(scope.workspace).length;
return deletableBlocksLength > 0 ? 'enabled' : 'disabled';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
callback(scope: Scope) {
if (!scope.workspace) {
return;
}
@@ -335,47 +310,39 @@ const registerDeleteAll = function() {
scopeType: ContextMenuRegistry.ScopeType.WORKSPACE,
id: 'workspaceDelete',
weight: 6,
};
} as AnyDuringMigration;
ContextMenuRegistry.registry.register(deleteOption);
};
exports.registerDeleteAll = registerDeleteAll;
/**
* Registers all workspace-scoped context menu items.
* @private
*/
const registerWorkspaceOptions_ = function() {
}
/** Registers all workspace-scoped context menu items. */
function registerWorkspaceOptions_() {
registerUndo();
registerRedo();
registerCleanup();
registerCollapse();
registerExpand();
registerDeleteAll();
};
}
/**
* Option to duplicate a block.
* @alias Blockly.ContextMenuItems.registerDuplicate
*/
const registerDuplicate = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const duplicateOption = {
displayText: function() {
export function registerDuplicate() {
const duplicateOption: RegistryItem = {
displayText() {
return Msg['DUPLICATE_BLOCK'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
preconditionFn(scope: Scope) {
const block = scope.block;
if (!block.isInFlyout && block.isDeletable() && block.isMovable()) {
if (block.isDuplicatable()) {
if (!block!.isInFlyout && block!.isDeletable() && block!.isMovable()) {
if (block!.isDuplicatable()) {
return 'enabled';
}
return 'disabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
callback(scope: Scope) {
if (scope.block) {
clipboard.duplicate(scope.block);
}
@@ -385,43 +352,38 @@ const registerDuplicate = function() {
weight: 1,
};
ContextMenuRegistry.registry.register(duplicateOption);
};
exports.registerDuplicate = registerDuplicate;
}
/**
* Option to add or remove block-level comment.
* @alias Blockly.ContextMenuItems.registerComment
*/
const registerComment = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const commentOption = {
displayText: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
if (scope.block.getCommentIcon()) {
export function registerComment() {
const commentOption: RegistryItem = {
displayText(scope: Scope) {
if (scope.block!.getCommentIcon()) {
// If there's already a comment, option is to remove.
return Msg['REMOVE_COMMENT'];
}
// If there's no comment yet, option is to add.
return Msg['ADD_COMMENT'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
preconditionFn(scope: Scope) {
const block = scope.block;
// IE doesn't support necessary features for comment editing.
if (!userAgent.IE && !block.isInFlyout &&
block.workspace.options.comments && !block.isCollapsed() &&
block.isEditable()) {
if (!userAgent.IE && !block!.isInFlyout &&
block!.workspace!.options.comments && !block!.isCollapsed() &&
block!.isEditable()) {
return 'enabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
callback(scope: Scope) {
const block = scope.block;
if (block.getCommentIcon()) {
block.setCommentText(null);
if (block!.getCommentIcon()) {
block!.setCommentText(null);
} else {
block.setCommentText('');
block!.setCommentText('');
}
},
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
@@ -429,113 +391,98 @@ const registerComment = function() {
weight: 2,
};
ContextMenuRegistry.registry.register(commentOption);
};
exports.registerComment = registerComment;
}
/**
* Option to inline variables.
* @alias Blockly.ContextMenuItems.registerInline
*/
const registerInline = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const inlineOption = {
displayText: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
return (scope.block.getInputsInline()) ? Msg['EXTERNAL_INPUTS'] :
Msg['INLINE_INPUTS'];
export function registerInline() {
const inlineOption: RegistryItem = {
displayText(scope: Scope) {
return scope.block!.getInputsInline() ? Msg['EXTERNAL_INPUTS'] :
Msg['INLINE_INPUTS'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
preconditionFn(scope: Scope) {
const block = scope.block;
if (!block.isInFlyout && block.isMovable() && !block.isCollapsed()) {
for (let i = 1; i < block.inputList.length; i++) {
if (!block!.isInFlyout && block!.isMovable() && !block!.isCollapsed()) {
for (let i = 1; i < block!.inputList.length; i++) {
// Only display this option if there are two value or dummy inputs
// next to each other.
if (block.inputList[i - 1].type !== inputTypes.STATEMENT &&
block.inputList[i].type !== inputTypes.STATEMENT) {
if (block!.inputList[i - 1].type !== inputTypes.STATEMENT &&
block!.inputList[i].type !== inputTypes.STATEMENT) {
return 'enabled';
}
}
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
scope.block.setInputsInline(!scope.block.getInputsInline());
callback(scope: Scope) {
scope.block!.setInputsInline(!scope.block!.getInputsInline());
},
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
id: 'blockInline',
weight: 3,
};
ContextMenuRegistry.registry.register(inlineOption);
};
exports.registerInline = registerInline;
}
/**
* Option to collapse or expand a block.
* @alias Blockly.ContextMenuItems.registerCollapseExpandBlock
*/
const registerCollapseExpandBlock = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const collapseExpandOption = {
displayText: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
return scope.block.isCollapsed() ? Msg['EXPAND_BLOCK'] :
Msg['COLLAPSE_BLOCK'];
export function registerCollapseExpandBlock() {
const collapseExpandOption: RegistryItem = {
displayText(scope: Scope) {
return scope.block!.isCollapsed() ? Msg['EXPAND_BLOCK'] :
Msg['COLLAPSE_BLOCK'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
preconditionFn(scope: Scope) {
const block = scope.block;
if (!block.isInFlyout && block.isMovable() &&
block.workspace.options.collapse) {
if (!block!.isInFlyout && block!.isMovable() &&
block!.workspace!.options.collapse) {
return 'enabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
scope.block.setCollapsed(!scope.block.isCollapsed());
callback(scope: Scope) {
scope.block!.setCollapsed(!scope.block!.isCollapsed());
},
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
id: 'blockCollapseExpand',
weight: 4,
};
ContextMenuRegistry.registry.register(collapseExpandOption);
};
exports.registerCollapseExpandBlock = registerCollapseExpandBlock;
}
/**
* Option to disable or enable a block.
* @alias Blockly.ContextMenuItems.registerDisable
*/
const registerDisable = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const disableOption = {
displayText: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
return (scope.block.isEnabled()) ? Msg['DISABLE_BLOCK'] :
Msg['ENABLE_BLOCK'];
export function registerDisable() {
const disableOption: RegistryItem = {
displayText(scope: Scope) {
return scope.block!.isEnabled() ? Msg['DISABLE_BLOCK'] :
Msg['ENABLE_BLOCK'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
preconditionFn(scope: Scope) {
const block = scope.block;
if (!block.isInFlyout && block.workspace.options.disable &&
block.isEditable()) {
if (block.getInheritedDisabled()) {
if (!block!.isInFlyout && block!.workspace!.options.disable &&
block!.isEditable()) {
if (block!.getInheritedDisabled()) {
return 'disabled';
}
return 'enabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
callback(scope: Scope) {
const block = scope.block;
const group = eventUtils.getGroup();
if (!group) {
eventUtils.setGroup(true);
}
block.setEnabled(!block.isEnabled());
block!.setEnabled(!block!.isEnabled());
if (!group) {
eventUtils.setGroup(false);
}
@@ -545,39 +492,34 @@ const registerDisable = function() {
weight: 5,
};
ContextMenuRegistry.registry.register(disableOption);
};
exports.registerDisable = registerDisable;
}
/**
* Option to delete a block.
* @alias Blockly.ContextMenuItems.registerDelete
*/
const registerDelete = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const deleteOption = {
displayText: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
export function registerDelete() {
const deleteOption: RegistryItem = {
displayText(scope: Scope) {
const block = scope.block;
// Count the number of blocks that are nested in this block.
let descendantCount = block.getDescendants(false).length;
const nextBlock = block.getNextBlock();
let descendantCount = block!.getDescendants(false).length;
const nextBlock = block!.getNextBlock();
if (nextBlock) {
// Blocks in the current stack would survive this block's deletion.
descendantCount -= nextBlock.getDescendants(false).length;
}
return (descendantCount === 1) ?
return descendantCount === 1 ?
Msg['DELETE_BLOCK'] :
Msg['DELETE_X_BLOCKS'].replace('%1', String(descendantCount));
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
if (!scope.block.isInFlyout && scope.block.isDeletable()) {
preconditionFn(scope: Scope) {
if (!scope.block!.isInFlyout && scope.block!.isDeletable()) {
return 'enabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
callback(scope: Scope) {
if (scope.block) {
scope.block.checkAndDelete();
}
@@ -587,46 +529,38 @@ const registerDelete = function() {
weight: 6,
};
ContextMenuRegistry.registry.register(deleteOption);
};
exports.registerDelete = registerDelete;
}
/**
* Option to open help for a block.
* @alias Blockly.ContextMenuItems.registerHelp
*/
const registerHelp = function() {
/** @type {!ContextMenuRegistry.RegistryItem} */
const helpOption = {
displayText: function() {
export function registerHelp() {
const helpOption: RegistryItem = {
displayText() {
return Msg['HELP'];
},
preconditionFn: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
preconditionFn(scope: Scope) {
const block = scope.block;
const url = (typeof block.helpUrl === 'function') ? block.helpUrl() :
block.helpUrl;
const url = typeof block!.helpUrl === 'function' ? block!.helpUrl() :
block!.helpUrl;
if (url) {
return 'enabled';
}
return 'hidden';
},
callback: function(/** @type {!ContextMenuRegistry.Scope} */
scope) {
scope.block.showHelp();
callback(scope: Scope) {
scope.block!.showHelp();
},
scopeType: ContextMenuRegistry.ScopeType.BLOCK,
id: 'blockHelp',
weight: 7,
};
ContextMenuRegistry.registry.register(helpOption);
};
exports.registerHelp = registerHelp;
}
/**
* Registers all block-scoped context menu items.
* @private
*/
const registerBlockOptions_ = function() {
/** Registers all block-scoped context menu items. */
function registerBlockOptions_() {
registerDuplicate();
registerComment();
registerInline();
@@ -634,18 +568,17 @@ const registerBlockOptions_ = function() {
registerDisable();
registerDelete();
registerHelp();
};
}
/**
* Registers all default context menu items. This should be called once per
* instance of ContextMenuRegistry.
* @package
* @alias Blockly.ContextMenuItems.registerDefaultOptions
* @internal
*/
const registerDefaultOptions = function() {
export function registerDefaultOptions() {
registerWorkspaceOptions_();
registerBlockOptions_();
};
exports.registerDefaultOptions = registerDefaultOptions;
}
registerDefaultOptions();

View File

@@ -7,18 +7,16 @@
/**
* @fileoverview Registry for context menu option items.
*/
'use strict';
/**
* Registry for context menu option items.
* @class
*/
goog.module('Blockly.ContextMenuRegistry');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.ContextMenuRegistry');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
import type {BlockSvg} from './block_svg.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
@@ -27,33 +25,28 @@ const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
* from ContextMenuRegistry.registry.
* @alias Blockly.ContextMenuRegistry
*/
class ContextMenuRegistry {
/**
* Resets the existing singleton instance of ContextMenuRegistry.
*/
export class ContextMenuRegistry {
static registry: ContextMenuRegistry;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
private registry_!: {[key: string]: RegistryItem};
/** Resets the existing singleton instance of ContextMenuRegistry. */
constructor() {
this.reset();
}
/**
* Clear and recreate the registry.
*/
/** Clear and recreate the registry. */
reset() {
/**
* Registry of all registered RegistryItems, keyed by ID.
* @type {!Object<string, !ContextMenuRegistry.RegistryItem>}
* @private
*/
/** Registry of all registered RegistryItems, keyed by ID. */
this.registry_ = Object.create(null);
}
/**
* Registers a RegistryItem.
* @param {!ContextMenuRegistry.RegistryItem} item Context menu item to
* register.
* @param item Context menu item to register.
* @throws {Error} if an item with the given ID already exists.
*/
register(item) {
register(item: RegistryItem) {
if (this.registry_[item.id]) {
throw Error('Menu item with ID "' + item.id + '" is already registered.');
}
@@ -62,10 +55,10 @@ class ContextMenuRegistry {
/**
* Unregisters a RegistryItem with the given ID.
* @param {string} id The ID of the RegistryItem to remove.
* @param id The ID of the RegistryItem to remove.
* @throws {Error} if an item with the given ID does not exist.
*/
unregister(id) {
unregister(id: string) {
if (!this.registry_[id]) {
throw new Error('Menu item with ID "' + id + '" not found.');
}
@@ -73,11 +66,10 @@ class ContextMenuRegistry {
}
/**
* @param {string} id The ID of the RegistryItem to get.
* @return {?ContextMenuRegistry.RegistryItem} RegistryItem or null if not
* found
* @param id The ID of the RegistryItem to get.
* @return RegistryItem or null if not found
*/
getItem(id) {
getItem(id: string): RegistryItem|null {
return this.registry_[id] || null;
}
@@ -85,15 +77,15 @@ class ContextMenuRegistry {
* Gets the valid context menu options for the given scope type (e.g. block or
* workspace) and scope. Blocks are only shown if the preconditionFn shows
* they should not be hidden.
* @param {!ContextMenuRegistry.ScopeType} scopeType Type of scope where menu
* should be shown (e.g. on a block or on a workspace)
* @param {!ContextMenuRegistry.Scope} scope Current scope of context menu
* (i.e., the exact workspace or block being clicked on)
* @return {!Array<!ContextMenuRegistry.ContextMenuOption>} the list of
* ContextMenuOptions
* @param scopeType Type of scope where menu should be shown (e.g. on a block
* or on a workspace)
* @param scope Current scope of context menu (i.e., the exact workspace or
* block being clicked on)
* @return the list of ContextMenuOptions
*/
getContextMenuOptions(scopeType, scope) {
const menuOptions = [];
getContextMenuOptions(scopeType: ScopeType, scope: Scope):
ContextMenuOption[] {
const menuOptions: AnyDuringMigration[] = [];
const registry = this.registry_;
Object.keys(registry).forEach(function(id) {
const item = registry[id];
@@ -103,12 +95,11 @@ class ContextMenuRegistry {
const displayText = typeof item.displayText === 'function' ?
item.displayText(scope) :
item.displayText;
/** @type {!ContextMenuRegistry.ContextMenuOption} */
const menuOption = {
const menuOption: ContextMenuOption = {
text: displayText,
enabled: (precondition === 'enabled'),
enabled: precondition === 'enabled',
callback: item.callback,
scope: scope,
scope,
weight: item.weight,
};
menuOptions.push(menuOption);
@@ -122,68 +113,70 @@ class ContextMenuRegistry {
}
}
/**
* Where this menu item should be rendered. If the menu item should be rendered
* in multiple scopes, e.g. on both a block and a workspace, it should be
* registered for each scope.
* @enum {string}
*/
ContextMenuRegistry.ScopeType = {
BLOCK: 'block',
WORKSPACE: 'workspace',
};
export namespace ContextMenuRegistry {
/**
* Where this menu item should be rendered. If the menu item should be
* rendered in multiple scopes, e.g. on both a block and a workspace, it
* should be registered for each scope.
*/
export enum ScopeType {
BLOCK = 'block',
WORKSPACE = 'workspace',
}
/**
* The actual workspace/block where the menu is being rendered. This is passed
* to callback and displayText functions that depend on this information.
* @typedef {{
* block: (BlockSvg|undefined),
* workspace: (WorkspaceSvg|undefined)
* }}
*/
ContextMenuRegistry.Scope;
/**
* The actual workspace/block where the menu is being rendered. This is passed
* to callback and displayText functions that depend on this information.
*/
export interface Scope {
block: BlockSvg|undefined;
workspace: WorkspaceSvg|undefined;
}
/**
* A menu item as entered in the registry.
* @typedef {{
* callback: function(!ContextMenuRegistry.Scope),
* scopeType: !ContextMenuRegistry.ScopeType,
* displayText: ((function(!ContextMenuRegistry.Scope):string)|string),
* preconditionFn: function(!ContextMenuRegistry.Scope):string,
* weight: number,
* id: string
* }}
*/
ContextMenuRegistry.RegistryItem;
/**
* A menu item as entered in the registry.
*/
export interface RegistryItem {
callback: (p1: Scope) => AnyDuringMigration;
scopeType: ScopeType;
displayText: ((p1: Scope) => string)|string;
preconditionFn: (p1: Scope) => string;
weight: number;
id: string;
}
/**
* A menu item as presented to contextmenu.js.
* @typedef {{
* text: string,
* enabled: boolean,
* callback: function(!ContextMenuRegistry.Scope),
* scope: !ContextMenuRegistry.Scope,
* weight: number
* }}
*/
ContextMenuRegistry.ContextMenuOption;
/**
* A menu item as presented to contextmenu.js.
*/
export interface ContextMenuOption {
text: string;
enabled: boolean;
callback: (p1: Scope) => AnyDuringMigration;
scope: Scope;
weight: number;
}
/**
* A subset of ContextMenuOption corresponding to what was publicly documented.
* ContextMenuOption should be preferred for new code.
* @typedef {{
* text: string,
* enabled: boolean,
* callback: function(!ContextMenuRegistry.Scope),
* }}
*/
ContextMenuRegistry.LegacyContextMenuOption;
/**
* A subset of ContextMenuOption corresponding to what was publicly
* documented. ContextMenuOption should be preferred for new code.
*/
export interface LegacyContextMenuOption {
text: string;
enabled: boolean;
callback: (p1: Scope) => AnyDuringMigration;
}
/**
* Singleton instance of this class. All interactions with this class should be
* done on this object.
* @type {!ContextMenuRegistry}
*/
ContextMenuRegistry.registry = new ContextMenuRegistry();
/**
* Singleton instance of this class. All interactions with this class should
* be done on this object.
*/
ContextMenuRegistry.registry = new ContextMenuRegistry();
}
exports.ContextMenuRegistry = ContextMenuRegistry;
export type ScopeType = ContextMenuRegistry.ScopeType;
export const ScopeType = ContextMenuRegistry.ScopeType;
export type Scope = ContextMenuRegistry.Scope;
export type RegistryItem = ContextMenuRegistry.RegistryItem;
export type ContextMenuOption = ContextMenuRegistry.ContextMenuOption;
export type LegacyContextMenuOption =
ContextMenuRegistry.LegacyContextMenuOption;

View File

@@ -7,32 +7,27 @@
/**
* @fileoverview Inject Blockly's CSS synchronously.
*/
'use strict';
/**
* Inject Blockly's CSS synchronously.
* @namespace Blockly.Css
*/
goog.module('Blockly.Css');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Css');
const deprecation = goog.require('Blockly.utils.deprecation');
import * as deprecation from './utils/deprecation.js';
/**
* Has CSS already been injected?
* @type {boolean}
* @private
*/
/** Has CSS already been injected? */
let injected = false;
/**
* Add some CSS to the blob that will be injected later. Allows optional
* components such as fields and the toolbox to store separate CSS.
* @param {string|!Array<string>} cssContent Multiline CSS string or an array of
* single lines of CSS.
* @param cssContent Multiline CSS string or an array of single lines of CSS.
* @alias Blockly.Css.register
*/
const register = function(cssContent) {
export function register(cssContent: string|string[]) {
if (injected) {
throw Error('CSS already injected');
}
@@ -41,13 +36,12 @@ const register = function(cssContent) {
deprecation.warn(
'Registering CSS by passing an array of strings', 'September 2021',
'September 2022', 'css.register passing a multiline string');
content += ('\n' + cssContent.join('\n'));
content += '\n' + cssContent.join('\n');
} else {
// Add new cssContent in the global content.
content += ('\n' + cssContent);
content += '\n' + cssContent;
}
};
exports.register = register;
}
/**
* Inject the CSS into the DOM. This is preferable over using a regular CSS
@@ -55,12 +49,12 @@ exports.register = register;
* a) It loads synchronously and doesn't force a redraw later.
* b) It speeds up loading by not blocking on a separate HTTP transfer.
* c) The CSS content may be made dynamic depending on init options.
* @param {boolean} hasCss If false, don't inject CSS
* (providing CSS becomes the document's responsibility).
* @param {string} pathToMedia Path from page to the Blockly media directory.
* @param hasCss If false, don't inject CSS (providing CSS becomes the
* document's responsibility).
* @param pathToMedia Path from page to the Blockly media directory.
* @alias Blockly.Css.inject
*/
const inject = function(hasCss, pathToMedia) {
export function inject(hasCss: boolean, pathToMedia: string) {
// Only inject the CSS once.
if (injected) {
return;
@@ -81,14 +75,13 @@ const inject = function(hasCss, pathToMedia) {
const cssTextNode = document.createTextNode(cssContent);
cssNode.appendChild(cssTextNode);
document.head.insertBefore(cssNode, document.head.firstChild);
};
exports.inject = inject;
}
/**
* The CSS content for Blockly.
* @alias Blockly.Css.content
*/
let content = (`
let content = `
.blocklySvg {
background-color: #fff;
outline: none;
@@ -564,5 +557,4 @@ let content = (`
float: right;
margin-right: -24px;
}
`);
exports.content = content;
`;

View File

@@ -9,53 +9,46 @@
* bubble that is dropped on top of it.
*/
'use strict';
/**
* The abstract class for a component that can delete a block or
* bubble that is dropped on top of it.
* @class
*/
goog.module('Blockly.DeleteArea');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.DeleteArea');
const {BlockSvg} = goog.require('Blockly.BlockSvg');
const {DragTarget} = goog.require('Blockly.DragTarget');
/* eslint-disable-next-line no-unused-vars */
const {IDeleteArea} = goog.require('Blockly.IDeleteArea');
/* eslint-disable-next-line no-unused-vars */
const {IDraggable} = goog.requireType('Blockly.IDraggable');
import {BlockSvg} from './block_svg.js';
import {DragTarget} from './drag_target.js';
import type {IDeleteArea} from './interfaces/i_delete_area.js';
import type {IDraggable} from './interfaces/i_draggable.js';
/**
* Abstract class for a component that can delete a block or bubble that is
* dropped on top of it.
* @extends {DragTarget}
* @implements {IDeleteArea}
* @alias Blockly.DeleteArea
*/
class DeleteArea extends DragTarget {
export class DeleteArea extends DragTarget implements IDeleteArea {
/**
* Whether the last block or bubble dragged over this delete area would be
* deleted if dropped on this component.
* This property is not updated after the block or bubble is deleted.
*/
protected wouldDelete_ = false;
/**
* The unique id for this component that is used to register with the
* ComponentManager.
*/
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
override id!: string;
/**
* Constructor for DeleteArea. Should not be called directly, only by a
* subclass.
*/
constructor() {
super();
/**
* Whether the last block or bubble dragged over this delete area would be
* deleted if dropped on this component.
* This property is not updated after the block or bubble is deleted.
* @type {boolean}
* @protected
*/
this.wouldDelete_ = false;
/**
* The unique id for this component that is used to register with the
* ComponentManager.
* @type {string}
*/
this.id;
}
/**
@@ -63,16 +56,14 @@ class DeleteArea extends DragTarget {
* this area.
* This method should check if the element is deletable and is always called
* before onDragEnter/onDragOver/onDragExit.
* @param {!IDraggable} element The block or bubble currently being
* dragged.
* @param {boolean} couldConnect Whether the element could could connect to
* another.
* @return {boolean} Whether the element provided would be deleted if dropped
* on this area.
* @param element The block or bubble currently being dragged.
* @param couldConnect Whether the element could could connect to another.
* @return Whether the element provided would be deleted if dropped on this
* area.
*/
wouldDelete(element, couldConnect) {
wouldDelete(element: IDraggable, couldConnect: boolean): boolean {
if (element instanceof BlockSvg) {
const block = /** @type {BlockSvg} */ (element);
const block = (element);
const couldDeleteBlock = !block.getParent() && block.isDeletable();
this.updateWouldDelete_(couldDeleteBlock && !couldConnect);
} else {
@@ -83,12 +74,9 @@ class DeleteArea extends DragTarget {
/**
* Updates the internal wouldDelete_ state.
* @param {boolean} wouldDelete The new value for the wouldDelete state.
* @protected
* @param wouldDelete The new value for the wouldDelete state.
*/
updateWouldDelete_(wouldDelete) {
protected updateWouldDelete_(wouldDelete: boolean) {
this.wouldDelete_ = wouldDelete;
}
}
exports.DeleteArea = DeleteArea;

View File

@@ -9,99 +9,119 @@
* alert/confirmation dialogs.
*/
'use strict';
/**
* Wrapper functions around JS functions for showing alert/confirmation dialogs.
* @namespace Blockly.dialog
*/
goog.module('Blockly.dialog');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.dialog');
let alertImplementation = function(message, opt_callback) {
let alertImplementation = function(
message: AnyDuringMigration, opt_callback: AnyDuringMigration) {
window.alert(message);
if (opt_callback) {
opt_callback();
}
};
let confirmImplementation = function(message, callback) {
let confirmImplementation = function(
message: AnyDuringMigration, callback: AnyDuringMigration) {
callback(window.confirm(message));
};
let promptImplementation = function(message, defaultValue, callback) {
let promptImplementation = function(
message: AnyDuringMigration, defaultValue: AnyDuringMigration,
callback: AnyDuringMigration) {
callback(window.prompt(message, defaultValue));
};
/**
* Wrapper to window.alert() that app developers may override via setAlert to
* provide alternatives to the modal browser window.
* @param {string} message The message to display to the user.
* @param {function()=} opt_callback The callback when the alert is dismissed.
* @param message The message to display to the user.
* @param opt_callback The callback when the alert is dismissed.
* @alias Blockly.dialog.alert
*/
const alert = function(message, opt_callback) {
export function alert(
message: string, opt_callback?: () => AnyDuringMigration) {
alertImplementation(message, opt_callback);
};
exports.alert = alert;
}
/**
* Sets the function to be run when Blockly.dialog.alert() is called.
* @param {!function(string, function()=)} alertFunction The function to be run.
* @param alertFunction The function to be run.
* @see Blockly.dialog.alert
* @alias Blockly.dialog.setAlert
*/
const setAlert = function(alertFunction) {
export function setAlert(
alertFunction: (p1: string, p2?: () => AnyDuringMigration) =>
AnyDuringMigration) {
alertImplementation = alertFunction;
};
exports.setAlert = setAlert;
}
/**
* Wrapper to window.confirm() that app developers may override via setConfirm
* to provide alternatives to the modal browser window.
* @param {string} message The message to display to the user.
* @param {!function(boolean)} callback The callback for handling user response.
* @param message The message to display to the user.
* @param callback The callback for handling user response.
* @alias Blockly.dialog.confirm
*/
const confirm = function(message, callback) {
export function confirm(
message: string, callback: (p1: boolean) => AnyDuringMigration) {
TEST_ONLY.confirmInternal(message, callback);
}
/**
* Private version of confirm for stubbing in tests.
*/
function confirmInternal(
message: string, callback: (p1: boolean) => AnyDuringMigration) {
confirmImplementation(message, callback);
};
exports.confirm = confirm;
}
/**
* Sets the function to be run when Blockly.dialog.confirm() is called.
* @param {!function(string, !function(boolean))} confirmFunction The function
* to be run.
* @param confirmFunction The function to be run.
* @see Blockly.dialog.confirm
* @alias Blockly.dialog.setConfirm
*/
const setConfirm = function(confirmFunction) {
export function setConfirm(
confirmFunction: (p1: string, p2: (p1: boolean) => AnyDuringMigration) =>
AnyDuringMigration) {
confirmImplementation = confirmFunction;
};
exports.setConfirm = setConfirm;
}
/**
* Wrapper to window.prompt() that app developers may override via setPrompt to
* provide alternatives to the modal browser window. Built-in browser prompts
* are often used for better text input experience on mobile device. We strongly
* recommend testing mobile when overriding this.
* @param {string} message The message to display to the user.
* @param {string} defaultValue The value to initialize the prompt with.
* @param {!function(?string)} callback The callback for handling user response.
* @param message The message to display to the user.
* @param defaultValue The value to initialize the prompt with.
* @param callback The callback for handling user response.
* @alias Blockly.dialog.prompt
*/
const prompt = function(message, defaultValue, callback) {
export function prompt(
message: string, defaultValue: string,
callback: (p1: string|null) => AnyDuringMigration) {
promptImplementation(message, defaultValue, callback);
};
exports.prompt = prompt;
}
/**
* Sets the function to be run when Blockly.dialog.prompt() is called.
* @param {!function(string, string, !function(?string))} promptFunction The
* function to be run.
* @param promptFunction The function to be run.
* @see Blockly.dialog.prompt
* @alias Blockly.dialog.setPrompt
*/
const setPrompt = function(promptFunction) {
export function setPrompt(
promptFunction:
(p1: string, p2: string, p3: (p1: string|null) => AnyDuringMigration) =>
AnyDuringMigration) {
promptImplementation = promptFunction;
};
exports.setPrompt = setPrompt;
}
export const TEST_ONLY = {
confirmInternal,
}

View File

@@ -9,88 +9,74 @@
* block or bubble is dragged over or dropped on top of it.
*/
'use strict';
/**
* The abstract class for a component with custom behaviour when a
* block or bubble is dragged over or dropped on top of it.
* @class
*/
goog.module('Blockly.DragTarget');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.DragTarget');
/* eslint-disable-next-line no-unused-vars */
const {IDragTarget} = goog.require('Blockly.IDragTarget');
/* eslint-disable-next-line no-unused-vars */
const {IDraggable} = goog.requireType('Blockly.IDraggable');
/* eslint-disable-next-line no-unused-vars */
const {Rect} = goog.requireType('Blockly.utils.Rect');
import type {IDragTarget} from './interfaces/i_drag_target.js';
import type {IDraggable} from './interfaces/i_draggable.js';
import type {Rect} from './utils/rect.js';
/**
* Abstract class for a component with custom behaviour when a block or bubble
* is dragged over or dropped on top of it.
* @implements {IDragTarget}
* @alias Blockly.DragTarget
*/
class DragTarget {
export class DragTarget implements IDragTarget {
/**
* The unique id for this component that is used to register with the
* ComponentManager.
*/
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
id!: string;
/**
* Constructor for DragTarget. It exists to add the id property and should not
* be called directly, only by a subclass.
*/
constructor() {
/**
* The unique id for this component that is used to register with the
* ComponentManager.
* @type {string}
*/
this.id;
}
constructor() {}
/**
* Handles when a cursor with a block or bubble enters this drag target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
* @param _dragElement The block or bubble currently being dragged.
*/
onDragEnter(_dragElement) {
// no-op
}
onDragEnter(_dragElement: IDraggable) {}
// no-op
/**
* Handles when a cursor with a block or bubble is dragged over this drag
* target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
* @param _dragElement The block or bubble currently being dragged.
*/
onDragOver(_dragElement) {
// no-op
}
onDragOver(_dragElement: IDraggable) {}
// no-op
/**
* Handles when a cursor with a block or bubble exits this drag target.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
* @param _dragElement The block or bubble currently being dragged.
*/
onDragExit(_dragElement) {
// no-op
}
onDragExit(_dragElement: IDraggable) {}
// no-op
/**
* Handles when a block or bubble is dropped on this component.
* Should not handle delete here.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
* @param _dragElement The block or bubble currently being dragged.
*/
onDrop(_dragElement) {
// no-op
}
onDrop(_dragElement: IDraggable) {}
// no-op
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to the Blockly injection div.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
* @return The component's bounding box. Null if drag target area should be
* ignored.
*/
getClientRect() {
getClientRect(): Rect|null {
return null;
}
@@ -98,14 +84,11 @@ class DragTarget {
* Returns whether the provided block or bubble should not be moved after
* being dropped on this component. If true, the element will return to where
* it was when the drag started.
* @param {!IDraggable} _dragElement The block or bubble currently being
* dragged.
* @return {boolean} Whether the block or bubble provided should be returned
* to drag start.
* @param _dragElement The block or bubble currently being dragged.
* @return Whether the block or bubble provided should be returned to drag
* start.
*/
shouldPreventMove(_dragElement) {
shouldPreventMove(_dragElement: IDraggable): boolean {
return false;
}
}
exports.DragTarget = DragTarget;

View File

@@ -10,188 +10,133 @@
* The drop-down can be kept inside the workspace, animate in/out, etc.
*/
'use strict';
/**
* A div that floats on top of the workspace, for drop-down menus.
* @class
*/
goog.module('Blockly.dropDownDiv');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.dropDownDiv');
const common = goog.require('Blockly.common');
const dom = goog.require('Blockly.utils.dom');
const math = goog.require('Blockly.utils.math');
const style = goog.require('Blockly.utils.style');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {Field} = goog.requireType('Blockly.Field');
const {Rect} = goog.require('Blockly.utils.Rect');
/* eslint-disable-next-line no-unused-vars */
const {Size} = goog.requireType('Blockly.utils.Size');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
import type {BlockSvg} from './block_svg.js';
import * as common from './common.js';
import type {Field} from './field.js';
import * as dom from './utils/dom.js';
import * as math from './utils/math.js';
import {Rect} from './utils/rect.js';
import type {Size} from './utils/size.js';
import * as style from './utils/style.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Arrow size in px. Should match the value in CSS
* (need to position pre-render).
* @type {number}
* @const
*/
const ARROW_SIZE = 16;
exports.ARROW_SIZE = ARROW_SIZE;
export const ARROW_SIZE = 16;
/**
* Drop-down border size in px. Should match the value in CSS (need to position
* the arrow).
* @type {number}
* @const
*/
const BORDER_SIZE = 1;
exports.BORDER_SIZE = BORDER_SIZE;
export const BORDER_SIZE = 1;
/**
* Amount the arrow must be kept away from the edges of the main drop-down div,
* in px.
* @type {number}
* @const
*/
const ARROW_HORIZONTAL_PADDING = 12;
exports.ARROW_HORIZONTAL_PADDING = ARROW_HORIZONTAL_PADDING;
export const ARROW_HORIZONTAL_PADDING = 12;
/**
* Amount drop-downs should be padded away from the source, in px.
* @type {number}
* @const
*/
const PADDING_Y = 16;
exports.PADDING_Y = PADDING_Y;
/** Amount drop-downs should be padded away from the source, in px. */
export const PADDING_Y = 16;
/**
* Length of animations in seconds.
* @type {number}
* @const
*/
const ANIMATION_TIME = 0.25;
exports.ANIMATION_TIME = ANIMATION_TIME;
/** Length of animations in seconds. */
export const ANIMATION_TIME = 0.25;
/**
* Timer for animation out, to be cleared if we need to immediately hide
* without disrupting new shows.
* @type {?number}
*/
let animateOutTimer = null;
let animateOutTimer: AnyDuringMigration = null;
/**
* Callback for when the drop-down is hidden.
* @type {?Function}
*/
let onHide = null;
/** Callback for when the drop-down is hidden. */
let onHide: Function|null = null;
/**
* A class name representing the current owner's workspace renderer.
* @type {string}
*/
/** A class name representing the current owner's workspace renderer. */
let renderedClassName = '';
/**
* A class name representing the current owner's workspace theme.
* @type {string}
*/
/** A class name representing the current owner's workspace theme. */
let themeClassName = '';
/**
* The content element.
* @type {!HTMLDivElement}
*/
let div;
/** The content element. */
let div: HTMLDivElement;
/**
* The content element.
* @type {!HTMLDivElement}
*/
let content;
/** The content element. */
let content: HTMLDivElement;
/**
* The arrow element.
* @type {!HTMLDivElement}
*/
let arrow;
/** The arrow element. */
let arrow: HTMLDivElement;
/**
* Drop-downs will appear within the bounds of this element if possible.
* Set in setBoundsElement.
* @type {?Element}
*/
let boundsElement = null;
let boundsElement: Element|null = null;
/**
* The object currently using the drop-down.
* @type {?Object}
*/
let owner = null;
/** The object currently using the drop-down. */
let owner: AnyDuringMigration|null = null;
/**
* Whether the dropdown was positioned to a field or the source block.
* @type {?boolean}
*/
let positionToField = null;
/** Whether the dropdown was positioned to a field or the source block. */
let positionToField: boolean|null = null;
/**
* Dropdown bounds info object used to encapsulate sizing information about a
* bounding element (bounding box and width/height).
* @typedef {{
* top:number,
* left:number,
* bottom:number,
* right:number,
* width:number,
* height:number
* }}
*/
let BoundsInfo;
exports.BoundsInfo = BoundsInfo;
export interface BoundsInfo {
top: number;
left: number;
bottom: number;
right: number;
width: number;
height: number;
}
/**
* Dropdown position metrics.
* @typedef {{
* initialX:number,
* initialY:number,
* finalX:number,
* finalY:number,
* arrowX:?number,
* arrowY:?number,
* arrowAtTop:?boolean,
* arrowVisible:boolean
* }}
*/
let PositionMetrics;
exports.PositionMetrics = PositionMetrics;
/** Dropdown position metrics. */
export interface PositionMetrics {
initialX: number;
initialY: number;
finalX: number;
finalY: number;
arrowX: number|null;
arrowY: number|null;
arrowAtTop: boolean|null;
arrowVisible: boolean;
}
/**
* Create and insert the DOM element for this div.
* @package
* @internal
*/
const createDom = function() {
export function createDom() {
if (div) {
return; // Already created.
}
div = /** @type {!HTMLDivElement} */ (document.createElement('div'));
div = document.createElement('div');
div.className = 'blocklyDropDownDiv';
const parentDiv = common.getParentContainer() || document.body;
parentDiv.appendChild(div);
content = /** @type {!HTMLDivElement} */ (document.createElement('div'));
content = document.createElement('div');
content.className = 'blocklyDropDownContent';
div.appendChild(content);
arrow = /** @type {!HTMLDivElement} */ (document.createElement('div'));
arrow = document.createElement('div');
arrow.className = 'blocklyDropDownArrow';
div.appendChild(arrow);
div.style.opacity = 0;
// AnyDuringMigration because: Type 'number' is not assignable to type
// 'string'.
div.style.opacity = 0 as AnyDuringMigration;
// Transition animation for transform: translate() and opacity.
div.style.transition = 'transform ' + ANIMATION_TIME + 's, ' +
'opacity ' + ANIMATION_TIME + 's';
@@ -204,127 +149,114 @@ const createDom = function() {
div.addEventListener('focusout', function() {
dom.removeClass(div, 'blocklyFocused');
});
};
exports.createDom = createDom;
}
/**
* Set an element to maintain bounds within. Drop-downs will appear
* within the box of this element if possible.
* @param {?Element} boundsElem Element to bind drop-down to.
* @param boundsElem Element to bind drop-down to.
*/
const setBoundsElement = function(boundsElem) {
export function setBoundsElement(boundsElem: Element|null) {
boundsElement = boundsElem;
};
exports.setBoundsElement = setBoundsElement;
}
/**
* Provide the div for inserting content into the drop-down.
* @return {!Element} Div to populate with content.
* @return Div to populate with content.
*/
const getContentDiv = function() {
export function getContentDiv(): Element {
return content;
};
exports.getContentDiv = getContentDiv;
}
/**
* Clear the content of the drop-down.
*/
const clearContent = function() {
/** Clear the content of the drop-down. */
export function clearContent() {
content.textContent = '';
content.style.width = '';
};
exports.clearContent = clearContent;
}
/**
* Set the colour for the drop-down.
* @param {string} backgroundColour Any CSS colour for the background.
* @param {string} borderColour Any CSS colour for the border.
* @param backgroundColour Any CSS colour for the background.
* @param borderColour Any CSS colour for the border.
*/
const setColour = function(backgroundColour, borderColour) {
export function setColour(backgroundColour: string, borderColour: string) {
div.style.backgroundColor = backgroundColour;
div.style.borderColor = borderColour;
};
exports.setColour = setColour;
}
/**
* Shortcut to show and place the drop-down with positioning determined
* by a particular block. The primary position will be below the block,
* and the secondary position above the block. Drop-down will be
* constrained to the block's workspace.
* @param {!Field} field The field showing the drop-down.
* @param {!BlockSvg} block Block to position the drop-down around.
* @param {Function=} opt_onHide Optional callback for when the drop-down is
* hidden.
* @param {number=} opt_secondaryYOffset Optional Y offset for above-block
* positioning.
* @return {boolean} True if the menu rendered below block; false if above.
* @param field The field showing the drop-down.
* @param block Block to position the drop-down around.
* @param opt_onHide Optional callback for when the drop-down is hidden.
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
* @return True if the menu rendered below block; false if above.
*/
const showPositionedByBlock = function(
field, block, opt_onHide, opt_secondaryYOffset) {
export function showPositionedByBlock(
field: Field, block: BlockSvg, opt_onHide?: Function,
opt_secondaryYOffset?: number): boolean {
return showPositionedByRect(
getScaledBboxOfBlock(block), field, opt_onHide, opt_secondaryYOffset);
};
exports.showPositionedByBlock = showPositionedByBlock;
}
/**
* Shortcut to show and place the drop-down with positioning determined
* by a particular field. The primary position will be below the field,
* and the secondary position above the field. Drop-down will be
* constrained to the block's workspace.
* @param {!Field} field The field to position the dropdown against.
* @param {Function=} opt_onHide Optional callback for when the drop-down is
* hidden.
* @param {number=} opt_secondaryYOffset Optional Y offset for above-block
* positioning.
* @return {boolean} True if the menu rendered below block; false if above.
* @param field The field to position the dropdown against.
* @param opt_onHide Optional callback for when the drop-down is hidden.
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
* @return True if the menu rendered below block; false if above.
*/
const showPositionedByField = function(
field, opt_onHide, opt_secondaryYOffset) {
export function showPositionedByField(
field: Field, opt_onHide?: Function,
opt_secondaryYOffset?: number): boolean {
positionToField = true;
return showPositionedByRect(
getScaledBboxOfField(field), field, opt_onHide, opt_secondaryYOffset);
};
exports.showPositionedByField = showPositionedByField;
}
/**
* Get the scaled bounding box of a block.
* @param {!BlockSvg} block The block.
* @return {!Rect} The scaled bounding box of the block.
* @param block The block.
* @return The scaled bounding box of the block.
*/
const getScaledBboxOfBlock = function(block) {
function getScaledBboxOfBlock(block: BlockSvg): Rect {
const blockSvg = block.getSvgRoot();
const scale = block.workspace.scale;
const scale = block.workspace!.scale;
const scaledHeight = block.height * scale;
const scaledWidth = block.width * scale;
const xy = style.getPageOffset(blockSvg);
return new Rect(xy.y, xy.y + scaledHeight, xy.x, xy.x + scaledWidth);
};
}
/**
* Get the scaled bounding box of a field.
* @param {!Field} field The field.
* @return {!Rect} The scaled bounding box of the field.
* @param field The field.
* @return The scaled bounding box of the field.
*/
const getScaledBboxOfField = function(field) {
function getScaledBboxOfField(field: Field): Rect {
const bBox = field.getScaledBBox();
return new Rect(bBox.top, bBox.bottom, bBox.left, bBox.right);
};
}
/**
* Helper method to show and place the drop-down with positioning determined
* by a scaled bounding box. The primary position will be below the rect,
* and the secondary position above the rect. Drop-down will be constrained to
* the block's workspace.
* @param {!Rect} bBox The scaled bounding box.
* @param {!Field} field The field to position the dropdown against.
* @param {Function=} opt_onHide Optional callback for when the drop-down is
* hidden.
* @param {number=} opt_secondaryYOffset Optional Y offset for above-block
* positioning.
* @return {boolean} True if the menu rendered below block; false if above.
* @param bBox The scaled bounding box.
* @param field The field to position the dropdown against.
* @param opt_onHide Optional callback for when the drop-down is hidden.
* @param opt_secondaryYOffset Optional Y offset for above-block positioning.
* @return True if the menu rendered below block; false if above.
*/
const showPositionedByRect = function(
bBox, field, opt_onHide, opt_secondaryYOffset) {
function showPositionedByRect(
bBox: Rect, field: Field, opt_onHide?: Function,
opt_secondaryYOffset?: number): boolean {
// If we can fit it, render below the block.
const primaryX = bBox.left + (bBox.right - bBox.left) / 2;
const primaryY = bBox.bottom;
@@ -334,19 +266,17 @@ const showPositionedByRect = function(
if (opt_secondaryYOffset) {
secondaryY += opt_secondaryYOffset;
}
const sourceBlock = /** @type {!BlockSvg} */ (field.getSourceBlock());
const sourceBlock = field.getSourceBlock() as BlockSvg;
// Set bounds to main workspace; show the drop-down.
let workspace = sourceBlock.workspace;
let workspace = sourceBlock.workspace!;
while (workspace.options.parentWorkspace) {
workspace =
/** @type {!WorkspaceSvg} */ (workspace.options.parentWorkspace);
workspace = workspace.options.parentWorkspace;
}
setBoundsElement(
/** @type {?Element} */ (workspace.getParentSvg().parentNode));
setBoundsElement(workspace.getParentSvg().parentNode as Element | null);
return show(
field, sourceBlock.RTL, primaryX, primaryY, secondaryX, secondaryY,
opt_onHide);
};
}
/**
* Show and place the drop-down.
@@ -356,28 +286,26 @@ const showPositionedByRect = function(
* will point there, and the container will be positioned below it.
* If we can't maintain the container bounds at the primary point, fall-back to
* the secondary point and position above.
* @param {?Object} newOwner The object showing the drop-down
* @param {boolean} rtl Right-to-left (true) or left-to-right (false).
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
* @param {number} secondaryX Secondary/alternative origin point x, in absolute
* px.
* @param {number} secondaryY Secondary/alternative origin point y, in absolute
* px.
* @param {Function=} opt_onHide Optional callback for when the drop-down is
* hidden.
* @return {boolean} True if the menu rendered at the primary origin point.
* @package
* @param newOwner The object showing the drop-down
* @param rtl Right-to-left (true) or left-to-right (false).
* @param primaryX Desired origin point x, in absolute px.
* @param primaryY Desired origin point y, in absolute px.
* @param secondaryX Secondary/alternative origin point x, in absolute px.
* @param secondaryY Secondary/alternative origin point y, in absolute px.
* @param opt_onHide Optional callback for when the drop-down is hidden.
* @return True if the menu rendered at the primary origin point.
* @internal
*/
const show = function(
newOwner, rtl, primaryX, primaryY, secondaryX, secondaryY, opt_onHide) {
export function show(
newOwner: AnyDuringMigration|null, rtl: boolean, primaryX: number,
primaryY: number, secondaryX: number, secondaryY: number,
opt_onHide?: Function): boolean {
owner = newOwner;
onHide = opt_onHide || null;
// Set direction.
div.style.direction = rtl ? 'rtl' : 'ltr';
const mainWorkspace =
/** @type {!WorkspaceSvg} */ (common.getMainWorkspace());
const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;
renderedClassName = mainWorkspace.getRenderer().getClassName();
themeClassName = mainWorkspace.getTheme().getClassName();
dom.addClass(div, renderedClassName);
@@ -391,23 +319,21 @@ const show = function(
// making the dropdown appear to fly in from (0, 0).
// Using both `left`, `top` for the initial translation and then `translate`
// for the animated transition to final X, Y is a workaround.
return positionInternal(primaryX, primaryY, secondaryX, secondaryY);
};
exports.show = show;
}
const internal = {};
/**
* Get sizing info about the bounding element.
* @return {!BoundsInfo} An object containing size
* information about the bounding element (bounding box and width/height).
* @return An object containing size information about the bounding element
* (bounding box and width/height).
*/
internal.getBoundsInfo = function() {
const boundPosition = style.getPageOffset(
/** @type {!Element} */ (boundsElement));
const boundSize = style.getSize(
/** @type {!Element} */ (boundsElement));
// AnyDuringMigration because: Property 'getBoundsInfo' does not exist on type
// '{}'.
(internal as AnyDuringMigration).getBoundsInfo = function(): BoundsInfo {
const boundPosition = style.getPageOffset(boundsElement as Element);
const boundSize = style.getSize(boundsElement as Element);
return {
left: boundPosition.x,
@@ -422,20 +348,22 @@ internal.getBoundsInfo = function() {
/**
* Helper to position the drop-down and the arrow, maintaining bounds.
* See explanation of origin points in show.
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
* @param {number} secondaryX Secondary/alternative origin point x,
* in absolute px.
* @param {number} secondaryY Secondary/alternative origin point y,
* in absolute px.
* @return {!PositionMetrics} Various final metrics,
* including rendered positions for drop-down and arrow.
* @param primaryX Desired origin point x, in absolute px.
* @param primaryY Desired origin point y, in absolute px.
* @param secondaryX Secondary/alternative origin point x, in absolute px.
* @param secondaryY Secondary/alternative origin point y, in absolute px.
* @return Various final metrics, including rendered positions for drop-down and
* arrow.
*/
internal.getPositionMetrics = function(
primaryX, primaryY, secondaryX, secondaryY) {
const boundsInfo = internal.getBoundsInfo();
const divSize = style.getSize(
/** @type {!Element} */ (div));
// AnyDuringMigration because: Property 'getPositionMetrics' does not exist on
// type '{}'.
(internal as AnyDuringMigration).getPositionMetrics = function(
primaryX: number, primaryY: number, secondaryX: number,
secondaryY: number): PositionMetrics {
// AnyDuringMigration because: Property 'getBoundsInfo' does not exist on
// type '{}'.
const boundsInfo = (internal as AnyDuringMigration).getBoundsInfo();
const divSize = style.getSize(div as Element);
// Can we fit in-bounds below the target?
if (primaryY + divSize.height < boundsInfo.bottom) {
@@ -460,17 +388,18 @@ internal.getPositionMetrics = function(
/**
* Get the metrics for positioning the div below the source.
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
* @param {!BoundsInfo} boundsInfo An object containing size
* information about the bounding element (bounding box and width/height).
* @param {!Size} divSize An object containing information about
* the size of the DropDownDiv (width & height).
* @return {!PositionMetrics} Various final metrics,
* including rendered positions for drop-down and arrow.
* @param primaryX Desired origin point x, in absolute px.
* @param primaryY Desired origin point y, in absolute px.
* @param boundsInfo An object containing size information about the bounding
* element (bounding box and width/height).
* @param divSize An object containing information about the size of the
* DropDownDiv (width & height).
* @return Various final metrics, including rendered positions for drop-down and
* arrow.
*/
const getPositionBelowMetrics = function(
primaryX, primaryY, boundsInfo, divSize) {
function getPositionBelowMetrics(
primaryX: number, primaryY: number, boundsInfo: BoundsInfo,
divSize: Size): PositionMetrics {
const xCoords =
getPositionX(primaryX, boundsInfo.left, boundsInfo.right, divSize.width);
@@ -481,59 +410,59 @@ const getPositionBelowMetrics = function(
initialX: xCoords.divX,
initialY: primaryY,
finalX: xCoords.divX, // X position remains constant during animation.
finalY: finalY,
finalY,
arrowX: xCoords.arrowX,
arrowY: arrowY,
arrowY,
arrowAtTop: true,
arrowVisible: true,
};
};
}
/**
* Get the metrics for positioning the div above the source.
* @param {number} secondaryX Secondary/alternative origin point x,
* in absolute px.
* @param {number} secondaryY Secondary/alternative origin point y,
* in absolute px.
* @param {!BoundsInfo} boundsInfo An object containing size
* information about the bounding element (bounding box and width/height).
* @param {!Size} divSize An object containing information about
* the size of the DropDownDiv (width & height).
* @return {!PositionMetrics} Various final metrics,
* including rendered positions for drop-down and arrow.
* @param secondaryX Secondary/alternative origin point x, in absolute px.
* @param secondaryY Secondary/alternative origin point y, in absolute px.
* @param boundsInfo An object containing size information about the bounding
* element (bounding box and width/height).
* @param divSize An object containing information about the size of the
* DropDownDiv (width & height).
* @return Various final metrics, including rendered positions for drop-down and
* arrow.
*/
const getPositionAboveMetrics = function(
secondaryX, secondaryY, boundsInfo, divSize) {
function getPositionAboveMetrics(
secondaryX: number, secondaryY: number, boundsInfo: BoundsInfo,
divSize: Size): PositionMetrics {
const xCoords = getPositionX(
secondaryX, boundsInfo.left, boundsInfo.right, divSize.width);
const arrowY = divSize.height - (BORDER_SIZE * 2) - (ARROW_SIZE / 2);
const arrowY = divSize.height - BORDER_SIZE * 2 - ARROW_SIZE / 2;
const finalY = secondaryY - divSize.height - PADDING_Y;
const initialY = secondaryY - divSize.height; // No padding on Y.
return {
initialX: xCoords.divX,
initialY: initialY,
initialY,
finalX: xCoords.divX, // X position remains constant during animation.
finalY: finalY,
finalY,
arrowX: xCoords.arrowX,
arrowY: arrowY,
arrowY,
arrowAtTop: false,
arrowVisible: true,
};
};
}
/**
* Get the metrics for positioning the div at the top of the page.
* @param {number} sourceX Desired origin point x, in absolute px.
* @param {!BoundsInfo} boundsInfo An object containing size
* information about the bounding element (bounding box and width/height).
* @param {!Size} divSize An object containing information about
* the size of the DropDownDiv (width & height).
* @return {!PositionMetrics} Various final metrics,
* including rendered positions for drop-down and arrow.
* @param sourceX Desired origin point x, in absolute px.
* @param boundsInfo An object containing size information about the bounding
* element (bounding box and width/height).
* @param divSize An object containing information about the size of the
* DropDownDiv (width & height).
* @return Various final metrics, including rendered positions for drop-down and
* arrow.
*/
const getPositionTopOfPageMetrics = function(sourceX, boundsInfo, divSize) {
function getPositionTopOfPageMetrics(
sourceX: number, boundsInfo: BoundsInfo, divSize: Size): PositionMetrics {
const xCoords =
getPositionX(sourceX, boundsInfo.left, boundsInfo.right, divSize.width);
@@ -548,22 +477,22 @@ const getPositionTopOfPageMetrics = function(sourceX, boundsInfo, divSize) {
arrowY: null,
arrowVisible: false,
};
};
}
/**
* Get the x positions for the left side of the DropDownDiv and the arrow,
* accounting for the bounds of the workspace.
* @param {number} sourceX Desired origin point x, in absolute px.
* @param {number} boundsLeft The left edge of the bounding element, in
* absolute px.
* @param {number} boundsRight The right edge of the bounding element, in
* absolute px.
* @param {number} divWidth The width of the div in px.
* @return {{divX: number, arrowX: number}} An object containing metrics for
* the x positions of the left side of the DropDownDiv and the arrow.
* @package
* @param sourceX Desired origin point x, in absolute px.
* @param boundsLeft The left edge of the bounding element, in absolute px.
* @param boundsRight The right edge of the bounding element, in absolute px.
* @param divWidth The width of the div in px.
* @return An object containing metrics for the x positions of the left side of
* the DropDownDiv and the arrow.
* @internal
*/
const getPositionX = function(sourceX, boundsLeft, boundsRight, divWidth) {
export function getPositionX(
sourceX: number, boundsLeft: number, boundsRight: number,
divWidth: number): {divX: number, arrowX: number} {
let divX = sourceX;
// Offset the topLeft coord so that the dropdowndiv is centered.
divX -= divWidth / 2;
@@ -580,27 +509,27 @@ const getPositionX = function(sourceX, boundsLeft, boundsRight, divWidth) {
relativeArrowX = math.clamp(
horizPadding, relativeArrowX, divWidth - horizPadding - ARROW_SIZE);
return {arrowX: relativeArrowX, divX: divX};
};
exports.getPositionX = getPositionX;
return {arrowX: relativeArrowX, divX};
}
/**
* Is the container visible?
* @return {boolean} True if visible.
* @return True if visible.
*/
const isVisible = function() {
export function isVisible(): boolean {
return !!owner;
};
exports.isVisible = isVisible;
}
/**
* Hide the menu only if it is owned by the provided object.
* @param {?Object} divOwner Object which must be owning the drop-down to hide.
* @param {boolean=} opt_withoutAnimation True if we should hide the dropdown
* without animating.
* @return {boolean} True if hidden.
* @param divOwner Object which must be owning the drop-down to hide.
* @param opt_withoutAnimation True if we should hide the dropdown without
* animating.
* @return True if hidden.
*/
const hideIfOwner = function(divOwner, opt_withoutAnimation) {
export function hideIfOwner(
divOwner: AnyDuringMigration|null,
opt_withoutAnimation?: boolean): boolean {
if (owner === divOwner) {
if (opt_withoutAnimation) {
hideWithoutAnimation();
@@ -610,17 +539,16 @@ const hideIfOwner = function(divOwner, opt_withoutAnimation) {
return true;
}
return false;
};
exports.hideIfOwner = hideIfOwner;
}
/**
* Hide the menu, triggering animation.
*/
const hide = function() {
/** Hide the menu, triggering animation. */
export function hide() {
// Start the animation by setting the translation and fading out.
// Reset to (initialX, initialY) - i.e., no translation.
div.style.transform = 'translate(0, 0)';
div.style.opacity = 0;
// AnyDuringMigration because: Type 'number' is not assignable to type
// 'string'.
div.style.opacity = 0 as AnyDuringMigration;
// Finish animation - reset all values to default.
animateOutTimer = setTimeout(function() {
hideWithoutAnimation();
@@ -629,13 +557,10 @@ const hide = function() {
onHide();
onHide = null;
}
};
exports.hide = hide;
}
/**
* Hide the menu, without animation.
*/
const hideWithoutAnimation = function() {
/** Hide the menu, without animation. */
export function hideWithoutAnimation() {
if (!isVisible()) {
return;
}
@@ -648,7 +573,9 @@ const hideWithoutAnimation = function() {
div.style.transform = '';
div.style.left = '';
div.style.top = '';
div.style.opacity = 0;
// AnyDuringMigration because: Type 'number' is not assignable to type
// 'string'.
div.style.opacity = 0 as AnyDuringMigration;
div.style.display = 'none';
div.style.backgroundColor = '';
div.style.borderColor = '';
@@ -668,23 +595,25 @@ const hideWithoutAnimation = function() {
dom.removeClass(div, themeClassName);
themeClassName = '';
}
(/** @type {!WorkspaceSvg} */ (common.getMainWorkspace())).markFocused();
};
exports.hideWithoutAnimation = hideWithoutAnimation;
(common.getMainWorkspace() as WorkspaceSvg).markFocused();
}
/**
* Set the dropdown div's position.
* @param {number} primaryX Desired origin point x, in absolute px.
* @param {number} primaryY Desired origin point y, in absolute px.
* @param {number} secondaryX Secondary/alternative origin point x,
* in absolute px.
* @param {number} secondaryY Secondary/alternative origin point y,
* in absolute px.
* @return {boolean} True if the menu rendered at the primary origin point.
* @param primaryX Desired origin point x, in absolute px.
* @param primaryY Desired origin point y, in absolute px.
* @param secondaryX Secondary/alternative origin point x, in absolute px.
* @param secondaryY Secondary/alternative origin point y, in absolute px.
* @return True if the menu rendered at the primary origin point.
*/
const positionInternal = function(primaryX, primaryY, secondaryX, secondaryY) {
function positionInternal(
primaryX: number, primaryY: number, secondaryX: number,
secondaryY: number): boolean {
// AnyDuringMigration because: Property 'getPositionMetrics' does not exist
// on type '{}'.
const metrics =
internal.getPositionMetrics(primaryX, primaryY, secondaryX, secondaryY);
(internal as AnyDuringMigration)
.getPositionMetrics(primaryX, primaryY, secondaryX, secondaryY);
// Update arrow CSS.
if (metrics.arrowVisible) {
@@ -710,7 +639,9 @@ const positionInternal = function(primaryX, primaryY, secondaryX, secondaryY) {
// Show the div.
div.style.display = 'block';
div.style.opacity = 1;
// AnyDuringMigration because: Type 'number' is not assignable to type
// 'string'.
div.style.opacity = 1 as AnyDuringMigration;
// Add final translate, animated through `transition`.
// Coordinates are relative to (initialX, initialY),
// where the drop-down is absolutely positioned.
@@ -719,22 +650,22 @@ const positionInternal = function(primaryX, primaryY, secondaryX, secondaryY) {
div.style.transform = 'translate(' + dx + 'px,' + dy + 'px)';
return !!metrics.arrowAtTop;
};
}
/**
* Repositions the dropdownDiv on window resize. If it doesn't know how to
* calculate the new position, it will just hide it instead.
* @package
* @internal
*/
const repositionForWindowResize = function() {
export function repositionForWindowResize() {
// This condition mainly catches the dropdown div when it is being used as a
// dropdown. It is important not to close it in this case because on Android,
// when a field is focused, the soft keyboard opens triggering a window resize
// event and we want the dropdown div to stick around so users can type into
// it.
if (owner) {
const field = /** @type {!Field} */ (owner);
const block = /** @type {!BlockSvg} */ (field.getSourceBlock());
const field = owner as Field;
const block = field.getSourceBlock() as BlockSvg;
const bBox = positionToField ? getScaledBboxOfField(field) :
getScaledBboxOfBlock(block);
// If we can fit it, render below the block.
@@ -747,7 +678,6 @@ const repositionForWindowResize = function() {
} else {
hide();
}
};
exports.repositionForWindowResize = repositionForWindowResize;
}
exports.TEST_ONLY = internal;
export const TEST_ONLY = internal;

View File

@@ -7,115 +7,117 @@
/**
* @fileoverview Events fired as a result of actions in Blockly's editor.
*/
'use strict';
/**
* Events fired as a result of actions in Blockly's editor.
* @namespace Blockly.Events
*/
goog.module('Blockly.Events');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events');
const eventUtils = goog.require('Blockly.Events.utils');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
const {BlockChange} = goog.require('Blockly.Events.BlockChange');
const {BlockCreate} = goog.require('Blockly.Events.BlockCreate');
const {BlockDelete} = goog.require('Blockly.Events.BlockDelete');
const {BlockDrag} = goog.require('Blockly.Events.BlockDrag');
const {BlockMove} = goog.require('Blockly.Events.BlockMove');
const {BubbleOpen} = goog.require('Blockly.Events.BubbleOpen');
const {Click} = goog.require('Blockly.Events.Click');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
const {CommentChange} = goog.require('Blockly.Events.CommentChange');
const {CommentCreate} = goog.require('Blockly.Events.CommentCreate');
const {CommentDelete} = goog.require('Blockly.Events.CommentDelete');
const {CommentMove} = goog.require('Blockly.Events.CommentMove');
const {FinishedLoading} = goog.require('Blockly.Events.FinishedLoading');
const {MarkerMove} = goog.require('Blockly.Events.MarkerMove');
const {Selected} = goog.require('Blockly.Events.Selected');
const {ThemeChange} = goog.require('Blockly.Events.ThemeChange');
const {ToolboxItemSelect} = goog.require('Blockly.Events.ToolboxItemSelect');
const {TrashcanOpen} = goog.require('Blockly.Events.TrashcanOpen');
const {UiBase} = goog.require('Blockly.Events.UiBase');
const {Ui} = goog.require('Blockly.Events.Ui');
const {VarBase} = goog.require('Blockly.Events.VarBase');
const {VarCreate} = goog.require('Blockly.Events.VarCreate');
const {VarDelete} = goog.require('Blockly.Events.VarDelete');
const {VarRename} = goog.require('Blockly.Events.VarRename');
const {ViewportChange} = goog.require('Blockly.Events.ViewportChange');
import * as deprecation from '../utils/deprecation.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
import {BlockBase} from './events_block_base.js';
import {BlockChange} from './events_block_change.js';
import {BlockCreate} from './events_block_create.js';
import {BlockDelete} from './events_block_delete.js';
import {BlockDrag} from './events_block_drag.js';
import {BlockMove} from './events_block_move.js';
import {BubbleOpen} from './events_bubble_open.js';
import {Click} from './events_click.js';
import {CommentBase} from './events_comment_base.js';
import {CommentChange} from './events_comment_change.js';
import {CommentCreate} from './events_comment_create.js';
import {CommentDelete} from './events_comment_delete.js';
import {CommentMove} from './events_comment_move.js';
import {MarkerMove} from './events_marker_move.js';
import {Selected} from './events_selected.js';
import {ThemeChange} from './events_theme_change.js';
import {ToolboxItemSelect} from './events_toolbox_item_select.js';
import {TrashcanOpen} from './events_trashcan_open.js';
import {Ui} from './events_ui.js';
import {UiBase} from './events_ui_base.js';
import {VarBase} from './events_var_base.js';
import {VarCreate} from './events_var_create.js';
import {VarDelete} from './events_var_delete.js';
import {VarRename} from './events_var_rename.js';
import {ViewportChange} from './events_viewport.js';
import * as eventUtils from './utils.js';
import {FinishedLoading} from './workspace_events.js';
// Events.
exports.Abstract = AbstractEvent;
exports.BubbleOpen = BubbleOpen;
exports.BlockBase = BlockBase;
exports.BlockChange = BlockChange;
exports.BlockCreate = BlockCreate;
exports.BlockDelete = BlockDelete;
exports.BlockDrag = BlockDrag;
exports.BlockMove = BlockMove;
exports.Click = Click;
exports.CommentBase = CommentBase;
exports.CommentChange = CommentChange;
exports.CommentCreate = CommentCreate;
exports.CommentDelete = CommentDelete;
exports.CommentMove = CommentMove;
exports.FinishedLoading = FinishedLoading;
exports.MarkerMove = MarkerMove;
exports.Selected = Selected;
exports.ThemeChange = ThemeChange;
exports.ToolboxItemSelect = ToolboxItemSelect;
exports.TrashcanOpen = TrashcanOpen;
exports.Ui = Ui;
exports.UiBase = UiBase;
exports.VarBase = VarBase;
exports.VarCreate = VarCreate;
exports.VarDelete = VarDelete;
exports.VarRename = VarRename;
exports.ViewportChange = ViewportChange;
export const Abstract = AbstractEvent;
export {BubbleOpen};
export {BlockBase};
export {BlockChange};
export {BlockCreate};
export {BlockDelete};
export {BlockDrag};
export {BlockMove};
export {Click};
export {CommentBase};
export {CommentChange};
export {CommentCreate};
export {CommentDelete};
export {CommentMove};
export {FinishedLoading};
export {MarkerMove};
export {Selected};
export {ThemeChange};
export {ToolboxItemSelect};
export {TrashcanOpen};
export {Ui};
export {UiBase};
export {VarBase};
export {VarCreate};
export {VarDelete};
export {VarRename};
export {ViewportChange};
// Event types.
exports.BLOCK_CHANGE = eventUtils.BLOCK_CHANGE;
exports.BLOCK_CREATE = eventUtils.BLOCK_CREATE;
exports.BLOCK_DELETE = eventUtils.BLOCK_DELETE;
exports.BLOCK_DRAG = eventUtils.BLOCK_DRAG;
exports.BLOCK_MOVE = eventUtils.BLOCK_MOVE;
exports.BUBBLE_OPEN = eventUtils.BUBBLE_OPEN;
exports.BumpEvent = eventUtils.BumpEvent;
exports.BUMP_EVENTS = eventUtils.BUMP_EVENTS;
exports.CHANGE = eventUtils.CHANGE;
exports.CLICK = eventUtils.CLICK;
exports.COMMENT_CHANGE = eventUtils.COMMENT_CHANGE;
exports.COMMENT_CREATE = eventUtils.COMMENT_CREATE;
exports.COMMENT_DELETE = eventUtils.COMMENT_DELETE;
exports.COMMENT_MOVE = eventUtils.COMMENT_MOVE;
exports.CREATE = eventUtils.CREATE;
exports.DELETE = eventUtils.DELETE;
exports.FINISHED_LOADING = eventUtils.FINISHED_LOADING;
exports.MARKER_MOVE = eventUtils.MARKER_MOVE;
exports.MOVE = eventUtils.MOVE;
exports.SELECTED = eventUtils.SELECTED;
exports.THEME_CHANGE = eventUtils.THEME_CHANGE;
exports.TOOLBOX_ITEM_SELECT = eventUtils.TOOLBOX_ITEM_SELECT;
exports.TRASHCAN_OPEN = eventUtils.TRASHCAN_OPEN;
exports.UI = eventUtils.UI;
exports.VAR_CREATE = eventUtils.VAR_CREATE;
exports.VAR_DELETE = eventUtils.VAR_DELETE;
exports.VAR_RENAME = eventUtils.VAR_RENAME;
exports.VIEWPORT_CHANGE = eventUtils.VIEWPORT_CHANGE;
export const BLOCK_CHANGE = eventUtils.BLOCK_CHANGE;
export const BLOCK_CREATE = eventUtils.BLOCK_CREATE;
export const BLOCK_DELETE = eventUtils.BLOCK_DELETE;
export const BLOCK_DRAG = eventUtils.BLOCK_DRAG;
export const BLOCK_MOVE = eventUtils.BLOCK_MOVE;
export const BUBBLE_OPEN = eventUtils.BUBBLE_OPEN;
export type BumpEvent = eventUtils.BumpEvent;
export const BUMP_EVENTS = eventUtils.BUMP_EVENTS;
export const CHANGE = eventUtils.CHANGE;
export const CLICK = eventUtils.CLICK;
export const COMMENT_CHANGE = eventUtils.COMMENT_CHANGE;
export const COMMENT_CREATE = eventUtils.COMMENT_CREATE;
export const COMMENT_DELETE = eventUtils.COMMENT_DELETE;
export const COMMENT_MOVE = eventUtils.COMMENT_MOVE;
export const CREATE = eventUtils.CREATE;
export const DELETE = eventUtils.DELETE;
export const FINISHED_LOADING = eventUtils.FINISHED_LOADING;
export const MARKER_MOVE = eventUtils.MARKER_MOVE;
export const MOVE = eventUtils.MOVE;
export const SELECTED = eventUtils.SELECTED;
export const THEME_CHANGE = eventUtils.THEME_CHANGE;
export const TOOLBOX_ITEM_SELECT = eventUtils.TOOLBOX_ITEM_SELECT;
export const TRASHCAN_OPEN = eventUtils.TRASHCAN_OPEN;
export const UI = eventUtils.UI;
export const VAR_CREATE = eventUtils.VAR_CREATE;
export const VAR_DELETE = eventUtils.VAR_DELETE;
export const VAR_RENAME = eventUtils.VAR_RENAME;
export const VIEWPORT_CHANGE = eventUtils.VIEWPORT_CHANGE;
// Event utils.
exports.clearPendingUndo = eventUtils.clearPendingUndo;
exports.disable = eventUtils.disable;
exports.enable = eventUtils.enable;
exports.filter = eventUtils.filter;
exports.fire = eventUtils.fire;
exports.fromJson = eventUtils.fromJson;
exports.getDescendantIds = eventUtils.getDescendantIds;
exports.get = eventUtils.get;
exports.getGroup = eventUtils.getGroup;
exports.getRecordUndo = eventUtils.getRecordUndo;
exports.isEnabled = eventUtils.isEnabled;
exports.setGroup = eventUtils.setGroup;
exports.setRecordUndo = eventUtils.setRecordUndo;
exports.disableOrphans = eventUtils.disableOrphans;
export const clearPendingUndo = eventUtils.clearPendingUndo;
export const disable = eventUtils.disable;
export const enable = eventUtils.enable;
export const filter = eventUtils.filter;
export const fire = eventUtils.fire;
export const fromJson = eventUtils.fromJson;
export const getDescendantIds = eventUtils.getDescendantIds;
export const get = eventUtils.get;
export const getGroup = eventUtils.getGroup;
export const getRecordUndo = eventUtils.getRecordUndo;
export const isEnabled = eventUtils.isEnabled;
export const setGroup = eventUtils.setGroup;
export const setRecordUndo = eventUtils.setRecordUndo;
export const disableOrphans = eventUtils.disableOrphans;

View File

@@ -8,118 +8,99 @@
* @fileoverview Abstract class for events fired as a result of actions in
* Blockly's editor.
*/
'use strict';
/**
* Abstract class for events fired as a result of actions in
* Blockly's editor.
* @class
*/
goog.module('Blockly.Events.Abstract');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Abstract');
const eventUtils = goog.require('Blockly.Events.utils');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
import * as common from '../common.js';
import type {Workspace} from '../workspace.js';
import * as eventUtils from './utils.js';
/**
* Abstract class for an event.
* @abstract
* @alias Blockly.Events.Abstract
*/
class Abstract {
/**
* @alias Blockly.Events.Abstract
*/
export abstract class Abstract {
/** Whether or not the event is blank (to be populated by fromJson). */
isBlank: boolean|null = null;
/** The workspace identifier for this event. */
workspaceId?: string = undefined;
group: string;
recordUndo: boolean;
/** Whether or not the event is a UI event. */
isUiEvent = false;
/** Type of this event. */
type?: string = undefined;
/** @alias Blockly.Events.Abstract */
constructor() {
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {?boolean}
*/
this.isBlank = null;
/**
* The workspace identifier for this event.
* @type {string|undefined}
*/
this.workspaceId = undefined;
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = eventUtils.getGroup();
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
/** Sets whether the event should be added to the undo stack. */
this.recordUndo = eventUtils.getRecordUndo();
/**
* Whether or not the event is a UI event.
* @type {boolean}
*/
this.isUiEvent = false;
/**
* Type of this event.
* @type {string|undefined}
*/
this.type = undefined;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
toJson(): AnyDuringMigration {
const json = {'type': this.type};
if (this.group) {
json['group'] = this.group;
(json as AnyDuringMigration)['group'] = this.group;
}
return json;
}
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
fromJson(json: AnyDuringMigration) {
this.isBlank = false;
this.group = json['group'];
}
/**
* Does this event record any change of state?
* @return {boolean} True if null, false if something changed.
* @return True if null, false if something changed.
*/
isNull() {
isNull(): boolean {
return false;
}
/**
* Run an event.
* @param {boolean} _forward True if run forward, false if run backward
* (undo).
* @param _forward True if run forward, false if run backward (undo).
*/
run(_forward) {
// Defined by subclasses.
}
run(_forward: boolean) {}
// Defined by subclasses.
/**
* Get workspace the event belongs to.
* @return {!Workspace} The workspace the event belongs to.
* @return The workspace the event belongs to.
* @throws {Error} if workspace is null.
* @protected
* @internal
*/
getEventWorkspace_() {
getEventWorkspace_(): Workspace {
let workspace;
if (this.workspaceId) {
const {Workspace} = goog.module.get('Blockly.Workspace');
workspace = Workspace.getById(this.workspaceId);
workspace = common.getWorkspaceById(this.workspaceId);
}
if (!workspace) {
throw Error(
@@ -129,5 +110,3 @@ class Abstract {
return workspace;
}
}
exports.Abstract = Abstract;

View File

@@ -7,51 +7,48 @@
/**
* @fileoverview Base class for all types of block events.
*/
'use strict';
/**
* Base class for all types of block events.
* @class
*/
goog.module('Blockly.Events.BlockBase');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockBase');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
import type {Block} from '../block.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
/**
* Abstract class for a block event.
* @extends {AbstractEvent}
* @alias Blockly.Events.BlockBase
*/
class BlockBase extends AbstractEvent {
export class BlockBase extends AbstractEvent {
override isBlank: AnyDuringMigration;
blockId: string;
override workspaceId: string;
/**
* @param {!Block=} opt_block The block this event corresponds to.
* @param opt_block The block this event corresponds to.
* Undefined for a blank event.
*/
constructor(opt_block) {
constructor(opt_block?: Block) {
super();
this.isBlank = typeof opt_block === 'undefined';
/**
* The block ID for the block this event pertains to
* @type {string}
*/
this.blockId = this.isBlank ? '' : opt_block.id;
/** The block ID for the block this event pertains to */
this.blockId = this.isBlank ? '' : opt_block!.id;
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = this.isBlank ? '' : opt_block.workspace.id;
/** The workspace identifier for this event. */
this.workspaceId = this.isBlank ? '' : opt_block!.workspace?.id ?? '';
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['blockId'] = this.blockId;
return json;
@@ -59,12 +56,10 @@ class BlockBase extends AbstractEvent {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.blockId = json['blockId'];
}
}
exports.BlockBase = BlockBase;

View File

@@ -7,45 +7,49 @@
/**
* @fileoverview Class for a block change event.
*/
'use strict';
/**
* Class for a block change event.
* @class
*/
goog.module('Blockly.Events.BlockChange');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockChange');
const Xml = goog.require('Blockly.Xml');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
import type {Block} from '../block.js';
import type {BlockSvg} from '../block_svg.js';
import * as registry from '../registry.js';
import * as Xml from '../xml.js';
import {BlockBase} from './events_block_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a block change event.
* @extends {BlockBase}
* @alias Blockly.Events.BlockChange
*/
class BlockChange extends BlockBase {
export class BlockChange extends BlockBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
element!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
name!: string|null;
oldValue: AnyDuringMigration;
newValue: AnyDuringMigration;
/**
* @param {!Block=} opt_block The changed block. Undefined for a blank
* event.
* @param {string=} opt_element One of 'field', 'comment', 'disabled', etc.
* @param {?string=} opt_name Name of input or field affected, or null.
* @param {*=} opt_oldValue Previous value of element.
* @param {*=} opt_newValue New value of element.
* @param opt_block The changed block. Undefined for a blank event.
* @param opt_element One of 'field', 'comment', 'disabled', etc.
* @param opt_name Name of input or field affected, or null.
* @param opt_oldValue Previous value of element.
* @param opt_newValue New value of element.
*/
constructor(opt_block, opt_element, opt_name, opt_oldValue, opt_newValue) {
constructor(
opt_block?: Block, opt_element?: string, opt_name?: string|null,
opt_oldValue?: AnyDuringMigration, opt_newValue?: AnyDuringMigration) {
super(opt_block);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.BLOCK_CHANGE;
if (!opt_block) {
@@ -59,9 +63,9 @@ class BlockChange extends BlockBase {
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['element'] = this.element;
if (this.name) {
@@ -74,9 +78,9 @@ class BlockChange extends BlockBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.element = json['element'];
this.name = json['name'];
@@ -86,26 +90,25 @@ class BlockChange extends BlockBase {
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
* @return False if something changed.
*/
isNull() {
override isNull(): boolean {
return this.oldValue === this.newValue;
}
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
if (!block) {
console.warn('Can\'t change non-existent block: ' + this.blockId);
return;
}
// Assume the block is rendered so that then we can check.
const blockSvg = /** @type {!BlockSvg} */ (block);
const blockSvg = block as BlockSvg;
if (blockSvg.mutator) {
// Close the mutator (if open) since we don't want to update it.
blockSvg.mutator.setVisible(false);
@@ -113,7 +116,7 @@ class BlockChange extends BlockBase {
const value = forward ? this.newValue : this.oldValue;
switch (this.element) {
case 'field': {
const field = block.getField(this.name);
const field = block.getField(this.name!);
if (field) {
field.setValue(value);
} else {
@@ -122,7 +125,7 @@ class BlockChange extends BlockBase {
break;
}
case 'comment':
block.setCommentText(/** @type {string} */ (value) || null);
block.setCommentText(value as string || null);
break;
case 'collapsed':
block.setCollapsed(!!value);
@@ -134,14 +137,11 @@ class BlockChange extends BlockBase {
block.setInputsInline(!!value);
break;
case 'mutation': {
const oldState = BlockChange.getExtraBlockState_(
/** @type {!BlockSvg} */ (block));
const oldState = BlockChange.getExtraBlockState_(block as BlockSvg);
if (block.loadExtraState) {
block.loadExtraState(
JSON.parse(/** @type {string} */ (value) || '{}'));
block.loadExtraState(JSON.parse(value as string || '{}'));
} else if (block.domToMutation) {
block.domToMutation(
Xml.textToDom(/** @type {string} */ (value) || '<mutation/>'));
block.domToMutation(Xml.textToDom(value as string || '<mutation/>'));
}
eventUtils.fire(
new BlockChange(block, 'mutation', null, oldState, value));
@@ -157,12 +157,11 @@ class BlockChange extends BlockBase {
/**
* Returns the extra state of the given block (either as XML or a JSO,
* depending on the block's definition).
* @param {!BlockSvg} block The block to get the extra state of.
* @return {string} A stringified version of the extra state of the given
* block.
* @package
* @param block The block to get the extra state of.
* @return A stringified version of the extra state of the given block.
* @internal
*/
static getExtraBlockState_(block) {
static getExtraBlockState_(block: BlockSvg): string {
if (block.saveExtraState) {
const state = block.saveExtraState();
return state ? JSON.stringify(state) : '';
@@ -175,5 +174,3 @@ class BlockChange extends BlockBase {
}
registry.register(registry.Type.EVENT, eventUtils.CHANGE, BlockChange);
exports.BlockChange = BlockChange;

View File

@@ -7,45 +7,46 @@
/**
* @fileoverview Class for a block creation event.
*/
'use strict';
/**
* Class for a block creation event.
* @class
*/
goog.module('Blockly.Events.BlockCreate');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockCreate');
const Xml = goog.require('Blockly.Xml');
const blocks = goog.require('Blockly.serialization.blocks');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
import type {Block} from '../block.js';
import * as registry from '../registry.js';
import * as blocks from '../serialization/blocks.js';
import * as Xml from '../xml.js';
import {BlockBase} from './events_block_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a block creation event.
* @extends {BlockBase}
* @alias Blockly.Events.BlockCreate
*/
class BlockCreate extends BlockBase {
/**
* @param {!Block=} opt_block The created block. Undefined for a blank
* event.
*/
constructor(opt_block) {
export class BlockCreate extends BlockBase {
override type: string;
xml: AnyDuringMigration;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
ids!: string[];
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
json!: blocks.State;
/** @param opt_block The created block. Undefined for a blank event. */
constructor(opt_block?: Block) {
super(opt_block);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.BLOCK_CREATE;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
if (opt_block.isShadow()) {
// Moving shadow blocks is handled via disconnection.
this.recordUndo = false;
@@ -54,19 +55,15 @@ class BlockCreate extends BlockBase {
this.xml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block);
/**
* JSON representation of the block that was just created.
* @type {!blocks.State}
*/
this.json = /** @type {!blocks.State} */ (
blocks.save(opt_block, {addCoordinates: true}));
/** JSON representation of the block that was just created. */
this.json = blocks.save(opt_block, {addCoordinates: true}) as blocks.State;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['xml'] = Xml.domToText(this.xml);
json['ids'] = this.ids;
@@ -79,13 +76,13 @@ class BlockCreate extends BlockBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
this.ids = json['ids'];
this.json = /** @type {!blocks.State} */ (json['json']);
this.json = json['json'] as blocks.State;
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
}
@@ -93,9 +90,9 @@ class BlockCreate extends BlockBase {
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (forward) {
blocks.append(this.json, workspace);
@@ -115,5 +112,3 @@ class BlockCreate extends BlockBase {
}
registry.register(registry.Type.EVENT, eventUtils.CREATE, BlockCreate);
exports.BlockCreate = BlockCreate;

View File

@@ -7,45 +7,48 @@
/**
* @fileoverview Class for a block delete event.
*/
'use strict';
/**
* Class for a block delete event.
* @class
*/
goog.module('Blockly.Events.BlockDelete');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockDelete');
const Xml = goog.require('Blockly.Xml');
const blocks = goog.require('Blockly.serialization.blocks');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
import type {Block} from '../block.js';
import * as registry from '../registry.js';
import * as blocks from '../serialization/blocks.js';
import * as Xml from '../xml.js';
import {BlockBase} from './events_block_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a block deletion event.
* @extends {BlockBase}
* @alias Blockly.Events.BlockDelete
*/
class BlockDelete extends BlockBase {
/**
* @param {!Block=} opt_block The deleted block. Undefined for a blank
* event.
*/
constructor(opt_block) {
export class BlockDelete extends BlockBase {
override type: string;
oldXml: AnyDuringMigration;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
ids!: string[];
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
wasShadow!: boolean;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldJson!: blocks.State;
/** @param opt_block The deleted block. Undefined for a blank event. */
constructor(opt_block?: Block) {
super(opt_block);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.BLOCK_DELETE;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
}
if (opt_block.getParent()) {
throw Error('Connected blocks cannot be deleted.');
}
@@ -57,25 +60,19 @@ class BlockDelete extends BlockBase {
this.oldXml = Xml.blockToDomWithXY(opt_block);
this.ids = eventUtils.getDescendantIds(opt_block);
/**
* Was the block that was just deleted a shadow?
* @type {boolean}
*/
/** Was the block that was just deleted a shadow? */
this.wasShadow = opt_block.isShadow();
/**
* JSON representation of the block that was just deleted.
* @type {!blocks.State}
*/
this.oldJson = /** @type {!blocks.State} */ (
blocks.save(opt_block, {addCoordinates: true}));
/** JSON representation of the block that was just deleted. */
this.oldJson =
blocks.save(opt_block, {addCoordinates: true}) as blocks.State;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['oldXml'] = Xml.domToText(this.oldXml);
json['ids'] = this.ids;
@@ -89,15 +86,15 @@ class BlockDelete extends BlockBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.oldXml = Xml.textToDom(json['oldXml']);
this.ids = json['ids'];
this.wasShadow =
json['wasShadow'] || this.oldXml.tagName.toLowerCase() === 'shadow';
this.oldJson = /** @type {!blocks.State} */ (json['oldJson']);
this.oldJson = json['oldJson'] as blocks.State;
if (json['recordUndo'] !== undefined) {
this.recordUndo = json['recordUndo'];
}
@@ -105,9 +102,9 @@ class BlockDelete extends BlockBase {
/**
* Run a deletion event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (forward) {
for (let i = 0; i < this.ids.length; i++) {
@@ -127,5 +124,3 @@ class BlockDelete extends BlockBase {
}
registry.register(registry.Type.EVENT, eventUtils.DELETE, BlockDelete);
exports.BlockDelete = BlockDelete;

View File

@@ -7,64 +7,59 @@
/**
* @fileoverview Events fired as a block drag.
*/
'use strict';
/**
* Events fired as a block drag.
* @class
*/
goog.module('Blockly.Events.BlockDrag');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockDrag');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import type {Block} from '../block.js';
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a block drag event.
* @extends {UiBase}
* @alias Blockly.Events.BlockDrag
*/
class BlockDrag extends UiBase {
export class BlockDrag extends UiBase {
blockId: AnyDuringMigration;
isStart?: boolean;
blocks?: Block[];
override type: string;
/**
* @param {!Block=} opt_block The top block in the stack that is being
* dragged. Undefined for a blank event.
* @param {boolean=} opt_isStart Whether this is the start of a block drag.
* @param opt_block The top block in the stack that is being dragged.
* Undefined for a blank event.
* @param opt_isStart Whether this is the start of a block drag.
* Undefined for a blank event.
* @param {!Array<!Block>=} opt_blocks The blocks affected by this
* drag. Undefined for a blank event.
* @param opt_blocks The blocks affected by this drag. Undefined for a blank
* event.
*/
constructor(opt_block, opt_isStart, opt_blocks) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
constructor(opt_block?: Block, opt_isStart?: boolean, opt_blocks?: Block[]) {
const workspaceId = opt_block ? opt_block.workspace?.id : undefined;
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
/**
* Whether this is the start of a block drag.
* @type {boolean|undefined}
*/
/** Whether this is the start of a block drag. */
this.isStart = opt_isStart;
/**
* The blocks affected by this drag event.
* @type {!Array<!Block>|undefined}
*/
/** The blocks affected by this drag event. */
this.blocks = opt_blocks;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.BLOCK_DRAG;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['isStart'] = this.isStart;
json['blockId'] = this.blockId;
@@ -74,9 +69,9 @@ class BlockDrag extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.isStart = json['isStart'];
this.blockId = json['blockId'];
@@ -85,5 +80,3 @@ class BlockDrag extends UiBase {
}
registry.register(registry.Type.EVENT, eventUtils.BLOCK_DRAG, BlockDrag);
exports.BlockDrag = BlockDrag;

View File

@@ -7,53 +7,57 @@
/**
* @fileoverview Class for a block move event.
*/
'use strict';
/**
* Class for a block move event.
* @class
*/
goog.module('Blockly.Events.BlockMove');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BlockMove');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {BlockBase} = goog.require('Blockly.Events.BlockBase');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
import type {Block} from '../block.js';
import {ConnectionType} from '../connection_type.js';
import * as registry from '../registry.js';
import {Coordinate} from '../utils/coordinate.js';
/**
* @typedef {{
* parentId: string,
* inputName: string,
* coordinate: ?Coordinate,
* }}
*/
let BlockLocation; // eslint-disable-line no-unused-vars
import {BlockBase} from './events_block_base.js';
import * as eventUtils from './utils.js';
interface BlockLocation {
parentId: string;
inputName: string;
coordinate: Coordinate|null;
} // eslint-disable-line no-unused-vars
/**
* Class for a block move event. Created before the move.
* @extends {BlockBase}
* @alias Blockly.Events.BlockMove
*/
class BlockMove extends BlockBase {
/**
* @param {!Block=} opt_block The moved block. Undefined for a blank
* event.
*/
constructor(opt_block) {
export class BlockMove extends BlockBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldParentId!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldInputName!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldCoordinate!: Coordinate|null;
newParentId: string|null = null;
newInputName: string|null = null;
newCoordinate: Coordinate|null = null;
/** @param opt_block The moved block. Undefined for a blank event. */
constructor(opt_block?: Block) {
super(opt_block);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.BLOCK_MOVE;
if (!opt_block) {
return; // Blank event to be populated by fromJson.
return;
}
// Blank event to be populated by fromJson.
if (opt_block.isShadow()) {
// Moving shadow blocks is handled via disconnection.
this.recordUndo = false;
@@ -63,17 +67,13 @@ class BlockMove extends BlockBase {
this.oldParentId = location.parentId;
this.oldInputName = location.inputName;
this.oldCoordinate = location.coordinate;
this.newParentId = null;
this.newInputName = null;
this.newCoordinate = null;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
if (this.newParentId) {
json['newParentId'] = this.newParentId;
@@ -93,9 +93,9 @@ class BlockMove extends BlockBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.newParentId = json['newParentId'];
this.newInputName = json['newInputName'];
@@ -108,9 +108,7 @@ class BlockMove extends BlockBase {
}
}
/**
* Record the block's new location. Called after the move.
*/
/** Record the block's new location. Called after the move. */
recordNew() {
const location = this.currentLocation_();
this.newParentId = location.parentId;
@@ -121,31 +119,32 @@ class BlockMove extends BlockBase {
/**
* Returns the parentId and input if the block is connected,
* or the XY location if disconnected.
* @return {!BlockLocation} Collection of location info.
* @private
* @return Collection of location info.
*/
currentLocation_() {
private currentLocation_(): BlockLocation {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
const location = {};
const parent = block.getParent();
const location = {} as BlockLocation;
const parent = block!.getParent();
if (parent) {
location.parentId = parent.id;
const input = parent.getInputWithBlock(block);
// AnyDuringMigration because: Argument of type 'Block | null' is not
// assignable to parameter of type 'Block'.
const input = parent.getInputWithBlock(block as AnyDuringMigration);
if (input) {
location.inputName = input.name;
}
} else {
location.coordinate = block.getRelativeToSurfaceXY();
location.coordinate = block!.getRelativeToSurfaceXY();
}
return location;
}
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
* @return False if something changed.
*/
isNull() {
override isNull(): boolean {
return this.oldParentId === this.newParentId &&
this.oldInputName === this.newInputName &&
Coordinate.equals(this.oldCoordinate, this.newCoordinate);
@@ -153,9 +152,9 @@ class BlockMove extends BlockBase {
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const block = workspace.getBlockById(this.blockId);
if (!block) {
@@ -165,7 +164,7 @@ class BlockMove extends BlockBase {
const parentId = forward ? this.newParentId : this.oldParentId;
const inputName = forward ? this.newInputName : this.oldInputName;
const coordinate = forward ? this.newCoordinate : this.oldCoordinate;
let parentBlock;
let parentBlock: Block|null;
if (parentId) {
parentBlock = workspace.getBlockById(parentId);
if (!parentBlock) {
@@ -182,19 +181,18 @@ class BlockMove extends BlockBase {
} else {
let blockConnection = block.outputConnection;
if (!blockConnection ||
(block.previousConnection &&
block.previousConnection.isConnected())) {
block.previousConnection && block.previousConnection.isConnected()) {
blockConnection = block.previousConnection;
}
let parentConnection;
const connectionType = blockConnection.type;
if (inputName) {
const input = parentBlock.getInput(inputName);
const input = parentBlock!.getInput(inputName);
if (input) {
parentConnection = input.connection;
}
} else if (connectionType === ConnectionType.PREVIOUS_STATEMENT) {
parentConnection = parentBlock.nextConnection;
parentConnection = parentBlock!.nextConnection;
}
if (parentConnection) {
blockConnection.connect(parentConnection);
@@ -206,5 +204,3 @@ class BlockMove extends BlockBase {
}
registry.register(registry.Type.EVENT, eventUtils.MOVE, BlockMove);
exports.BlockMove = BlockMove;

View File

@@ -7,65 +7,59 @@
/**
* @fileoverview Events fired as a result of bubble open.
*/
'use strict';
/**
* Events fired as a result of bubble open.
* @class
*/
goog.module('Blockly.Events.BubbleOpen');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.BubbleOpen');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import type {BlockSvg} from '../block_svg.js';
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a bubble open event.
* @extends {UiBase}
* @alias Blockly.Events.BubbleOpen
*/
class BubbleOpen extends UiBase {
export class BubbleOpen extends UiBase {
blockId: string|null;
isOpen?: boolean;
bubbleType?: string;
override type: string;
/**
* @param {BlockSvg} opt_block The associated block. Undefined for a
* blank event.
* @param {boolean=} opt_isOpen Whether the bubble is opening (false if
* closing). Undefined for a blank event.
* @param {string=} opt_bubbleType The type of bubble. One of 'mutator',
* 'comment'
* or 'warning'. Undefined for a blank event.
* @param opt_block The associated block. Undefined for a blank event.
* @param opt_isOpen Whether the bubble is opening (false if closing).
* Undefined for a blank event.
* @param opt_bubbleType The type of bubble. One of 'mutator', 'comment' or
* 'warning'. Undefined for a blank event.
*/
constructor(opt_block, opt_isOpen, opt_bubbleType) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
constructor(
opt_block: BlockSvg, opt_isOpen?: boolean, opt_bubbleType?: string) {
const workspaceId = opt_block ? opt_block.workspace!.id : undefined;
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
/**
* Whether the bubble is opening (false if closing).
* @type {boolean|undefined}
*/
/** Whether the bubble is opening (false if closing). */
this.isOpen = opt_isOpen;
/**
* The type of bubble. One of 'mutator', 'comment', or 'warning'.
* @type {string|undefined}
*/
/** The type of bubble. One of 'mutator', 'comment', or 'warning'. */
this.bubbleType = opt_bubbleType;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.BUBBLE_OPEN;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['isOpen'] = this.isOpen;
json['bubbleType'] = this.bubbleType;
@@ -75,9 +69,9 @@ class BubbleOpen extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.isOpen = json['isOpen'];
this.bubbleType = json['bubbleType'];
@@ -86,5 +80,3 @@ class BubbleOpen extends UiBase {
}
registry.register(registry.Type.EVENT, eventUtils.BUBBLE_OPEN, BubbleOpen);
exports.BubbleOpen = BubbleOpen;

View File

@@ -7,62 +7,61 @@
/**
* @fileoverview Events fired as a result of UI click in Blockly's editor.
*/
'use strict';
/**
* Events fired as a result of UI click in Blockly's editor.
* @class
*/
goog.module('Blockly.Events.Click');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Click');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import type {Block} from '../block.js';
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a click event.
* @extends {UiBase}
* @alias Blockly.Events.Click
*/
class Click extends UiBase {
export class Click extends UiBase {
blockId: AnyDuringMigration;
targetType?: string;
override type: string;
/**
* @param {?Block=} opt_block The affected block. Null for click events
* that do not have an associated block (i.e. workspace click). Undefined
* for a blank event.
* @param {?string=} opt_workspaceId The workspace identifier for this event.
* @param opt_block The affected block. Null for click events that do not have
* an associated block (i.e. workspace click). Undefined for a blank
* event.
* @param opt_workspaceId The workspace identifier for this event.
* Not used if block is passed. Undefined for a blank event.
* @param {string=} opt_targetType The type of element targeted by this click
* event. Undefined for a blank event.
* @param opt_targetType The type of element targeted by this click event.
* Undefined for a blank event.
*/
constructor(opt_block, opt_workspaceId, opt_targetType) {
let workspaceId = opt_block ? opt_block.workspace.id : opt_workspaceId;
constructor(
opt_block?: Block|null, opt_workspaceId?: string|null,
opt_targetType?: string) {
let workspaceId = opt_block ? opt_block.workspace!.id : opt_workspaceId;
if (workspaceId === null) {
workspaceId = undefined;
}
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
/**
* The type of element targeted by this click event.
* @type {string|undefined}
*/
/** The type of element targeted by this click event. */
this.targetType = opt_targetType;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.CLICK;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['targetType'] = this.targetType;
if (this.blockId) {
@@ -73,9 +72,9 @@ class Click extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.targetType = json['targetType'];
this.blockId = json['blockId'];
@@ -83,5 +82,3 @@ class Click extends UiBase {
}
registry.register(registry.Type.EVENT, eventUtils.CLICK, Click);
exports.Click = Click;

View File

@@ -7,76 +7,64 @@
/**
* @fileoverview Base class for comment events.
*/
'use strict';
/**
* Base class for comment events.
* @class
*/
goog.module('Blockly.Events.CommentBase');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentBase');
const Xml = goog.require('Blockly.Xml');
const eventUtils = goog.require('Blockly.Events.utils');
const utilsXml = goog.require('Blockly.utils.xml');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {CommentCreate} = goog.requireType('Blockly.Events.CommentCreate');
/* eslint-disable-next-line no-unused-vars */
const {CommentDelete} = goog.requireType('Blockly.Events.CommentDelete');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
import * as utilsXml from '../utils/xml.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import * as Xml from '../xml.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
import type {CommentCreate} from './events_comment_create.js';
import type {CommentDelete} from './events_comment_delete.js';
import * as eventUtils from './utils.js';
/**
* Abstract class for a comment event.
* @extends {AbstractEvent}
* @alias Blockly.Events.CommentBase
*/
class CommentBase extends AbstractEvent {
export class CommentBase extends AbstractEvent {
override isBlank: boolean;
commentId: string;
override workspaceId: string;
/**
* @param {!WorkspaceComment=} opt_comment The comment this event
* corresponds to. Undefined for a blank event.
* @param opt_comment The comment this event corresponds to. Undefined for a
* blank event.
*/
constructor(opt_comment) {
constructor(opt_comment?: WorkspaceComment) {
super();
/**
* Whether or not an event is blank.
* @type {boolean}
*/
/** Whether or not an event is blank. */
this.isBlank = typeof opt_comment === 'undefined';
/**
* The ID of the comment this event pertains to.
* @type {string}
*/
this.commentId = this.isBlank ? '' : opt_comment.id;
/** The ID of the comment this event pertains to. */
this.commentId = this.isBlank ? '' : opt_comment!.id;
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = this.isBlank ? '' : opt_comment.workspace.id;
/** The workspace identifier for this event. */
this.workspaceId = this.isBlank ? '' : opt_comment!.workspace.id;
/**
* The event group id for the group this event belongs to. Groups define
* events that should be treated as an single action from the user's
* perspective, and should be undone together.
* @type {string}
*/
this.group = eventUtils.getGroup();
/**
* Sets whether the event should be added to the undo stack.
* @type {boolean}
*/
/** Sets whether the event should be added to the undo stack. */
this.recordUndo = eventUtils.getRecordUndo();
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
if (this.commentId) {
json['commentId'] = this.commentId;
@@ -86,20 +74,20 @@ class CommentBase extends AbstractEvent {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.commentId = json['commentId'];
}
/**
* Helper function for Comment[Create|Delete]
* @param {!CommentCreate|!CommentDelete} event
* The event to run.
* @param {boolean} create if True then Create, if False then Delete
* @param event The event to run.
* @param create if True then Create, if False then Delete
*/
static CommentCreateDeleteHelper(event, create) {
static CommentCreateDeleteHelper(
event: CommentCreate|CommentDelete, create: boolean) {
const workspace = event.getEventWorkspace_();
if (create) {
const xmlElement = utilsXml.createElement('xml');
@@ -117,5 +105,3 @@ class CommentBase extends AbstractEvent {
}
}
}
exports.CommentBase = CommentBase;

View File

@@ -7,40 +7,45 @@
/**
* @fileoverview Class for comment change event.
*/
'use strict';
/**
* Class for comment change event.
* @class
*/
goog.module('Blockly.Events.CommentChange');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentChange');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import {CommentBase} from './events_comment_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a comment change event.
* @extends {CommentBase}
* @alias Blockly.Events.CommentChange
*/
class CommentChange extends CommentBase {
export class CommentChange extends CommentBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldContents_!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
newContents_!: string;
/**
* @param {!WorkspaceComment=} opt_comment The comment that is being
* changed. Undefined for a blank event.
* @param {string=} opt_oldContents Previous contents of the comment.
* @param {string=} opt_newContents New contents of the comment.
* @param opt_comment The comment that is being changed. Undefined for a
* blank event.
* @param opt_oldContents Previous contents of the comment.
* @param opt_newContents New contents of the comment.
*/
constructor(opt_comment, opt_oldContents, opt_newContents) {
constructor(
opt_comment?: WorkspaceComment, opt_oldContents?: string,
opt_newContents?: string) {
super(opt_comment);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.COMMENT_CHANGE;
if (!opt_comment) {
@@ -55,9 +60,9 @@ class CommentChange extends CommentBase {
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['oldContents'] = this.oldContents_;
json['newContents'] = this.newContents_;
@@ -66,9 +71,9 @@ class CommentChange extends CommentBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.oldContents_ = json['oldContents'];
this.newContents_ = json['newContents'];
@@ -76,17 +81,17 @@ class CommentChange extends CommentBase {
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
* @return False if something changed.
*/
isNull() {
override isNull(): boolean {
return this.oldContents_ === this.newContents_;
}
/**
* Run a change event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
@@ -101,5 +106,3 @@ class CommentChange extends CommentBase {
registry.register(
registry.Type.EVENT, eventUtils.COMMENT_CHANGE, CommentChange);
exports.CommentChange = CommentChange;

View File

@@ -7,54 +7,54 @@
/**
* @fileoverview Class for comment creation event.
*/
'use strict';
/**
* Class for comment creation event.
* @class
*/
goog.module('Blockly.Events.CommentCreate');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentCreate');
const Xml = goog.require('Blockly.Xml');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import * as Xml from '../xml.js';
import {CommentBase} from './events_comment_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a comment creation event.
* @extends {CommentBase}
* @alias Blockly.Events.CommentCreate
*/
class CommentCreate extends CommentBase {
export class CommentCreate extends CommentBase {
override type: string;
xml: AnyDuringMigration;
/**
* @param {!WorkspaceComment=} opt_comment The created comment.
* @param opt_comment The created comment.
* Undefined for a blank event.
*/
constructor(opt_comment) {
constructor(opt_comment?: WorkspaceComment) {
super(opt_comment);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.COMMENT_CREATE;
if (!opt_comment) {
return; // Blank event to be populated by fromJson.
return;
}
// Blank event to be populated by fromJson.
this.xml = opt_comment.toXmlWithXY();
}
// TODO (#1266): "Full" and "minimal" serialization.
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['xml'] = Xml.domToText(this.xml);
return json;
@@ -62,23 +62,21 @@ class CommentCreate extends CommentBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.xml = Xml.textToDom(json['xml']);
}
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
CommentBase.CommentCreateDeleteHelper(this, forward);
}
}
registry.register(
registry.Type.EVENT, eventUtils.COMMENT_CREATE, CommentCreate);
exports.CommentCreate = CommentCreate;

View File

@@ -7,38 +7,37 @@
/**
* @fileoverview Class for comment deletion event.
*/
'use strict';
/**
* Class for comment deletion event.
* @class
*/
goog.module('Blockly.Events.CommentDelete');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentDelete');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
import * as registry from '../registry.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import {CommentBase} from './events_comment_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a comment deletion event.
* @extends {CommentBase}
* @alias Blockly.Events.CommentDelete
*/
class CommentDelete extends CommentBase {
export class CommentDelete extends CommentBase {
override type: string;
xml: AnyDuringMigration;
/**
* @param {!WorkspaceComment=} opt_comment The deleted comment.
* @param opt_comment The deleted comment.
* Undefined for a blank event.
*/
constructor(opt_comment) {
constructor(opt_comment?: WorkspaceComment) {
super(opt_comment);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.COMMENT_DELETE;
if (!opt_comment) {
@@ -47,35 +46,32 @@ class CommentDelete extends CommentBase {
this.xml = opt_comment.toXmlWithXY();
}
// TODO (#1266): "Full" and "minimal" serialization.
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
return json;
}
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
}
/**
* Run a creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
CommentBase.CommentCreateDeleteHelper(this, !forward);
}
}
registry.register(
registry.Type.EVENT, eventUtils.COMMENT_DELETE, CommentDelete);
exports.CommentDelete = CommentDelete;

View File

@@ -7,39 +7,47 @@
/**
* @fileoverview Class for comment move event.
*/
'use strict';
/**
* Class for comment move event.
* @class
*/
goog.module('Blockly.Events.CommentMove');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.CommentMove');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {CommentBase} = goog.require('Blockly.Events.CommentBase');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceComment} = goog.requireType('Blockly.WorkspaceComment');
import * as registry from '../registry.js';
import {Coordinate} from '../utils/coordinate.js';
import type {WorkspaceComment} from '../workspace_comment.js';
import {CommentBase} from './events_comment_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a comment move event. Created before the move.
* @extends {CommentBase}
* @alias Blockly.Events.CommentMove
*/
class CommentMove extends CommentBase {
export class CommentMove extends CommentBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
comment_!: WorkspaceComment;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldCoordinate_!: Coordinate;
/** The location after the move, in workspace coordinates. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Coordinate'.
newCoordinate_: Coordinate = null as AnyDuringMigration;
/**
* @param {!WorkspaceComment=} opt_comment The comment that is being
* moved. Undefined for a blank event.
* @param opt_comment The comment that is being moved. Undefined for a blank
* event.
*/
constructor(opt_comment) {
constructor(opt_comment?: WorkspaceComment) {
super(opt_comment);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.COMMENT_MOVE;
if (!opt_comment) {
@@ -49,21 +57,11 @@ class CommentMove extends CommentBase {
/**
* The comment that is being moved. Will be cleared after recording the new
* location.
* @type {WorkspaceComment}
*/
this.comment_ = opt_comment;
/**
* The location before the move, in workspace coordinates.
* @type {!Coordinate}
*/
/** The location before the move, in workspace coordinates. */
this.oldCoordinate_ = opt_comment.getXY();
/**
* The location after the move, in workspace coordinates.
* @type {Coordinate}
*/
this.newCoordinate_ = null;
}
/**
@@ -77,25 +75,26 @@ class CommentMove extends CommentBase {
'same event twice.');
}
this.newCoordinate_ = this.comment_.getXY();
this.comment_ = null;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'WorkspaceComment'.
this.comment_ = null as AnyDuringMigration;
}
/**
* Override the location before the move. Use this if you don't create the
* event until the end of the move, but you know the original location.
* @param {!Coordinate} xy The location before the move,
* in workspace coordinates.
* @param xy The location before the move, in workspace coordinates.
*/
setOldCoordinate(xy) {
setOldCoordinate(xy: Coordinate) {
this.oldCoordinate_ = xy;
}
// TODO (#1266): "Full" and "minimal" serialization.
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
if (this.oldCoordinate_) {
json['oldCoordinate'] = Math.round(this.oldCoordinate_.x) + ',' +
@@ -110,9 +109,9 @@ class CommentMove extends CommentBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
if (json['oldCoordinate']) {
@@ -127,17 +126,17 @@ class CommentMove extends CommentBase {
/**
* Does this event record any change of state?
* @return {boolean} False if something changed.
* @return False if something changed.
*/
isNull() {
override isNull(): boolean {
return Coordinate.equals(this.oldCoordinate_, this.newCoordinate_);
}
/**
* Run a move event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
const comment = workspace.getCommentById(this.commentId);
if (!comment) {
@@ -153,5 +152,3 @@ class CommentMove extends CommentBase {
}
registry.register(registry.Type.EVENT, eventUtils.COMMENT_MOVE, CommentMove);
exports.CommentMove = CommentMove;

View File

@@ -7,83 +7,74 @@
/**
* @fileoverview Events fired as a result of a marker move.
*/
'use strict';
/**
* Events fired as a result of a marker move.
* @class
*/
goog.module('Blockly.Events.MarkerMove');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.MarkerMove');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {ASTNode} = goog.require('Blockly.ASTNode');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {UiBase} = goog.require('Blockly.Events.UiBase');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
import type {Block} from '../block.js';
import {ASTNode} from '../keyboard_nav/ast_node.js';
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a marker move event.
* @extends {UiBase}
* @alias Blockly.Events.MarkerMove
*/
class MarkerMove extends UiBase {
export class MarkerMove extends UiBase {
blockId: string|null;
oldNode?: ASTNode|null;
newNode?: ASTNode;
isCursor?: boolean;
override type: string;
/**
* @param {?Block=} opt_block The affected block. Null if current node
* is of type workspace. Undefined for a blank event.
* @param {boolean=} isCursor Whether this is a cursor event. Undefined for a
* blank event.
* @param {?ASTNode=} opt_oldNode The old node the marker used to be on.
* @param opt_block The affected block. Null if current node is of type
* workspace. Undefined for a blank event.
* @param isCursor Whether this is a cursor event. Undefined for a blank
* event.
* @param opt_oldNode The old node the marker used to be on.
* Undefined for a blank event.
* @param {!ASTNode=} opt_newNode The new node the marker is now on.
* @param opt_newNode The new node the marker is now on.
* Undefined for a blank event.
*/
constructor(opt_block, isCursor, opt_oldNode, opt_newNode) {
let workspaceId = opt_block ? opt_block.workspace.id : undefined;
constructor(
opt_block?: Block|null, isCursor?: boolean, opt_oldNode?: ASTNode|null,
opt_newNode?: ASTNode) {
let workspaceId = opt_block ? opt_block.workspace!.id : undefined;
if (opt_newNode && opt_newNode.getType() === ASTNode.types.WORKSPACE) {
workspaceId = (/** @type {!Workspace} */ (opt_newNode.getLocation())).id;
workspaceId = (opt_newNode.getLocation() as Workspace).id;
}
super(workspaceId);
/**
* The workspace identifier for this event.
* @type {?string}
*/
/** The workspace identifier for this event. */
this.blockId = opt_block ? opt_block.id : null;
/**
* The old node the marker used to be on.
* @type {?ASTNode|undefined}
*/
/** The old node the marker used to be on. */
this.oldNode = opt_oldNode;
/**
* The new node the marker is now on.
* @type {ASTNode|undefined}
*/
/** The new node the marker is now on. */
this.newNode = opt_newNode;
/**
* Whether this is a cursor event.
* @type {boolean|undefined}
*/
/** Whether this is a cursor event. */
this.isCursor = isCursor;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.MARKER_MOVE;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['isCursor'] = this.isCursor;
json['blockId'] = this.blockId;
@@ -94,9 +85,9 @@ class MarkerMove extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.isCursor = json['isCursor'];
this.blockId = json['blockId'];
@@ -106,5 +97,3 @@ class MarkerMove extends UiBase {
}
registry.register(registry.Type.EVENT, eventUtils.MARKER_MOVE, MarkerMove);
exports.MarkerMove = MarkerMove;

View File

@@ -7,60 +7,57 @@
/**
* @fileoverview Events fired as a result of element select action.
*/
'use strict';
/**
* Events fired as a result of element select action.
* @class
*/
goog.module('Blockly.Events.Selected');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Selected');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a selected event.
* @extends {UiBase}
* @alias Blockly.Events.Selected
*/
class Selected extends UiBase {
export class Selected extends UiBase {
oldElementId?: string|null;
newElementId?: string|null;
override type: string;
/**
* @param {?string=} opt_oldElementId The ID of the previously selected
* element. Null if no element last selected. Undefined for a blank event.
* @param {?string=} opt_newElementId The ID of the selected element. Null if
* no element currently selected (deselect). Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* @param opt_oldElementId The ID of the previously selected element. Null if
* no element last selected. Undefined for a blank event.
* @param opt_newElementId The ID of the selected element. Null if no element
* currently selected (deselect). Undefined for a blank event.
* @param opt_workspaceId The workspace identifier for this event.
* Null if no element previously selected. Undefined for a blank event.
*/
constructor(opt_oldElementId, opt_newElementId, opt_workspaceId) {
constructor(
opt_oldElementId?: string|null, opt_newElementId?: string|null,
opt_workspaceId?: string) {
super(opt_workspaceId);
/**
* The id of the last selected element.
* @type {?string|undefined}
*/
/** The id of the last selected element. */
this.oldElementId = opt_oldElementId;
/**
* The id of the selected element.
* @type {?string|undefined}
*/
/** The id of the selected element. */
this.newElementId = opt_newElementId;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.SELECTED;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['oldElementId'] = this.oldElementId;
json['newElementId'] = this.newElementId;
@@ -69,9 +66,9 @@ class Selected extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.oldElementId = json['oldElementId'];
this.newElementId = json['newElementId'];
@@ -79,5 +76,3 @@ class Selected extends UiBase {
}
registry.register(registry.Type.EVENT, eventUtils.SELECTED, Selected);
exports.Selected = Selected;

View File

@@ -7,51 +7,48 @@
/**
* @fileoverview Events fired as a result of a theme update.
*/
'use strict';
/**
* Events fired as a result of a theme update.
* @class
*/
goog.module('Blockly.Events.ThemeChange');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ThemeChange');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a theme change event.
* @extends {UiBase}
* @alias Blockly.Events.ThemeChange
*/
class ThemeChange extends UiBase {
export class ThemeChange extends UiBase {
themeName?: string;
override type: string;
/**
* @param {string=} opt_themeName The theme name. Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* @param opt_themeName The theme name. Undefined for a blank event.
* @param opt_workspaceId The workspace identifier for this event.
* event. Undefined for a blank event.
*/
constructor(opt_themeName, opt_workspaceId) {
constructor(opt_themeName?: string, opt_workspaceId?: string) {
super(opt_workspaceId);
/**
* The theme name.
* @type {string|undefined}
*/
/** The theme name. */
this.themeName = opt_themeName;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.THEME_CHANGE;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['themeName'] = this.themeName;
return json;
@@ -59,14 +56,12 @@ class ThemeChange extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.themeName = json['themeName'];
}
}
registry.register(registry.Type.EVENT, eventUtils.THEME_CHANGE, ThemeChange);
exports.ThemeChange = ThemeChange;

View File

@@ -7,60 +7,57 @@
/**
* @fileoverview Events fired as a result of selecting an item on the toolbox.
*/
'use strict';
/**
* Events fired as a result of selecting an item on the toolbox.
* @class
*/
goog.module('Blockly.Events.ToolboxItemSelect');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ToolboxItemSelect');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a toolbox item select event.
* @extends {UiBase}
* @alias Blockly.Events.ToolboxItemSelect
*/
class ToolboxItemSelect extends UiBase {
export class ToolboxItemSelect extends UiBase {
oldItem?: string|null;
newItem?: string|null;
override type: string;
/**
* @param {?string=} opt_oldItem The previously selected toolbox item.
* @param opt_oldItem The previously selected toolbox item.
* Undefined for a blank event.
* @param {?string=} opt_newItem The newly selected toolbox item. Undefined
* for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* @param opt_newItem The newly selected toolbox item. Undefined for a blank
* event.
* @param opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
*/
constructor(opt_oldItem, opt_newItem, opt_workspaceId) {
constructor(
opt_oldItem?: string|null, opt_newItem?: string|null,
opt_workspaceId?: string) {
super(opt_workspaceId);
/**
* The previously selected toolbox item.
* @type {?string|undefined}
*/
/** The previously selected toolbox item. */
this.oldItem = opt_oldItem;
/**
* The newly selected toolbox item.
* @type {?string|undefined}
*/
/** The newly selected toolbox item. */
this.newItem = opt_newItem;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.TOOLBOX_ITEM_SELECT;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['oldItem'] = this.oldItem;
json['newItem'] = this.newItem;
@@ -69,9 +66,9 @@ class ToolboxItemSelect extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.oldItem = json['oldItem'];
this.newItem = json['newItem'];
@@ -80,5 +77,3 @@ class ToolboxItemSelect extends UiBase {
registry.register(
registry.Type.EVENT, eventUtils.TOOLBOX_ITEM_SELECT, ToolboxItemSelect);
exports.ToolboxItemSelect = ToolboxItemSelect;

View File

@@ -7,52 +7,49 @@
/**
* @fileoverview Events fired as a result of trashcan flyout open and close.
*/
'use strict';
/**
* Events fired as a result of trashcan flyout open and close.
* @class
*/
goog.module('Blockly.Events.TrashcanOpen');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.TrashcanOpen');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a trashcan open event.
* @extends {UiBase}
* @alias Blockly.Events.TrashcanOpen
*/
class TrashcanOpen extends UiBase {
export class TrashcanOpen extends UiBase {
isOpen?: boolean;
override type: string;
/**
* @param {boolean=} opt_isOpen Whether the trashcan flyout is opening (false
* if opening). Undefined for a blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* @param opt_isOpen Whether the trashcan flyout is opening (false if
* opening). Undefined for a blank event.
* @param opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
*/
constructor(opt_isOpen, opt_workspaceId) {
constructor(opt_isOpen?: boolean, opt_workspaceId?: string) {
super(opt_workspaceId);
/**
* Whether the trashcan flyout is opening (false if closing).
* @type {boolean|undefined}
*/
/** Whether the trashcan flyout is opening (false if closing). */
this.isOpen = opt_isOpen;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.TRASHCAN_OPEN;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['isOpen'] = this.isOpen;
return json;
@@ -60,14 +57,12 @@ class TrashcanOpen extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.isOpen = json['isOpen'];
}
}
registry.register(registry.Type.EVENT, eventUtils.TRASHCAN_OPEN, TrashcanOpen);
exports.TrashcanOpen = TrashcanOpen;

View File

@@ -8,39 +8,45 @@
* @fileoverview (Deprecated) Events fired as a result of UI actions in
* Blockly's editor.
*/
'use strict';
/**
* (Deprecated) Events fired as a result of UI actions in
* Blockly's editor.
* @class
*/
goog.module('Blockly.Events.Ui');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.Ui');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import type {Block} from '../block.js';
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a UI event.
* @extends {UiBase}
* @deprecated December 2020. Instead use a more specific UI event.
* @alias Blockly.Events.Ui
*/
class Ui extends UiBase {
export class Ui extends UiBase {
blockId: AnyDuringMigration;
element: AnyDuringMigration;
oldValue: AnyDuringMigration;
newValue: AnyDuringMigration;
override type: string;
/**
* @param {?Block=} opt_block The affected block. Null for UI events
* that do not have an associated block. Undefined for a blank event.
* @param {string=} opt_element One of 'selected', 'comment', 'mutatorOpen',
* etc.
* @param {*=} opt_oldValue Previous value of element.
* @param {*=} opt_newValue New value of element.
* @param opt_block The affected block. Null for UI events that do not have
* an associated block. Undefined for a blank event.
* @param opt_element One of 'selected', 'comment', 'mutatorOpen', etc.
* @param opt_oldValue Previous value of element.
* @param opt_newValue New value of element.
*/
constructor(opt_block, opt_element, opt_oldValue, opt_newValue) {
const workspaceId = opt_block ? opt_block.workspace.id : undefined;
constructor(
opt_block?: Block|null, opt_element?: string,
opt_oldValue?: AnyDuringMigration, opt_newValue?: AnyDuringMigration) {
const workspaceId = opt_block ? opt_block.workspace!.id : undefined;
super(workspaceId);
this.blockId = opt_block ? opt_block.id : null;
@@ -48,18 +54,15 @@ class Ui extends UiBase {
this.oldValue = typeof opt_oldValue === 'undefined' ? '' : opt_oldValue;
this.newValue = typeof opt_newValue === 'undefined' ? '' : opt_newValue;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.UI;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['element'] = this.element;
if (this.newValue !== undefined) {
@@ -73,9 +76,9 @@ class Ui extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.element = json['element'];
this.newValue = json['newValue'];
@@ -84,5 +87,3 @@ class Ui extends UiBase {
}
registry.register(registry.Type.EVENT, eventUtils.UI, Ui);
exports.Ui = Ui;

View File

@@ -8,16 +8,16 @@
* @fileoverview Base class for events fired as a result of UI actions in
* Blockly's editor.
*/
'use strict';
/**
* Base class for events fired as a result of UI actions in
* Blockly's editor.
* @class
*/
goog.module('Blockly.Events.UiBase');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.UiBase');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
import {Abstract as AbstractEvent} from './events_abstract.js';
/**
@@ -26,38 +26,29 @@ const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
* editing to work (e.g. scrolling the workspace, zooming, opening toolbox
* categories).
* UI events do not undo or redo.
* @extends {AbstractEvent}
* @alias Blockly.Events.UiBase
*/
class UiBase extends AbstractEvent {
export class UiBase extends AbstractEvent {
override isBlank: boolean;
override workspaceId: string;
// UI events do not undo or redo.
override recordUndo = false;
/** Whether or not the event is a UI event. */
override isUiEvent = true;
/**
* @param {string=} opt_workspaceId The workspace identifier for this event.
* @param opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
*/
constructor(opt_workspaceId) {
constructor(opt_workspaceId?: string) {
super();
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {boolean}
*/
/** Whether or not the event is blank (to be populated by fromJson). */
this.isBlank = typeof opt_workspaceId === 'undefined';
/**
* The workspace identifier for this event.
* @type {string}
*/
/** The workspace identifier for this event. */
this.workspaceId = opt_workspaceId ? opt_workspaceId : '';
// UI events do not undo or redo.
this.recordUndo = false;
/**
* Whether or not the event is a UI event.
* @type {boolean}
*/
this.isUiEvent = true;
}
}
exports.UiBase = UiBase;

View File

@@ -7,51 +7,48 @@
/**
* @fileoverview Abstract class for a variable event.
*/
'use strict';
/**
* Abstract class for a variable event.
* @class
*/
goog.module('Blockly.Events.VarBase');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarBase');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {VariableModel} = goog.requireType('Blockly.VariableModel');
import type {VariableModel} from '../variable_model.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
/**
* Abstract class for a variable event.
* @extends {AbstractEvent}
* @alias Blockly.Events.VarBase
*/
class VarBase extends AbstractEvent {
export class VarBase extends AbstractEvent {
override isBlank: AnyDuringMigration;
varId: string;
override workspaceId: string;
/**
* @param {!VariableModel=} opt_variable The variable this event
* corresponds to. Undefined for a blank event.
* @param opt_variable The variable this event corresponds to. Undefined for
* a blank event.
*/
constructor(opt_variable) {
constructor(opt_variable?: VariableModel) {
super();
this.isBlank = typeof opt_variable === 'undefined';
/**
* The variable id for the variable this event pertains to.
* @type {string}
*/
this.varId = this.isBlank ? '' : opt_variable.getId();
/** The variable id for the variable this event pertains to. */
this.varId = this.isBlank ? '' : opt_variable!.getId();
/**
* The workspace identifier for this event.
* @type {string}
*/
this.workspaceId = this.isBlank ? '' : opt_variable.workspace.id;
/** The workspace identifier for this event. */
this.workspaceId = this.isBlank ? '' : opt_variable!.workspace.id;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['varId'] = this.varId;
return json;
@@ -59,12 +56,10 @@ class VarBase extends AbstractEvent {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.varId = json['varId'];
}
}
exports.VarBase = VarBase;

View File

@@ -7,53 +7,54 @@
/**
* @fileoverview Class for a variable creation event.
*/
'use strict';
/**
* Class for a variable creation event.
* @class
*/
goog.module('Blockly.Events.VarCreate');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarCreate');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {VarBase} = goog.require('Blockly.Events.VarBase');
/* eslint-disable-next-line no-unused-vars */
const {VariableModel} = goog.requireType('Blockly.VariableModel');
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase} from './events_var_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a variable creation event.
* @extends {VarBase}
* @alias Blockly.Events.VarCreate
*/
class VarCreate extends VarBase {
export class VarCreate extends VarBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
varType!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
varName!: string;
/**
* @param {!VariableModel=} opt_variable The created variable. Undefined
* for a blank event.
* @param opt_variable The created variable. Undefined for a blank event.
*/
constructor(opt_variable) {
constructor(opt_variable?: VariableModel) {
super(opt_variable);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.VAR_CREATE;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
this.varType = opt_variable.type;
this.varName = opt_variable.name;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['varType'] = this.varType;
json['varName'] = this.varName;
@@ -62,9 +63,9 @@ class VarCreate extends VarBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.varType = json['varType'];
this.varName = json['varName'];
@@ -72,9 +73,9 @@ class VarCreate extends VarBase {
/**
* Run a variable creation event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.createVariable(this.varName, this.varType, this.varId);
@@ -85,5 +86,3 @@ class VarCreate extends VarBase {
}
registry.register(registry.Type.EVENT, eventUtils.VAR_CREATE, VarCreate);
exports.VarCreate = VarCreate;

View File

@@ -7,53 +7,54 @@
/**
* @fileoverview Classes for all types of variable events.
*/
'use strict';
/**
* Classes for all types of variable events.
* @class
*/
goog.module('Blockly.Events.VarDelete');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarDelete');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {VarBase} = goog.require('Blockly.Events.VarBase');
/* eslint-disable-next-line no-unused-vars */
const {VariableModel} = goog.requireType('Blockly.VariableModel');
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase} from './events_var_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a variable deletion event.
* @extends {VarBase}
* @alias Blockly.Events.VarDelete
*/
class VarDelete extends VarBase {
export class VarDelete extends VarBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
varType!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
varName!: string;
/**
* @param {!VariableModel=} opt_variable The deleted variable. Undefined
* for a blank event.
* @param opt_variable The deleted variable. Undefined for a blank event.
*/
constructor(opt_variable) {
constructor(opt_variable?: VariableModel) {
super(opt_variable);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.VAR_DELETE;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
this.varType = opt_variable.type;
this.varName = opt_variable.name;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['varType'] = this.varType;
json['varName'] = this.varName;
@@ -62,9 +63,9 @@ class VarDelete extends VarBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.varType = json['varType'];
this.varName = json['varName'];
@@ -72,9 +73,9 @@ class VarDelete extends VarBase {
/**
* Run a variable deletion event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.deleteVariableById(this.varId);
@@ -85,5 +86,3 @@ class VarDelete extends VarBase {
}
registry.register(registry.Type.EVENT, eventUtils.VAR_DELETE, VarDelete);
exports.VarDelete = VarDelete;

View File

@@ -7,54 +7,55 @@
/**
* @fileoverview Class for a variable rename event.
*/
'use strict';
/**
* Class for a variable rename event.
* @class
*/
goog.module('Blockly.Events.VarRename');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.VarRename');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {VarBase} = goog.require('Blockly.Events.VarBase');
/* eslint-disable-next-line no-unused-vars */
const {VariableModel} = goog.requireType('Blockly.VariableModel');
import * as registry from '../registry.js';
import type {VariableModel} from '../variable_model.js';
import {VarBase} from './events_var_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a variable rename event.
* @extends {VarBase}
* @alias Blockly.Events.VarRename
*/
class VarRename extends VarBase {
export class VarRename extends VarBase {
override type: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
oldName!: string;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
newName!: string;
/**
* @param {!VariableModel=} opt_variable The renamed variable. Undefined
* for a blank event.
* @param {string=} newName The new name the variable will be changed to.
* @param opt_variable The renamed variable. Undefined for a blank event.
* @param newName The new name the variable will be changed to.
*/
constructor(opt_variable, newName) {
constructor(opt_variable?: VariableModel, newName?: string) {
super(opt_variable);
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.VAR_RENAME;
if (!opt_variable) {
return; // Blank event to be populated by fromJson.
}
this.oldName = opt_variable.name;
this.newName = typeof newName === 'undefined' ? '' : newName;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['oldName'] = this.oldName;
json['newName'] = this.newName;
@@ -63,9 +64,9 @@ class VarRename extends VarBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.oldName = json['oldName'];
this.newName = json['newName'];
@@ -73,9 +74,9 @@ class VarRename extends VarBase {
/**
* Run a variable rename event.
* @param {boolean} forward True if run forward, false if run backward (undo).
* @param forward True if run forward, false if run backward (undo).
*/
run(forward) {
override run(forward: boolean) {
const workspace = this.getEventWorkspace_();
if (forward) {
workspace.renameVariableById(this.varId, this.newName);
@@ -86,5 +87,3 @@ class VarRename extends VarBase {
}
registry.register(registry.Type.EVENT, eventUtils.VAR_RENAME, VarRename);
exports.VarRename = VarRename;

View File

@@ -7,79 +7,74 @@
/**
* @fileoverview Events fired as a result of a viewport change.
*/
'use strict';
/**
* Events fired as a result of a viewport change.
* @class
*/
goog.module('Blockly.Events.ViewportChange');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.ViewportChange');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {UiBase} = goog.require('Blockly.Events.UiBase');
import * as registry from '../registry.js';
import {UiBase} from './events_ui_base.js';
import * as eventUtils from './utils.js';
/**
* Class for a viewport change event.
* @extends {UiBase}
* @alias Blockly.Events.ViewportChange
*/
class ViewportChange extends UiBase {
export class ViewportChange extends UiBase {
viewTop?: number;
viewLeft?: number;
scale?: number;
oldScale?: number;
override type: string;
/**
* @param {number=} opt_top Top-edge of the visible portion of the workspace,
* relative to the workspace origin. Undefined for a blank event.
* @param {number=} opt_left Left-edge of the visible portion of the
* workspace relative to the workspace origin. Undefined for a blank
* event.
* @param {number=} opt_scale The scale of the workspace. Undefined for a
* blank event.
* @param {string=} opt_workspaceId The workspace identifier for this event.
* @param opt_top Top-edge of the visible portion of the workspace, relative
* to the workspace origin. Undefined for a blank event.
* @param opt_left Left-edge of the visible portion of the workspace relative
* to the workspace origin. Undefined for a blank event.
* @param opt_scale The scale of the workspace. Undefined for a blank event.
* @param opt_workspaceId The workspace identifier for this event.
* Undefined for a blank event.
* @param {number=} opt_oldScale The old scale of the workspace. Undefined for
* a blank event.
* @param opt_oldScale The old scale of the workspace. Undefined for a blank
* event.
*/
constructor(opt_top, opt_left, opt_scale, opt_workspaceId, opt_oldScale) {
constructor(
opt_top?: number, opt_left?: number, opt_scale?: number,
opt_workspaceId?: string, opt_oldScale?: number) {
super(opt_workspaceId);
/**
* Top-edge of the visible portion of the workspace, relative to the
* workspace origin.
* @type {number|undefined}
*/
this.viewTop = opt_top;
/**
* Left-edge of the visible portion of the workspace, relative to the
* workspace origin.
* @type {number|undefined}
*/
this.viewLeft = opt_left;
/**
* The scale of the workspace.
* @type {number|undefined}
*/
/** The scale of the workspace. */
this.scale = opt_scale;
/**
* The old scale of the workspace.
* @type {number|undefined}
*/
/** The old scale of the workspace. */
this.oldScale = opt_oldScale;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.VIEWPORT_CHANGE;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = super.toJson();
json['viewTop'] = this.viewTop;
json['viewLeft'] = this.viewLeft;
@@ -90,9 +85,9 @@ class ViewportChange extends UiBase {
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
super.fromJson(json);
this.viewTop = json['viewTop'];
this.viewLeft = json['viewLeft'];
@@ -103,5 +98,3 @@ class ViewportChange extends UiBase {
registry.register(
registry.Type.EVENT, eventUtils.VIEWPORT_CHANGE, ViewportChange);
exports.ViewportChange = ViewportChange;

View File

@@ -8,283 +8,213 @@
* @fileoverview Helper methods for events that are fired as a result of
* actions in Blockly's editor.
*/
'use strict';
/**
* Helper methods for events that are fired as a result of
* actions in Blockly's editor.
* @namespace Blockly.Events.utils
*/
goog.module('Blockly.Events.utils');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.utils');
const idGenerator = goog.require('Blockly.utils.idGenerator');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Abstract} = goog.requireType('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {BlockChange} = goog.requireType('Blockly.Events.BlockChange');
/* eslint-disable-next-line no-unused-vars */
const {BlockCreate} = goog.requireType('Blockly.Events.BlockCreate');
/* eslint-disable-next-line no-unused-vars */
const {BlockMove} = goog.requireType('Blockly.Events.BlockMove');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {CommentCreate} = goog.requireType('Blockly.Events.CommentCreate');
/* eslint-disable-next-line no-unused-vars */
const {CommentMove} = goog.requireType('Blockly.Events.CommentMove');
/* eslint-disable-next-line no-unused-vars */
const {ViewportChange} = goog.requireType('Blockly.Events.ViewportChange');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
import type {Block} from '../block.js';
import * as common from '../common.js';
import * as registry from '../registry.js';
import * as idGenerator from '../utils/idgenerator.js';
import type {Workspace} from '../workspace.js';
import type {WorkspaceSvg} from '../workspace_svg.js';
import type {Abstract} from './events_abstract.js';
import type {BlockChange} from './events_block_change.js';
import type {BlockCreate} from './events_block_create.js';
import type {BlockMove} from './events_block_move.js';
import type {CommentCreate} from './events_comment_create.js';
import type {CommentMove} from './events_comment_move.js';
import type {ViewportChange} from './events_viewport.js';
/**
* Group ID for new events. Grouped events are indivisible.
* @type {string}
*/
/** Group ID for new events. Grouped events are indivisible. */
let group = '';
/**
* Sets whether the next event should be added to the undo stack.
* @type {boolean}
*/
/** Sets whether the next event should be added to the undo stack. */
let recordUndo = true;
/**
* Sets whether events should be added to the undo stack.
* @param {boolean} newValue True if events should be added to the undo stack.
* @param newValue True if events should be added to the undo stack.
* @alias Blockly.Events.utils.setRecordUndo
*/
const setRecordUndo = function(newValue) {
export function setRecordUndo(newValue: boolean) {
recordUndo = newValue;
};
exports.setRecordUndo = setRecordUndo;
}
/**
* Returns whether or not events will be added to the undo stack.
* @returns {boolean} True if events will be added to the undo stack.
* @returns True if events will be added to the undo stack.
* @alias Blockly.Events.utils.getRecordUndo
*/
const getRecordUndo = function() {
export function getRecordUndo(): boolean {
return recordUndo;
};
exports.getRecordUndo = getRecordUndo;
}
/**
* Allow change events to be created and fired.
* @type {number}
*/
/** Allow change events to be created and fired. */
let disabled = 0;
/**
* Name of event that creates a block. Will be deprecated for BLOCK_CREATE.
* @const
* @alias Blockly.Events.utils.CREATE
*/
const CREATE = 'create';
exports.CREATE = CREATE;
export const CREATE = 'create';
/**
* Name of event that creates a block.
* @const
* @alias Blockly.Events.utils.BLOCK_CREATE
*/
const BLOCK_CREATE = CREATE;
exports.BLOCK_CREATE = BLOCK_CREATE;
export const BLOCK_CREATE = CREATE;
/**
* Name of event that deletes a block. Will be deprecated for BLOCK_DELETE.
* @const
* @alias Blockly.Events.utils.DELETE
*/
const DELETE = 'delete';
exports.DELETE = DELETE;
export const DELETE = 'delete';
/**
* Name of event that deletes a block.
* @const
* @alias Blockly.Events.utils.BLOCK_DELETE
*/
const BLOCK_DELETE = DELETE;
exports.BLOCK_DELETE = BLOCK_DELETE;
export const BLOCK_DELETE = DELETE;
/**
* Name of event that changes a block. Will be deprecated for BLOCK_CHANGE.
* @const
* @alias Blockly.Events.utils.CHANGE
*/
const CHANGE = 'change';
exports.CHANGE = CHANGE;
export const CHANGE = 'change';
/**
* Name of event that changes a block.
* @const
* @alias Blockly.Events.utils.BLOCK_CHANGE
*/
const BLOCK_CHANGE = CHANGE;
exports.BLOCK_CHANGE = BLOCK_CHANGE;
export const BLOCK_CHANGE = CHANGE;
/**
* Name of event that moves a block. Will be deprecated for BLOCK_MOVE.
* @const
* @alias Blockly.Events.utils.MOVE
*/
const MOVE = 'move';
exports.MOVE = MOVE;
export const MOVE = 'move';
/**
* Name of event that moves a block.
* @const
* @alias Blockly.Events.utils.BLOCK_MOVE
*/
const BLOCK_MOVE = MOVE;
exports.BLOCK_MOVE = BLOCK_MOVE;
export const BLOCK_MOVE = MOVE;
/**
* Name of event that creates a variable.
* @const
* @alias Blockly.Events.utils.VAR_CREATE
*/
const VAR_CREATE = 'var_create';
exports.VAR_CREATE = VAR_CREATE;
export const VAR_CREATE = 'var_create';
/**
* Name of event that deletes a variable.
* @const
* @alias Blockly.Events.utils.VAR_DELETE
*/
const VAR_DELETE = 'var_delete';
exports.VAR_DELETE = VAR_DELETE;
export const VAR_DELETE = 'var_delete';
/**
* Name of event that renames a variable.
* @const
* @alias Blockly.Events.utils.VAR_RENAME
*/
const VAR_RENAME = 'var_rename';
exports.VAR_RENAME = VAR_RENAME;
export const VAR_RENAME = 'var_rename';
/**
* Name of generic event that records a UI change.
* @const
* @alias Blockly.Events.utils.UI
*/
const UI = 'ui';
exports.UI = UI;
export const UI = 'ui';
/**
* Name of event that record a block drags a block.
* @const
* @alias Blockly.Events.utils.BLOCK_DRAG
*/
const BLOCK_DRAG = 'drag';
exports.BLOCK_DRAG = BLOCK_DRAG;
export const BLOCK_DRAG = 'drag';
/**
* Name of event that records a change in selected element.
* @const
* @alias Blockly.Events.utils.SELECTED
*/
const SELECTED = 'selected';
exports.SELECTED = SELECTED;
export const SELECTED = 'selected';
/**
* Name of event that records a click.
* @const
* @alias Blockly.Events.utils.CLICK
*/
const CLICK = 'click';
exports.CLICK = CLICK;
export const CLICK = 'click';
/**
* Name of event that records a marker move.
* @const
* @alias Blockly.Events.utils.MARKER_MOVE
*/
const MARKER_MOVE = 'marker_move';
exports.MARKER_MOVE = MARKER_MOVE;
export const MARKER_MOVE = 'marker_move';
/**
* Name of event that records a bubble open.
* @const
* @alias Blockly.Events.utils.BUBBLE_OPEN
*/
const BUBBLE_OPEN = 'bubble_open';
exports.BUBBLE_OPEN = BUBBLE_OPEN;
export const BUBBLE_OPEN = 'bubble_open';
/**
* Name of event that records a trashcan open.
* @const
* @alias Blockly.Events.utils.TRASHCAN_OPEN
*/
const TRASHCAN_OPEN = 'trashcan_open';
exports.TRASHCAN_OPEN = TRASHCAN_OPEN;
export const TRASHCAN_OPEN = 'trashcan_open';
/**
* Name of event that records a toolbox item select.
* @const
* @alias Blockly.Events.utils.TOOLBOX_ITEM_SELECT
*/
const TOOLBOX_ITEM_SELECT = 'toolbox_item_select';
exports.TOOLBOX_ITEM_SELECT = TOOLBOX_ITEM_SELECT;
export const TOOLBOX_ITEM_SELECT = 'toolbox_item_select';
/**
* Name of event that records a theme change.
* @const
* @alias Blockly.Events.utils.THEME_CHANGE
*/
const THEME_CHANGE = 'theme_change';
exports.THEME_CHANGE = THEME_CHANGE;
export const THEME_CHANGE = 'theme_change';
/**
* Name of event that records a viewport change.
* @const
* @alias Blockly.Events.utils.VIEWPORT_CHANGE
*/
const VIEWPORT_CHANGE = 'viewport_change';
exports.VIEWPORT_CHANGE = VIEWPORT_CHANGE;
export const VIEWPORT_CHANGE = 'viewport_change';
/**
* Name of event that creates a comment.
* @const
* @alias Blockly.Events.utils.COMMENT_CREATE
*/
const COMMENT_CREATE = 'comment_create';
exports.COMMENT_CREATE = COMMENT_CREATE;
export const COMMENT_CREATE = 'comment_create';
/**
* Name of event that deletes a comment.
* @const
* @alias Blockly.Events.utils.COMMENT_DELETE
*/
const COMMENT_DELETE = 'comment_delete';
exports.COMMENT_DELETE = COMMENT_DELETE;
export const COMMENT_DELETE = 'comment_delete';
/**
* Name of event that changes a comment.
* @const
* @alias Blockly.Events.utils.COMMENT_CHANGE
*/
const COMMENT_CHANGE = 'comment_change';
exports.COMMENT_CHANGE = COMMENT_CHANGE;
export const COMMENT_CHANGE = 'comment_change';
/**
* Name of event that moves a comment.
* @const
* @alias Blockly.Events.utils.COMMENT_MOVE
*/
const COMMENT_MOVE = 'comment_move';
exports.COMMENT_MOVE = COMMENT_MOVE;
export const COMMENT_MOVE = 'comment_move';
/**
* Name of event that records a workspace load.
* @alias Blockly.Events.utils.FINISHED_LOADING
*/
const FINISHED_LOADING = 'finished_loading';
exports.FINISHED_LOADING = FINISHED_LOADING;
export const FINISHED_LOADING = 'finished_loading';
/**
* Type of events that cause objects to be bumped back into the visible
@@ -292,12 +222,9 @@ exports.FINISHED_LOADING = FINISHED_LOADING;
*
* Not to be confused with bumping so that disconnected connections do not
* appear connected.
* @typedef {!BlockCreate|!BlockMove|
* !CommentCreate|!CommentMove}
* @alias Blockly.Events.utils.BumpEvent
*/
let BumpEvent;
exports.BumpEvent = BumpEvent;
export type BumpEvent = BlockCreate|BlockMove|CommentCreate|CommentMove;
/**
* List of events that cause objects to be bumped back into the visible
@@ -305,24 +232,27 @@ exports.BumpEvent = BumpEvent;
*
* Not to be confused with bumping so that disconnected connections do not
* appear connected.
* @const
* @alias Blockly.Events.utils.BUMP_EVENTS
*/
const BUMP_EVENTS = [BLOCK_CREATE, BLOCK_MOVE, COMMENT_CREATE, COMMENT_MOVE];
exports.BUMP_EVENTS = BUMP_EVENTS;
export const BUMP_EVENTS: string[] =
[BLOCK_CREATE, BLOCK_MOVE, COMMENT_CREATE, COMMENT_MOVE];
/**
* List of events queued for firing.
* @type {!Array<!Abstract>}
*/
const FIRE_QUEUE = [];
/** List of events queued for firing. */
const FIRE_QUEUE: Abstract[] = [];
/**
* Create a custom event and fire it.
* @param {!Abstract} event Custom data for event.
* @param event Custom data for event.
* @alias Blockly.Events.utils.fire
*/
const fire = function(event) {
export function fire(event: Abstract) {
TEST_ONLY.fireInternal(event);
}
/**
* Private version of fireInternal for stubbing in tests.
*/
function fireInternal(event: Abstract) {
if (!isEnabled()) {
return;
}
@@ -331,36 +261,34 @@ const fire = function(event) {
setTimeout(fireNow, 0);
}
FIRE_QUEUE.push(event);
};
exports.fire = fire;
}
/**
* Fire all queued events.
*/
const fireNow = function() {
/** Fire all queued events. */
function fireNow() {
const queue = filter(FIRE_QUEUE, true);
FIRE_QUEUE.length = 0;
for (let i = 0, event; (event = queue[i]); i++) {
for (let i = 0, event; event = queue[i]; i++) {
if (!event.workspaceId) {
continue;
}
const {Workspace} = goog.module.get('Blockly.Workspace');
const eventWorkspace = Workspace.getById(event.workspaceId);
const eventWorkspace = common.getWorkspaceById(event.workspaceId);
if (eventWorkspace) {
eventWorkspace.fireChangeListener(event);
}
}
};
}
/**
* Filter the queued events and merge duplicates.
* @param {!Array<!Abstract>} queueIn Array of events.
* @param {boolean} forward True if forward (redo), false if backward (undo).
* @return {!Array<!Abstract>} Array of filtered events.
* @param queueIn Array of events.
* @param forward True if forward (redo), false if backward (undo).
* @return Array of filtered events.
* @alias Blockly.Events.utils.filter
*/
const filter = function(queueIn, forward) {
let queue = queueIn.slice(); // Shallow copy of queue.
export function filter(queueIn: Abstract[], forward: boolean): Abstract[] {
let queue = queueIn.slice();
// Shallow copy of queue.
if (!forward) {
// Undo is merged in reverse order.
queue.reverse();
@@ -368,12 +296,12 @@ const filter = function(queueIn, forward) {
const mergedQueue = [];
const hash = Object.create(null);
// Merge duplicates.
for (let i = 0, event; (event = queue[i]); i++) {
for (let i = 0, event; event = queue[i]; i++) {
if (!event.isNull()) {
// Treat all UI events as the same type in hash table.
const eventType = event.isUiEvent ? UI : event.type;
// TODO(#5927): Check whether `blockId` exists before accessing it.
const blockId = /** @type {?} */ (event).blockId;
const blockId = (event as AnyDuringMigration).blockId;
const key = [eventType, blockId, event.workspaceId].join(' ');
const lastEntry = hash[key];
@@ -382,23 +310,24 @@ const filter = function(queueIn, forward) {
// Each item in the hash table has the event and the index of that event
// in the input array. This lets us make sure we only merge adjacent
// move events.
hash[key] = {event: event, index: i};
hash[key] = {event, index: i};
mergedQueue.push(event);
} else if (event.type === MOVE && lastEntry.index === i - 1) {
const moveEvent = /** @type {!BlockMove} */ (event);
const moveEvent = event as BlockMove;
// Merge move events.
lastEvent.newParentId = moveEvent.newParentId;
lastEvent.newInputName = moveEvent.newInputName;
lastEvent.newCoordinate = moveEvent.newCoordinate;
lastEntry.index = i;
} else if (
event.type === CHANGE && event.element === lastEvent.element &&
event.name === lastEvent.name) {
const changeEvent = /** @type {!BlockChange} */ (event);
event.type === CHANGE &&
(event as BlockChange).element === lastEvent.element &&
(event as BlockChange).name === lastEvent.name) {
const changeEvent = event as BlockChange;
// Merge change events.
lastEvent.newValue = changeEvent.newValue;
} else if (event.type === VIEWPORT_CHANGE) {
const viewportEvent = /** @type {!ViewportChange} */ (event);
const viewportEvent = event as ViewportChange;
// Merge viewport change events.
lastEvent.viewTop = viewportEvent.viewTop;
lastEvent.viewLeft = viewportEvent.viewLeft;
@@ -409,7 +338,7 @@ const filter = function(queueIn, forward) {
} else {
// Collision: newer events should merge into this event to maintain
// order.
hash[key] = {event: event, index: i};
hash[key] = {event, index: i};
mergedQueue.push(event);
}
}
@@ -424,107 +353,110 @@ const filter = function(queueIn, forward) {
}
// Move mutation events to the top of the queue.
// Intentionally skip first event.
for (let i = 1, event; (event = queue[i]); i++) {
if (event.type === CHANGE && event.element === 'mutation') {
for (let i = 1, event; event = queue[i]; i++) {
// AnyDuringMigration because: Property 'element' does not exist on type
// 'Abstract'.
if (event.type === CHANGE &&
(event as AnyDuringMigration).element === 'mutation') {
queue.unshift(queue.splice(i, 1)[0]);
}
}
return queue;
};
exports.filter = filter;
}
/**
* Modify pending undo events so that when they are fired they don't land
* in the undo stack. Called by Workspace.clearUndo.
* @alias Blockly.Events.utils.clearPendingUndo
*/
const clearPendingUndo = function() {
for (let i = 0, event; (event = FIRE_QUEUE[i]); i++) {
export function clearPendingUndo() {
for (let i = 0, event; event = FIRE_QUEUE[i]; i++) {
event.recordUndo = false;
}
};
exports.clearPendingUndo = clearPendingUndo;
}
/**
* Stop sending events. Every call to this function MUST also call enable.
* @alias Blockly.Events.utils.disable
*/
const disable = function() {
export function disable() {
disabled++;
};
exports.disable = disable;
}
/**
* Start sending events. Unless events were already disabled when the
* corresponding call to disable was made.
* @alias Blockly.Events.utils.enable
*/
const enable = function() {
export function enable() {
disabled--;
};
exports.enable = enable;
}
/**
* Returns whether events may be fired or not.
* @return {boolean} True if enabled.
* @return True if enabled.
* @alias Blockly.Events.utils.isEnabled
*/
const isEnabled = function() {
export function isEnabled(): boolean {
return disabled === 0;
};
exports.isEnabled = isEnabled;
}
/**
* Current group.
* @return {string} ID string.
* @return ID string.
* @alias Blockly.Events.utils.getGroup
*/
const getGroup = function() {
export function getGroup(): string {
return group;
};
exports.getGroup = getGroup;
}
/**
* Start or stop a group.
* @param {boolean|string} state True to start new group, false to end group.
* @param state True to start new group, false to end group.
* String to set group explicitly.
* @alias Blockly.Events.utils.setGroup
*/
const setGroup = function(state) {
export function setGroup(state: boolean|string) {
TEST_ONLY.setGroupInternal(state);
}
/**
* Private version of setGroup for stubbing in tests.
*/
function setGroupInternal(state: boolean|string) {
if (typeof state === 'boolean') {
group = state ? idGenerator.genUid() : '';
} else {
group = state;
}
};
exports.setGroup = setGroup;
}
/**
* Compute a list of the IDs of the specified block and all its descendants.
* @param {!Block} block The root block.
* @return {!Array<string>} List of block IDs.
* @param block The root block.
* @return List of block IDs.
* @alias Blockly.Events.utils.getDescendantIds
* @package
* @internal
*/
const getDescendantIds = function(block) {
export function getDescendantIds(block: Block): string[] {
const ids = [];
const descendants = block.getDescendants(false);
for (let i = 0, descendant; (descendant = descendants[i]); i++) {
for (let i = 0, descendant; descendant = descendants[i]; i++) {
ids[i] = descendant.id;
}
return ids;
};
exports.getDescendantIds = getDescendantIds;
}
/**
* Decode the JSON into an event.
* @param {!Object} json JSON representation.
* @param {!Workspace} workspace Target workspace for event.
* @return {!Abstract} The event represented by the JSON.
* @param json JSON representation.
* @param workspace Target workspace for event.
* @return The event represented by the JSON.
* @throws {Error} if an event type is not found in the registry.
* @alias Blockly.Events.utils.fromJson
*/
const fromJson = function(json, workspace) {
export function fromJson(
json: AnyDuringMigration, workspace: Workspace): Abstract {
const eventClass = get(json['type']);
if (!eventClass) {
throw Error('Unknown event type.');
@@ -533,39 +465,35 @@ const fromJson = function(json, workspace) {
event.fromJson(json);
event.workspaceId = workspace.id;
return event;
};
exports.fromJson = fromJson;
}
/**
* Gets the class for a specific event type from the registry.
* @param {string} eventType The type of the event to get.
* @return {?function(new:Abstract, ...?)} The event class with
* the given type or null if none exists.
* @param eventType The type of the event to get.
* @return The event class with the given type or null if none exists.
* @alias Blockly.Events.utils.get
*/
const get = function(eventType) {
export function get(eventType: string):
(new (...p1: AnyDuringMigration[]) => Abstract)|null {
return registry.getClass(registry.Type.EVENT, eventType);
};
exports.get = get;
}
/**
* Enable/disable a block depending on whether it is properly connected.
* Use this on applications where all blocks should be connected to a top block.
* Recommend setting the 'disable' option to 'false' in the config so that
* users don't try to re-enable disabled orphan blocks.
* @param {!Abstract} event Custom data for event.
* @param event Custom data for event.
* @alias Blockly.Events.utils.disableOrphans
*/
const disableOrphans = function(event) {
export function disableOrphans(event: Abstract) {
if (event.type === MOVE || event.type === CREATE) {
const blockEvent = /** @type {!BlockMove|!BlockCreate} */ (event);
const blockEvent = event as BlockMove | BlockCreate;
if (!blockEvent.workspaceId) {
return;
}
const {Workspace} = goog.module.get('Blockly.Workspace');
const eventWorkspace =
/** @type {!WorkspaceSvg} */ (
Workspace.getById(blockEvent.workspaceId));
common.getWorkspaceById(blockEvent.workspaceId) as WorkspaceSvg;
let block = eventWorkspace.getBlockById(blockEvent.blockId);
if (block) {
// Changing blocks as part of this event shouldn't be undoable.
@@ -575,7 +503,7 @@ const disableOrphans = function(event) {
const parent = block.getParent();
if (parent && parent.isEnabled()) {
const children = block.getDescendants(false);
for (let i = 0, child; (child = children[i]); i++) {
for (let i = 0, child; child = children[i]; i++) {
child.setEnabled(true);
}
} else if (
@@ -591,10 +519,11 @@ const disableOrphans = function(event) {
}
}
}
};
exports.disableOrphans = disableOrphans;
}
exports.TEST_ONLY = {
export const TEST_ONLY = {
FIRE_QUEUE,
fireNow,
fireInternal,
setGroupInternal,
};

View File

@@ -7,19 +7,19 @@
/**
* @fileoverview Class for a finished loading workspace event.
*/
'use strict';
/**
* Class for a finished loading workspace event.
* @class
*/
goog.module('Blockly.Events.FinishedLoading');
import * as goog from '../../closure/goog/goog.js';
goog.declareModuleId('Blockly.Events.FinishedLoading');
const eventUtils = goog.require('Blockly.Events.utils');
const registry = goog.require('Blockly.registry');
const {Abstract: AbstractEvent} = goog.require('Blockly.Events.Abstract');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
import * as registry from '../registry.js';
import type {Workspace} from '../workspace.js';
import {Abstract as AbstractEvent} from './events_abstract.js';
import * as eventUtils from './utils.js';
/**
@@ -27,60 +27,55 @@ const {Workspace} = goog.requireType('Blockly.Workspace');
* Used to notify the developer when the workspace has finished loading (i.e
* domToWorkspace).
* Finished loading events do not record undo or redo.
* @extends {AbstractEvent}
* @alias Blockly.Events.FinishedLoading
*/
class FinishedLoading extends AbstractEvent {
export class FinishedLoading extends AbstractEvent {
override isBlank: boolean;
override workspaceId: string;
// Workspace events do not undo or redo.
override recordUndo = false;
override type: string;
override group: AnyDuringMigration;
/**
* @param {!Workspace=} opt_workspace The workspace that has finished
* loading. Undefined for a blank event.
* @param opt_workspace The workspace that has finished loading. Undefined
* for a blank event.
*/
constructor(opt_workspace) {
constructor(opt_workspace?: Workspace) {
super();
/**
* Whether or not the event is blank (to be populated by fromJson).
* @type {boolean}
*/
/** Whether or not the event is blank (to be populated by fromJson). */
this.isBlank = typeof opt_workspace === 'undefined';
/**
* The workspace identifier for this event.
* @type {string}
*/
/** The workspace identifier for this event. */
this.workspaceId = opt_workspace ? opt_workspace.id : '';
// Workspace events do not undo or redo.
this.recordUndo = false;
/**
* Type of this event.
* @type {string}
*/
/** Type of this event. */
this.type = eventUtils.FINISHED_LOADING;
}
/**
* Encode the event as JSON.
* @return {!Object} JSON representation.
* @return JSON representation.
*/
toJson() {
override toJson(): AnyDuringMigration {
const json = {
'type': this.type,
};
if (this.group) {
json['group'] = this.group;
(json as AnyDuringMigration)['group'] = this.group;
}
if (this.workspaceId) {
json['workspaceId'] = this.workspaceId;
(json as AnyDuringMigration)['workspaceId'] = this.workspaceId;
}
return json;
}
/**
* Decode the JSON event.
* @param {!Object} json JSON representation.
* @param json JSON representation.
*/
fromJson(json) {
override fromJson(json: AnyDuringMigration) {
this.isBlank = false;
this.workspaceId = json['workspaceId'];
this.group = json['group'];
@@ -89,5 +84,3 @@ class FinishedLoading extends AbstractEvent {
registry.register(
registry.Type.EVENT, eventUtils.FINISHED_LOADING, FinishedLoading);
exports.FinishedLoading = FinishedLoading;

View File

@@ -10,7 +10,6 @@
* are applied using Block.applyExtension(), or the JSON "extensions"
* array attribute.
*/
'use strict';
/**
* Extensions are functions that help initialize blocks, usually
@@ -19,35 +18,36 @@
* array attribute.
* @namespace Blockly.Extensions
*/
goog.module('Blockly.Extensions');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Extensions');
const parsing = goog.require('Blockly.utils.parsing');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
goog.requireType('Blockly.Mutator');
// Unused import preserved for side-effects. Remove if unneeded.
// import './mutator.js';
import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
import {FieldDropdown} from './field_dropdown.js';
import {Mutator} from './mutator.js';
import * as parsing from './utils/parsing.js';
/**
* The set of all registered extensions, keyed by extension name/id.
* @private
*/
/** The set of all registered extensions, keyed by extension name/id. */
const allExtensions = Object.create(null);
exports.TEST_ONLY = {allExtensions};
export const TEST_ONLY = {allExtensions};
/**
* Registers a new extension function. Extensions are functions that help
* initialize blocks, usually adding dynamic behavior such as onchange
* handlers and mutators. These are applied using Block.applyExtension(), or
* the JSON "extensions" array attribute.
* @param {string} name The name of this extension.
* @param {Function} initFn The function to initialize an extended block.
* @param name The name of this extension.
* @param initFn The function to initialize an extended block.
* @throws {Error} if the extension name is empty, the extension is already
* registered, or extensionFn is not a function.
* @alias Blockly.Extensions.register
*/
const register = function(name, initFn) {
if ((typeof name !== 'string') || (name.trim() === '')) {
export function register(name: string, initFn: Function) {
if (typeof name !== 'string' || name.trim() === '') {
throw Error('Error: Invalid extension name "' + name + '"');
}
if (allExtensions[name]) {
@@ -57,113 +57,98 @@ const register = function(name, initFn) {
throw Error('Error: Extension "' + name + '" must be a function');
}
allExtensions[name] = initFn;
};
exports.register = register;
}
/**
* Registers a new extension function that adds all key/value of mixinObj.
* @param {string} name The name of this extension.
* @param {!Object} mixinObj The values to mix in.
* @param name The name of this extension.
* @param mixinObj The values to mix in.
* @throws {Error} if the extension name is empty or the extension is already
* registered.
* @alias Blockly.Extensions.registerMixin
*/
const registerMixin = function(name, mixinObj) {
export function registerMixin(name: string, mixinObj: AnyDuringMigration) {
if (!mixinObj || typeof mixinObj !== 'object') {
throw Error('Error: Mixin "' + name + '" must be a object');
}
register(
name,
/** @this {Block} */
function() {
this.mixin(mixinObj);
});
};
exports.registerMixin = registerMixin;
register(name, function(this: Block) {
this.mixin(mixinObj);
});
}
/**
* Registers a new extension function that adds a mutator to the block.
* At register time this performs some basic sanity checks on the mutator.
* The wrapper may also add a mutator dialog to the block, if both compose and
* decompose are defined on the mixin.
* @param {string} name The name of this mutator extension.
* @param {!Object} mixinObj The values to mix in.
* @param {(function())=} opt_helperFn An optional function to apply after
* mixing in the object.
* @param {!Array<string>=} opt_blockList A list of blocks to appear in the
* flyout of the mutator dialog.
* @param name The name of this mutator extension.
* @param mixinObj The values to mix in.
* @param opt_helperFn An optional function to apply after mixing in the object.
* @param opt_blockList A list of blocks to appear in the flyout of the mutator
* dialog.
* @throws {Error} if the mutation is invalid or can't be applied to the block.
* @alias Blockly.Extensions.registerMutator
*/
const registerMutator = function(name, mixinObj, opt_helperFn, opt_blockList) {
export function registerMutator(
name: string, mixinObj: AnyDuringMigration,
opt_helperFn?: () => AnyDuringMigration, opt_blockList?: string[]) {
const errorPrefix = 'Error when registering mutator "' + name + '": ';
checkHasMutatorProperties(errorPrefix, mixinObj);
const hasMutatorDialog = checkMutatorDialog(mixinObj, errorPrefix);
if (opt_helperFn && (typeof opt_helperFn !== 'function')) {
if (opt_helperFn && typeof opt_helperFn !== 'function') {
throw Error(errorPrefix + 'Extension "' + name + '" is not a function');
}
// Sanity checks passed.
register(
name,
/** @this {Block} */
function() {
if (hasMutatorDialog) {
const {Mutator} = goog.module.get('Blockly.Mutator');
if (!Mutator) {
throw Error(errorPrefix + 'Missing require for Blockly.Mutator');
}
this.setMutator(new Mutator(opt_blockList || []));
}
// Mixin the object.
this.mixin(mixinObj);
register(name, function(this: Block) {
if (hasMutatorDialog) {
this.setMutator(new Mutator(this as BlockSvg, opt_blockList || []));
}
// Mixin the object.
this.mixin(mixinObj);
if (opt_helperFn) {
opt_helperFn.apply(this);
}
});
};
exports.registerMutator = registerMutator;
if (opt_helperFn) {
opt_helperFn.apply(this);
}
});
}
/**
* Unregisters the extension registered with the given name.
* @param {string} name The name of the extension to unregister.
* @param name The name of the extension to unregister.
* @alias Blockly.Extensions.unregister
*/
const unregister = function(name) {
export function unregister(name: string) {
if (isRegistered(name)) {
delete allExtensions[name];
} else {
console.warn(
'No extension mapping for name "' + name + '" found to unregister');
}
};
exports.unregister = unregister;
}
/**
* Returns whether an extension is registered with the given name.
* @param {string} name The name of the extension to check for.
* @return {boolean} True if the extension is registered. False if it is
* not registered.
* @param name The name of the extension to check for.
* @return True if the extension is registered. False if it is not registered.
* @alias Blockly.Extensions.isRegistered
*/
const isRegistered = function(name) {
export function isRegistered(name: string): boolean {
return !!allExtensions[name];
};
exports.isRegistered = isRegistered;
}
/**
* Applies an extension method to a block. This should only be called during
* block construction.
* @param {string} name The name of the extension.
* @param {!Block} block The block to apply the named extension to.
* @param {boolean} isMutator True if this extension defines a mutator.
* @param name The name of the extension.
* @param block The block to apply the named extension to.
* @param isMutator True if this extension defines a mutator.
* @throws {Error} if the extension is not found.
* @alias Blockly.Extensions.apply
*/
const apply = function(name, block, isMutator) {
export function apply(name: string, block: Block, isMutator: boolean) {
const extensionFn = allExtensions[name];
if (typeof extensionFn !== 'function') {
throw Error('Error: Extension "' + name + '" not found.');
@@ -184,25 +169,23 @@ const apply = function(name, block, isMutator) {
checkHasMutatorProperties(errorPrefix, block);
} else {
if (!mutatorPropertiesMatch(
/** @type {!Array<Object>} */ (mutatorProperties), block)) {
mutatorProperties as AnyDuringMigration[], block)) {
throw Error(
'Error when applying extension "' + name + '": ' +
'mutation properties changed when applying a non-mutator extension.');
}
}
};
exports.apply = apply;
}
/**
* Check that the given block does not have any of the four mutator properties
* defined on it. This function should be called before applying a mutator
* extension to a block, to make sure we are not overwriting properties.
* @param {string} mutationName The name of the mutation to reference in error
* messages.
* @param {!Block} block The block to check.
* @param mutationName The name of the mutation to reference in error messages.
* @param block The block to check.
* @throws {Error} if any of the properties already exist on the block.
*/
const checkNoMutatorProperties = function(mutationName, block) {
function checkNoMutatorProperties(mutationName: string, block: Block) {
const properties = getMutatorProperties(block);
if (properties.length) {
throw Error(
@@ -210,66 +193,70 @@ const checkNoMutatorProperties = function(mutationName, block) {
'" to a block that already has mutator functions.' +
' Block id: ' + block.id);
}
};
}
/**
* Checks if the given object has both the 'mutationToDom' and 'domToMutation'
* functions.
* @param {?} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @param object The object to check.
* @param errorPrefix The string to prepend to any error message.
* @return True if the object has both functions. False if it has neither
* function.
* @throws {Error} if the object has only one of the functions, or either is not
* actually a function.
*/
const checkXmlHooks = function(object, errorPrefix) {
function checkXmlHooks(
object: AnyDuringMigration, errorPrefix: string): boolean {
return checkHasFunctionPair(
object.mutationToDom, object.domToMutation,
errorPrefix + ' mutationToDom/domToMutation');
};
}
/**
* Checks if the given object has both the 'saveExtraState' and 'loadExtraState'
* functions.
* @param {?} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @param object The object to check.
* @param errorPrefix The string to prepend to any error message.
* @return True if the object has both functions. False if it has neither
* function.
* @throws {Error} if the object has only one of the functions, or either is not
* actually a function.
*/
const checkJsonHooks = function(object, errorPrefix) {
function checkJsonHooks(
object: AnyDuringMigration, errorPrefix: string): boolean {
return checkHasFunctionPair(
object.saveExtraState, object.loadExtraState,
errorPrefix + ' saveExtraState/loadExtraState');
};
}
/**
* Checks if the given object has both the 'compose' and 'decompose' functions.
* @param {?} object The object to check.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} if the object has only one of the functions, or either is
* not actually a function.
* @param object The object to check.
* @param errorPrefix The string to prepend to any error message.
* @return True if the object has both functions. False if it has neither
* function.
* @throws {Error} if the object has only one of the functions, or either is not
* actually a function.
*/
const checkMutatorDialog = function(object, errorPrefix) {
function checkMutatorDialog(
object: AnyDuringMigration, errorPrefix: string): boolean {
return checkHasFunctionPair(
object.compose, object.decompose, errorPrefix + ' compose/decompose');
};
}
/**
* Checks that both or neither of the given functions exist and that they are
* indeed functions.
* @param {*} func1 The first function in the pair.
* @param {*} func2 The second function in the pair.
* @param {string} errorPrefix The string to prepend to any error message.
* @return {boolean} True if the object has both functions. False if it has
* neither function.
* @throws {Error} If the object has only one of the functions, or either is
* not actually a function.
* @param func1 The first function in the pair.
* @param func2 The second function in the pair.
* @param errorPrefix The string to prepend to any error message.
* @return True if the object has both functions. False if it has neither
* function.
* @throws {Error} If the object has only one of the functions, or either is not
* actually a function.
*/
const checkHasFunctionPair = function(func1, func2, errorPrefix) {
function checkHasFunctionPair(
func1: AnyDuringMigration, func2: AnyDuringMigration,
errorPrefix: string): boolean {
if (func1 && func2) {
if (typeof func1 !== 'function' || typeof func2 !== 'function') {
throw Error(errorPrefix + ' must be a function');
@@ -279,14 +266,15 @@ const checkHasFunctionPair = function(func1, func2, errorPrefix) {
return false;
}
throw Error(errorPrefix + 'Must have both or neither functions');
};
}
/**
* Checks that the given object required mutator properties.
* @param {string} errorPrefix The string to prepend to any error message.
* @param {!Object} object The object to inspect.
* @param errorPrefix The string to prepend to any error message.
* @param object The object to inspect.
*/
const checkHasMutatorProperties = function(errorPrefix, object) {
function checkHasMutatorProperties(
errorPrefix: string, object: AnyDuringMigration) {
const hasXmlHooks = checkXmlHooks(object, errorPrefix);
const hasJsonHooks = checkJsonHooks(object, errorPrefix);
if (!hasXmlHooks && !hasJsonHooks) {
@@ -297,15 +285,15 @@ const checkHasMutatorProperties = function(errorPrefix, object) {
// A block with a mutator isn't required to have a mutation dialog, but
// it should still have both or neither of compose and decompose.
checkMutatorDialog(object, errorPrefix);
};
}
/**
* Get a list of values of mutator properties on the given block.
* @param {!Block} block The block to inspect.
* @return {!Array<Object>} A list with all of the defined properties, which
* should be functions, but may be anything other than undefined.
* @param block The block to inspect.
* @return A list with all of the defined properties, which should be functions,
* but may be anything other than undefined.
*/
const getMutatorProperties = function(block) {
function getMutatorProperties(block: Block): AnyDuringMigration[] {
const result = [];
// List each function explicitly by reference to allow for renaming
// during compilation.
@@ -328,17 +316,18 @@ const getMutatorProperties = function(block) {
result.push(block.decompose);
}
return result;
};
}
/**
* Check that the current mutator properties match a list of old mutator
* properties. This should be called after applying a non-mutator extension,
* to verify that the extension didn't change properties it shouldn't.
* @param {!Array<Object>} oldProperties The old values to compare to.
* @param {!Block} block The block to inspect for new values.
* @return {boolean} True if the property lists match.
* @param oldProperties The old values to compare to.
* @param block The block to inspect for new values.
* @return True if the property lists match.
*/
const mutatorPropertiesMatch = function(oldProperties, block) {
function mutatorPropertiesMatch(
oldProperties: AnyDuringMigration[], block: Block): boolean {
const newProperties = getMutatorProperties(block);
if (newProperties.length !== oldProperties.length) {
return false;
@@ -349,15 +338,15 @@ const mutatorPropertiesMatch = function(oldProperties, block) {
}
}
return true;
};
}
/**
* Calls a function after the page has loaded, possibly immediately.
* @param {function()} fn Function to run.
* @param fn Function to run.
* @throws Error Will throw if no global document can be found (e.g., Node.js).
* @package
* @internal
*/
const runAfterPageLoad = function(fn) {
export function runAfterPageLoad(fn: () => AnyDuringMigration) {
if (typeof document !== 'object') {
throw Error('runAfterPageLoad() requires browser document.');
}
@@ -372,8 +361,7 @@ const runAfterPageLoad = function(fn) {
}
}, 10);
}
};
exports.runAfterPageLoad = runAfterPageLoad;
}
/**
* Builds an extension function that will map a dropdown value to a tooltip
@@ -388,16 +376,16 @@ exports.runAfterPageLoad = runAfterPageLoad;
* loading the first block of any given type, the extension will validate every
* dropdown option has a matching tooltip in the lookupTable. Errors are
* reported as warnings in the console, and are never fatal.
* @param {string} dropdownName The name of the field whose value is the key
* to the lookup table.
* @param {!Object<string, string>} lookupTable The table of field values to
* tooltip text.
* @return {!Function} The extension function.
* @param dropdownName The name of the field whose value is the key to the
* lookup table.
* @param lookupTable The table of field values to tooltip text.
* @return The extension function.
* @alias Blockly.Extensions.buildTooltipForDropdown
*/
const buildTooltipForDropdown = function(dropdownName, lookupTable) {
export function buildTooltipForDropdown(
dropdownName: string, lookupTable: {[key: string]: string}): Function {
// List of block types already validated, to minimize duplicate warnings.
const blockTypesChecked = [];
const blockTypesChecked: AnyDuringMigration[] = [];
// Check the tooltip string messages for invalid references.
// Wait for load, in case Blockly.Msg is not yet populated.
@@ -412,17 +400,14 @@ const buildTooltipForDropdown = function(dropdownName, lookupTable) {
});
}
/**
* The actual extension.
* @this {Block}
*/
const extensionFn = function() {
/** The actual extension. */
function extensionFn(this: Block) {
if (this.type && blockTypesChecked.indexOf(this.type) === -1) {
checkDropdownOptionsInTable(this, dropdownName, lookupTable);
blockTypesChecked.push(this.type);
}
this.setTooltip(function() {
this.setTooltip(function(this: Block) {
const value = String(this.getFieldValue(dropdownName));
let tooltip = lookupTable[value];
if (tooltip === null) {
@@ -431,7 +416,7 @@ const buildTooltipForDropdown = function(dropdownName, lookupTable) {
let warning = 'No tooltip mapping for value ' + value + ' of field ' +
dropdownName;
if (this.type !== null) {
warning += (' of block type ' + this.type);
warning += ' of block type ' + this.type;
}
console.warn(warning + '.');
}
@@ -440,19 +425,19 @@ const buildTooltipForDropdown = function(dropdownName, lookupTable) {
}
return tooltip;
}.bind(this));
};
}
return extensionFn;
};
exports.buildTooltipForDropdown = buildTooltipForDropdown;
}
/**
* Checks all options keys are present in the provided string lookup table.
* Emits console warnings when they are not.
* @param {!Block} block The block containing the dropdown
* @param {string} dropdownName The name of the dropdown
* @param {!Object<string, string>} lookupTable The string lookup table
* @param block The block containing the dropdown
* @param dropdownName The name of the dropdown
* @param lookupTable The string lookup table
*/
const checkDropdownOptionsInTable = function(block, dropdownName, lookupTable) {
function checkDropdownOptionsInTable(
block: Block, dropdownName: string, lookupTable: {[key: string]: string}) {
// Validate all dropdown options have values.
const dropdown = block.getField(dropdownName);
if (dropdown instanceof FieldDropdown && !dropdown.isOptionListDynamic()) {
@@ -466,19 +451,20 @@ const checkDropdownOptionsInTable = function(block, dropdownName, lookupTable) {
}
}
}
};
}
/**
* Builds an extension function that will install a dynamic tooltip. The
* tooltip message should include the string '%1' and that string will be
* replaced with the text of the named field.
* @param {string} msgTemplate The template form to of the message text, with
* %1 placeholder.
* @param {string} fieldName The field with the replacement text.
* @return {!Function} The extension function.
* @param msgTemplate The template form to of the message text, with %1
* placeholder.
* @param fieldName The field with the replacement text.
* @return The extension function.
* @alias Blockly.Extensions.buildTooltipWithFieldText
*/
const buildTooltipWithFieldText = function(msgTemplate, fieldName) {
export function buildTooltipWithFieldText(
msgTemplate: string, fieldName: string): Function {
// Check the tooltip string messages for invalid references.
// Wait for load, in case Blockly.Msg is not yet populated.
// runAfterPageLoad() does not run in a Node.js environment due to lack
@@ -490,34 +476,29 @@ const buildTooltipWithFieldText = function(msgTemplate, fieldName) {
});
}
/**
* The actual extension.
* @this {Block}
*/
const extensionFn = function() {
this.setTooltip(function() {
/** The actual extension. */
function extensionFn(this: Block) {
this.setTooltip(function(this: Block) {
const field = this.getField(fieldName);
return parsing.replaceMessageReferences(msgTemplate)
.replace('%1', field ? field.getText() : '');
}.bind(this));
};
}
return extensionFn;
};
exports.buildTooltipWithFieldText = buildTooltipWithFieldText;
}
/**
* Configures the tooltip to mimic the parent block when connected. Otherwise,
* uses the tooltip text at the time this extension is initialized. This takes
* advantage of the fact that all other values from JSON are initialized before
* extensions.
* @this {Block}
*/
const extensionParentTooltip = function() {
function extensionParentTooltip(this: Block) {
const tooltipWhenNotConnected = this.tooltip;
this.setTooltip(function() {
this.setTooltip(function(this: Block) {
const parent = this.getParent();
return (parent && parent.getInputsInline() && parent.tooltip) ||
return parent && parent.getInputsInline() && parent.tooltip ||
tooltipWhenNotConnected;
}.bind(this));
};
}
register('parent_tooltip_when_inline', extensionParentTooltip);

File diff suppressed because it is too large Load Diff

View File

@@ -7,154 +7,165 @@
/**
* @fileoverview Angle input field.
*/
'use strict';
/**
* Angle input field.
* @class
*/
goog.module('Blockly.FieldAngle');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldAngle');
const Css = goog.require('Blockly.Css');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const math = goog.require('Blockly.utils.math');
const userAgent = goog.require('Blockly.utils.userAgent');
const {BlockSvg} = goog.require('Blockly.BlockSvg');
const {Field} = goog.require('Blockly.Field');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Svg} = goog.require('Blockly.utils.Svg');
import {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as Css from './css.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInput} from './field_textinput.js';
import * as dom from './utils/dom.js';
import {KeyCodes} from './utils/keycodes.js';
import * as math from './utils/math.js';
import type {Sentinel} from './utils/sentinel.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
/**
* Class for an editable angle field.
* @extends {FieldTextInput}
* @alias Blockly.FieldAngle
*/
class FieldAngle extends FieldTextInput {
export class FieldAngle extends FieldTextInput {
/** The default value for this field. */
// protected override DEFAULT_VALUE = 0;
/**
* @param {(string|number|!Sentinel)=} opt_value The initial value of
* the field. Should cast to a number. Defaults to 0.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a
* validated number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/angle#creation}
* for a list of properties this parameter supports.
* The default amount to round angles to when using a mouse or keyboard nav
* input. Must be a positive integer to support keyboard navigation.
*/
constructor(opt_value, opt_validator, opt_config) {
static readonly ROUND = 15;
/** Half the width of protractor image. */
static readonly HALF = 100 / 2;
/**
* Default property describing which direction makes an angle field's value
* increase. Angle increases clockwise (true) or counterclockwise (false).
*/
static readonly CLOCKWISE = false;
/**
* The default offset of 0 degrees (and all angles). Always offsets in the
* counterclockwise direction, regardless of the field's clockwise property.
* Usually either 0 (0 = right) or 90 (0 = up).
*/
static readonly OFFSET = 0;
/**
* The default maximum angle to allow before wrapping.
* Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180).
*/
static readonly WRAP = 360;
/**
* Radius of protractor circle. Slightly smaller than protractor size since
* otherwise SVG crops off half the border at the edges.
*/
static readonly RADIUS: number = FieldAngle.HALF - 1;
private clockwise_: boolean;
private offset_: number;
private wrap_: number;
private round_: number;
/** The angle picker's SVG element. */
private editor_: SVGElement|null = null;
/** The angle picker's gauge path depending on the value. */
gauge_: SVGElement|null = null;
/** The angle picker's line drawn representing the value's angle. */
line_: SVGElement|null = null;
/** The degree symbol for this field. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGTSpanElement'.
protected symbol_: SVGTSpanElement = null as AnyDuringMigration;
/** Wrapper click event data. */
private clickWrapper_: browserEvents.Data|null = null;
/** Surface click event data. */
private clickSurfaceWrapper_: browserEvents.Data|null = null;
/** Surface mouse move event data. */
private moveSurfaceWrapper_: browserEvents.Data|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/**
* @param opt_value The initial value of the field. Should cast to a number.
* Defaults to 0. Also accepts Field.SKIP_SETUP if you wish to skip setup
* (only used by subclasses that want to handle configuration and setting
* the field value after their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* field's value. Takes in a number & returns a validated number, or null
* to abort the change.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/angle#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|number|Sentinel, opt_validator?: Function,
opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
/**
* Should the angle increase as the angle picker is moved clockwise (true)
* or counterclockwise (false)
* @see FieldAngle.CLOCKWISE
* @type {boolean}
* @private
*/
this.clockwise_ = FieldAngle.CLOCKWISE;
/**
* The offset of zero degrees (and all other angles).
* @see FieldAngle.OFFSET
* @type {number}
* @private
*/
this.offset_ = FieldAngle.OFFSET;
/**
* The maximum angle to allow before wrapping.
* @see FieldAngle.WRAP
* @type {number}
* @private
*/
this.wrap_ = FieldAngle.WRAP;
/**
* The amount to round angles to when using a mouse or keyboard nav input.
* @see FieldAngle.ROUND
* @type {number}
* @private
*/
this.round_ = FieldAngle.ROUND;
/**
* The angle picker's SVG element.
* @type {?SVGElement}
* @private
*/
this.editor_ = null;
/**
* The angle picker's gauge path depending on the value.
* @type {?SVGElement}
*/
this.gauge_ = null;
/**
* The angle picker's line drawn representing the value's angle.
* @type {?SVGElement}
*/
this.line_ = null;
/**
* The degree symbol for this field.
* @type {SVGTSpanElement}
* @protected
*/
this.symbol_ = null;
/**
* Wrapper click event data.
* @type {?browserEvents.Data}
* @private
*/
this.clickWrapper_ = null;
/**
* Surface click event data.
* @type {?browserEvents.Data}
* @private
*/
this.clickSurfaceWrapper_ = null;
/**
* Surface mouse move event data.
* @type {?browserEvents.Data}
* @private
*/
this.moveSurfaceWrapper_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
* @param config A map of options to configure the field based on.
*/
configure_(config) {
override configure_(config: AnyDuringMigration) {
super.configure_(config);
switch (config['mode']) {
@@ -202,40 +213,37 @@ class FieldAngle extends FieldTextInput {
/**
* Create the block UI for this field.
* @package
* @internal
*/
initView() {
override initView() {
super.initView();
// Add the degree symbol to the left of the number, even in RTL (issue
// #2380)
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {}, null);
this.symbol_ = dom.createSvgElement(Svg.TSPAN, {});
this.symbol_.appendChild(document.createTextNode('\u00B0'));
this.textElement_.appendChild(this.symbol_);
}
/**
* Updates the graph when the field rerenders.
* @protected
* @override
*/
render_() {
/** Updates the graph when the field rerenders. */
protected override render_() {
super.render_();
this.updateGraph_();
}
/**
* Create and show the angle field's editor.
* @param {Event=} opt_e Optional mouse event that triggered the field to
* open, or undefined if triggered programmatically.
* @protected
* @param opt_e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
*/
showEditor_(opt_e) {
protected override showEditor_(opt_e?: Event) {
// Mobile browsers have issues with in-line textareas (focus & keyboards).
const noFocus = userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD;
super.showEditor_(opt_e, noFocus);
this.dropdownCreate_();
dropDownDiv.getContentDiv().appendChild(this.editor_);
// AnyDuringMigration because: Argument of type 'SVGElement | null' is not
// assignable to parameter of type 'Node'.
dropDownDiv.getContentDiv().appendChild(this.editor_ as AnyDuringMigration);
if (this.sourceBlock_ instanceof BlockSvg) {
dropDownDiv.setColour(
@@ -243,27 +251,25 @@ class FieldAngle extends FieldTextInput {
this.sourceBlock_.style.colourTertiary);
}
dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
// AnyDuringMigration because: Argument of type 'this' is not assignable to
// parameter of type 'Field'.
dropDownDiv.showPositionedByField(
this as AnyDuringMigration, this.dropdownDispose_.bind(this));
this.updateGraph_();
}
/**
* Create the angle dropdown editor.
* @private
*/
dropdownCreate_() {
const svg = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'height': (FieldAngle.HALF * 2) + 'px',
'width': (FieldAngle.HALF * 2) + 'px',
'style': 'touch-action: none',
},
null);
/** Create the angle dropdown editor. */
private dropdownCreate_() {
const svg = dom.createSvgElement(Svg.SVG, {
'xmlns': dom.SVG_NS,
'xmlns:html': dom.HTML_NS,
'xmlns:xlink': dom.XLINK_NS,
'version': '1.1',
'height': FieldAngle.HALF * 2 + 'px',
'width': FieldAngle.HALF * 2 + 'px',
'style': 'touch-action: none',
});
const circle = dom.createSvgElement(
Svg.CIRCLE, {
'cx': FieldAngle.HALF,
@@ -312,11 +318,8 @@ class FieldAngle extends FieldTextInput {
this.editor_ = svg;
}
/**
* Disposes of events and DOM-references belonging to the angle editor.
* @private
*/
dropdownDispose_() {
/** Disposes of events and DOM-references belonging to the angle editor. */
private dropdownDispose_() {
if (this.clickWrapper_) {
browserEvents.unbind(this.clickWrapper_);
this.clickWrapper_ = null;
@@ -333,25 +336,25 @@ class FieldAngle extends FieldTextInput {
this.line_ = null;
}
/**
* Hide the editor.
* @private
*/
hide_() {
/** Hide the editor. */
private hide_() {
dropDownDiv.hideIfOwner(this);
WidgetDiv.hide();
}
/**
* Set the angle to match the mouse's position.
* @param {!Event} e Mouse move event.
* @protected
* @param e Mouse move event.
*/
onMouseMove_(e) {
protected onMouseMove_(e: Event) {
// Calculate angle.
const bBox = this.gauge_.ownerSVGElement.getBoundingClientRect();
const dx = e.clientX - bBox.left - FieldAngle.HALF;
const dy = e.clientY - bBox.top - FieldAngle.HALF;
const bBox = this.gauge_!.ownerSVGElement!.getBoundingClientRect();
// AnyDuringMigration because: Property 'clientX' does not exist on type
// 'Event'.
const dx = (e as AnyDuringMigration).clientX - bBox.left - FieldAngle.HALF;
// AnyDuringMigration because: Property 'clientY' does not exist on type
// 'Event'.
const dy = (e as AnyDuringMigration).clientY - bBox.top - FieldAngle.HALF;
let angle = Math.atan(-dy / dx);
if (isNaN(angle)) {
// This shouldn't happen, but let's not let this error propagate further.
@@ -379,10 +382,9 @@ class FieldAngle extends FieldTextInput {
* Handles and displays values that are input via mouse or arrow key input.
* These values need to be rounded and wrapped before being displayed so
* that the text input's value is appropriate.
* @param {number} angle New angle.
* @private
* @param angle New angle.
*/
displayMouseOrKeyboardValue_(angle) {
private displayMouseOrKeyboardValue_(angle: number) {
if (this.round_) {
angle = Math.round(angle / this.round_) * this.round_;
}
@@ -392,11 +394,8 @@ class FieldAngle extends FieldTextInput {
}
}
/**
* Redraw the graph with the current angle.
* @private
*/
updateGraph_() {
/** Redraw the graph with the current angle. */
private updateGraph_() {
if (!this.gauge_) {
return;
}
@@ -428,36 +427,46 @@ class FieldAngle extends FieldTextInput {
' 0 ', largeFlag, ' ', clockwiseFlag, ' ', x2, ',', y2, ' z');
}
this.gauge_.setAttribute('d', path.join(''));
this.line_.setAttribute('x2', x2);
this.line_.setAttribute('y2', y2);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.line_!.setAttribute('x2', x2 as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.line_!.setAttribute('y2', y2 as AnyDuringMigration);
}
/**
* Handle key down to the editor.
* @param {!Event} e Keyboard event.
* @protected
* @override
* @param e Keyboard event.
*/
onHtmlInputKeyDown_(e) {
protected override onHtmlInputKeyDown_(e: Event) {
super.onHtmlInputKeyDown_(e);
let multiplier;
if (e.keyCode === KeyCodes.LEFT) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).keyCode === KeyCodes.LEFT) {
// decrement (increment in RTL)
multiplier = this.sourceBlock_.RTL ? 1 : -1;
} else if (e.keyCode === KeyCodes.RIGHT) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.RIGHT) {
// increment (decrement in RTL)
multiplier = this.sourceBlock_.RTL ? -1 : 1;
} else if (e.keyCode === KeyCodes.DOWN) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.DOWN) {
// decrement
multiplier = -1;
} else if (e.keyCode === KeyCodes.UP) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.UP) {
// increment
multiplier = 1;
}
if (multiplier) {
const value = /** @type {number} */ (this.getValue());
this.displayMouseOrKeyboardValue_(value + (multiplier * this.round_));
const value = this.getValue() as number;
this.displayMouseOrKeyboardValue_(value + multiplier * this.round_);
e.preventDefault();
e.stopPropagation();
}
@@ -465,12 +474,11 @@ class FieldAngle extends FieldTextInput {
/**
* Ensure that the input value is a valid angle.
* @param {*=} opt_newValue The input value.
* @return {?number} A valid angle, or null if invalid.
* @protected
* @override
* @param opt_newValue The input value.
* @return A valid angle, or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
number|null {
const value = Number(opt_newValue);
if (isNaN(value) || !isFinite(value)) {
return null;
@@ -480,11 +488,10 @@ class FieldAngle extends FieldTextInput {
/**
* Wraps the value so that it is in the range (-360 + wrap, wrap).
* @param {number} value The value to wrap.
* @return {number} The wrapped value.
* @private
* @param value The value to wrap.
* @return The wrapped value.
*/
wrapValue_(value) {
private wrapValue_(value: number): number {
value %= 360;
if (value < 0) {
value += 360;
@@ -497,71 +504,19 @@ class FieldAngle extends FieldTextInput {
/**
* Construct a FieldAngle from a JSON arg object.
* @param {!Object} options A JSON object with options (angle).
* @return {!FieldAngle} The new field instance.
* @package
* @param options A JSON object with options (angle).
* @return The new field instance.
* @nocollapse
* @override
* @internal
*/
static fromJson(options) {
static override fromJson(options: AnyDuringMigration): FieldAngle {
// `this` might be a subclass of FieldAngle if that class doesn't override
// the static fromJson method.
return new this(options['angle'], undefined, options);
}
}
/**
* The default value for this field.
* @type {*}
* @protected
*/
FieldAngle.prototype.DEFAULT_VALUE = 0;
/**
* The default amount to round angles to when using a mouse or keyboard nav
* input. Must be a positive integer to support keyboard navigation.
* @const {number}
*/
FieldAngle.ROUND = 15;
/**
* Half the width of protractor image.
* @const {number}
*/
FieldAngle.HALF = 100 / 2;
/**
* Default property describing which direction makes an angle field's value
* increase. Angle increases clockwise (true) or counterclockwise (false).
* @const {boolean}
*/
FieldAngle.CLOCKWISE = false;
/**
* The default offset of 0 degrees (and all angles). Always offsets in the
* counterclockwise direction, regardless of the field's clockwise property.
* Usually either 0 (0 = right) or 90 (0 = up).
* @const {number}
*/
FieldAngle.OFFSET = 0;
/**
* The default maximum angle to allow before wrapping.
* Usually either 360 (for 0 to 359.9) or 180 (for -179.9 to 180).
* @const {number}
*/
FieldAngle.WRAP = 360;
/**
* Radius of protractor circle. Slightly smaller than protractor size since
* otherwise SVG crops off half the border at the edges.
* @const {number}
*/
FieldAngle.RADIUS = FieldAngle.HALF - 1;
/**
* CSS for angle field. See css.js for use.
*/
/** CSS for angle field. See css.js for use. */
Css.register(`
.blocklyAngleCircle {
stroke: #444;
@@ -591,4 +546,4 @@ Css.register(`
fieldRegistry.register('field_angle', FieldAngle);
exports.FieldAngle = FieldAngle;
(FieldAngle.prototype as AnyDuringMigration).DEFAULT_VALUE = 0;

View File

@@ -7,82 +7,86 @@
/**
* @fileoverview Checkbox field. Checked or not checked.
*/
'use strict';
/**
* Checkbox field. Checked or not checked.
* @class
*/
goog.module('Blockly.FieldCheckbox');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldCheckbox');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockChange');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as dom from './utils/dom.js';
import type {Sentinel} from './utils/sentinel.js';
/**
* Class for a checkbox field.
* @extends {Field}
* @alias Blockly.FieldCheckbox
*/
class FieldCheckbox extends Field {
export class FieldCheckbox extends Field {
/** Default character for the checkmark. */
static readonly CHECK_CHAR = '\u2713';
private checkChar_: string;
/**
* @param {(string|boolean|!Sentinel)=} opt_value The initial value of
* the field. Should either be 'TRUE', 'FALSE' or a boolean. Defaults to
* 'FALSE'.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a value ('TRUE' or 'FALSE') &
* returns a validated value ('TRUE' or 'FALSE'), or null to abort the
* change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/checkbox#creation}
* for a list of properties this parameter supports.
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
constructor(opt_value, opt_validator, opt_config) {
override SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates editability.
*/
override CURSOR = 'default';
override value_: AnyDuringMigration;
/**
* @param opt_value The initial value of the field. Should either be 'TRUE',
* 'FALSE' or a boolean. Defaults to 'FALSE'. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* field's value. Takes in a value ('TRUE' or 'FALSE') & returns a
* validated value ('TRUE' or 'FALSE'), or null to abort the change.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/checkbox#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|boolean|Sentinel, opt_validator?: Function,
opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
/**
* Character for the check mark. Used to apply a different check mark
* character to individual fields.
* @type {string}
* @private
*/
this.checkChar_ = FieldCheckbox.CHECK_CHAR;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates editability.
* @type {string}
*/
this.CURSOR = 'default';
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
* @param config A map of options to configure the field based on.
*/
configure_(config) {
protected override configure_(config: AnyDuringMigration) {
super.configure_(config);
if (config['checkCharacter']) {
this.checkChar_ = config['checkCharacter'];
@@ -91,11 +95,10 @@ class FieldCheckbox extends Field {
/**
* Saves this field's value.
* @return {*} The boolean value held by this field.
* @override
* @package
* @return The boolean value held by this field.
* @internal
*/
saveState() {
override saveState(): AnyDuringMigration {
const legacyState = this.saveLegacyState(FieldCheckbox);
if (legacyState !== null) {
return legacyState;
@@ -105,58 +108,48 @@ class FieldCheckbox extends Field {
/**
* Create the block UI for this checkbox.
* @package
* @internal
*/
initView() {
override initView() {
super.initView();
dom.addClass(
/** @type {!SVGTextElement} **/ (this.textElement_), 'blocklyCheckbox');
dom.addClass((this.textElement_), 'blocklyCheckbox');
this.textElement_.style.display = this.value_ ? 'block' : 'none';
}
/**
* @override
*/
render_() {
override render_() {
if (this.textContent_) {
this.textContent_.nodeValue = this.getDisplayText_();
}
this.updateSize_(this.getConstants().FIELD_CHECKBOX_X_OFFSET);
this.updateSize_(this.getConstants()!.FIELD_CHECKBOX_X_OFFSET);
}
/**
* @override
*/
getDisplayText_() {
override getDisplayText_() {
return this.checkChar_;
}
/**
* Set the character used for the check mark.
* @param {?string} character The character to use for the check mark, or
* null to use the default.
* @param character The character to use for the check mark, or null to use
* the default.
*/
setCheckCharacter(character) {
setCheckCharacter(character: string|null) {
this.checkChar_ = character || FieldCheckbox.CHECK_CHAR;
this.forceRerender();
}
/**
* Toggle the state of the checkbox on click.
* @protected
*/
showEditor_() {
/** Toggle the state of the checkbox on click. */
protected override showEditor_() {
this.setValue(!this.value_);
}
/**
* Ensure that the input value is valid ('TRUE' or 'FALSE').
* @param {*=} opt_newValue The input value.
* @return {?string} A valid value ('TRUE' or 'FALSE), or null if invalid.
* @protected
* @param opt_newValue The input value.
* @return A valid value ('TRUE' or 'FALSE), or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (opt_newValue === true || opt_newValue === 'TRUE') {
return 'TRUE';
}
@@ -168,11 +161,10 @@ class FieldCheckbox extends Field {
/**
* Update the value of the field, and update the checkElement.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a either 'TRUE' or 'FALSE'.
* @protected
* @param newValue The value to be saved. The default validator guarantees
* that this is a either 'TRUE' or 'FALSE'.
*/
doValueUpdate_(newValue) {
protected override doValueUpdate_(newValue: AnyDuringMigration) {
this.value_ = this.convertValueToBool_(newValue);
// Update visual.
if (this.textElement_) {
@@ -182,26 +174,25 @@ class FieldCheckbox extends Field {
/**
* Get the value of this field, either 'TRUE' or 'FALSE'.
* @return {string} The value of this field.
* @return The value of this field.
*/
getValue() {
override getValue(): string {
return this.value_ ? 'TRUE' : 'FALSE';
}
/**
* Get the boolean value of this field.
* @return {boolean} The boolean value of this field.
* @return The boolean value of this field.
*/
getValueBoolean() {
return /** @type {boolean} */ (this.value_);
getValueBoolean(): boolean {
return this.value_ as boolean;
}
/**
* Get the text of this field. Used when the block is collapsed.
* @return {string} Text representing the value of this field
* ('true' or 'false').
* @return Text representing the value of this field ('true' or 'false').
*/
getText() {
override getText(): string {
return String(this.convertValueToBool_(this.value_));
}
@@ -210,11 +201,10 @@ class FieldCheckbox extends Field {
*
* Converts 'TRUE' to true and 'FALSE' to false correctly, everything else
* is cast to a boolean.
* @param {*} value The value to convert.
* @return {boolean} The converted value.
* @private
* @param value The value to convert.
* @return The converted value.
*/
convertValueToBool_(value) {
private convertValueToBool_(value: AnyDuringMigration): boolean {
if (typeof value === 'string') {
return value === 'TRUE';
} else {
@@ -224,32 +214,18 @@ class FieldCheckbox extends Field {
/**
* Construct a FieldCheckbox from a JSON arg object.
* @param {!Object} options A JSON object with options (checked).
* @return {!FieldCheckbox} The new field instance.
* @package
* @param options A JSON object with options (checked).
* @return The new field instance.
* @nocollapse
* @internal
*/
static fromJson(options) {
static fromJson(options: AnyDuringMigration): FieldCheckbox {
// `this` might be a subclass of FieldCheckbox if that class doesn't
// 'override' the static fromJson method.
return new this(options['checked'], undefined, options);
}
}
/**
* The default value for this field.
* @type {*}
* @protected
*/
FieldCheckbox.prototype.DEFAULT_VALUE = false;
/**
* Default character for the checkmark.
* @type {string}
* @const
*/
FieldCheckbox.CHECK_CHAR = '\u2713';
fieldRegistry.register('field_checkbox', FieldCheckbox);
exports.FieldCheckbox = FieldCheckbox;
(FieldCheckbox.prototype as AnyDuringMigration).DEFAULT_VALUE = false;

View File

@@ -7,164 +7,174 @@
/**
* @fileoverview Colour input field.
*/
'use strict';
/**
* Colour input field.
* @class
*/
goog.module('Blockly.FieldColour');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldColour');
const Css = goog.require('Blockly.Css');
const aria = goog.require('Blockly.utils.aria');
const browserEvents = goog.require('Blockly.browserEvents');
const colour = goog.require('Blockly.utils.colour');
const dom = goog.require('Blockly.utils.dom');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const idGenerator = goog.require('Blockly.utils.idGenerator');
const {BlockSvg} = goog.require('Blockly.BlockSvg');
const {Field} = goog.require('Blockly.Field');
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Size} = goog.require('Blockly.utils.Size');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockChange');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
import {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as Css from './css.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as aria from './utils/aria.js';
import * as colour from './utils/colour.js';
import * as dom from './utils/dom.js';
import * as idGenerator from './utils/idgenerator.js';
import {KeyCodes} from './utils/keycodes.js';
import type {Sentinel} from './utils/sentinel.js';
import {Size} from './utils/size.js';
/**
* Class for a colour input field.
* @extends {Field}
* @alias Blockly.FieldColour
*/
class FieldColour extends Field {
export class FieldColour extends Field {
/**
* @param {(string|!Sentinel)=} opt_value The initial value of the
* field. Should be in '#rrggbb' format. Defaults to the first value in
* the default colour array.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* An array of colour strings for the palette.
* Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
* All colour pickers use this unless overridden with setColours.
*/
static COLOURS: string[] = [
// grays
'#ffffff', '#cccccc', '#c0c0c0', '#999999',
'#666666', '#333333', '#000000', // reds
'#ffcccc', '#ff6666', '#ff0000', '#cc0000',
'#990000', '#660000', '#330000', // oranges
'#ffcc99', '#ff9966', '#ff9900', '#ff6600',
'#cc6600', '#993300', '#663300', // yellows
'#ffff99', '#ffff66', '#ffcc66', '#ffcc33',
'#cc9933', '#996633', '#663333', // olives
'#ffffcc', '#ffff33', '#ffff00', '#ffcc00',
'#999900', '#666600', '#333300', // greens
'#99ff99', '#66ff99', '#33ff33', '#33cc00',
'#009900', '#006600', '#003300', // turquoises
'#99ffff', '#33ffff', '#66cccc', '#00cccc',
'#339999', '#336666', '#003333', // blues
'#ccffff', '#66ffff', '#33ccff', '#3366ff',
'#3333ff', '#000099', '#000066', // purples
'#ccccff', '#9999ff', '#6666cc', '#6633ff',
'#6600cc', '#333399', '#330099', // violets
'#ffccff', '#ff99ff', '#cc66cc', '#cc33cc',
'#993399', '#663366', '#330033',
];
/**
* An array of tooltip strings for the palette. If not the same length as
* COLOURS, the colour's hex code will be used for any missing titles.
* All colour pickers use this unless overridden with setColours.
*/
static TITLES: string[] = [];
/**
* Number of columns in the palette.
* All colour pickers use this unless overridden with setColumns.
*/
static COLUMNS = 7;
/** The field's colour picker element. */
private picker_: Element|null = null;
/** Index of the currently highlighted element. */
private highlightedIndex_: number|null = null;
/** Mouse click event data. */
private onClickWrapper_: browserEvents.Data|null = null;
/** Mouse move event data. */
private onMouseMoveWrapper_: browserEvents.Data|null = null;
/** Mouse enter event data. */
private onMouseEnterWrapper_: browserEvents.Data|null = null;
/** Mouse leave event data. */
private onMouseLeaveWrapper_: browserEvents.Data|null = null;
/** Key down event data. */
private onKeyDownWrapper_: browserEvents.Data|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/** Mouse cursor style when over the hotspot that initiates the editor. */
override CURSOR = 'default';
/**
* Used to tell if the field needs to be rendered the next time the block is
* rendered. Colour fields are statically sized, and only need to be
* rendered at initialization.
*/
protected override isDirty_ = false;
/** Array of colours used by this field. If null, use the global list. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'string[]'.
private colours_: string[] = null as AnyDuringMigration;
/**
* Array of colour tooltips used by this field. If null, use the global
* list.
*/
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'string[]'.
private titles_: string[] = null as AnyDuringMigration;
/**
* Number of colour columns used by this field. If 0, use the global
* setting. By default use the global constants for columns.
*/
private columns_ = 0;
override size_: AnyDuringMigration;
override clickTarget_: AnyDuringMigration;
override value_: AnyDuringMigration;
/**
* @param opt_value The initial value of the field. Should be in '#rrggbb'
* format. Defaults to the first value in the default colour array. Also
* accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a colour string & returns a
* validated colour string ('#rrggbb' format), or null to abort the
* change.Blockly.
* @param {Object=} opt_config A map of options used to configure the field.
* @param opt_validator A function that is called to validate changes to the
* field's value. Takes in a colour string & returns a validated colour
* string ('#rrggbb' format), or null to abort the change.Blockly.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/colour}
* for a list of properties this parameter supports.
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/colour}
* for a list of properties this parameter supports.
*/
constructor(opt_value, opt_validator, opt_config) {
constructor(
opt_value?: string|Sentinel, opt_validator?: Function,
opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
/**
* The field's colour picker element.
* @type {?Element}
* @private
*/
this.picker_ = null;
/**
* Index of the currently highlighted element.
* @type {?number}
* @private
*/
this.highlightedIndex_ = null;
/**
* Mouse click event data.
* @type {?browserEvents.Data}
* @private
*/
this.onClickWrapper_ = null;
/**
* Mouse move event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseMoveWrapper_ = null;
/**
* Mouse enter event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseEnterWrapper_ = null;
/**
* Mouse leave event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseLeaveWrapper_ = null;
/**
* Key down event data.
* @type {?browserEvents.Data}
* @private
*/
this.onKeyDownWrapper_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates the editor.
* @type {string}
*/
this.CURSOR = 'default';
/**
* Used to tell if the field needs to be rendered the next time the block is
* rendered. Colour fields are statically sized, and only need to be
* rendered at initialization.
* @type {boolean}
* @protected
*/
this.isDirty_ = false;
/**
* Array of colours used by this field. If null, use the global list.
* @type {Array<string>}
* @private
*/
this.colours_ = null;
/**
* Array of colour tooltips used by this field. If null, use the global
* list.
* @type {Array<string>}
* @private
*/
this.titles_ = null;
/**
* Number of colour columns used by this field. If 0, use the global
* setting. By default use the global constants for columns.
* @type {number}
* @private
*/
this.columns_ = 0;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
* @param config A map of options to configure the field based on.
*/
configure_(config) {
protected override configure_(config: AnyDuringMigration) {
super.configure_(config);
if (config['colourOptions']) {
this.colours_ = config['colourOptions'];
@@ -177,13 +187,13 @@ class FieldColour extends Field {
/**
* Create the block UI for this colour field.
* @package
* @internal
*/
initView() {
override initView() {
this.size_ = new Size(
this.getConstants().FIELD_COLOUR_DEFAULT_WIDTH,
this.getConstants().FIELD_COLOUR_DEFAULT_HEIGHT);
if (!this.getConstants().FIELD_COLOUR_FULL_BLOCK) {
this.getConstants()!.FIELD_COLOUR_DEFAULT_WIDTH,
this.getConstants()!.FIELD_COLOUR_DEFAULT_HEIGHT);
if (!this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) {
this.createBorderRect_();
this.borderRect_.style['fillOpacity'] = '1';
} else if (this.sourceBlock_ instanceof BlockSvg) {
@@ -191,28 +201,25 @@ class FieldColour extends Field {
}
}
/**
* @override
*/
applyColour() {
if (!this.getConstants().FIELD_COLOUR_FULL_BLOCK) {
override applyColour() {
if (!this.getConstants()!.FIELD_COLOUR_FULL_BLOCK) {
if (this.borderRect_) {
this.borderRect_.style.fill = /** @type {string} */ (this.getValue());
this.borderRect_.style.fill = this.getValue() as string;
}
} else if (this.sourceBlock_ instanceof BlockSvg) {
this.sourceBlock_.pathObject.svgPath.setAttribute(
'fill', /** @type {string} */ (this.getValue()));
'fill', this.getValue() as string);
this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff');
}
}
/**
* Ensure that the input value is a valid colour.
* @param {*=} opt_newValue The input value.
* @return {?string} A valid colour, or null if invalid.
* @protected
* @param opt_newValue The input value.
* @return A valid colour, or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (typeof opt_newValue !== 'string') {
return null;
}
@@ -221,29 +228,28 @@ class FieldColour extends Field {
/**
* Update the value of this colour field, and update the displayed colour.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a colour in '#rrggbb' format.
* @protected
* @param newValue The value to be saved. The default validator guarantees
* that this is a colour in '#rrggbb' format.
*/
doValueUpdate_(newValue) {
protected override doValueUpdate_(newValue: AnyDuringMigration) {
this.value_ = newValue;
if (this.borderRect_) {
this.borderRect_.style.fill = /** @type {string} */ (newValue);
this.borderRect_.style.fill = newValue as string;
} else if (
this.sourceBlock_ && this.sourceBlock_.rendered &&
this.sourceBlock_ instanceof BlockSvg) {
this.sourceBlock_.pathObject.svgPath.setAttribute(
'fill', /** @type {string} */ (newValue));
'fill', newValue as string);
this.sourceBlock_.pathObject.svgPath.setAttribute('stroke', '#fff');
}
}
/**
* Get the text for this field. Used when the block is collapsed.
* @return {string} Text representing the value of this field.
* @return Text representing the value of this field.
*/
getText() {
let colour = /** @type {string} */ (this.value_);
override getText(): string {
let colour = this.value_ as string;
// Try to use #rgb format if possible, rather than #rrggbb.
if (/^#(.)\1(.)\2(.)\3$/.test(colour)) {
colour = '#' + colour[1] + colour[3] + colour[5];
@@ -253,13 +259,13 @@ class FieldColour extends Field {
/**
* Set a custom colour grid for this field.
* @param {Array<string>} colours Array of colours for this block,
* or null to use default (FieldColour.COLOURS).
* @param {Array<string>=} opt_titles Optional array of colour tooltips,
* or null to use default (FieldColour.TITLES).
* @return {!FieldColour} Returns itself (for method chaining).
* @param colours Array of colours for this block, or null to use default
* (FieldColour.COLOURS).
* @param opt_titles Optional array of colour tooltips, or null to use default
* (FieldColour.TITLES).
* @return Returns itself (for method chaining).
*/
setColours(colours, opt_titles) {
setColours(colours: string[], opt_titles?: string[]): FieldColour {
this.colours_ = colours;
if (opt_titles) {
this.titles_ = opt_titles;
@@ -269,36 +275,36 @@ class FieldColour extends Field {
/**
* Set a custom grid size for this field.
* @param {number} columns Number of columns for this block,
* or 0 to use default (FieldColour.COLUMNS).
* @return {!FieldColour} Returns itself (for method chaining).
* @param columns Number of columns for this block, or 0 to use default
* (FieldColour.COLUMNS).
* @return Returns itself (for method chaining).
*/
setColumns(columns) {
setColumns(columns: number): FieldColour {
this.columns_ = columns;
return this;
}
/**
* Create and show the colour field's editor.
* @protected
*/
showEditor_() {
/** Create and show the colour field's editor. */
protected override showEditor_() {
this.dropdownCreate_();
dropDownDiv.getContentDiv().appendChild(this.picker_);
// AnyDuringMigration because: Argument of type 'Element | null' is not
// assignable to parameter of type 'Node'.
dropDownDiv.getContentDiv().appendChild(this.picker_ as AnyDuringMigration);
dropDownDiv.showPositionedByField(this, this.dropdownDispose_.bind(this));
// Focus so we can start receiving keyboard events.
this.picker_.focus({preventScroll: true});
// AnyDuringMigration because: Property 'focus' does not exist on type
// 'Element'.
(this.picker_ as AnyDuringMigration)!.focus({preventScroll: true});
}
/**
* Handle a click on a colour cell.
* @param {!MouseEvent} e Mouse event.
* @private
* @param e Mouse event.
*/
onClick_(e) {
const cell = /** @type {!Element} */ (e.target);
private onClick_(e: MouseEvent) {
const cell = e.target as Element;
const colour = cell && cell.getAttribute('data-colour');
if (colour !== null) {
this.setValue(colour);
@@ -309,10 +315,9 @@ class FieldColour extends Field {
/**
* Handle a key down event. Navigate around the grid with the
* arrow keys. Enter selects the highlighted colour.
* @param {!KeyboardEvent} e Keyboard event.
* @private
* @param e Keyboard event.
*/
onKeyDown_(e) {
private onKeyDown_(e: KeyboardEvent) {
let handled = false;
if (e.keyCode === KeyCodes.UP) {
this.moveHighlightBy_(0, -1);
@@ -345,11 +350,10 @@ class FieldColour extends Field {
/**
* Move the currently highlighted position by dx and dy.
* @param {number} dx Change of x
* @param {number} dy Change of y
* @private
* @param dx Change of x
* @param dy Change of y
*/
moveHighlightBy_(dx, dy) {
private moveHighlightBy_(dx: number, dy: number) {
if (!this.highlightedIndex_) {
return;
}
@@ -396,40 +400,38 @@ class FieldColour extends Field {
}
// Move the highlight to the new coordinates.
const cell =
/** @type {!Element} */ (this.picker_.childNodes[y].childNodes[x]);
const index = (y * columns) + x;
const cell = this.picker_!.childNodes[y].childNodes[x] as Element;
const index = y * columns + x;
this.setHighlightedCell_(cell, index);
}
/**
* Handle a mouse move event. Highlight the hovered colour.
* @param {!MouseEvent} e Mouse event.
* @private
* @param e Mouse event.
*/
onMouseMove_(e) {
const cell = /** @type {!Element} */ (e.target);
private onMouseMove_(e: MouseEvent) {
const cell = e.target as Element;
const index = cell && Number(cell.getAttribute('data-index'));
if (index !== null && index !== this.highlightedIndex_) {
this.setHighlightedCell_(cell, index);
}
}
/**
* Handle a mouse enter event. Focus the picker.
* @private
*/
onMouseEnter_() {
this.picker_.focus({preventScroll: true});
/** Handle a mouse enter event. Focus the picker. */
private onMouseEnter_() {
// AnyDuringMigration because: Property 'focus' does not exist on type
// 'Element'.
(this.picker_ as AnyDuringMigration)!.focus({preventScroll: true});
}
/**
* Handle a mouse leave event. Blur the picker and unhighlight
* the currently highlighted colour.
* @private
*/
onMouseLeave_() {
this.picker_.blur();
private onMouseLeave_() {
// AnyDuringMigration because: Property 'blur' does not exist on type
// 'Element'.
(this.picker_ as AnyDuringMigration)!.blur();
const highlighted = this.getHighlighted_();
if (highlighted) {
dom.removeClass(highlighted, 'blocklyColourHighlighted');
@@ -438,10 +440,9 @@ class FieldColour extends Field {
/**
* Returns the currently highlighted item (if any).
* @return {?HTMLElement} Highlighted item (null if none).
* @private
* @return Highlighted item (null if none).
*/
getHighlighted_() {
private getHighlighted_(): HTMLElement|null {
if (!this.highlightedIndex_) {
return null;
}
@@ -449,21 +450,20 @@ class FieldColour extends Field {
const columns = this.columns_ || FieldColour.COLUMNS;
const x = this.highlightedIndex_ % columns;
const y = Math.floor(this.highlightedIndex_ / columns);
const row = this.picker_.childNodes[y];
const row = this.picker_!.childNodes[y];
if (!row) {
return null;
}
const col = /** @type {HTMLElement} */ (row.childNodes[x]);
const col = row.childNodes[x] as HTMLElement;
return col;
}
/**
* Update the currently highlighted cell.
* @param {!Element} cell the new cell to highlight
* @param {number} index the index of the new cell
* @private
* @param cell the new cell to highlight
* @param index the index of the new cell
*/
setHighlightedCell_(cell, index) {
private setHighlightedCell_(cell: Element, index: number) {
// Unhighlight the current item.
const highlighted = this.getHighlighted_();
if (highlighted) {
@@ -475,23 +475,21 @@ class FieldColour extends Field {
this.highlightedIndex_ = index;
// Update accessibility roles.
// AnyDuringMigration because: Argument of type 'string | null' is not
// assignable to parameter of type 'string | number | boolean | string[]'.
aria.setState(
/** @type {!Element} */ (this.picker_), aria.State.ACTIVEDESCENDANT,
cell.getAttribute('id'));
this.picker_ as Element, aria.State.ACTIVEDESCENDANT,
cell.getAttribute('id') as AnyDuringMigration);
}
/**
* Create a colour picker dropdown editor.
* @private
*/
dropdownCreate_() {
/** Create a colour picker dropdown editor. */
private dropdownCreate_() {
const columns = this.columns_ || FieldColour.COLUMNS;
const colours = this.colours_ || FieldColour.COLOURS;
const titles = this.titles_ || FieldColour.TITLES;
const selectedColour = this.getValue();
// Create the palette.
const table =
/** @type {!HTMLTableElement} */ (document.createElement('table'));
const table = (document.createElement('table'));
table.className = 'blocklyColourTable';
table.tabIndex = 0;
table.dir = 'ltr';
@@ -500,21 +498,22 @@ class FieldColour extends Field {
aria.setState(
table, aria.State.ROWCOUNT, Math.floor(colours.length / columns));
aria.setState(table, aria.State.COLCOUNT, columns);
let row;
let row: Element;
for (let i = 0; i < colours.length; i++) {
if (i % columns === 0) {
row = document.createElement('tr');
aria.setRole(row, aria.Role.ROW);
table.appendChild(row);
}
const cell =
/** @type {!HTMLTableCellElement} */ (document.createElement('td'));
row.appendChild(cell);
cell.setAttribute(
'data-colour', colours[i]); // This becomes the value, if clicked.
const cell = (document.createElement('td'));
row!.appendChild(cell);
// This becomes the value, if clicked.
cell.setAttribute('data-colour', colours[i]);
cell.title = titles[i] || colours[i];
cell.id = idGenerator.getNextUniqueId();
cell.setAttribute('data-index', i);
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
cell.setAttribute('data-index', i as AnyDuringMigration);
aria.setRole(cell, aria.Role.GRIDCELL);
aria.setState(cell, aria.State.LABEL, colours[i]);
aria.setState(cell, aria.State.SELECTED, colours[i] === selectedColour);
@@ -540,11 +539,8 @@ class FieldColour extends Field {
this.picker_ = table;
}
/**
* Disposes of events and DOM-references belonging to the colour editor.
* @private
*/
dropdownDispose_() {
/** Disposes of events and DOM-references belonging to the colour editor. */
private dropdownDispose_() {
if (this.onClickWrapper_) {
browserEvents.unbind(this.onClickWrapper_);
this.onClickWrapper_ = null;
@@ -571,131 +567,25 @@ class FieldColour extends Field {
/**
* Construct a FieldColour from a JSON arg object.
* @param {!Object} options A JSON object with options (colour).
* @return {!FieldColour} The new field instance.
* @package
* @param options A JSON object with options (colour).
* @return The new field instance.
* @nocollapse
* @internal
*/
static fromJson(options) {
static fromJson(options: AnyDuringMigration): FieldColour {
// `this` might be a subclass of FieldColour if that class doesn't override
// the static fromJson method.
return new this(options['colour'], undefined, options);
}
}
/**
* An array of colour strings for the palette.
* Copied from goog.ui.ColorPicker.SIMPLE_GRID_COLORS
* All colour pickers use this unless overridden with setColours.
* @type {!Array<string>}
*/
FieldColour.COLOURS = [
// grays
'#ffffff',
'#cccccc',
'#c0c0c0',
'#999999',
'#666666',
'#333333',
'#000000',
// reds
'#ffcccc',
'#ff6666',
'#ff0000',
'#cc0000',
'#990000',
'#660000',
'#330000',
// oranges
'#ffcc99',
'#ff9966',
'#ff9900',
'#ff6600',
'#cc6600',
'#993300',
'#663300',
// yellows
'#ffff99',
'#ffff66',
'#ffcc66',
'#ffcc33',
'#cc9933',
'#996633',
'#663333',
// olives
'#ffffcc',
'#ffff33',
'#ffff00',
'#ffcc00',
'#999900',
'#666600',
'#333300',
// greens
'#99ff99',
'#66ff99',
'#33ff33',
'#33cc00',
'#009900',
'#006600',
'#003300',
// turquoises
'#99ffff',
'#33ffff',
'#66cccc',
'#00cccc',
'#339999',
'#336666',
'#003333',
// blues
'#ccffff',
'#66ffff',
'#33ccff',
'#3366ff',
'#3333ff',
'#000099',
'#000066',
// purples
'#ccccff',
'#9999ff',
'#6666cc',
'#6633ff',
'#6600cc',
'#333399',
'#330099',
// violets
'#ffccff',
'#ff99ff',
'#cc66cc',
'#cc33cc',
'#993399',
'#663366',
'#330033',
];
/** The default value for this field. */
// AnyDuringMigration because: Property 'DEFAULT_VALUE' is protected and only
// accessible within class 'FieldColour' and its subclasses.
(FieldColour.prototype as AnyDuringMigration).DEFAULT_VALUE =
FieldColour.COLOURS[0];
/**
* The default value for this field.
* @type {*}
* @protected
*/
FieldColour.prototype.DEFAULT_VALUE = FieldColour.COLOURS[0];
/**
* An array of tooltip strings for the palette. If not the same length as
* COLOURS, the colour's hex code will be used for any missing titles.
* All colour pickers use this unless overridden with setColours.
* @type {!Array<string>}
*/
FieldColour.TITLES = [];
/**
* Number of columns in the palette.
* All colour pickers use this unless overridden with setColumns.
*/
FieldColour.COLUMNS = 7;
/**
* CSS for colour picker. See css.js for use.
*/
/** CSS for colour picker. See css.js for use. */
Css.register(`
.blocklyColourTable {
border-collapse: collapse;
@@ -728,5 +618,3 @@ Css.register(`
`);
fieldRegistry.register('field_colour', FieldColour);
exports.FieldColour = FieldColour;

View File

@@ -9,7 +9,6 @@
* In the interests of a consistent UI, the toolbox shares some functions and
* properties with the context menu.
*/
'use strict';
/**
* Dropdown input field. Used for editable titles and variables.
@@ -17,103 +16,114 @@
* properties with the context menu.
* @class
*/
goog.module('Blockly.FieldDropdown');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldDropdown');
const aria = goog.require('Blockly.utils.aria');
const dom = goog.require('Blockly.utils.dom');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const parsing = goog.require('Blockly.utils.parsing');
const userAgent = goog.require('Blockly.utils.userAgent');
const utilsString = goog.require('Blockly.utils.string');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
const {Field} = goog.require('Blockly.Field');
const {MenuItem} = goog.require('Blockly.MenuItem');
const {Menu} = goog.require('Blockly.Menu');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Svg} = goog.require('Blockly.utils.Svg');
import type {BlockSvg} from './block_svg.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Menu} from './menu.js';
import {MenuItem} from './menuitem.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import * as utilsString from './utils/string.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
/**
* Class for an editable dropdown field.
* @extends {Field}
* @alias Blockly.FieldDropdown
*/
class FieldDropdown extends Field {
export class FieldDropdown extends Field {
/** Horizontal distance that a checkmark overhangs the dropdown. */
static CHECKMARK_OVERHANG = 25;
/**
* @param {(!Array<!Array>|!Function|!Sentinel)} menuGenerator
* A non-empty array of options for a dropdown list, or a function which
* generates these options.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a language-neutral dropdown
* option & returns a validated language-neutral dropdown option, or null
* to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* Maximum height of the dropdown menu, as a percentage of the viewport
* height.
*/
static MAX_MENU_HEIGHT_VH = 0.45;
static ARROW_CHAR: AnyDuringMigration;
/** A reference to the currently selected menu item. */
private selectedMenuItem_: MenuItem|null = null;
/** The dropdown menu. */
protected menu_: Menu|null = null;
/**
* SVG image element if currently selected option is an image, or null.
*/
private imageElement_: SVGImageElement|null = null;
/** Tspan based arrow element. */
private arrow_: SVGTSpanElement|null = null;
/** SVG based arrow element. */
private svgArrow_: SVGElement|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/** Mouse cursor style when over the hotspot that initiates the editor. */
override CURSOR = 'default';
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
protected menuGenerator_!: AnyDuringMigration[][]|
((this: FieldDropdown) => AnyDuringMigration[][]);
/** A cache of the most recently generated options. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'string[][]'.
private generatedOptions_: string[][] = null as AnyDuringMigration;
/**
* The prefix field label, of common words set after options are trimmed.
* @internal
*/
override prefixField: string|null = null;
/**
* The suffix field label, of common words set after options are trimmed.
* @internal
*/
override suffixField: string|null = null;
// TODO(b/109816955): remove '!', see go/strict-prop-init-fix.
private selectedOption_!: Array<string|ImageProperties>;
override clickTarget_: AnyDuringMigration;
/**
* @param menuGenerator A non-empty array of options for a dropdown list, or a
* function which generates these options. Also accepts Field.SKIP_SETUP
* if you wish to skip setup (only used by subclasses that want to handle
* configuration and setting the field value after their own constructors
* have run).
* @param opt_validator A function that is called to validate changes to the
* field's value. Takes in a language-neutral dropdown option & returns a
* validated language-neutral dropdown option, or null to abort the
* change.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/dropdown#creation}
* for a list of properties this parameter supports.
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/dropdown#creation}
* for a list of properties this parameter supports.
* @throws {TypeError} If `menuGenerator` options are incorrectly structured.
*/
constructor(menuGenerator, opt_validator, opt_config) {
constructor(
menuGenerator: AnyDuringMigration[][]|Function|Sentinel,
opt_validator?: Function, opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
/**
* A reference to the currently selected menu item.
* @type {?MenuItem}
* @private
*/
this.selectedMenuItem_ = null;
/**
* The dropdown menu.
* @type {?Menu}
* @protected
*/
this.menu_ = null;
/**
* SVG image element if currently selected option is an image, or null.
* @type {?SVGImageElement}
* @private
*/
this.imageElement_ = null;
/**
* Tspan based arrow element.
* @type {?SVGTSpanElement}
* @private
*/
this.arrow_ = null;
/**
* SVG based arrow element.
* @type {?SVGElement}
* @private
*/
this.svgArrow_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates the editor.
* @type {string}
*/
this.CURSOR = 'default';
// 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)) {
validateOptions(menuGenerator);
@@ -122,60 +132,34 @@ class FieldDropdown extends Field {
/**
* An array of options for a dropdown list,
* or a function which generates these options.
* @type {(!Array<!Array>|!function(this:FieldDropdown): !Array<!Array>)}
* @protected
*/
this.menuGenerator_ =
/**
* @type {(!Array<!Array>|
* !function(this:FieldDropdown):!Array<!Array>)}
*/
(menuGenerator);
/**
* A cache of the most recently generated options.
* @type {Array<!Array<string>>}
* @private
*/
this.generatedOptions_ = null;
/**
* The prefix field label, of common words set after options are trimmed.
* @type {?string}
* @package
*/
this.prefixField = null;
/**
* The suffix field label, of common words set after options are trimmed.
* @type {?string}
* @package
*/
this.suffixField = null;
this.menuGenerator_ = menuGenerator as AnyDuringMigration[][] |
((this: FieldDropdown) => AnyDuringMigration[][]);
this.trimOptions_();
/**
* The currently selected option. The field is initialized with the
* first option selected.
* @type {!Array<string|!ImageProperties>}
* @private
*/
this.selectedOption_ = this.getOptions(false)[0];
if (opt_config) this.configure_(opt_config);
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(this.selectedOption_[1]);
if (opt_validator) this.setValidator(opt_validator);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
/**
* Sets the field's value based on the given XML element. Should only be
* called by Blockly.Xml.
* @param {!Element} fieldElement The element containing info about the
* field's state.
* @package
* @param fieldElement The element containing info about the field's state.
* @internal
*/
fromXml(fieldElement) {
override fromXml(fieldElement: Element) {
if (this.isOptionListDynamic()) {
this.getOptions(false);
}
@@ -184,11 +168,10 @@ class FieldDropdown extends Field {
/**
* Sets the field's value based on the given state.
* @param {*} state The state to apply to the dropdown field.
* @override
* @package
* @param state The state to apply to the dropdown field.
* @internal
*/
loadState(state) {
override loadState(state: AnyDuringMigration) {
if (this.loadLegacyState(FieldDropdown, state)) {
return;
}
@@ -200,20 +183,19 @@ class FieldDropdown extends Field {
/**
* Create the block UI for this dropdown.
* @package
* @internal
*/
initView() {
override initView() {
if (this.shouldAddBorderRect_()) {
this.createBorderRect_();
} else {
this.clickTarget_ =
(/** @type {!BlockSvg} */ (this.sourceBlock_)).getSvgRoot();
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
}
this.createTextElement_();
this.imageElement_ = dom.createSvgElement(Svg.IMAGE, {}, this.fieldGroup_);
if (this.getConstants().FIELD_DROPDOWN_SVG_ARROW) {
if (this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW) {
this.createSVGArrow_();
} else {
this.createTextArrow_();
@@ -226,76 +208,79 @@ class FieldDropdown extends Field {
/**
* Whether or not the dropdown should add a border rect.
* @return {boolean} True if the dropdown field should add a border rect.
* @protected
* @return True if the dropdown field should add a border rect.
*/
shouldAddBorderRect_() {
return !this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
(this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW &&
!this.sourceBlock_.isShadow());
protected shouldAddBorderRect_(): boolean {
return !this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW &&
!this.sourceBlock_.isShadow();
}
/**
* Create a tspan based arrow.
* @protected
*/
createTextArrow_() {
/** Create a tspan based arrow. */
protected createTextArrow_() {
this.arrow_ = dom.createSvgElement(Svg.TSPAN, {}, this.textElement_);
this.arrow_.appendChild(document.createTextNode(
this.arrow_!.appendChild(document.createTextNode(
this.sourceBlock_.RTL ? FieldDropdown.ARROW_CHAR + ' ' :
' ' + FieldDropdown.ARROW_CHAR));
if (this.sourceBlock_.RTL) {
this.textElement_.insertBefore(this.arrow_, this.textContent_);
// AnyDuringMigration because: Argument of type 'SVGTSpanElement | null'
// is not assignable to parameter of type 'Node'.
this.textElement_.insertBefore(
this.arrow_ as AnyDuringMigration, this.textContent_);
} else {
this.textElement_.appendChild(this.arrow_);
// AnyDuringMigration because: Argument of type 'SVGTSpanElement | null'
// is not assignable to parameter of type 'Node'.
this.textElement_.appendChild(this.arrow_ as AnyDuringMigration);
}
}
/**
* Create an SVG based arrow.
* @protected
*/
createSVGArrow_() {
/** Create an SVG based arrow. */
protected createSVGArrow_() {
this.svgArrow_ = dom.createSvgElement(
Svg.IMAGE, {
'height': this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
'width': this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
'height': this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
'width': this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE + 'px',
},
this.fieldGroup_);
this.svgArrow_.setAttributeNS(
this.svgArrow_!.setAttributeNS(
dom.XLINK_NS, 'xlink:href',
this.getConstants().FIELD_DROPDOWN_SVG_ARROW_DATAURI);
this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_DATAURI);
}
/**
* Create a dropdown menu under the text.
* @param {Event=} opt_e Optional mouse event that triggered the field to
* open, or undefined if triggered programmatically.
* @protected
* @param opt_e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
*/
showEditor_(opt_e) {
protected override showEditor_(opt_e?: Event) {
this.dropdownCreate_();
if (opt_e && typeof opt_e.clientX === 'number') {
this.menu_.openingCoords = new Coordinate(opt_e.clientX, opt_e.clientY);
// AnyDuringMigration because: Property 'clientX' does not exist on type
// 'Event'.
if (opt_e && typeof (opt_e as AnyDuringMigration).clientX === 'number') {
// AnyDuringMigration because: Property 'clientY' does not exist on type
// 'Event'. AnyDuringMigration because: Property 'clientX' does not exist
// on type 'Event'.
this.menu_!.openingCoords = new Coordinate(
(opt_e as AnyDuringMigration).clientX,
(opt_e as AnyDuringMigration).clientY);
} else {
this.menu_.openingCoords = null;
this.menu_!.openingCoords = null;
}
// Remove any pre-existing elements in the dropdown.
dropDownDiv.clearContent();
// Element gets created in render.
this.menu_.render(dropDownDiv.getContentDiv());
const menuElement = /** @type {!Element} */ (this.menu_.getElement());
this.menu_!.render(dropDownDiv.getContentDiv());
const menuElement = this.menu_!.getElement() as Element;
dom.addClass(menuElement, 'blocklyDropdownMenu');
if (this.getConstants().FIELD_DROPDOWN_COLOURED_DIV) {
const primaryColour = (this.sourceBlock_.isShadow()) ?
this.sourceBlock_.getParent().getColour() :
if (this.getConstants()!.FIELD_DROPDOWN_COLOURED_DIV) {
const primaryColour = this.sourceBlock_.isShadow() ?
this.sourceBlock_.getParent()!.getColour() :
this.sourceBlock_.getColour();
const borderColour = (this.sourceBlock_.isShadow()) ?
(/** @type {!BlockSvg} */ (this.sourceBlock_.getParent()))
.style.colourTertiary :
(/** @type {!BlockSvg} */ (this.sourceBlock_)).style.colourTertiary;
const borderColour = this.sourceBlock_.isShadow() ?
(this.sourceBlock_.getParent() as BlockSvg).style.colourTertiary :
(this.sourceBlock_ as BlockSvg).style.colourTertiary;
dropDownDiv.setColour(primaryColour, borderColour);
}
@@ -304,20 +289,17 @@ class FieldDropdown extends Field {
// Focusing needs to be handled after the menu is rendered and positioned.
// Otherwise it will cause a page scroll to get the misplaced menu in
// view. See issue #1329.
this.menu_.focus();
this.menu_!.focus();
if (this.selectedMenuItem_) {
this.menu_.setHighlighted(this.selectedMenuItem_);
this.menu_!.setHighlighted(this.selectedMenuItem_);
}
this.applyColour();
}
/**
* Create the dropdown editor.
* @private
*/
dropdownCreate_() {
/** Create the dropdown editor. */
private dropdownCreate_() {
const menu = new Menu();
menu.setRole(aria.Role.LISTBOX);
this.menu_ = menu;
@@ -349,9 +331,8 @@ class FieldDropdown extends Field {
/**
* Disposes of events and DOM-references belonging to the dropdown editor.
* @private
*/
dropdownDispose_() {
private dropdownDispose_() {
if (this.menu_) {
this.menu_.dispose();
}
@@ -362,30 +343,27 @@ class FieldDropdown extends Field {
/**
* Handle an action in the dropdown menu.
* @param {!MenuItem} menuItem The MenuItem selected within menu.
* @private
* @param menuItem The MenuItem selected within menu.
*/
handleMenuActionEvent_(menuItem) {
private handleMenuActionEvent_(menuItem: MenuItem) {
dropDownDiv.hideIfOwner(this, true);
this.onItemSelected_(/** @type {!Menu} */ (this.menu_), menuItem);
this.onItemSelected_(this.menu_ as Menu, menuItem);
}
/**
* Handle the selection of an item in the dropdown menu.
* @param {!Menu} menu The Menu component clicked.
* @param {!MenuItem} menuItem The MenuItem selected within menu.
* @protected
* @param menu The Menu component clicked.
* @param menuItem The MenuItem selected within menu.
*/
onItemSelected_(menu, menuItem) {
protected onItemSelected_(menu: Menu, menuItem: MenuItem) {
this.setValue(menuItem.getValue());
}
/**
* Factor out common words in statically defined options.
* Create prefix and/or suffix labels.
* @private
*/
trimOptions_() {
private trimOptions_() {
const options = this.menuGenerator_;
if (!Array.isArray(options)) {
return;
@@ -433,42 +411,45 @@ class FieldDropdown extends Field {
}
/**
* @return {boolean} True if the option list is generated by a function.
* @return True if the option list is generated by a function.
* Otherwise false.
*/
isOptionListDynamic() {
isOptionListDynamic(): boolean {
return typeof this.menuGenerator_ === 'function';
}
/**
* Return a list of the options for this dropdown.
* @param {boolean=} opt_useCache For dynamic options, whether or not to use
* the cached options or to re-generate them.
* @return {!Array<!Array>} A non-empty array of option tuples:
* @param opt_useCache For dynamic options, whether or not to use the cached
* options or to re-generate them.
* @return A non-empty array of option tuples:
* (human-readable text or image, language-neutral name).
* @throws {TypeError} If generated options are incorrectly structured.
*/
getOptions(opt_useCache) {
getOptions(opt_useCache?: boolean): AnyDuringMigration[][] {
if (this.isOptionListDynamic()) {
if (!this.generatedOptions_ || !opt_useCache) {
this.generatedOptions_ = this.menuGenerator_.call(this);
// AnyDuringMigration because: Property 'call' does not exist on type
// 'any[][] | ((this: FieldDropdown) => any[][])'.
this.generatedOptions_ =
(this.menuGenerator_ as AnyDuringMigration).call(this);
validateOptions(this.generatedOptions_);
}
return this.generatedOptions_;
}
return /** @type {!Array<!Array<string>>} */ (this.menuGenerator_);
return this.menuGenerator_ as string[][];
}
/**
* Ensure that the input value is a valid language-neutral option.
* @param {*=} opt_newValue The input value.
* @return {?string} A valid language-neutral option, or null if invalid.
* @protected
* @param opt_newValue The input value.
* @return A valid language-neutral option, or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
let isValueValid = false;
const options = this.getOptions(true);
for (let i = 0, option; (option = options[i]); i++) {
for (let i = 0, option; option = options[i]; i++) {
// Options are tuples of human-readable text and language-neutral values.
if (option[1] === opt_newValue) {
isValueValid = true;
@@ -484,19 +465,18 @@ class FieldDropdown extends Field {
}
return null;
}
return /** @type {string} */ (opt_newValue);
return opt_newValue as string;
}
/**
* Update the value of this dropdown field.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is one of the valid dropdown options.
* @protected
* @param newValue The value to be saved. The default validator guarantees
* that this is one of the valid dropdown options.
*/
doValueUpdate_(newValue) {
protected override doValueUpdate_(newValue: AnyDuringMigration) {
super.doValueUpdate_(newValue);
const options = this.getOptions(true);
for (let i = 0, option; (option = options[i]); i++) {
for (let i = 0, option; option = options[i]; i++) {
if (option[1] === this.value_) {
this.selectedOption_ = option;
}
@@ -505,10 +485,10 @@ class FieldDropdown extends Field {
/**
* Updates the dropdown arrow to match the colour/style of the block.
* @package
* @internal
*/
applyColour() {
const style = (/** @type {!BlockSvg} */ (this.sourceBlock_)).style;
override applyColour() {
const style = (this.sourceBlock_ as BlockSvg).style;
if (this.borderRect_) {
this.borderRect_.setAttribute('stroke', style.colourTertiary);
if (this.menu_) {
@@ -527,20 +507,16 @@ class FieldDropdown extends Field {
}
}
/**
* Draws the border with the correct width.
* @protected
*/
render_() {
/** Draws the border with the correct width. */
protected override render_() {
// Hide both elements.
this.textContent_.nodeValue = '';
this.imageElement_.style.display = 'none';
this.imageElement_!.style.display = 'none';
// Show correct element.
const option = this.selectedOption_ && this.selectedOption_[0];
if (option && typeof option === 'object') {
this.renderSelectedImage_(
/** @type {!ImageProperties} */ (option));
this.renderSelectedImage_((option));
} else {
this.renderSelectedText_();
}
@@ -550,16 +526,20 @@ class FieldDropdown extends Field {
/**
* Renders the selected option, which must be an image.
* @param {!ImageProperties} imageJson Selected
* option that must be an image.
* @private
* @param imageJson Selected option that must be an image.
*/
renderSelectedImage_(imageJson) {
this.imageElement_.style.display = '';
this.imageElement_.setAttributeNS(
private renderSelectedImage_(imageJson: ImageProperties) {
this.imageElement_!.style.display = '';
this.imageElement_!.setAttributeNS(
dom.XLINK_NS, 'xlink:href', imageJson.src);
this.imageElement_.setAttribute('height', imageJson.height);
this.imageElement_.setAttribute('width', imageJson.width);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.imageElement_!.setAttribute(
'height', imageJson.height as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.imageElement_!.setAttribute(
'width', imageJson.width as AnyDuringMigration);
const imageHeight = Number(imageJson.height);
const imageWidth = Number(imageJson.width);
@@ -567,21 +547,21 @@ class FieldDropdown extends Field {
// Height and width include the border rect.
const hasBorder = !!this.borderRect_;
const height = Math.max(
hasBorder ? this.getConstants().FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
imageHeight + IMAGE_Y_PADDING);
const xPadding =
hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0;
let arrowWidth = 0;
if (this.svgArrow_) {
arrowWidth = this.positionSVGArrow_(
imageWidth + xPadding,
height / 2 - this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);
height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);
} else {
arrowWidth = dom.getFastTextWidth(
/** @type {!SVGTSpanElement} */ (this.arrow_),
this.getConstants().FIELD_TEXT_FONTSIZE,
this.getConstants().FIELD_TEXT_FONTWEIGHT,
this.getConstants().FIELD_TEXT_FONTFAMILY);
this.arrow_ as SVGTSpanElement,
this.getConstants()!.FIELD_TEXT_FONTSIZE,
this.getConstants()!.FIELD_TEXT_FONTWEIGHT,
this.getConstants()!.FIELD_TEXT_FONTFAMILY);
}
this.size_.width = imageWidth + arrowWidth + xPadding * 2;
this.size_.height = height;
@@ -589,44 +569,41 @@ class FieldDropdown extends Field {
let arrowX = 0;
if (this.sourceBlock_.RTL) {
const imageX = xPadding + arrowWidth;
this.imageElement_.setAttribute('x', imageX);
this.imageElement_!.setAttribute('x', imageX.toString());
} else {
arrowX = imageWidth + arrowWidth;
this.textElement_.setAttribute('text-anchor', 'end');
this.imageElement_.setAttribute('x', xPadding);
this.imageElement_!.setAttribute('x', xPadding.toString());
}
this.imageElement_.setAttribute('y', height / 2 - imageHeight / 2);
this.imageElement_!.setAttribute(
'y', (height / 2 - imageHeight / 2).toString());
this.positionTextElement_(arrowX + xPadding, imageWidth + arrowWidth);
}
/**
* Renders the selected option, which must be text.
* @private
*/
renderSelectedText_() {
/** Renders the selected option, which must be text. */
private renderSelectedText_() {
// Retrieves the selected option to display through getText_.
this.textContent_.nodeValue = this.getDisplayText_();
dom.addClass(
/** @type {!Element} */ (this.textElement_), 'blocklyDropdownText');
dom.addClass(this.textElement_ as Element, 'blocklyDropdownText');
this.textElement_.setAttribute('text-anchor', 'start');
// Height and width include the border rect.
const hasBorder = !!this.borderRect_;
const height = Math.max(
hasBorder ? this.getConstants().FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
this.getConstants().FIELD_TEXT_HEIGHT);
hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
this.getConstants()!.FIELD_TEXT_HEIGHT);
const textWidth = dom.getFastTextWidth(
this.textElement_, this.getConstants().FIELD_TEXT_FONTSIZE,
this.getConstants().FIELD_TEXT_FONTWEIGHT,
this.getConstants().FIELD_TEXT_FONTFAMILY);
this.textElement_, this.getConstants()!.FIELD_TEXT_FONTSIZE,
this.getConstants()!.FIELD_TEXT_FONTWEIGHT,
this.getConstants()!.FIELD_TEXT_FONTFAMILY);
const xPadding =
hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0;
let arrowWidth = 0;
if (this.svgArrow_) {
arrowWidth = this.positionSVGArrow_(
textWidth + xPadding,
height / 2 - this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);
height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2);
}
this.size_.width = textWidth + arrowWidth + xPadding * 2;
this.size_.height = height;
@@ -636,20 +613,19 @@ class FieldDropdown extends Field {
/**
* Position a drop-down arrow at the appropriate location at render-time.
* @param {number} x X position the arrow is being rendered at, in px.
* @param {number} y Y position the arrow is being rendered at, in px.
* @return {number} Amount of space the arrow is taking up, in px.
* @private
* @param x X position the arrow is being rendered at, in px.
* @param y Y position the arrow is being rendered at, in px.
* @return Amount of space the arrow is taking up, in px.
*/
positionSVGArrow_(x, y) {
private positionSVGArrow_(x: number, y: number): number {
if (!this.svgArrow_) {
return 0;
}
const hasBorder = !!this.borderRect_;
const xPadding =
hasBorder ? this.getConstants().FIELD_BORDER_RECT_X_PADDING : 0;
const textPadding = this.getConstants().FIELD_DROPDOWN_SVG_ARROW_PADDING;
const svgArrowSize = this.getConstants().FIELD_DROPDOWN_SVG_ARROW_SIZE;
hasBorder ? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING : 0;
const textPadding = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_PADDING;
const svgArrowSize = this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE;
const arrowX = this.sourceBlock_.RTL ? xPadding : x + textPadding;
this.svgArrow_.setAttribute(
'transform', 'translate(' + arrowX + ',' + y + ')');
@@ -660,11 +636,9 @@ class FieldDropdown extends Field {
* Use the `getText_` developer hook to override the field's text
* representation. Get the selected option text. If the selected option is an
* image we return the image alt text.
* @return {?string} Selected option text.
* @protected
* @override
* @return Selected option text.
*/
getText_() {
protected override getText_(): string|null {
if (!this.selectedOption_) {
return null;
}
@@ -677,12 +651,12 @@ class FieldDropdown extends Field {
/**
* Construct a FieldDropdown from a JSON arg object.
* @param {!Object} options A JSON object with options (options).
* @return {!FieldDropdown} The new field instance.
* @package
* @param options A JSON object with options (options).
* @return The new field instance.
* @nocollapse
* @internal
*/
static fromJson(options) {
static fromJson(options: AnyDuringMigration): FieldDropdown {
// `this` might be a subclass of FieldDropdown if that class doesn't
// override the static fromJson method.
return new this(options['options'], undefined, options);
@@ -691,13 +665,15 @@ class FieldDropdown extends Field {
/**
* Use the calculated prefix and suffix lengths to trim all of the options in
* the given array.
* @param {!Array<!Array>} options Array of option tuples:
* @param options Array of option tuples:
* (human-readable text or image, language-neutral name).
* @param {number} prefixLength The length of the common prefix.
* @param {number} suffixLength The length of the common suffix
* @return {!Array<!Array>} A new array with all of the option text trimmed.
* @param prefixLength The length of the common prefix.
* @param suffixLength The length of the common suffix
* @return A new array with all of the option text trimmed.
*/
static applyTrim_(options, prefixLength, suffixLength) {
static applyTrim_(
options: AnyDuringMigration[][], prefixLength: number,
suffixLength: number): AnyDuringMigration[][] {
const newOptions = [];
// Remove the prefix and suffix from the options.
for (let i = 0; i < options.length; i++) {
@@ -710,53 +686,32 @@ class FieldDropdown extends Field {
}
}
/**
* Dropdown image properties.
* @typedef {{
* src:string,
* alt:string,
* width:number,
* height:number
* }}
*/
let ImageProperties; // eslint-disable-line no-unused-vars
/**
* Horizontal distance that a checkmark overhangs the dropdown.
*/
FieldDropdown.CHECKMARK_OVERHANG = 25;
/**
* Maximum height of the dropdown menu, as a percentage of the viewport height.
*/
FieldDropdown.MAX_MENU_HEIGHT_VH = 0.45;
/** Dropdown image properties. */
interface ImageProperties {
src: string;
alt: string;
width: number;
height: number;
}
/**
* The y offset from the top of the field to the top of the image, if an image
* is selected.
* @type {number}
* @const
*/
const IMAGE_Y_OFFSET = 5;
/**
* The total vertical padding above and below an image.
* @type {number}
* @const
*/
const IMAGE_Y_PADDING = IMAGE_Y_OFFSET * 2;
/** The total vertical padding above and below an image. */
const IMAGE_Y_PADDING: number = IMAGE_Y_OFFSET * 2;
/**
* Android can't (in 2014) display "▾", so use "▼" instead.
*/
/** Android can't (in 2014) display "▾", so use "▼" instead. */
FieldDropdown.ARROW_CHAR = userAgent.ANDROID ? '\u25BC' : '\u25BE';
/**
* Validates the data structure to be processed as an options list.
* @param {?} options The proposed dropdown options.
* @param options The proposed dropdown options.
* @throws {TypeError} If proposed options are incorrectly structured.
*/
const validateOptions = function(options) {
function validateOptions(options: AnyDuringMigration) {
if (!Array.isArray(options)) {
throw TypeError('FieldDropdown options must be an array.');
}
@@ -779,8 +734,8 @@ const validateOptions = function(options) {
'a string. Found ' + tuple[1] + ' in: ',
tuple);
} else if (
tuple[0] && (typeof tuple[0] !== 'string') &&
(typeof tuple[0].src !== 'string')) {
tuple[0] && typeof tuple[0] !== 'string' &&
typeof tuple[0].src !== 'string') {
foundError = true;
console.error(
'Invalid option[' + i + ']: Each FieldDropdown option must have a ' +
@@ -791,8 +746,6 @@ const validateOptions = function(options) {
if (foundError) {
throw TypeError('Found invalid FieldDropdown options.');
}
};
}
fieldRegistry.register('field_dropdown', FieldDropdown);
exports.FieldDropdown = FieldDropdown;

View File

@@ -7,49 +7,84 @@
/**
* @fileoverview Image field. Used for pictures, icons, etc.
*/
'use strict';
/**
* Image field. Used for pictures, icons, etc.
* @class
*/
goog.module('Blockly.FieldImage');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldImage');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const parsing = goog.require('Blockly.utils.parsing');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Size} = goog.require('Blockly.utils.Size');
const {Svg} = goog.require('Blockly.utils.Svg');
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
/**
* Class for an image on a block.
* @extends {Field}
* @alias Blockly.FieldImage
*/
class FieldImage extends Field {
export class FieldImage extends Field {
/**
* @param {string|!Sentinel} src The URL of the image.
* Vertical padding below the image, which is included in the reported height
* of the field.
*/
private static readonly Y_PADDING = 1;
protected override size_: Size;
private readonly imageHeight_: number;
/** The function to be called when this field is clicked. */
private clickHandler_: ((p1: FieldImage) => AnyDuringMigration)|null = null;
/** The rendered field's image element. */
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGImageElement'.
private imageElement_: SVGImageElement = null as AnyDuringMigration;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
*/
override readonly EDITABLE = false;
/**
* Used to tell if the field needs to be rendered the next time the block is
* rendered. Image fields are statically sized, and only need to be
* rendered at initialization.
*/
protected override isDirty_ = false;
/** Whether to flip this image in RTL. */
private flipRtl_ = false;
/** Alt text of this image. */
private altText_ = '';
override value_: AnyDuringMigration;
/**
* @param src The URL of the image.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {!(string|number)} width Width of the image.
* @param {!(string|number)} height Height of the image.
* @param {string=} opt_alt Optional alt text for when block is collapsed.
* @param {function(!FieldImage)=} opt_onClick Optional function to be
* called when the image is clicked. If opt_onClick is defined, opt_alt
* must also be defined.
* @param {boolean=} opt_flipRtl Whether to flip the icon in RTL.
* @param {Object=} opt_config A map of options used to configure the field.
* subclasses that want to handle configuration and setting the field value
* after their own constructors have run).
* @param width Width of the image.
* @param height Height of the image.
* @param opt_alt Optional alt text for when block is collapsed.
* @param opt_onClick Optional function to be called when the image is
* clicked. If opt_onClick is defined, opt_alt must also be defined.
* @param opt_flipRtl Whether to flip the icon in RTL.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/image#creation}
* for a list of properties this parameter supports.
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/image#creation}
* for a list of properties this parameter supports.
*/
constructor(
src, width, height, opt_alt, opt_onClick, opt_flipRtl, opt_config) {
src: string|Sentinel, width: string|number, height: string|number,
opt_alt?: string, opt_onClick?: (p1: FieldImage) => AnyDuringMigration,
opt_flipRtl?: boolean, opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
// Return early.
@@ -69,71 +104,21 @@ class FieldImage extends Field {
' than 0.');
}
/**
* The size of the area rendered by the field.
* @type {Size}
* @protected
* @override
*/
/** The size of the area rendered by the field. */
this.size_ = new Size(imageWidth, imageHeight + FieldImage.Y_PADDING);
/**
* Store the image height, since it is different from the field height.
* @type {number}
* @private
*/
this.imageHeight_ = imageHeight;
/**
* The function to be called when this field is clicked.
* @type {?function(!FieldImage)}
* @private
*/
this.clickHandler_ = null;
if (typeof opt_onClick === 'function') {
this.clickHandler_ = opt_onClick;
}
/**
* The rendered field's image element.
* @type {SVGImageElement}
* @private
*/
this.imageElement_ = null;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
* @const
*/
this.EDITABLE = false;
/**
* Used to tell if the field needs to be rendered the next time the block is
* rendered. Image fields are statically sized, and only need to be
* rendered at initialization.
* @type {boolean}
* @protected
*/
this.isDirty_ = false;
/**
* Whether to flip this image in RTL.
* @type {boolean}
* @private
*/
this.flipRtl_ = false;
/**
* Alt text of this image.
* @type {string}
* @private
*/
this.altText_ = '';
if (src === Field.SKIP_SETUP) return;
if (src === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
@@ -146,11 +131,9 @@ class FieldImage extends Field {
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
* @param config A map of options to configure the field based on.
*/
configure_(config) {
protected override configure_(config: AnyDuringMigration) {
super.configure_(config);
this.flipRtl_ = !!config['flipRtl'];
this.altText_ = parsing.replaceMessageReferences(config['alt']) || '';
@@ -158,9 +141,9 @@ class FieldImage extends Field {
/**
* Create the block UI for this image.
* @package
* @internal
*/
initView() {
override initView() {
this.imageElement_ = dom.createSvgElement(
Svg.IMAGE, {
'height': this.imageHeight_ + 'px',
@@ -169,27 +152,23 @@ class FieldImage extends Field {
},
this.fieldGroup_);
this.imageElement_.setAttributeNS(
dom.XLINK_NS, 'xlink:href', /** @type {string} */ (this.value_));
dom.XLINK_NS, 'xlink:href', this.value_ as string);
if (this.clickHandler_) {
this.imageElement_.style.cursor = 'pointer';
}
}
/**
* @override
*/
updateSize_() {
// NOP
}
override updateSize_() {}
// NOP
/**
* Ensure that the input value (the source URL) is a string.
* @param {*=} opt_newValue The input value.
* @return {?string} A string, or null if invalid.
* @protected
* @param opt_newValue The input value.
* @return A string, or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (typeof opt_newValue !== 'string') {
return null;
}
@@ -198,11 +177,10 @@ class FieldImage extends Field {
/**
* Update the value of this image field, and update the displayed image.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
* @param newValue The value to be saved. The default validator guarantees
* that this is a string.
*/
doValueUpdate_(newValue) {
protected override doValueUpdate_(newValue: AnyDuringMigration) {
this.value_ = newValue;
if (this.imageElement_) {
this.imageElement_.setAttributeNS(
@@ -212,19 +190,17 @@ class FieldImage extends Field {
/**
* Get whether to flip this image in RTL
* @return {boolean} True if we should flip in RTL.
* @override
* @return True if we should flip in RTL.
*/
getFlipRtl() {
override getFlipRtl(): boolean {
return this.flipRtl_;
}
/**
* Set the alt text of this image.
* @param {?string} alt New alt text.
* @public
* @param alt New alt text.
*/
setAlt(alt) {
setAlt(alt: string|null) {
if (alt === this.altText_) {
return;
}
@@ -237,9 +213,8 @@ class FieldImage extends Field {
/**
* If field click is called, and click handler defined,
* call the handler.
* @protected
*/
showEditor_() {
protected override showEditor_() {
if (this.clickHandler_) {
this.clickHandler_(this);
}
@@ -247,10 +222,10 @@ class FieldImage extends Field {
/**
* Set the function that is called when this image is clicked.
* @param {?function(!FieldImage)} func The function that is called
* when the image is clicked, or null to remove.
* @param func The function that is called when the image is clicked, or null
* to remove.
*/
setOnClickHandler(func) {
setOnClickHandler(func: ((p1: FieldImage) => AnyDuringMigration)|null) {
this.clickHandler_ = func;
}
@@ -258,24 +233,22 @@ class FieldImage extends Field {
* Use the `getText_` developer hook to override the field's text
* representation.
* Return the image alt text instead.
* @return {?string} The image alt text.
* @protected
* @override
* @return The image alt text.
*/
getText_() {
protected override getText_(): string|null {
return this.altText_;
}
/**
* Construct a FieldImage from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (src, width, height,
* alt, and flipRtl).
* @return {!FieldImage} The new field instance.
* @package
* @param options A JSON object with options (src, width, height, alt, and
* flipRtl).
* @return The new field instance.
* @nocollapse
* @internal
*/
static fromJson(options) {
static fromJson(options: AnyDuringMigration): FieldImage {
// `this` might be a subclass of FieldImage if that class doesn't override
// the static fromJson method.
return new this(
@@ -284,21 +257,6 @@ class FieldImage extends Field {
}
}
/**
* The default value for this field.
* @type {*}
* @protected
*/
FieldImage.prototype.DEFAULT_VALUE = '';
/**
* Vertical padding below the image, which is included in the reported height of
* the field.
* @type {number}
* @private
*/
FieldImage.Y_PADDING = 1;
fieldRegistry.register('field_image', FieldImage);
exports.FieldImage = FieldImage;
(FieldImage.prototype as AnyDuringMigration).DEFAULT_VALUE = '';

View File

@@ -8,60 +8,56 @@
* @fileoverview Non-editable, non-serializable text field. Used for titles,
* labels, etc.
*/
'use strict';
/**
* Non-editable, non-serializable text field. Used for titles,
* labels, etc.
* @class
*/
goog.module('Blockly.FieldLabel');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldLabel');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const parsing = goog.require('Blockly.utils.parsing');
const {Field} = goog.require('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
/**
* Class for a non-editable, non-serializable text field.
* @extends {Field}
* @alias Blockly.FieldLabel
*/
class FieldLabel extends Field {
export class FieldLabel extends Field {
/** The html class name to use for this field. */
private class_: string|null = null;
/**
* @param {(string|!Sentinel)=} opt_value The initial value of the
* field. Should cast to a string. Defaults to an empty string if null or
* undefined.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
*/
override EDITABLE = false;
/**
* @param opt_value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_class Optional CSS class for the field's text.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label#creation}
* for a list of properties this parameter supports.
* for a list of properties this parameter supports.
*/
constructor(opt_value, opt_class, opt_config) {
constructor(
opt_value?: string|Sentinel, opt_class?: string,
opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
/**
* The html class name to use for this field.
* @type {?string}
* @private
*/
this.class_ = null;
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
this.EDITABLE = false;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
} else {
@@ -70,33 +66,29 @@ class FieldLabel extends Field {
this.setValue(opt_value);
}
/**
* @override
*/
configure_(config) {
override configure_(config: AnyDuringMigration) {
super.configure_(config);
this.class_ = config['class'];
}
/**
* Create block UI for this label.
* @package
* @internal
*/
initView() {
override initView() {
this.createTextElement_();
if (this.class_) {
dom.addClass(
/** @type {!SVGTextElement} */ (this.textElement_), this.class_);
dom.addClass((this.textElement_), this.class_);
}
}
/**
* Ensure that the input value casts to a valid string.
* @param {*=} opt_newValue The input value.
* @return {?string} A valid string, or null if invalid.
* @protected
* @param opt_newValue The input value.
* @return A valid string, or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
@@ -105,9 +97,9 @@ class FieldLabel extends Field {
/**
* Set the CSS class applied to the field's textElement_.
* @param {?string} cssClass The new CSS class name, or null to remove.
* @param cssClass The new CSS class name, or null to remove.
*/
setClass(cssClass) {
setClass(cssClass: string|null) {
if (this.textElement_) {
// This check isn't necessary, but it's faster than letting removeClass
// figure it out.
@@ -124,12 +116,12 @@ class FieldLabel extends Field {
/**
* Construct a FieldLabel from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @return {!FieldLabel} The new field instance.
* @package
* @param options A JSON object with options (text, and class).
* @return The new field instance.
* @nocollapse
* @internal
*/
static fromJson(options) {
static fromJson(options: AnyDuringMigration): FieldLabel {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldLabel if that class doesn't override
// the static fromJson method.
@@ -137,13 +129,6 @@ class FieldLabel extends Field {
}
}
/**
* The default value for this field.
* @type {*}
* @protected
*/
FieldLabel.prototype.DEFAULT_VALUE = '';
fieldRegistry.register('field_label', FieldLabel);
exports.FieldLabel = FieldLabel;
(FieldLabel.prototype as AnyDuringMigration).DEFAULT_VALUE = '';

View File

@@ -9,7 +9,6 @@
* normal label but is serialized to XML. It may only be
* edited programmatically.
*/
'use strict';
/**
* Non-editable, serializable text field. Behaves like a
@@ -17,57 +16,56 @@
* edited programmatically.
* @class
*/
goog.module('Blockly.FieldLabelSerializable');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldLabelSerializable');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const parsing = goog.require('Blockly.utils.parsing');
const {FieldLabel} = goog.require('Blockly.FieldLabel');
import {FieldLabel} from './field_label.js';
import * as fieldRegistry from './field_registry.js';
import * as parsing from './utils/parsing.js';
/**
* Class for a non-editable, serializable text field.
* @extends {FieldLabel}
* @alias Blockly.FieldLabelSerializable
*/
class FieldLabelSerializable extends FieldLabel {
export class FieldLabelSerializable extends FieldLabel {
/**
* @param {string=} opt_value The initial value of the field. Should cast to a
* string. Defaults to an empty string if null or undefined.
* @param {string=} opt_class Optional CSS class for the field's text.
* @param {Object=} opt_config A map of options used to configure the field.
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
*/
override EDITABLE = false;
/**
* Serializable fields are saved by the XML renderer, non-serializable
* fields are not. This field should be serialized, but only edited
* programmatically.
*/
override SERIALIZABLE = true;
/**
* @param opt_value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined.
* @param opt_class Optional CSS class for the field's text.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/label-serializable#creation}
* for a list of properties this parameter supports.
* for a list of properties this parameter supports.
*/
constructor(opt_value, opt_class, opt_config) {
constructor(
opt_value?: string, opt_class?: string, opt_config?: AnyDuringMigration) {
super(String(opt_value ?? ''), opt_class, opt_config);
/**
* Editable fields usually show some sort of UI indicating they are
* editable. This field should not.
* @type {boolean}
*/
this.EDITABLE = false;
/**
* Serializable fields are saved by the XML renderer, non-serializable
* fields are not. This field should be serialized, but only edited
* programmatically.
* @type {boolean}
*/
this.SERIALIZABLE = true;
}
/**
* Construct a FieldLabelSerializable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and class).
* @return {!FieldLabelSerializable} The new field instance.
* @package
* @param options A JSON object with options (text, and class).
* @return The new field instance.
* @nocollapse
* @override
* @internal
*/
static fromJson(options) {
static override fromJson(options: AnyDuringMigration):
FieldLabelSerializable {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldLabelSerializable if that class
// doesn't override the static fromJson method.
@@ -76,5 +74,3 @@ class FieldLabelSerializable extends FieldLabel {
}
fieldRegistry.register('field_label_serializable', FieldLabelSerializable);
exports.FieldLabelSerializable = FieldLabelSerializable;

View File

@@ -7,124 +7,120 @@
/**
* @fileoverview Text Area field.
*/
'use strict';
/**
* Text Area field.
* @class
*/
goog.module('Blockly.FieldMultilineInput');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldMultilineInput');
const Css = goog.require('Blockly.Css');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const aria = goog.require('Blockly.utils.aria');
const dom = goog.require('Blockly.utils.dom');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const parsing = goog.require('Blockly.utils.parsing');
const userAgent = goog.require('Blockly.utils.userAgent');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
const {Field} = goog.require('Blockly.Field');
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Svg} = goog.require('Blockly.utils.Svg');
import * as Css from './css.js';
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInput} from './field_textinput.js';
import * as aria from './utils/aria.js';
import * as dom from './utils/dom.js';
import {KeyCodes} from './utils/keycodes.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
/**
* Class for an editable text area field.
* @extends {FieldTextInput}
* @alias Blockly.FieldMultilineInput
*/
class FieldMultilineInput extends FieldTextInput {
export class FieldMultilineInput extends FieldTextInput {
/**
* @param {(string|!Sentinel)=} opt_value The initial content of the
* field. Should cast to a string. Defaults to an empty string if null or
* undefined.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator An optional function that is called
* to validate any constraints on what the user entered. Takes the new
* text as an argument and returns either the accepted text, a replacement
* text, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
* for a list of properties this parameter supports.
* The SVG group element that will contain a text element for each text row
* when initialized.
*/
constructor(opt_value, opt_validator, opt_config) {
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGGElement'.
textGroup_: SVGGElement = null as AnyDuringMigration;
/**
* Defines the maximum number of lines of field.
* If exceeded, scrolling functionality is enabled.
*/
protected maxLines_: number = Infinity;
/** Whether Y overflow is currently occurring. */
protected isOverflowedY_ = false;
/**
* @param opt_value The initial content of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_validator An optional function that is called to validate any
* constraints on what the user entered. Takes the new text as an
* argument and returns either the accepted text, a replacement text, or
* null to abort the change.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/multiline-text-input#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value?: string|Sentinel, opt_validator?: Function,
opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
/**
* The SVG group element that will contain a text element for each text row
* when initialized.
* @type {SVGGElement}
*/
this.textGroup_ = null;
/**
* Defines the maximum number of lines of field.
* If exceeded, scrolling functionality is enabled.
* @type {number}
* @protected
*/
this.maxLines_ = Infinity;
/**
* Whether Y overflow is currently occurring.
* @type {boolean}
* @protected
*/
this.isOverflowedY_ = false;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
/**
* @override
*/
configure_(config) {
override configure_(config: AnyDuringMigration) {
super.configure_(config);
config['maxLines'] && this.setMaxLines(config['maxLines']);
}
/**
* Serializes this field's value to XML. Should only be called by Blockly.Xml.
* @param {!Element} fieldElement The element to populate with info about the
* field's state.
* @return {!Element} The element containing info about the field's state.
* @package
* @param fieldElement The element to populate with info about the field's
* state.
* @return The element containing info about the field's state.
* @internal
*/
toXml(fieldElement) {
override toXml(fieldElement: Element): Element {
// Replace '\n' characters with HTML-escaped equivalent '&#10'. This is
// needed so the plain-text representation of the XML produced by
// `Blockly.Xml.domToText` will appear on a single line (this is a
// limitation of the plain-text format).
fieldElement.textContent =
(/** @type {string} */ (this.getValue())).replace(/\n/g, '&#10;');
(this.getValue() as string).replace(/\n/g, '&#10;');
return fieldElement;
}
/**
* Sets the field's value based on the given XML element. Should only be
* called by Blockly.Xml.
* @param {!Element} fieldElement The element containing info about the
* field's state.
* @package
* @param fieldElement The element containing info about the field's state.
* @internal
*/
fromXml(fieldElement) {
this.setValue(fieldElement.textContent.replace(/&#10;/g, '\n'));
override fromXml(fieldElement: Element) {
this.setValue(fieldElement.textContent!.replace(/&#10;/g, '\n'));
}
/**
* Saves this field's value.
* @return {*} The state of this field.
* @package
* @return The state of this field.
* @internal
*/
saveState() {
override saveState(): AnyDuringMigration {
const legacyState = this.saveLegacyState(FieldMultilineInput);
if (legacyState !== null) {
return legacyState;
@@ -134,12 +130,10 @@ class FieldMultilineInput extends FieldTextInput {
/**
* Sets the field's value based on the given state.
* @param {*} state The state of the variable to assign to this variable
* field.
* @override
* @package
* @param state The state of the variable to assign to this variable field.
* @internal
*/
loadState(state) {
override loadState(state: AnyDuringMigration) {
if (this.loadLegacyState(Field, state)) {
return;
}
@@ -148,9 +142,9 @@ class FieldMultilineInput extends FieldTextInput {
/**
* Create the block UI for this field.
* @package
* @internal
*/
initView() {
override initView() {
this.createBorderRect_();
this.textGroup_ = dom.createSvgElement(
Svg.G, {
@@ -162,11 +156,9 @@ class FieldMultilineInput extends FieldTextInput {
/**
* Get the text from this field as displayed on screen. May differ from
* getText due to ellipsis, and other formatting.
* @return {string} Currently displayed text.
* @protected
* @override
* @return Currently displayed text.
*/
getDisplayText_() {
protected override getDisplayText_(): string {
let textLines = this.getText();
if (!textLines) {
// Prevent the field from disappearing if empty.
@@ -205,23 +197,19 @@ class FieldMultilineInput extends FieldTextInput {
* field, and updates the text of the field if it is not currently being
* edited (i.e. handled by the htmlInput_). Is being redefined here to update
* overflow state of the field.
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
* @param newValue The value to be saved. The default validator guarantees
* that this is a string.
*/
doValueUpdate_(newValue) {
protected override doValueUpdate_(newValue: AnyDuringMigration) {
super.doValueUpdate_(newValue);
this.isOverflowedY_ = this.value_.split('\n').length > this.maxLines_;
}
/**
* Updates the text of the textElement.
* @protected
*/
render_() {
/** Updates the text of the textElement. */
protected override render_() {
// Remove all text group children.
let currentChild;
while ((currentChild = this.textGroup_.firstChild)) {
while (currentChild = this.textGroup_.firstChild) {
this.textGroup_.removeChild(currentChild);
}
@@ -229,14 +217,14 @@ class FieldMultilineInput extends FieldTextInput {
const lines = this.getDisplayText_().split('\n');
let y = 0;
for (let i = 0; i < lines.length; i++) {
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
const lineHeight = this.getConstants()!.FIELD_TEXT_HEIGHT +
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING;
const span = dom.createSvgElement(
Svg.TEXT, {
'class': 'blocklyText blocklyMultilineText',
'x': this.getConstants().FIELD_BORDER_RECT_X_PADDING,
'y': y + this.getConstants().FIELD_BORDER_RECT_Y_PADDING,
'dy': this.getConstants().FIELD_TEXT_BASELINE,
'x': this.getConstants()!.FIELD_BORDER_RECT_X_PADDING,
'y': y + this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING,
'dy': this.getConstants()!.FIELD_TEXT_BASELINE,
},
this.textGroup_);
span.appendChild(document.createTextNode(lines[i]));
@@ -244,7 +232,7 @@ class FieldMultilineInput extends FieldTextInput {
}
if (this.isBeingEdited_) {
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
const htmlInput = this.htmlInput_ as HTMLElement;
if (this.isOverflowedY_) {
dom.addClass(htmlInput, 'blocklyHtmlTextAreaInputOverflowedY');
} else {
@@ -263,7 +251,7 @@ class FieldMultilineInput extends FieldTextInput {
} else {
this.resizeEditor_();
}
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
const htmlInput = this.htmlInput_ as HTMLElement;
if (!this.isTextValid_) {
dom.addClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, true);
@@ -274,22 +262,19 @@ class FieldMultilineInput extends FieldTextInput {
}
}
/**
* Updates the size of the field based on the text.
* @protected
*/
updateSize_() {
/** Updates the size of the field based on the text. */
protected override updateSize_() {
const nodes = this.textGroup_.childNodes;
let totalWidth = 0;
let totalHeight = 0;
for (let i = 0; i < nodes.length; i++) {
const tspan = /** @type {!SVGTextElement} */ (nodes[i]);
const tspan = nodes[i] as SVGTextElement;
const textWidth = dom.getTextWidth(tspan);
if (textWidth > totalWidth) {
totalWidth = textWidth;
}
totalHeight += this.getConstants().FIELD_TEXT_HEIGHT +
(i > 0 ? this.getConstants().FIELD_BORDER_RECT_Y_PADDING : 0);
totalHeight += this.getConstants()!.FIELD_TEXT_HEIGHT +
(i > 0 ? this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING : 0);
}
if (this.isBeingEdited_) {
// The default width is based on the longest line in the display text,
@@ -300,9 +285,9 @@ class FieldMultilineInput extends FieldTextInput {
const actualEditorLines = this.value_.split('\n');
const dummyTextElement = dom.createSvgElement(
Svg.TEXT, {'class': 'blocklyText blocklyMultilineText'});
const fontSize = this.getConstants().FIELD_TEXT_FONTSIZE;
const fontWeight = this.getConstants().FIELD_TEXT_FONTWEIGHT;
const fontFamily = this.getConstants().FIELD_TEXT_FONTFAMILY;
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE;
const fontWeight = this.getConstants()!.FIELD_TEXT_FONTWEIGHT;
const fontFamily = this.getConstants()!.FIELD_TEXT_FONTFAMILY;
for (let i = 0; i < actualEditorLines.length; i++) {
if (actualEditorLines[i].length > this.maxDisplayLength) {
@@ -318,14 +303,19 @@ class FieldMultilineInput extends FieldTextInput {
}
const scrollbarWidth =
this.htmlInput_.offsetWidth - this.htmlInput_.clientWidth;
this.htmlInput_!.offsetWidth - this.htmlInput_!.clientWidth;
totalWidth += scrollbarWidth;
}
if (this.borderRect_) {
totalHeight += this.getConstants().FIELD_BORDER_RECT_Y_PADDING * 2;
totalWidth += this.getConstants().FIELD_BORDER_RECT_X_PADDING * 2;
this.borderRect_.setAttribute('width', totalWidth);
this.borderRect_.setAttribute('height', totalHeight);
totalHeight += this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING * 2;
totalWidth += this.getConstants()!.FIELD_BORDER_RECT_X_PADDING * 2;
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
this.borderRect_.setAttribute('width', totalWidth as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
this.borderRect_.setAttribute(
'height', totalHeight as AnyDuringMigration);
}
this.size_.width = totalWidth;
this.size_.height = totalHeight;
@@ -337,45 +327,45 @@ class FieldMultilineInput extends FieldTextInput {
* Show the inline free-text editor on top of the text.
* Overrides the default behaviour to force rerender in order to
* correct block size, based on editor text.
* @param {Event=} _opt_e Optional mouse event that triggered the field to
* open, or undefined if triggered programmatically.
* @param {boolean=} opt_quietInput True if editor should be created without
* focus. Defaults to false.
* @override
* @param _opt_e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
* @param opt_quietInput True if editor should be created without focus.
* Defaults to false.
*/
showEditor_(_opt_e, opt_quietInput) {
override showEditor_(_opt_e?: Event, opt_quietInput?: boolean) {
super.showEditor_(_opt_e, opt_quietInput);
this.forceRerender();
}
/**
* Create the text input editor widget.
* @return {!HTMLTextAreaElement} The newly created text input editor.
* @protected
* @return The newly created text input editor.
*/
widgetCreate_() {
protected override widgetCreate_(): HTMLTextAreaElement {
const div = WidgetDiv.getDiv();
const scale = this.workspace_.getScale();
const scale = this.workspace_!.getScale();
const htmlInput =
/** @type {HTMLTextAreaElement} */ (document.createElement('textarea'));
const htmlInput = (document.createElement('textarea'));
htmlInput.className = 'blocklyHtmlInput blocklyHtmlTextAreaInput';
htmlInput.setAttribute('spellcheck', this.spellcheck_);
const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
div.style.fontSize = fontSize;
// AnyDuringMigration because: Argument of type 'boolean' is not assignable
// to parameter of type 'string'.
htmlInput.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration);
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
div!.style.fontSize = fontSize;
htmlInput.style.fontSize = fontSize;
const borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
const borderRadius = FieldTextInput.BORDERRADIUS * scale + 'px';
htmlInput.style.borderRadius = borderRadius;
const paddingX = this.getConstants().FIELD_BORDER_RECT_X_PADDING * scale;
const paddingX = this.getConstants()!.FIELD_BORDER_RECT_X_PADDING * scale;
const paddingY =
this.getConstants().FIELD_BORDER_RECT_Y_PADDING * scale / 2;
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING * scale / 2;
htmlInput.style.padding = paddingY + 'px ' + paddingX + 'px ' + paddingY +
'px ' + paddingX + 'px';
const lineHeight = this.getConstants().FIELD_TEXT_HEIGHT +
this.getConstants().FIELD_BORDER_RECT_Y_PADDING;
htmlInput.style.lineHeight = (lineHeight * scale) + 'px';
const lineHeight = this.getConstants()!.FIELD_TEXT_HEIGHT +
this.getConstants()!.FIELD_BORDER_RECT_Y_PADDING;
htmlInput.style.lineHeight = lineHeight * scale + 'px';
div.appendChild(htmlInput);
div!.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.setAttribute('data-untyped-default-value', this.value_);
@@ -394,10 +384,10 @@ class FieldMultilineInput extends FieldTextInput {
/**
* Sets the maxLines config for this field.
* @param {number} maxLines Defines the maximum number of lines allowed,
* before scrolling functionality is enabled.
* @param maxLines Defines the maximum number of lines allowed, before
* scrolling functionality is enabled.
*/
setMaxLines(maxLines) {
setMaxLines(maxLines: number) {
if (typeof maxLines === 'number' && maxLines > 0 &&
maxLines !== this.maxLines_) {
this.maxLines_ = maxLines;
@@ -407,20 +397,21 @@ class FieldMultilineInput extends FieldTextInput {
/**
* Returns the maxLines config of this field.
* @return {number} The maxLines config value.
* @return The maxLines config value.
*/
getMaxLines() {
getMaxLines(): number {
return this.maxLines_;
}
/**
* Handle key down to the editor. Override the text input definition of this
* so as to not close the editor when enter is typed in.
* @param {!Event} e Keyboard event.
* @protected
* @param e Keyboard event.
*/
onHtmlInputKeyDown_(e) {
if (e.keyCode !== KeyCodes.ENTER) {
protected override onHtmlInputKeyDown_(e: Event) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).keyCode !== KeyCodes.ENTER) {
super.onHtmlInputKeyDown_(e);
}
}
@@ -428,13 +419,12 @@ class FieldMultilineInput extends FieldTextInput {
/**
* Construct a FieldMultilineInput from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and spellcheck).
* @return {!FieldMultilineInput} The new field instance.
* @package
* @param options A JSON object with options (text, and spellcheck).
* @return The new field instance.
* @nocollapse
* @override
* @internal
*/
static fromJson(options) {
static override fromJson(options: AnyDuringMigration): FieldMultilineInput {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldMultilineInput if that class doesn't
// override the static fromJson method.
@@ -442,9 +432,7 @@ class FieldMultilineInput extends FieldTextInput {
}
}
/**
* CSS for multiline field. See css.js for use.
*/
/** CSS for multiline field. See css.js for use. */
Css.register(`
.blocklyHtmlTextAreaInput {
font-family: monospace;
@@ -460,5 +448,3 @@ Css.register(`
`);
fieldRegistry.register('field_multilinetext', FieldMultilineInput);
exports.FieldMultilineInput = FieldMultilineInput;

View File

@@ -7,106 +7,92 @@
/**
* @fileoverview Number input field
*/
'use strict';
/**
* Number input field
* @class
*/
goog.module('Blockly.FieldNumber');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldNumber');
const aria = goog.require('Blockly.utils.aria');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const {Field} = goog.require('Blockly.Field');
const {FieldTextInput} = goog.require('Blockly.FieldTextInput');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {FieldTextInput} from './field_textinput.js';
import * as aria from './utils/aria.js';
import type {Sentinel} from './utils/sentinel.js';
/**
* Class for an editable number field.
* @extends {FieldTextInput}
* @alias Blockly.FieldNumber
*/
class FieldNumber extends FieldTextInput {
export class FieldNumber extends FieldTextInput {
/** The minimum value this number field can contain. */
protected min_: number = -Infinity;
/** The maximum value this number field can contain. */
protected max_: number = Infinity;
/** The multiple to which this fields value is rounded. */
protected precision_ = 0;
/**
* @param {(string|number|!Sentinel)=} opt_value The initial value of
* the field. Should cast to a number. Defaults to 0.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {?(string|number)=} opt_min Minimum value. Will only be used if
* opt_config is not provided.
* @param {?(string|number)=} opt_max Maximum value. Will only be used if
* opt_config is not provided.
* @param {?(string|number)=} opt_precision Precision for value. Will only be
* used if opt_config is not provided.
* @param {?Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a number & returns a validated
* number, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* The number of decimal places to allow, or null to allow any number of
* decimal digits.
*/
private decimalPlaces_: number|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/**
* @param opt_value The initial value of the field. Should cast to a number.
* Defaults to 0. Also accepts Field.SKIP_SETUP if you wish to skip setup
* (only used by subclasses that want to handle configuration and setting
* the field value after their own constructors have run).
* @param opt_min Minimum value. Will only be used if opt_config is not
* provided.
* @param opt_max Maximum value. Will only be used if opt_config is not
* provided.
* @param opt_precision Precision for value. Will only be used if opt_config
* is not provided.
* @param opt_validator A function that is called to validate changes to the
* field's value. Takes in a number & returns a validated number, or null
* to abort the change.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/number#creation}
* for a list of properties this parameter supports.
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/number#creation}
* for a list of properties this parameter supports.
*/
constructor(
opt_value, opt_min, opt_max, opt_precision, opt_validator, opt_config) {
opt_value?: string|number|Sentinel, opt_min?: string|number|null,
opt_max?: string|number|null, opt_precision?: string|number|null,
opt_validator?: Function|null, opt_config?: AnyDuringMigration) {
// Pass SENTINEL so that we can define properties before value validation.
super(Field.SKIP_SETUP);
/**
* The minimum value this number field can contain.
* @type {number}
* @protected
*/
this.min_ = -Infinity;
/**
* The maximum value this number field can contain.
* @type {number}
* @protected
*/
this.max_ = Infinity;
/**
* The multiple to which this fields value is rounded.
* @type {number}
* @protected
*/
this.precision_ = 0;
/**
* The number of decimal places to allow, or null to allow any number of
* decimal digits.
* @type {?number}
* @private
*/
this.decimalPlaces_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (opt_value === Field.SKIP_SETUP) return;
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
} else {
this.setConstraints(opt_min, opt_max, opt_precision);
}
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @override
* @param config A map of options to configure the field based on.
*/
configure_(config) {
override configure_(config: AnyDuringMigration) {
super.configure_(config);
this.setMinInternal_(config['min']);
this.setMaxInternal_(config['max']);
@@ -121,11 +107,13 @@ class FieldNumber extends FieldTextInput {
* precision. The least significant digit place is inferred from the
* precision. Integers values can be enforces by choosing an integer
* precision.
* @param {?(number|string|undefined)} min Minimum value.
* @param {?(number|string|undefined)} max Maximum value.
* @param {?(number|string|undefined)} precision Precision for value.
* @param min Minimum value.
* @param max Maximum value.
* @param precision Precision for value.
*/
setConstraints(min, max, precision) {
setConstraints(
min: number|string|undefined|null, max: number|string|undefined|null,
precision: number|string|undefined|null) {
this.setMinInternal_(min);
this.setMaxInternal_(max);
this.setPrecisionInternal_(precision);
@@ -135,9 +123,9 @@ class FieldNumber extends FieldTextInput {
/**
* Sets the minimum value this field can contain. Updates the value to
* reflect.
* @param {?(number|string|undefined)} min Minimum value.
* @param min Minimum value.
*/
setMin(min) {
setMin(min: number|string|undefined|null) {
this.setMinInternal_(min);
this.setValue(this.getValue());
}
@@ -145,10 +133,9 @@ class FieldNumber extends FieldTextInput {
/**
* Sets the minimum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} min Minimum value.
* @private
* @param min Minimum value.
*/
setMinInternal_(min) {
private setMinInternal_(min: number|string|undefined|null) {
if (min == null) {
this.min_ = -Infinity;
} else {
@@ -162,18 +149,18 @@ class FieldNumber extends FieldTextInput {
/**
* Returns the current minimum value this field can contain. Default is
* -Infinity.
* @return {number} The current minimum value this field can contain.
* @return The current minimum value this field can contain.
*/
getMin() {
getMin(): number {
return this.min_;
}
/**
* Sets the maximum value this field can contain. Updates the value to
* reflect.
* @param {?(number|string|undefined)} max Maximum value.
* @param max Maximum value.
*/
setMax(max) {
setMax(max: number|string|undefined|null) {
this.setMaxInternal_(max);
this.setValue(this.getValue());
}
@@ -181,10 +168,9 @@ class FieldNumber extends FieldTextInput {
/**
* Sets the maximum value this field can contain. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} max Maximum value.
* @private
* @param max Maximum value.
*/
setMaxInternal_(max) {
private setMaxInternal_(max: number|string|undefined|null) {
if (max == null) {
this.max_ = Infinity;
} else {
@@ -198,19 +184,18 @@ class FieldNumber extends FieldTextInput {
/**
* Returns the current maximum value this field can contain. Default is
* Infinity.
* @return {number} The current maximum value this field can contain.
* @return The current maximum value this field can contain.
*/
getMax() {
getMax(): number {
return this.max_;
}
/**
* Sets the precision of this field's value, i.e. the number to which the
* value is rounded. Updates the field to reflect.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
* @param precision The number to which the field's value is rounded.
*/
setPrecision(precision) {
setPrecision(precision: number|string|undefined|null) {
this.setPrecisionInternal_(precision);
this.setValue(this.getValue());
}
@@ -218,11 +203,9 @@ class FieldNumber extends FieldTextInput {
/**
* Sets the precision of this field's value. Called internally to avoid
* value updates.
* @param {?(number|string|undefined)} precision The number to which the
* field's value is rounded.
* @private
* @param precision The number to which the field's value is rounded.
*/
setPrecisionInternal_(precision) {
private setPrecisionInternal_(precision: number|string|undefined|null) {
this.precision_ = Number(precision) || 0;
let precisionString = String(this.precision_);
if (precisionString.indexOf('e') !== -1) {
@@ -245,21 +228,20 @@ class FieldNumber extends FieldTextInput {
* Returns the current precision of this field. The precision being the
* number to which the field's value is rounded. A precision of 0 means that
* the value is not rounded.
* @return {number} The number to which this field's value is rounded.
* @return The number to which this field's value is rounded.
*/
getPrecision() {
getPrecision(): number {
return this.precision_;
}
/**
* Ensure that the input value is a valid number (must fulfill the
* constraints placed on the field).
* @param {*=} opt_newValue The input value.
* @return {?number} A valid number, or null if invalid.
* @protected
* @override
* @param opt_newValue The input value.
* @return A valid number, or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
number|null {
if (opt_newValue === null) {
return null;
}
@@ -294,11 +276,9 @@ class FieldNumber extends FieldTextInput {
/**
* Create the number input editor widget.
* @return {!HTMLElement} The newly created number input editor.
* @protected
* @override
* @return The newly created number input editor.
*/
widgetCreate_() {
protected override widgetCreate_(): HTMLElement {
const htmlInput = super.widgetCreate_();
// Set the accessibility state
@@ -313,14 +293,12 @@ class FieldNumber extends FieldTextInput {
/**
* Construct a FieldNumber from a JSON arg object.
* @param {!Object} options A JSON object with options (value, min, max, and
* precision).
* @return {!FieldNumber} The new field instance.
* @package
* @param options A JSON object with options (value, min, max, and precision).
* @return The new field instance.
* @nocollapse
* @override
* @internal
*/
static fromJson(options) {
static override fromJson(options: AnyDuringMigration): FieldNumber {
// `this` might be a subclass of FieldNumber if that class doesn't override
// the static fromJson method.
return new this(
@@ -328,13 +306,6 @@ class FieldNumber extends FieldTextInput {
}
}
/**
* The default value for this field.
* @type {*}
* @protected
*/
FieldNumber.prototype.DEFAULT_VALUE = 0;
fieldRegistry.register('field_number', FieldNumber);
exports.FieldNumber = FieldNumber;
(FieldNumber.prototype as AnyDuringMigration).DEFAULT_VALUE = 0;

View File

@@ -9,7 +9,6 @@
* contains methods for registering those JSON definitions, and building the
* fields based on JSON.
*/
'use strict';
/**
* Fields can be created based on a JSON definition. This file
@@ -17,56 +16,61 @@
* fields based on JSON.
* @namespace Blockly.fieldRegistry
*/
goog.module('Blockly.fieldRegistry');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.fieldRegistry');
const registry = goog.require('Blockly.registry');
/* eslint-disable-next-line no-unused-vars */
const {Field} = goog.requireType('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {IRegistrableField} = goog.requireType('Blockly.IRegistrableField');
import type {Field} from './field.js';
import type {IRegistrableField} from './interfaces/i_registrable_field.js';
import * as registry from './registry.js';
/**
* Registers a field type.
* fieldRegistry.fromJson uses this registry to
* find the appropriate field type.
* @param {string} type The field type name as used in the JSON definition.
* @param {!IRegistrableField} fieldClass The field class containing a
* fromJson function that can construct an instance of the field.
* @throws {Error} if the type name is empty, the field is already
* registered, or the fieldClass is not an object containing a fromJson
* function.
* @param type The field type name as used in the JSON definition.
* @param fieldClass The field class containing a fromJson function that can
* construct an instance of the field.
* @throws {Error} if the type name is empty, the field is already registered,
* or the fieldClass is not an object containing a fromJson function.
* @alias Blockly.fieldRegistry.register
*/
const register = function(type, fieldClass) {
export function register(type: string, fieldClass: IRegistrableField) {
registry.register(registry.Type.FIELD, type, fieldClass);
};
exports.register = register;
}
/**
* Unregisters the field registered with the given type.
* @param {string} type The field type name as used in the JSON definition.
* @param type The field type name as used in the JSON definition.
* @alias Blockly.fieldRegistry.unregister
*/
const unregister = function(type) {
export function unregister(type: string) {
registry.unregister(registry.Type.FIELD, type);
};
exports.unregister = unregister;
}
/**
* Construct a Field from a JSON arg object.
* Finds the appropriate registered field by the type name as registered using
* fieldRegistry.register.
* @param {!Object} options A JSON object with a type and options specific
* to the field type.
* @return {?Field} The new field instance or null if a field wasn't
* found with the given type name
* @param options A JSON object with a type and options specific to the field
* type.
* @return The new field instance or null if a field wasn't found with the given
* type name
* @alias Blockly.fieldRegistry.fromJson
* @package
* @internal
*/
const fromJson = function(options) {
const fieldObject = /** @type {?IRegistrableField} */ (
registry.getObject(registry.Type.FIELD, options['type']));
export function fromJson(options: AnyDuringMigration): Field|null {
return TEST_ONLY.fromJsonInternal(options);
}
/**
* Private version of fromJson for stubbing in tests.
*/
function fromJsonInternal(options: AnyDuringMigration): Field|null {
const fieldObject =
registry.getObject(registry.Type.FIELD, options['type']) as
IRegistrableField |
null;
if (!fieldObject) {
console.warn(
'Blockly could not create a field of type ' + options['type'] +
@@ -76,5 +80,8 @@ const fromJson = function(options) {
return null;
}
return fieldObject.fromJson(options);
};
exports.fromJson = fromJson;
}
export const TEST_ONLY = {
fromJsonInternal,
}

View File

@@ -7,159 +7,136 @@
/**
* @fileoverview Text input field.
*/
'use strict';
/**
* Text input field.
* @class
*/
goog.module('Blockly.FieldTextInput');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldTextInput');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const aria = goog.require('Blockly.utils.aria');
const browserEvents = goog.require('Blockly.browserEvents');
const dialog = goog.require('Blockly.dialog');
const dom = goog.require('Blockly.utils.dom');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const eventUtils = goog.require('Blockly.Events.utils');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const parsing = goog.require('Blockly.utils.parsing');
const userAgent = goog.require('Blockly.utils.userAgent');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
const {Field} = goog.require('Blockly.Field');
const {KeyCodes} = goog.require('Blockly.utils.KeyCodes');
const {Msg} = goog.require('Blockly.Msg');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockChange');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import * as dialog from './dialog.js';
import * as dropDownDiv from './dropdowndiv.js';
import * as eventUtils from './events/utils.js';
import {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {Msg} from './msg.js';
import * as aria from './utils/aria.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import {KeyCodes} from './utils/keycodes.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Class for an editable text field.
* @alias Blockly.FieldTextInput
*/
class FieldTextInput extends Field {
export class FieldTextInput extends Field {
/**
* @param {(string|!Sentinel)=} opt_value The initial value of the
* field. Should cast to a string. Defaults to an empty string if null or
* undefined.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {?Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a string & returns a validated
* string, or null to abort the change.
* @param {Object=} opt_config A map of options used to configure the field.
* Pixel size of input border radius.
* Should match blocklyText's border-radius in CSS.
*/
static BORDERRADIUS = 4;
/** Allow browser to spellcheck this field. */
protected spellcheck_ = true;
/** The HTML input element. */
protected htmlInput_: HTMLInputElement|null = null;
/** True if the field's value is currently being edited via the UI. */
protected isBeingEdited_ = false;
/**
* True if the value currently displayed in the field's editory UI is valid.
*/
protected isTextValid_ = false;
/** Key down event data. */
private onKeyDownWrapper_: browserEvents.Data|null = null;
/** Key input event data. */
private onKeyInputWrapper_: browserEvents.Data|null = null;
/**
* Whether the field should consider the whole parent block to be its click
* target.
*/
fullBlockClickTarget_: boolean|null = false;
/** The workspace that this field belongs to. */
protected workspace_: WorkspaceSvg|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/** Mouse cursor style when over the hotspot that initiates the editor. */
override CURSOR = 'text';
override clickTarget_: AnyDuringMigration;
override value_: AnyDuringMigration;
override isDirty_: AnyDuringMigration;
/**
* @param opt_value The initial value of the field. Should cast to a string.
* Defaults to an empty string if null or undefined. Also accepts
* Field.SKIP_SETUP if you wish to skip setup (only used by subclasses
* that want to handle configuration and setting the field value after
* their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* field's value. Takes in a string & returns a validated string, or null
* to abort the change.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/text-input#creation}
* for a list of properties this parameter supports.
* for a list of properties this parameter supports.
*/
constructor(opt_value, opt_validator, opt_config) {
constructor(
opt_value?: string|Sentinel, opt_validator?: Function|null,
opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
/**
* Allow browser to spellcheck this field.
* @type {boolean}
* @protected
*/
this.spellcheck_ = true;
/**
* The HTML input element.
* @type {?HTMLInputElement}
* @protected
*/
this.htmlInput_ = null;
/**
* True if the field's value is currently being edited via the UI.
* @type {boolean}
* @private
*/
this.isBeingEdited_ = false;
/**
* True if the value currently displayed in the field's editory UI is valid.
* @type {boolean}
* @private
*/
this.isTextValid_ = false;
/**
* Key down event data.
* @type {?browserEvents.Data}
* @private
*/
this.onKeyDownWrapper_ = null;
/**
* Key input event data.
* @type {?browserEvents.Data}
* @private
*/
this.onKeyInputWrapper_ = null;
/**
* Whether the field should consider the whole parent block to be its click
* target.
* @type {?boolean}
*/
this.fullBlockClickTarget_ = false;
/**
* The workspace that this field belongs to.
* @type {?WorkspaceSvg}
* @protected
*/
this.workspace_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
/**
* Mouse cursor style when over the hotspot that initiates the editor.
* @type {string}
*/
this.CURSOR = 'text';
if (opt_value === Field.SKIP_SETUP) return;
if (opt_config) this.configure_(opt_config);
if (opt_value === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
}
this.setValue(opt_value);
if (opt_validator) this.setValidator(opt_validator);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
/**
* @override
*/
configure_(config) {
override configure_(config: AnyDuringMigration) {
super.configure_(config);
if (typeof config['spellcheck'] === 'boolean') {
this.spellcheck_ = config['spellcheck'];
}
}
/**
* @override
*/
initView() {
if (this.getConstants().FULL_BLOCK_FIELDS) {
/** @internal */
override initView() {
if (this.getConstants()!.FULL_BLOCK_FIELDS) {
// Step one: figure out if this is the only field on this block.
// Rendering is quite different in that case.
let nFields = 0;
let nConnections = 0;
// Count the number of fields, excluding text fields
for (let i = 0, input; (input = this.sourceBlock_.inputList[i]); i++) {
for (let j = 0; (input.fieldRow[j]); j++) {
for (let i = 0, input; input = this.sourceBlock_.inputList[i]; i++) {
for (let j = 0; input.fieldRow[j]; j++) {
nFields++;
}
if (input.connection) {
@@ -175,8 +152,7 @@ class FieldTextInput extends Field {
}
if (this.fullBlockClickTarget_) {
this.clickTarget_ =
(/** @type {!BlockSvg} */ (this.sourceBlock_)).getSvgRoot();
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
} else {
this.createBorderRect_();
}
@@ -185,11 +161,11 @@ class FieldTextInput extends Field {
/**
* Ensure that the input value casts to a valid string.
* @param {*=} opt_newValue The input value.
* @return {*} A valid string, or null if invalid.
* @protected
* @param opt_newValue The input value.
* @return A valid string, or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
AnyDuringMigration {
if (opt_newValue === null || opt_newValue === undefined) {
return null;
}
@@ -200,21 +176,20 @@ class FieldTextInput extends Field {
* Called by setValue if the text input is not valid. If the field is
* currently being edited it reverts value of the field to the previous
* value while allowing the display text to be handled by the htmlInput_.
* @param {*} _invalidValue The input value that was determined to be invalid.
* @param _invalidValue The input value that was determined to be invalid.
* This is not used by the text input because its display value is stored
* on the htmlInput_.
* @protected
*/
doValueInvalid_(_invalidValue) {
protected override doValueInvalid_(_invalidValue: AnyDuringMigration) {
if (this.isBeingEdited_) {
this.isTextValid_ = false;
const oldValue = this.value_;
// Revert value when the text becomes invalid.
this.value_ = this.htmlInput_.getAttribute('data-untyped-default-value');
this.value_ = this.htmlInput_!.getAttribute('data-untyped-default-value');
if (this.sourceBlock_ && eventUtils.isEnabled()) {
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))(
this.sourceBlock_, 'field', this.name || null, oldValue,
this.value_));
eventUtils.fire(new (eventUtils.get(eventUtils.BLOCK_CHANGE))!
(this.sourceBlock_, 'field', this.name || null,
oldValue, this.value_));
}
}
}
@@ -223,11 +198,10 @@ class FieldTextInput extends Field {
* Called by setValue if the text input is valid. Updates the value of the
* field, and updates the text of the field if it is not currently being
* edited (i.e. handled by the htmlInput_).
* @param {*} newValue The value to be saved. The default validator guarantees
* that this is a string.
* @protected
* @param newValue The value to be saved. The default validator guarantees
* that this is a string.
*/
doValueUpdate_(newValue) {
protected override doValueUpdate_(newValue: AnyDuringMigration) {
this.isTextValid_ = true;
this.value_ = newValue;
if (!this.isBeingEdited_) {
@@ -238,19 +212,17 @@ class FieldTextInput extends Field {
/**
* Updates text field to match the colour/style of the block.
* @package
* @internal
*/
applyColour() {
if (this.sourceBlock_ && this.getConstants().FULL_BLOCK_FIELDS) {
override applyColour() {
if (this.sourceBlock_ && this.getConstants()!.FULL_BLOCK_FIELDS) {
if (this.borderRect_) {
this.borderRect_.setAttribute(
'stroke',
(/** @type {!BlockSvg} */ (this.sourceBlock_))
.style.colourTertiary);
'stroke', (this.sourceBlock_ as BlockSvg).style.colourTertiary);
} else {
(/** @type {!BlockSvg} */ (this.sourceBlock_))
(this.sourceBlock_ as BlockSvg)
.pathObject.svgPath.setAttribute(
'fill', this.getConstants().FIELD_BORDER_RECT_COLOUR);
'fill', this.getConstants()!.FIELD_BORDER_RECT_COLOUR);
}
}
}
@@ -258,15 +230,14 @@ class FieldTextInput extends Field {
/**
* Updates the colour of the htmlInput given the current validity of the
* field's value.
* @protected
*/
render_() {
protected override render_() {
super.render_();
// This logic is done in render_ rather than doValueInvalid_ or
// doValueUpdate_ so that the code is more centralized.
if (this.isBeingEdited_) {
this.resizeEditor_();
const htmlInput = /** @type {!HTMLElement} */ (this.htmlInput_);
const htmlInput = this.htmlInput_ as HTMLElement;
if (!this.isTextValid_) {
dom.addClass(htmlInput, 'blocklyInvalidInput');
aria.setState(htmlInput, aria.State.INVALID, true);
@@ -279,28 +250,30 @@ class FieldTextInput extends Field {
/**
* Set whether this field is spellchecked by the browser.
* @param {boolean} check True if checked.
* @param check True if checked.
*/
setSpellcheck(check) {
setSpellcheck(check: boolean) {
if (check === this.spellcheck_) {
return;
}
this.spellcheck_ = check;
if (this.htmlInput_) {
this.htmlInput_.setAttribute('spellcheck', this.spellcheck_);
// AnyDuringMigration because: Argument of type 'boolean' is not
// assignable to parameter of type 'string'.
this.htmlInput_.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration);
}
}
/**
* Show the inline free-text editor on top of the text.
* @param {Event=} _opt_e Optional mouse event that triggered the field to
* open, or undefined if triggered programmatically.
* @param {boolean=} opt_quietInput True if editor should be created without
* focus. Defaults to false.
* @protected
* @param _opt_e Optional mouse event that triggered the field to open, or
* undefined if triggered programmatically.
* @param opt_quietInput True if editor should be created without focus.
* Defaults to false.
*/
showEditor_(_opt_e, opt_quietInput) {
this.workspace_ = (/** @type {!BlockSvg} */ (this.sourceBlock_)).workspace;
protected override showEditor_(_opt_e?: Event, opt_quietInput?: boolean) {
this.workspace_ = (this.sourceBlock_ as BlockSvg).workspace;
const quietInput = opt_quietInput || false;
if (!quietInput &&
(userAgent.MOBILE || userAgent.ANDROID || userAgent.IPAD)) {
@@ -313,30 +286,28 @@ class FieldTextInput extends Field {
/**
* Create and show a text input editor that is a prompt (usually a popup).
* Mobile browsers have issues with in-line textareas (focus and keyboards).
* @private
*/
showPromptEditor_() {
dialog.prompt(Msg['CHANGE_VALUE_TITLE'], this.getText(), function(text) {
// Text is null if user pressed cancel button.
if (text !== null) {
this.setValue(this.getValueFromEditorText_(text));
}
}.bind(this));
private showPromptEditor_() {
dialog.prompt(
Msg['CHANGE_VALUE_TITLE'], this.getText(), (text: string|null) => {
// Text is null if user pressed cancel button.
if (text !== null) {
this.setValue(this.getValueFromEditorText_(text));
}
});
}
/**
* Create and show a text input editor that sits directly over the text input.
* @param {boolean} quietInput True if editor should be created without
* focus.
* @private
* @param quietInput True if editor should be created without focus.
*/
showInlineEditor_(quietInput) {
private showInlineEditor_(quietInput: boolean) {
WidgetDiv.show(this, this.sourceBlock_.RTL, this.widgetDispose_.bind(this));
this.htmlInput_ = /** @type {!HTMLInputElement} */ (this.widgetCreate_());
this.htmlInput_ = this.widgetCreate_() as HTMLInputElement;
this.isBeingEdited_ = true;
if (!quietInput) {
(/** @type {!HTMLElement} */ (this.htmlInput_)).focus({
(this.htmlInput_ as HTMLElement).focus({
preventScroll: true,
});
this.htmlInput_.select();
@@ -345,24 +316,25 @@ class FieldTextInput extends Field {
/**
* Create the text input editor widget.
* @return {!HTMLElement} The newly created text input editor.
* @protected
* @return The newly created text input editor.
*/
widgetCreate_() {
protected widgetCreate_(): HTMLElement {
eventUtils.setGroup(true);
const div = WidgetDiv.getDiv();
dom.addClass(this.getClickTarget_(), 'editing');
const htmlInput =
/** @type {HTMLInputElement} */ (document.createElement('input'));
const htmlInput = (document.createElement('input'));
htmlInput.className = 'blocklyHtmlInput';
htmlInput.setAttribute('spellcheck', this.spellcheck_);
const scale = this.workspace_.getScale();
const fontSize = (this.getConstants().FIELD_TEXT_FONTSIZE * scale) + 'pt';
div.style.fontSize = fontSize;
// AnyDuringMigration because: Argument of type 'boolean' is not assignable
// to parameter of type 'string'.
htmlInput.setAttribute(
'spellcheck', this.spellcheck_ as AnyDuringMigration);
const scale = this.workspace_!.getScale();
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
div!.style.fontSize = fontSize;
htmlInput.style.fontSize = fontSize;
let borderRadius = (FieldTextInput.BORDERRADIUS * scale) + 'px';
let borderRadius = FieldTextInput.BORDERRADIUS * scale + 'px';
if (this.fullBlockClickTarget_) {
const bBox = this.getScaledBBox();
@@ -371,20 +343,19 @@ class FieldTextInput extends Field {
borderRadius = (bBox.bottom - bBox.top) / 2 + 'px';
// Pull stroke colour from the existing shadow block
const strokeColour = this.sourceBlock_.getParent() ?
(/** @type {!BlockSvg} */ (this.sourceBlock_.getParent()))
.style.colourTertiary :
(/** @type {!BlockSvg} */ (this.sourceBlock_)).style.colourTertiary;
htmlInput.style.border = (1 * scale) + 'px solid ' + strokeColour;
div.style.borderRadius = borderRadius;
div.style.transition = 'box-shadow 0.25s ease 0s';
if (this.getConstants().FIELD_TEXTINPUT_BOX_SHADOW) {
div.style.boxShadow =
'rgba(255, 255, 255, 0.3) 0 0 0 ' + (4 * scale) + 'px';
(this.sourceBlock_.getParent() as BlockSvg).style.colourTertiary :
(this.sourceBlock_ as BlockSvg).style.colourTertiary;
htmlInput.style.border = 1 * scale + 'px solid ' + strokeColour;
div!.style.borderRadius = borderRadius;
div!.style.transition = 'box-shadow 0.25s ease 0s';
if (this.getConstants()!.FIELD_TEXTINPUT_BOX_SHADOW) {
div!.style.boxShadow =
'rgba(255, 255, 255, 0.3) 0 0 0 ' + 4 * scale + 'px';
}
}
htmlInput.style.borderRadius = borderRadius;
div.appendChild(htmlInput);
div!.appendChild(htmlInput);
htmlInput.value = htmlInput.defaultValue = this.getEditorText_(this.value_);
htmlInput.setAttribute('data-untyped-default-value', this.value_);
@@ -400,9 +371,8 @@ class FieldTextInput extends Field {
/**
* Closes the editor, saves the results, and disposes of any events or
* DOM-references belonging to the editor.
* @protected
*/
widgetDispose_() {
protected widgetDispose_() {
// Non-disposal related things that we do when the editor closes.
this.isBeingEdited_ = false;
this.isTextValid_ = true;
@@ -413,7 +383,7 @@ class FieldTextInput extends Field {
// Actual disposal.
this.unbindInputEvents_();
const style = WidgetDiv.getDiv().style;
const style = WidgetDiv.getDiv()!.style;
style.width = 'auto';
style.height = 'auto';
style.fontSize = '';
@@ -426,20 +396,17 @@ class FieldTextInput extends Field {
/**
* A callback triggered when the user is done editing the field via the UI.
* @param {*} _value The new value of the field.
* @param _value The new value of the field.
*/
onFinishEditing_(_value) {
// NOP by default.
// TODO(#2496): Support people passing a func into the field.
}
onFinishEditing_(_value: AnyDuringMigration) {}
// NOP by default.
// TODO(#2496): Support people passing a func into the field.
/**
* Bind handlers for user input on the text input field's editor.
* @param {!HTMLElement} htmlInput The htmlInput to which event
* handlers will be bound.
* @protected
* @param htmlInput The htmlInput to which event handlers will be bound.
*/
bindInputEvents_(htmlInput) {
protected bindInputEvents_(htmlInput: HTMLElement) {
// Trap Enter without IME and Esc to hide.
this.onKeyDownWrapper_ = browserEvents.conditionalBind(
htmlInput, 'keydown', this, this.onHtmlInputKeyDown_);
@@ -448,11 +415,8 @@ class FieldTextInput extends Field {
htmlInput, 'input', this, this.onHtmlInputChange_);
}
/**
* Unbind handlers for user input and workspace size changes.
* @protected
*/
unbindInputEvents_() {
/** Unbind handlers for user input and workspace size changes. */
protected unbindInputEvents_() {
if (this.onKeyDownWrapper_) {
browserEvents.unbind(this.onKeyDownWrapper_);
this.onKeyDownWrapper_ = null;
@@ -465,34 +429,43 @@ class FieldTextInput extends Field {
/**
* Handle key down to the editor.
* @param {!Event} e Keyboard event.
* @protected
* @param e Keyboard event.
*/
onHtmlInputKeyDown_(e) {
if (e.keyCode === KeyCodes.ENTER) {
protected onHtmlInputKeyDown_(e: Event) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
if ((e as AnyDuringMigration).keyCode === KeyCodes.ENTER) {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
} else if (e.keyCode === KeyCodes.ESC) {
this.setValue(this.htmlInput_.getAttribute('data-untyped-default-value'));
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.ESC) {
this.setValue(
this.htmlInput_!.getAttribute('data-untyped-default-value'));
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
} else if (e.keyCode === KeyCodes.TAB) {
// AnyDuringMigration because: Property 'keyCode' does not exist on type
// 'Event'.
} else if ((e as AnyDuringMigration).keyCode === KeyCodes.TAB) {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
(/** @type {!BlockSvg} */ (this.sourceBlock_)).tab(this, !e.shiftKey);
// AnyDuringMigration because: Property 'shiftKey' does not exist on type
// 'Event'. AnyDuringMigration because: Argument of type 'this' is not
// assignable to parameter of type 'Field'.
(this.sourceBlock_ as BlockSvg)
.tab(this as AnyDuringMigration, !(e as AnyDuringMigration).shiftKey);
e.preventDefault();
}
}
/**
* Handle a change to the editor.
* @param {!Event} _e Keyboard event.
* @private
* @param _e Keyboard event.
*/
onHtmlInputChange_(_e) {
const text = this.htmlInput_.value;
if (text !== this.htmlInput_.getAttribute('data-old-value')) {
this.htmlInput_.setAttribute('data-old-value', text);
private onHtmlInputChange_(_e: Event) {
const text = this.htmlInput_!.value;
if (text !== this.htmlInput_!.getAttribute('data-old-value')) {
this.htmlInput_!.setAttribute('data-old-value', text);
const value = this.getValueFromEditorText_(text);
this.setValue(value);
@@ -505,46 +478,41 @@ class FieldTextInput extends Field {
* Set the HTML input value and the field's internal value. The difference
* between this and ``setValue`` is that this also updates the HTML input
* value whilst editing.
* @param {*} newValue New value.
* @protected
* @param newValue New value.
*/
setEditorValue_(newValue) {
protected setEditorValue_(newValue: AnyDuringMigration) {
this.isDirty_ = true;
if (this.isBeingEdited_) {
// In the case this method is passed an invalid value, we still
// pass it through the transformation method `getEditorText` to deal
// with. Otherwise, the internal field's state will be inconsistent
// with what's shown to the user.
this.htmlInput_.value = this.getEditorText_(newValue);
this.htmlInput_!.value = this.getEditorText_(newValue);
}
this.setValue(newValue);
}
/**
* Resize the editor to fit the text.
* @protected
*/
resizeEditor_() {
/** Resize the editor to fit the text. */
protected resizeEditor_() {
const div = WidgetDiv.getDiv();
const bBox = this.getScaledBBox();
div.style.width = bBox.right - bBox.left + 'px';
div.style.height = bBox.bottom - bBox.top + 'px';
div!.style.width = bBox.right - bBox.left + 'px';
div!.style.height = bBox.bottom - bBox.top + 'px';
// In RTL mode block fields and LTR input fields the left edge moves,
// whereas the right edge is fixed. Reposition the editor.
const x = this.sourceBlock_.RTL ? bBox.right - div.offsetWidth : bBox.left;
const x = this.sourceBlock_.RTL ? bBox.right - div!.offsetWidth : bBox.left;
const xy = new Coordinate(x, bBox.top);
div.style.left = xy.x + 'px';
div.style.top = xy.y + 'px';
div!.style.left = xy.x + 'px';
div!.style.top = xy.y + 'px';
}
/**
* Returns whether or not the field is tab navigable.
* @return {boolean} True if the field is tab navigable.
* @override
* @return True if the field is tab navigable.
*/
isTabNavigable() {
override isTabNavigable(): boolean {
return true;
}
@@ -553,11 +521,9 @@ class FieldTextInput extends Field {
* representation. When we're currently editing, return the current HTML value
* instead. Otherwise, return null which tells the field to use the default
* behaviour (which is a string cast of the field's value).
* @return {?string} The HTML value if we're editing, otherwise null.
* @protected
* @override
* @return The HTML value if we're editing, otherwise null.
*/
getText_() {
protected override getText_(): string|null {
if (this.isBeingEdited_ && this.htmlInput_) {
// We are currently editing, return the HTML input value instead.
return this.htmlInput_.value;
@@ -570,11 +536,10 @@ class FieldTextInput extends Field {
* Override this method if the field's HTML input representation is different
* than the field's value. This should be coupled with an override of
* `getValueFromEditorText_`.
* @param {*} value The value stored in this field.
* @return {string} The text to show on the HTML input.
* @protected
* @param value The value stored in this field.
* @return The text to show on the HTML input.
*/
getEditorText_(value) {
protected getEditorText_(value: AnyDuringMigration): string {
return String(value);
}
@@ -584,23 +549,22 @@ class FieldTextInput extends Field {
* Override this method if the field's HTML input representation is different
* than the field's value. This should be coupled with an override of
* `getEditorText_`.
* @param {string} text Text received from the HTML input.
* @return {*} The value to store.
* @protected
* @param text Text received from the HTML input.
* @return The value to store.
*/
getValueFromEditorText_(text) {
protected getValueFromEditorText_(text: string): AnyDuringMigration {
return text;
}
/**
* Construct a FieldTextInput from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (text, and spellcheck).
* @return {!FieldTextInput} The new field instance.
* @package
* @param options A JSON object with options (text, and spellcheck).
* @return The new field instance.
* @nocollapse
* @internal
*/
static fromJson(options) {
static fromJson(options: AnyDuringMigration): FieldTextInput {
const text = parsing.replaceMessageReferences(options['text']);
// `this` might be a subclass of FieldTextInput if that class doesn't
// override the static fromJson method.
@@ -608,19 +572,6 @@ class FieldTextInput extends Field {
}
}
/**
* The default value for this field.
* @type {*}
* @protected
*/
FieldTextInput.prototype.DEFAULT_VALUE = '';
/**
* Pixel size of input border radius.
* Should match blocklyText's border-radius in CSS.
*/
FieldTextInput.BORDERRADIUS = 4;
fieldRegistry.register('field_input', FieldTextInput);
exports.FieldTextInput = FieldTextInput;
(FieldTextInput.prototype as AnyDuringMigration).DEFAULT_VALUE = '';

View File

@@ -7,135 +7,123 @@
/**
* @fileoverview Variable input field.
*/
'use strict';
/**
* Variable input field.
* @class
*/
goog.module('Blockly.FieldVariable');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FieldVariable');
const Variables = goog.require('Blockly.Variables');
const Xml = goog.require('Blockly.Xml');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
const internalConstants = goog.require('Blockly.internalConstants');
const parsing = goog.require('Blockly.utils.parsing');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
const {Field} = goog.require('Blockly.Field');
const {FieldDropdown} = goog.require('Blockly.FieldDropdown');
/* eslint-disable-next-line no-unused-vars */
const {MenuItem} = goog.requireType('Blockly.MenuItem');
/* eslint-disable-next-line no-unused-vars */
const {Menu} = goog.requireType('Blockly.Menu');
const {Msg} = goog.require('Blockly.Msg');
/* eslint-disable-next-line no-unused-vars */
const {Sentinel} = goog.requireType('Blockly.utils.Sentinel');
const {Size} = goog.require('Blockly.utils.Size');
const {VariableModel} = goog.require('Blockly.VariableModel');
/** @suppress {extraRequire} */
goog.require('Blockly.Events.BlockChange');
// Unused import preserved for side-effects. Remove if unneeded.
import './events/events_block_change.js';
import type {Block} from './block.js';
import {Field} from './field.js';
import {FieldDropdown} from './field_dropdown.js';
import * as fieldRegistry from './field_registry.js';
import * as internalConstants from './internal_constants.js';
import type {Menu} from './menu.js';
import type {MenuItem} from './menuitem.js';
import {Msg} from './msg.js';
import * as parsing from './utils/parsing.js';
import type {Sentinel} from './utils/sentinel.js';
import {Size} from './utils/size.js';
import {VariableModel} from './variable_model.js';
import * as Variables from './variables.js';
import * as Xml from './xml.js';
/**
* Class for a variable's dropdown field.
* @extends {FieldDropdown}
* @alias Blockly.FieldVariable
*/
class FieldVariable extends FieldDropdown {
export class FieldVariable extends FieldDropdown {
protected override menuGenerator_: AnyDuringMigration[][]|
((this: FieldDropdown) => AnyDuringMigration[][]);
defaultVariableName: string;
/** The type of the default variable for this field. */
private defaultType_ = '';
/**
* @param {?string|!Sentinel} varName The default name for the variable.
* All of the types of variables that will be available in this field's
* dropdown.
*/
variableTypes: string[]|null = [];
protected override size_: Size;
/** The variable model associated with this field. */
private variable_: VariableModel|null = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
*/
override SERIALIZABLE = true;
/**
* @param varName The default name for the variable.
* If null, a unique variable name will be generated.
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
* subclasses that want to handle configuration and setting the field
* value after their own constructors have run).
* @param {Function=} opt_validator A function that is called to validate
* changes to the field's value. Takes in a variable ID & returns a
* validated variable ID, or null to abort the change.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. Will only be used if opt_config is not
* provided.
* @param {string=} opt_defaultType The type of variable to create if this
* field's value is not explicitly set. Defaults to ''. Will only be used
* if opt_config is not provided.
* @param {Object=} opt_config A map of options used to configure the field.
* subclasses that want to handle configuration and setting the field value
* after their own constructors have run).
* @param opt_validator A function that is called to validate changes to the
* field's value. Takes in a variable ID & returns a validated variable
* ID, or null to abort the change.
* @param opt_variableTypes A list of the types of variables to include in the
* dropdown. Will only be used if opt_config is not provided.
* @param opt_defaultType The type of variable to create if this field's value
* is not explicitly set. Defaults to ''. Will only be used if opt_config
* is not provided.
* @param opt_config A map of options used to configure the field.
* See the [field creation documentation]{@link
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/variable#creation}
* for a list of properties this parameter supports.
* https://developers.google.com/blockly/guides/create-custom-blocks/fields/built-in-fields/variable#creation}
* for a list of properties this parameter supports.
*/
constructor(
varName, opt_validator, opt_variableTypes, opt_defaultType, opt_config) {
varName: string|null|Sentinel, opt_validator?: Function,
opt_variableTypes?: string[], opt_defaultType?: string,
opt_config?: AnyDuringMigration) {
super(Field.SKIP_SETUP);
/**
* An array of options for a dropdown list,
* or a function which generates these options.
* @type {(!Array<!Array>|
* !function(this:FieldDropdown): !Array<!Array>)}
* @protected
*/
this.menuGenerator_ = FieldVariable.dropdownCreate;
// AnyDuringMigration because: Type '(this: FieldVariable) => any[][]' is
// not assignable to type 'any[][] | ((this: FieldDropdown) => any[][])'.
this.menuGenerator_ = FieldVariable.dropdownCreate as AnyDuringMigration;
/**
* The initial variable name passed to this field's constructor, or an
* empty string if a name wasn't provided. Used to create the initial
* variable.
* @type {string}
*/
this.defaultVariableName = typeof varName === 'string' ? varName : '';
/**
* The type of the default variable for this field.
* @type {string}
* @private
*/
this.defaultType_ = '';
/**
* All of the types of variables that will be available in this field's
* dropdown.
* @type {?Array<string>}
*/
this.variableTypes = [];
/**
* The size of the area rendered by the field.
* @type {Size}
* @protected
* @override
*/
/** The size of the area rendered by the field. */
this.size_ = new Size(0, 0);
/**
* The variable model associated with this field.
* @type {?VariableModel}
* @private
*/
this.variable_ = null;
/**
* Serializable fields are saved by the serializer, non-serializable fields
* are not. Editable fields should also be serializable.
* @type {boolean}
*/
this.SERIALIZABLE = true;
if (varName === Field.SKIP_SETUP) return;
if (varName === Field.SKIP_SETUP) {
return;
}
if (opt_config) {
this.configure_(opt_config);
} else {
this.setTypes_(opt_variableTypes, opt_defaultType);
}
if (opt_validator) this.setValidator(opt_validator);
if (opt_validator) {
this.setValidator(opt_validator);
}
}
/**
* Configure the field based on the given map of options.
* @param {!Object} config A map of options to configure the field based on.
* @protected
* @param config A map of options to configure the field based on.
*/
configure_(config) {
protected override configure_(config: AnyDuringMigration) {
super.configure_(config);
this.setTypes_(config['variableTypes'], config['defaultType']);
}
@@ -144,35 +132,31 @@ class FieldVariable extends FieldDropdown {
* Initialize the model for this field if it has not already been initialized.
* If the value has not been set to a variable by the first render, we make up
* a variable rather than let the value be invalid.
* @package
* @internal
*/
initModel() {
override initModel() {
if (this.variable_) {
return; // Initialization already happened.
}
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, null, this.defaultVariableName,
this.sourceBlock_.workspace!, null, this.defaultVariableName,
this.defaultType_);
// Don't call setValue because we don't want to cause a rerender.
this.doValueUpdate_(variable.getId());
}
/**
* @override
*/
shouldAddBorderRect_() {
override shouldAddBorderRect_() {
return super.shouldAddBorderRect_() &&
(!this.getConstants().FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
(!this.getConstants()!.FIELD_DROPDOWN_NO_BORDER_RECT_SHADOW ||
this.sourceBlock_.type !== 'variables_get');
}
/**
* Initialize this field based on the given XML.
* @param {!Element} fieldElement The element containing information about the
* variable field's state.
* @param fieldElement The element containing information about the variable
* field's state.
*/
fromXml(fieldElement) {
override fromXml(fieldElement: Element) {
const id = fieldElement.getAttribute('id');
const variableName = fieldElement.textContent;
// 'variabletype' should be lowercase, but until July 2019 it was sometimes
@@ -180,8 +164,11 @@ class FieldVariable extends FieldDropdown {
const variableType = fieldElement.getAttribute('variabletype') ||
fieldElement.getAttribute('variableType') || '';
// AnyDuringMigration because: Argument of type 'string | null' is not
// assignable to parameter of type 'string | undefined'.
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, id, variableName, variableType);
this.sourceBlock_.workspace!, id, variableName as AnyDuringMigration,
variableType);
// This should never happen :)
if (variableType !== null && variableType !== variable.type) {
@@ -197,69 +184,66 @@ class FieldVariable extends FieldDropdown {
/**
* Serialize this field to XML.
* @param {!Element} fieldElement The element to populate with info about the
* field's state.
* @return {!Element} The element containing info about the field's state.
* @param fieldElement The element to populate with info about the field's
* state.
* @return The element containing info about the field's state.
*/
toXml(fieldElement) {
override toXml(fieldElement: Element): Element {
// Make sure the variable is initialized.
this.initModel();
fieldElement.id = this.variable_.getId();
fieldElement.textContent = this.variable_.name;
if (this.variable_.type) {
fieldElement.setAttribute('variabletype', this.variable_.type);
fieldElement.id = this.variable_!.getId();
fieldElement.textContent = this.variable_!.name;
if (this.variable_!.type) {
fieldElement.setAttribute('variabletype', this.variable_!.type);
}
return fieldElement;
}
/**
* Saves this field's value.
* @param {boolean=} doFullSerialization If true, the variable field will
* serialize the full state of the field being referenced (ie ID, name,
* and type) rather than just a reference to it (ie ID).
* @return {*} The state of the variable field.
* @override
* @package
* @param doFullSerialization If true, the variable field will serialize the
* full state of the field being referenced (ie ID, name, and type) rather
* than just a reference to it (ie ID).
* @return The state of the variable field.
* @internal
*/
saveState(doFullSerialization) {
override saveState(doFullSerialization?: boolean): AnyDuringMigration {
const legacyState = this.saveLegacyState(FieldVariable);
if (legacyState !== null) {
return legacyState;
}
// Make sure the variable is initialized.
this.initModel();
const state = {'id': this.variable_.getId()};
const state = {'id': this.variable_!.getId()};
if (doFullSerialization) {
state['name'] = this.variable_.name;
state['type'] = this.variable_.type;
(state as AnyDuringMigration)['name'] = this.variable_!.name;
(state as AnyDuringMigration)['type'] = this.variable_!.type;
}
return state;
}
/**
* Sets the field's value based on the given state.
* @param {*} state The state of the variable to assign to this variable
* field.
* @override
* @package
* @param state The state of the variable to assign to this variable field.
* @internal
*/
loadState(state) {
override loadState(state: AnyDuringMigration) {
if (this.loadLegacyState(FieldVariable, state)) {
return;
}
// This is necessary so that blocks in the flyout can have custom var names.
const variable = Variables.getOrCreateVariablePackage(
this.sourceBlock_.workspace, state['id'] || null, state['name'],
this.sourceBlock_.workspace!, state['id'] || null, state['name'],
state['type'] || '');
this.setValue(variable.getId());
}
/**
* Attach this field to a block.
* @param {!Block} block The block containing this field.
* @param block The block containing this field.
*/
setSourceBlock(block) {
override setSourceBlock(block: Block) {
if (block.isShadow()) {
throw Error('Variable fields are not allowed to exist on shadow blocks.');
}
@@ -268,18 +252,18 @@ class FieldVariable extends FieldDropdown {
/**
* Get the variable's ID.
* @return {?string} Current variable's ID.
* @return Current variable's ID.
*/
getValue() {
override getValue(): string|null {
return this.variable_ ? this.variable_.getId() : null;
}
/**
* Get the text from this field, which is the selected variable's name.
* @return {string} The selected variable's name, or the empty string if no
* variable is selected.
* @return The selected variable's name, or the empty string if no variable is
* selected.
*/
getText() {
override getText(): string {
return this.variable_ ? this.variable_.name : '';
}
@@ -287,11 +271,10 @@ class FieldVariable extends FieldDropdown {
* Get the variable model for the selected variable.
* Not guaranteed to be in the variable map on the workspace (e.g. if accessed
* after the variable has been deleted).
* @return {?VariableModel} The selected variable, or null if none was
* selected.
* @package
* @return The selected variable, or null if none was selected.
* @internal
*/
getVariable() {
getVariable(): VariableModel|null {
return this.variable_;
}
@@ -300,9 +283,9 @@ class FieldVariable extends FieldDropdown {
* Returns null if the variable is not set, because validators should not
* run on the initial setValue call, because the field won't be attached to
* a block and workspace at that point.
* @return {?Function} Validation function, or null.
* @return Validation function, or null.
*/
getValidator() {
override getValidator(): Function|null {
// Validators shouldn't operate on the initial setValue call.
// Normally this is achieved by calling setValidator after setValue, but
// this is not a possibility with variable fields.
@@ -314,16 +297,16 @@ class FieldVariable extends FieldDropdown {
/**
* Ensure that the ID belongs to a valid variable of an allowed type.
* @param {*=} opt_newValue The ID of the new variable to set.
* @return {?string} The validated ID, or null if invalid.
* @protected
* @param opt_newValue The ID of the new variable to set.
* @return The validated ID, or null if invalid.
*/
doClassValidation_(opt_newValue) {
protected override doClassValidation_(opt_newValue?: AnyDuringMigration):
string|null {
if (opt_newValue === null) {
return null;
}
const newId = /** @type {string} */ (opt_newValue);
const variable = Variables.getVariable(this.sourceBlock_.workspace, newId);
const newId = opt_newValue as string;
const variable = Variables.getVariable(this.sourceBlock_.workspace!, newId);
if (!variable) {
console.warn(
'Variable id doesn\'t point to a real variable! ' +
@@ -345,22 +328,20 @@ class FieldVariable extends FieldDropdown {
*
* The variable ID should be valid at this point, but if a variable field
* validator returns a bad ID, this could break.
* @param {*} newId The value to be saved.
* @protected
* @param newId The value to be saved.
*/
doValueUpdate_(newId) {
this.variable_ = Variables.getVariable(
this.sourceBlock_.workspace, /** @type {string} */ (newId));
protected override doValueUpdate_(newId: AnyDuringMigration) {
this.variable_ =
Variables.getVariable(this.sourceBlock_.workspace!, newId as string);
super.doValueUpdate_(newId);
}
/**
* Check whether the given variable type is allowed on this field.
* @param {string} type The type to check.
* @return {boolean} True if the type is in the list of allowed types.
* @private
* @param type The type to check.
* @return True if the type is in the list of allowed types.
*/
typeIsAllowed_(type) {
private typeIsAllowed_(type: string): boolean {
const typeList = this.getVariableTypes_();
if (!typeList) {
return true; // If it's null, all types are valid.
@@ -375,11 +356,10 @@ class FieldVariable extends FieldDropdown {
/**
* Return a list of variable types to include in the dropdown.
* @return {!Array<string>} Array of variable types.
* @return Array of variable types.
* @throws {Error} if variableTypes is an empty array.
* @private
*/
getVariableTypes_() {
private getVariableTypes_(): string[] {
// TODO (#1513): Try to avoid calling this every time the field is edited.
let variableTypes = this.variableTypes;
if (variableTypes === null) {
@@ -401,14 +381,13 @@ class FieldVariable extends FieldDropdown {
/**
* Parse the optional arguments representing the allowed variable types and
* the default variable type.
* @param {Array<string>=} opt_variableTypes A list of the types of variables
* to include in the dropdown. If null or undefined, variables of all
* types will be displayed in the dropdown.
* @param {string=} opt_defaultType The type of the variable to create if this
* field's value is not explicitly set. Defaults to ''.
* @private
* @param opt_variableTypes A list of the types of variables to include in the
* dropdown. If null or undefined, variables of all types will be
* displayed in the dropdown.
* @param opt_defaultType The type of the variable to create if this field's
* value is not explicitly set. Defaults to ''.
*/
setTypes_(opt_variableTypes, opt_defaultType) {
private setTypes_(opt_variableTypes?: string[], opt_defaultType?: string) {
// If you expected that the default type would be the same as the only entry
// in the variable types array, tell the Blockly team by commenting on
// #1499.
@@ -445,10 +424,9 @@ class FieldVariable extends FieldDropdown {
* Refreshes the name of the variable by grabbing the name of the model.
* Used when a variable gets renamed, but the ID stays the same. Should only
* be called by the block.
* @override
* @package
* @internal
*/
refreshVariableName() {
override refreshVariableName() {
this.forceRerender();
}
@@ -456,23 +434,21 @@ class FieldVariable extends FieldDropdown {
* Handle the selection of an item in the variable dropdown menu.
* Special case the 'Rename variable...' and 'Delete variable...' options.
* In the rename case, prompt the user for a new name.
* @param {!Menu} menu The Menu component clicked.
* @param {!MenuItem} menuItem The MenuItem selected within menu.
* @protected
* @param menu The Menu component clicked.
* @param menuItem The MenuItem selected within menu.
*/
onItemSelected_(menu, menuItem) {
protected override onItemSelected_(menu: Menu, menuItem: MenuItem) {
const id = menuItem.getValue();
// Handle special cases.
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
if (id === internalConstants.RENAME_VARIABLE_ID) {
// Rename variable.
Variables.renameVariable(
this.sourceBlock_.workspace,
/** @type {!VariableModel} */ (this.variable_));
this.sourceBlock_.workspace, this.variable_ as VariableModel);
return;
} else if (id === internalConstants.DELETE_VARIABLE_ID) {
// Delete variable.
this.sourceBlock_.workspace.deleteVariableById(this.variable_.getId());
this.sourceBlock_.workspace.deleteVariableById(this.variable_!.getId());
return;
}
}
@@ -483,25 +459,23 @@ class FieldVariable extends FieldDropdown {
/**
* Overrides referencesVariables(), indicating this field refers to a
* variable.
* @return {boolean} True.
* @package
* @override
* @return True.
* @internal
*/
referencesVariables() {
override referencesVariables(): boolean {
return true;
}
/**
* Construct a FieldVariable from a JSON arg object,
* dereferencing any string table references.
* @param {!Object} options A JSON object with options (variable,
* variableTypes, and defaultType).
* @return {!FieldVariable} The new field instance.
* @package
* @param options A JSON object with options (variable, variableTypes, and
* defaultType).
* @return The new field instance.
* @nocollapse
* @override
* @internal
*/
static fromJson(options) {
static override fromJson(options: AnyDuringMigration): FieldVariable {
const varName = parsing.replaceMessageReferences(options['variable']);
// `this` might be a subclass of FieldVariable if that class doesn't
// override the static fromJson method.
@@ -511,17 +485,16 @@ class FieldVariable extends FieldDropdown {
/**
* Return a sorted list of variable names for variable dropdown menus.
* Include a special option at the end for creating a new variable name.
* @return {!Array<!Array>} Array of variable names/id tuples.
* @this {FieldVariable}
* @return Array of variable names/id tuples.
*/
static dropdownCreate() {
static dropdownCreate(this: FieldVariable): AnyDuringMigration[][] {
if (!this.variable_) {
throw Error(
'Tried to call dropdownCreate on a variable field with no' +
' variable selected.');
}
const name = this.getText();
let variableModelList = [];
let variableModelList: AnyDuringMigration[] = [];
if (this.sourceBlock_ && this.sourceBlock_.workspace) {
const variableTypes = this.getVariableTypes_();
// Get a copy of the list, so that adding rename and new variable options
@@ -554,5 +527,3 @@ class FieldVariable extends FieldDropdown {
}
fieldRegistry.register('field_variable', FieldVariable);
exports.FieldVariable = FieldVariable;

File diff suppressed because it is too large Load Diff

View File

@@ -7,142 +7,97 @@
/**
* @fileoverview Class for a button in the flyout.
*/
'use strict';
/**
* Class for a button in the flyout.
* @class
*/
goog.module('Blockly.FlyoutButton');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FlyoutButton');
const Css = goog.require('Blockly.Css');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const style = goog.require('Blockly.utils.style');
/* eslint-disable-next-line no-unused-vars */
const toolbox = goog.requireType('Blockly.utils.toolbox');
const parsing = goog.require('Blockly.utils.parsing');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
const {Svg} = goog.require('Blockly.utils.Svg');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
import * as browserEvents from './browser_events.js';
import * as Css from './css.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import * as parsing from './utils/parsing.js';
import * as style from './utils/style.js';
import {Svg} from './utils/svg.js';
import type * as toolbox from './utils/toolbox.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Class for a button or label in the flyout.
* @alias Blockly.FlyoutButton
*/
class FlyoutButton {
export class FlyoutButton {
/** The horizontal margin around the text in the button. */
static TEXT_MARGIN_X = 5;
/** The vertical margin around the text in the button. */
static TEXT_MARGIN_Y = 2;
private readonly text_: string;
private readonly position_: Coordinate;
private readonly callbackKey_: string;
private readonly cssClass_: string|null;
/** Mouse up event data. */
private onMouseUpWrapper_: browserEvents.Data|null = null;
info: toolbox.ButtonOrLabelInfo;
/** The width of the button's rect. */
width = 0;
/** The height of the button's rect. */
height = 0;
/** The root SVG group for the button or label. */
private svgGroup_: SVGGElement|null = null;
/** The SVG element with the text of the label or button. */
private svgText_: SVGTextElement|null = null;
/**
* @param {!WorkspaceSvg} workspace The workspace in which to place this
* button.
* @param {!WorkspaceSvg} targetWorkspace The flyout's target workspace.
* @param {!toolbox.ButtonOrLabelInfo} json
* The JSON specifying the label/button.
* @param {boolean} isLabel Whether this button should be styled as a label.
* @package
* @param workspace The workspace in which to place this button.
* @param targetWorkspace The flyout's target workspace.
* @param json The JSON specifying the label/button.
* @param isLabel_ Whether this button should be styled as a label.
* @internal
*/
constructor(workspace, targetWorkspace, json, isLabel) {
/**
* @type {!WorkspaceSvg}
* @private
*/
this.workspace_ = workspace;
/**
* @type {!WorkspaceSvg}
* @private
*/
this.targetWorkspace_ = targetWorkspace;
/**
* @type {string}
* @private
*/
constructor(
private readonly workspace: WorkspaceSvg,
private readonly targetWorkspace: WorkspaceSvg,
json: toolbox.ButtonOrLabelInfo, private readonly isLabel_: boolean) {
this.text_ = json['text'];
/**
* @type {!Coordinate}
* @private
*/
this.position_ = new Coordinate(0, 0);
/**
* Whether this button should be styled as a label.
* Labels behave the same as buttons, but are styled differently.
* @type {boolean}
* @private
*/
this.isLabel_ = isLabel;
/** The key to the function called when this button is clicked. */
this.callbackKey_ =
(json as
AnyDuringMigration)['callbackKey'] || /* Check the lower case version
too to satisfy IE */
(json as AnyDuringMigration)['callbackkey'];
/**
* The key to the function called when this button is clicked.
* @type {string}
* @private
*/
this.callbackKey_ = json['callbackKey'] ||
/* Check the lower case version too to satisfy IE */
json['callbackkey'];
/** If specified, a CSS class to add to this button. */
this.cssClass_ = (json as AnyDuringMigration)['web-class'] || null;
/**
* If specified, a CSS class to add to this button.
* @type {?string}
* @private
*/
this.cssClass_ = json['web-class'] || null;
/**
* Mouse up event data.
* @type {?browserEvents.Data}
* @private
*/
this.onMouseUpWrapper_ = null;
/**
* The JSON specifying the label / button.
* @type {!toolbox.ButtonOrLabelInfo}
*/
/** The JSON specifying the label / button. */
this.info = json;
/**
* The width of the button's rect.
* @type {number}
*/
this.width = 0;
/**
* The height of the button's rect.
* @type {number}
*/
this.height = 0;
/**
* The root SVG group for the button or label.
* @type {?SVGGElement}
* @private
*/
this.svgGroup_ = null;
/**
* The SVG element with the text of the label or button.
* @type {?SVGTextElement}
* @private
*/
this.svgText_ = null;
}
/**
* Create the button elements.
* @return {!SVGElement} The button's SVG group.
* @return The button's SVG group.
*/
createDom() {
createDom(): SVGElement {
let cssClass = this.isLabel_ ? 'blocklyFlyoutLabel' : 'blocklyFlyoutButton';
if (this.cssClass_) {
cssClass += ' ' + this.cssClass_;
}
this.svgGroup_ = dom.createSvgElement(
Svg.G, {'class': cssClass}, this.workspace_.getCanvas());
Svg.G, {'class': cssClass}, this.workspace.getCanvas());
let shadow;
if (!this.isLabel_) {
@@ -155,7 +110,7 @@ class FlyoutButton {
'x': 1,
'y': 1,
},
this.svgGroup_);
this.svgGroup_!);
}
// Background rectangle.
const rect = dom.createSvgElement(
@@ -165,7 +120,7 @@ class FlyoutButton {
'rx': 4,
'ry': 4,
},
this.svgGroup_);
this.svgGroup_!);
const svgText = dom.createSvgElement(
Svg.TEXT, {
@@ -174,16 +129,16 @@ class FlyoutButton {
'y': 0,
'text-anchor': 'middle',
},
this.svgGroup_);
this.svgGroup_!);
let text = parsing.replaceMessageReferences(this.text_);
if (this.workspace_.RTL) {
if (this.workspace.RTL) {
// Force text to be RTL by adding an RLM.
text += '\u200F';
}
svgText.textContent = text;
if (this.isLabel_) {
this.svgText_ = svgText;
this.workspace_.getThemeManager().subscribe(
this.workspace.getThemeManager().subscribe(
this.svgText_, 'flyoutForegroundColour', 'fill');
}
@@ -199,87 +154,79 @@ class FlyoutButton {
if (!this.isLabel_) {
this.width += 2 * FlyoutButton.TEXT_MARGIN_X;
this.height += 2 * FlyoutButton.TEXT_MARGIN_Y;
shadow.setAttribute('width', this.width);
shadow.setAttribute('height', this.height);
shadow?.setAttribute('width', this.width.toString());
shadow?.setAttribute('height', this.height.toString());
}
rect.setAttribute('width', this.width);
rect.setAttribute('height', this.height);
rect.setAttribute('width', this.width.toString());
rect.setAttribute('height', this.height.toString());
svgText.setAttribute('x', this.width / 2);
svgText.setAttribute('x', (this.width / 2).toString());
svgText.setAttribute(
'y', this.height / 2 - fontMetrics.height / 2 + fontMetrics.baseline);
'y',
(this.height / 2 - fontMetrics.height / 2 + fontMetrics.baseline)
.toString());
this.updateTransform_();
// AnyDuringMigration because: Argument of type 'SVGGElement | null' is not
// assignable to parameter of type 'EventTarget'.
this.onMouseUpWrapper_ = browserEvents.conditionalBind(
this.svgGroup_, 'mouseup', this, this.onMouseUp_);
return this.svgGroup_;
this.svgGroup_ as AnyDuringMigration, 'mouseup', this, this.onMouseUp_);
return this.svgGroup_!;
}
/**
* Correctly position the flyout button and make it visible.
*/
/** Correctly position the flyout button and make it visible. */
show() {
this.updateTransform_();
this.svgGroup_.setAttribute('display', 'block');
this.svgGroup_!.setAttribute('display', 'block');
}
/**
* Update SVG attributes to match internal state.
* @private
*/
updateTransform_() {
this.svgGroup_.setAttribute(
/** Update SVG attributes to match internal state. */
private updateTransform_() {
this.svgGroup_!.setAttribute(
'transform',
'translate(' + this.position_.x + ',' + this.position_.y + ')');
}
/**
* Move the button to the given x, y coordinates.
* @param {number} x The new x coordinate.
* @param {number} y The new y coordinate.
* @param x The new x coordinate.
* @param y The new y coordinate.
*/
moveTo(x, y) {
moveTo(x: number, y: number) {
this.position_.x = x;
this.position_.y = y;
this.updateTransform_();
}
/**
* @return {boolean} Whether or not the button is a label.
*/
isLabel() {
/** @return Whether or not the button is a label. */
isLabel(): boolean {
return this.isLabel_;
}
/**
* Location of the button.
* @return {!Coordinate} x, y coordinates.
* @package
* @return x, y coordinates.
* @internal
*/
getPosition() {
getPosition(): Coordinate {
return this.position_;
}
/**
* @return {string} Text of the button.
*/
getButtonText() {
/** @return Text of the button. */
getButtonText(): string {
return this.text_;
}
/**
* Get the button's target workspace.
* @return {!WorkspaceSvg} The target workspace of the flyout where this
* button resides.
* @return The target workspace of the flyout where this button resides.
*/
getTargetWorkspace() {
return this.targetWorkspace_;
getTargetWorkspace(): WorkspaceSvg {
return this.targetWorkspace;
}
/**
* Dispose of this button.
*/
/** Dispose of this button. */
dispose() {
if (this.onMouseUpWrapper_) {
browserEvents.unbind(this.onMouseUpWrapper_);
@@ -288,17 +235,16 @@ class FlyoutButton {
dom.removeNode(this.svgGroup_);
}
if (this.svgText_) {
this.workspace_.getThemeManager().unsubscribe(this.svgText_);
this.workspace.getThemeManager().unsubscribe(this.svgText_);
}
}
/**
* Do something when the button is clicked.
* @param {!Event} e Mouse up event.
* @private
* @param e Mouse up event.
*/
onMouseUp_(e) {
const gesture = this.targetWorkspace_.getGesture(e);
private onMouseUp_(e: Event) {
const gesture = this.targetWorkspace.getGesture(e);
if (gesture) {
gesture.cancel();
}
@@ -309,27 +255,19 @@ class FlyoutButton {
} else if (
!this.isLabel_ &&
!(this.callbackKey_ &&
this.targetWorkspace_.getButtonCallback(this.callbackKey_))) {
this.targetWorkspace.getButtonCallback(this.callbackKey_))) {
console.warn('Buttons should have callbacks. Button text: ' + this.text_);
} else if (!this.isLabel_) {
this.targetWorkspace_.getButtonCallback(this.callbackKey_)(this);
const callback =
this.targetWorkspace.getButtonCallback(this.callbackKey_);
if (callback) {
callback(this);
}
}
}
}
/**
* The horizontal margin around the text in the button.
*/
FlyoutButton.TEXT_MARGIN_X = 5;
/**
* The vertical margin around the text in the button.
*/
FlyoutButton.TEXT_MARGIN_Y = 2;
/**
* CSS for buttons and labels. See css.js for use.
*/
/** CSS for buttons and labels. See css.js for use. */
Css.register(`
.blocklyFlyoutButton {
fill: #888;
@@ -352,5 +290,3 @@ Css.register(`
opacity: 0;
}
`);
exports.FlyoutButton = FlyoutButton;

View File

@@ -7,55 +7,50 @@
/**
* @fileoverview Horizontal flyout tray containing blocks which may be created.
*/
'use strict';
/**
* Horizontal flyout tray containing blocks which may be created.
* @class
*/
goog.module('Blockly.HorizontalFlyout');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.HorizontalFlyout');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const browserEvents = goog.require('Blockly.browserEvents');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const registry = goog.require('Blockly.registry');
const toolbox = goog.require('Blockly.utils.toolbox');
/* eslint-disable-next-line no-unused-vars */
const {Coordinate} = goog.requireType('Blockly.utils.Coordinate');
const {Flyout} = goog.require('Blockly.Flyout');
/* eslint-disable-next-line no-unused-vars */
const {FlyoutItem} = goog.requireType('Blockly.Flyout');
/* eslint-disable-next-line no-unused-vars */
const {FlyoutButton} = goog.requireType('Blockly.FlyoutButton');
/* eslint-disable-next-line no-unused-vars */
const {Options} = goog.requireType('Blockly.Options');
const {Rect} = goog.require('Blockly.utils.Rect');
const {Scrollbar} = goog.require('Blockly.Scrollbar');
import * as browserEvents from './browser_events.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Flyout, FlyoutItem} from './flyout_base.js';
import type {FlyoutButton} from './flyout_button.js';
import type {Options} from './options.js';
import * as registry from './registry.js';
import {Scrollbar} from './scrollbar.js';
import type {Coordinate} from './utils/coordinate.js';
import {Rect} from './utils/rect.js';
import * as toolbox from './utils/toolbox.js';
import * as WidgetDiv from './widgetdiv.js';
/**
* Class for a flyout.
* @extends {Flyout}
* @alias Blockly.HorizontalFlyout
*/
class HorizontalFlyout extends Flyout {
/**
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
*/
constructor(workspaceOptions) {
export class HorizontalFlyout extends Flyout {
override horizontalLayout = true;
// Record the width for workspace metrics.
override width_: AnyDuringMigration;
// Record the height for workspace metrics and .position.
override height_: AnyDuringMigration;
/** @param workspaceOptions Dictionary of options for the workspace. */
constructor(workspaceOptions: Options) {
super(workspaceOptions);
this.horizontalLayout = true;
}
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a
* float between 0 and 1 specifying the degree of scrolling and a similar
* x property.
* @protected
* @param xyRatio Contains a y property which is a float between 0 and 1
* specifying the degree of scrolling and a similar x property.
*/
setMetrics_(xyRatio) {
protected override setMetrics_(xyRatio: {x: number, y: number}) {
if (!this.isVisible()) {
return;
}
@@ -78,22 +73,22 @@ class HorizontalFlyout extends Flyout {
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
* @return X coordinate.
*/
getX() {
override getX(): number {
// X is always 0 since this is a horizontal flyout.
return 0;
}
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
* @return Y coordinate.
*/
getY() {
override getY(): number {
if (!this.isVisible()) {
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const metricsManager = this.targetWorkspace!.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
@@ -101,15 +96,16 @@ class HorizontalFlyout extends Flyout {
let y = 0;
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// Trashcan flyout is opposite the main flyout.
if (this.targetWorkspace!.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
// Simple (flyout-only) toolbox.
if (this.targetWorkspace!.getToolbox()) {
if (atTop) {
y = toolboxMetrics.height;
} else {
y = viewMetrics.height - this.height_;
}
// Simple (flyout-only) toolbox.
} else {
if (atTop) {
y = 0;
@@ -118,7 +114,6 @@ class HorizontalFlyout extends Flyout {
y = viewMetrics.height;
}
}
// Trashcan flyout is opposite the main flyout.
} else {
if (atTop) {
y = 0;
@@ -134,17 +129,13 @@ class HorizontalFlyout extends Flyout {
return y;
}
/**
* Move the flyout to the edge of the workspace.
*/
position() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
/** Move the flyout to the edge of the workspace. */
override position() {
if (!this.isVisible() || !this.targetWorkspace!.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const metricsManager = this.targetWorkspace!.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the width for workspace metrics.
this.width_ = targetWorkspaceViewMetrics.width;
const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS;
@@ -159,16 +150,13 @@ class HorizontalFlyout extends Flyout {
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
* @param width The width of the flyout, not including the rounded corners.
* @param height The height of the flyout, not including rounded corners.
*/
setBackgroundPath_(width, height) {
private setBackgroundPath_(width: number, height: number) {
const atTop = this.toolboxPosition_ === toolbox.Position.TOP;
// Start at top left.
const path = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
const path: (string|number)[] = ['M 0,' + (atTop ? 0 : this.CORNER_RADIUS)];
if (atTop) {
// Top.
@@ -201,22 +189,19 @@ class HorizontalFlyout extends Flyout {
// Left.
path.push('z');
}
this.svgBackground_.setAttribute('d', path.join(' '));
this.svgBackground_!.setAttribute('d', path.join(' '));
}
/**
* Scroll the flyout to the top.
*/
scrollToStart() {
/** Scroll the flyout to the top. */
override scrollToStart() {
this.workspace_.scrollbar.setX(this.RTL ? Infinity : 0);
}
/**
* Scroll the flyout.
* @param {!WheelEvent} e Mouse wheel scroll event.
* @protected
* @param e Mouse wheel scroll event.
*/
wheel_(e) {
protected override wheel_(e: WheelEvent) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
const delta = scrollDelta.x || scrollDelta.y;
@@ -225,14 +210,13 @@ class HorizontalFlyout extends Flyout {
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.left - scrollMetrics.left) + delta;
const pos = viewMetrics.left - scrollMetrics.left + delta;
this.workspace_.scrollbar.setX(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and
// dropDownDiv.
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
@@ -241,13 +225,11 @@ class HorizontalFlyout extends Flyout {
/**
* Lay out the blocks in the flyout.
* @param {!Array<!FlyoutItem>} contents The blocks and buttons to lay
* out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
* @param contents The blocks and buttons to lay out.
* @param gaps The visible gaps between blocks.
*/
layout_(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
protected override layout_(contents: FlyoutItem[], gaps: number[]) {
this.workspace_.scale = this.targetWorkspace!.scale;
const margin = this.MARGIN;
let cursorX = margin + this.tabWidth_;
const cursorY = margin;
@@ -255,38 +237,42 @@ class HorizontalFlyout extends Flyout {
contents = contents.reverse();
}
for (let i = 0, item; (item = contents[i]); i++) {
for (let i = 0, item; item = contents[i]; i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
const allBlocks = block!.getDescendants(false);
for (let j = 0, child; child = allBlocks[j]; j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such
// a block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
block!.render();
const root = block!.getSvgRoot();
const blockHW = block!.getHeightWidth();
// Figure out where to place the block.
const tab = block.outputConnection ? this.tabWidth_ : 0;
const tab = block!.outputConnection ? this.tabWidth_ : 0;
let moveX;
if (this.RTL) {
moveX = cursorX + blockHW.width;
} else {
moveX = cursorX - tab;
}
block.moveBy(moveX, cursorY);
block!.moveBy(moveX, cursorY);
const rect = this.createRect_(block, moveX, cursorY, blockHW, i);
cursorX += (blockHW.width + gaps[i]);
// AnyDuringMigration because: Argument of type 'BlockSvg | undefined'
// is not assignable to parameter of type 'BlockSvg'.
const rect = this.createRect_(
block as AnyDuringMigration, moveX, cursorY, blockHW, i);
cursorX += blockHW.width + gaps[i];
this.addBlockListeners_(root, block, rect);
// AnyDuringMigration because: Argument of type 'BlockSvg | undefined'
// is not assignable to parameter of type 'BlockSvg'.
this.addBlockListeners_(root, block as AnyDuringMigration, rect);
} else if (item.type === 'button') {
const button = /** @type {!FlyoutButton} */ (item.button);
const button = item.button as FlyoutButton;
this.initFlyoutButton_(button, cursorX, cursorY);
cursorX += (button.width + gaps[i]);
cursorX += button.width + gaps[i];
}
}
}
@@ -295,12 +281,12 @@ class HorizontalFlyout extends Flyout {
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
* @param currentDragDeltaXY How far the pointer has moved from the position
* at mouse down, in pixel units.
* @return True if the drag is toward the workspace.
* @internal
*/
isDragTowardWorkspace(currentDragDeltaXY) {
override isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
@@ -308,8 +294,8 @@ class HorizontalFlyout extends Flyout {
const range = this.dragAngleRange_;
// Check for up or down dragging.
if ((dragDirection < 90 + range && dragDirection > 90 - range) ||
(dragDirection > -90 - range && dragDirection < -90 + range)) {
if (dragDirection < 90 + range && dragDirection > 90 - range ||
dragDirection > -90 - range && dragDirection < -90 + range) {
return true;
}
return false;
@@ -318,10 +304,10 @@ class HorizontalFlyout extends Flyout {
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
* @return The component's bounding box. Null if drag target area should be
* ignored.
*/
getClientRect() {
override getClientRect(): Rect|null {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
@@ -347,17 +333,16 @@ class HorizontalFlyout extends Flyout {
/**
* Compute height of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks right-aligned.
* @protected
*/
reflowInternal_() {
protected override reflowInternal_() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutHeight = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
for (let i = 0, block; block = blocks[i]; i++) {
flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
}
const buttons = this.buttons_;
for (let i = 0, button; (button = buttons[i]); i++) {
for (let i = 0, button; button = buttons[i]; i++) {
flyoutHeight = Math.max(flyoutHeight, button.height);
}
flyoutHeight += this.MARGIN * 1.5;
@@ -365,27 +350,28 @@ class HorizontalFlyout extends Flyout {
flyoutHeight += Scrollbar.scrollbarThickness;
if (this.height_ !== flyoutHeight) {
for (let i = 0, block; (block = blocks[i]); i++) {
for (let i = 0, block; block = blocks[i]; i++) {
if (this.rectMap_.has(block)) {
this.moveRectToBlock_(this.rectMap_.get(block), block);
// AnyDuringMigration because: Argument of type 'SVGElement |
// undefined' is not assignable to parameter of type 'SVGElement'.
this.moveRectToBlock_(
this.rectMap_.get(block) as AnyDuringMigration, block);
}
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
if (this.targetWorkspace!.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.TOP &&
!this.targetWorkspace.getToolbox()) {
!this.targetWorkspace!.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that
// (0,0) is in the correct position relative to the new absolute edge
// (ie toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX,
this.targetWorkspace.scrollY + flyoutHeight);
this.targetWorkspace!.translate(
this.targetWorkspace!.scrollX,
this.targetWorkspace!.scrollY + flyoutHeight);
}
// Record the height for workspace metrics and .position.
this.height_ = flyoutHeight;
this.position();
this.targetWorkspace.recordDragTargets();
this.targetWorkspace!.recordDragTargets();
}
}
}
@@ -393,5 +379,3 @@ class HorizontalFlyout extends Flyout {
registry.register(
registry.Type.FLYOUTS_HORIZONTAL_TOOLBOX, registry.DEFAULT,
HorizontalFlyout);
exports.HorizontalFlyout = HorizontalFlyout;

View File

@@ -7,51 +7,44 @@
/**
* @fileoverview Calculates and reports flyout workspace metrics.
*/
'use strict';
/**
* Calculates and reports flyout workspace metrics.
* @class
*/
goog.module('Blockly.FlyoutMetricsManager');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.FlyoutMetricsManager');
/* eslint-disable-next-line no-unused-vars */
const {IFlyout} = goog.requireType('Blockly.IFlyout');
const {MetricsManager} = goog.require('Blockly.MetricsManager');
/* eslint-disable-next-line no-unused-vars */
const {WorkspaceSvg} = goog.requireType('Blockly.WorkspaceSvg');
import type {IFlyout} from './interfaces/i_flyout.js';
import {ContainerRegion, MetricsManager} from './metrics_manager.js';
import type {WorkspaceSvg} from './workspace_svg.js';
/**
* Calculates metrics for a flyout's workspace.
* The metrics are mainly used to size scrollbars for the flyout.
* @extends {MetricsManager}
* @alias Blockly.FlyoutMetricsManager
*/
class FlyoutMetricsManager extends MetricsManager {
/**
* @param {!WorkspaceSvg} workspace The flyout's workspace.
* @param {!IFlyout} flyout The flyout.
*/
constructor(workspace, flyout) {
super(workspace);
export class FlyoutMetricsManager extends MetricsManager {
/** The flyout that owns the workspace to calculate metrics for. */
protected flyout_: IFlyout;
/**
* The flyout that owns the workspace to calculate metrics for.
* @type {!IFlyout}
* @protected
*/
/**
* @param workspace The flyout's workspace.
* @param flyout The flyout.
*/
constructor(workspace: WorkspaceSvg, flyout: IFlyout) {
super(workspace);
this.flyout_ = flyout;
}
/**
* Gets the bounding box of the blocks on the flyout's workspace.
* This is in workspace coordinates.
* @return {!SVGRect|{height: number, y: number, width: number, x: number}}
* The bounding box of the blocks on the workspace.
* @private
* @return The bounding box of the blocks on the workspace.
*/
getBoundingBox_() {
private getBoundingBox_(): SVGRect|
{height: number, y: number, width: number, x: number} {
let blockBoundingBox;
try {
blockBoundingBox = this.workspace_.getCanvas().getBBox();
@@ -64,10 +57,7 @@ class FlyoutMetricsManager extends MetricsManager {
return blockBoundingBox;
}
/**
* @override
*/
getContentMetrics(opt_getWorkspaceCoordinates) {
override getContentMetrics(opt_getWorkspaceCoordinates: boolean) {
// The bounding box is in workspace coordinates.
const blockBoundingBox = this.getBoundingBox_();
const scale = opt_getWorkspaceCoordinates ? 1 : this.workspace_.scale;
@@ -80,12 +70,12 @@ class FlyoutMetricsManager extends MetricsManager {
};
}
/**
* @override
*/
getScrollMetrics(
opt_getWorkspaceCoordinates, opt_viewMetrics, opt_contentMetrics) {
const contentMetrics = opt_contentMetrics || this.getContentMetrics();
override getScrollMetrics(
opt_getWorkspaceCoordinates: boolean, opt_viewMetrics: ContainerRegion,
opt_contentMetrics: ContainerRegion) {
// AnyDuringMigration because: Expected 1 arguments, but got 0.
const contentMetrics =
opt_contentMetrics || (this.getContentMetrics as AnyDuringMigration)();
const margin = this.flyout_.MARGIN * this.workspace_.scale;
const scale = opt_getWorkspaceCoordinates ? this.workspace_.scale : 1;
@@ -102,5 +92,3 @@ class FlyoutMetricsManager extends MetricsManager {
};
}
}
exports.FlyoutMetricsManager = FlyoutMetricsManager;

View File

@@ -7,58 +7,57 @@
/**
* @fileoverview Layout code for a vertical variant of the flyout.
*/
'use strict';
/**
* Layout code for a vertical variant of the flyout.
* @class
*/
goog.module('Blockly.VerticalFlyout');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.VerticalFlyout');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const browserEvents = goog.require('Blockly.browserEvents');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const registry = goog.require('Blockly.registry');
const toolbox = goog.require('Blockly.utils.toolbox');
/* eslint-disable-next-line no-unused-vars */
const {Coordinate} = goog.requireType('Blockly.utils.Coordinate');
const {Flyout} = goog.require('Blockly.Flyout');
/* eslint-disable-next-line no-unused-vars */
const {FlyoutItem} = goog.requireType('Blockly.Flyout');
/* eslint-disable-next-line no-unused-vars */
const {FlyoutButton} = goog.requireType('Blockly.FlyoutButton');
/* eslint-disable-next-line no-unused-vars */
const {Options} = goog.requireType('Blockly.Options');
const {Rect} = goog.require('Blockly.utils.Rect');
const {Scrollbar} = goog.require('Blockly.Scrollbar');
/** @suppress {extraRequire} */
goog.require('Blockly.Block');
/** @suppress {extraRequire} */
goog.require('Blockly.constants');
// Unused import preserved for side-effects. Remove if unneeded.
// import './block.js';
// Unused import preserved for side-effects. Remove if unneeded.
// import './constants.js';
import * as browserEvents from './browser_events.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Flyout, FlyoutItem} from './flyout_base.js';
import type {FlyoutButton} from './flyout_button.js';
import type {Options} from './options.js';
import * as registry from './registry.js';
import {Scrollbar} from './scrollbar.js';
import type {Coordinate} from './utils/coordinate.js';
import {Rect} from './utils/rect.js';
import * as toolbox from './utils/toolbox.js';
import * as WidgetDiv from './widgetdiv.js';
/**
* Class for a flyout.
* @extends {Flyout}
* @alias Blockly.VerticalFlyout
*/
class VerticalFlyout extends Flyout {
/**
* @param {!Options} workspaceOptions Dictionary of options for the
* workspace.
*/
constructor(workspaceOptions) {
export class VerticalFlyout extends Flyout {
/** The name of the vertical flyout in the registry. */
static registryName = 'verticalFlyout';
// Record the height for workspace metrics.
override height_: AnyDuringMigration;
// Record the width for workspace metrics and .position.
override width_: AnyDuringMigration;
/** @param workspaceOptions Dictionary of options for the workspace. */
constructor(workspaceOptions: Options) {
super(workspaceOptions);
}
/**
* Sets the translation of the flyout to match the scrollbars.
* @param {!{x:number,y:number}} xyRatio Contains a y property which is a
* float between 0 and 1 specifying the degree of scrolling and a similar
* x property.
* @protected
* @param xyRatio Contains a y property which is a float between 0 and 1
* specifying the degree of scrolling and a similar x property.
*/
setMetrics_(xyRatio) {
protected override setMetrics_(xyRatio: {x: number, y: number}) {
if (!this.isVisible()) {
return;
}
@@ -79,28 +78,29 @@ class VerticalFlyout extends Flyout {
/**
* Calculates the x coordinate for the flyout position.
* @return {number} X coordinate.
* @return X coordinate.
*/
getX() {
override getX(): number {
if (!this.isVisible()) {
return 0;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const metricsManager = this.targetWorkspace!.getMetricsManager();
const absoluteMetrics = metricsManager.getAbsoluteMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const toolboxMetrics = metricsManager.getToolboxMetrics();
let x = 0;
// If this flyout is not the trashcan flyout (e.g. toolbox or mutator).
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_) {
// Trashcan flyout is opposite the main flyout.
if (this.targetWorkspace!.toolboxPosition === this.toolboxPosition_) {
// If there is a category toolbox.
if (this.targetWorkspace.getToolbox()) {
// Simple (flyout-only) toolbox.
if (this.targetWorkspace!.getToolbox()) {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = toolboxMetrics.width;
} else {
x = viewMetrics.width - this.width_;
}
// Simple (flyout-only) toolbox.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
@@ -109,7 +109,6 @@ class VerticalFlyout extends Flyout {
x = viewMetrics.width;
}
}
// Trashcan flyout is opposite the main flyout.
} else {
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
x = 0;
@@ -127,24 +126,20 @@ class VerticalFlyout extends Flyout {
/**
* Calculates the y coordinate for the flyout position.
* @return {number} Y coordinate.
* @return Y coordinate.
*/
getY() {
override getY(): number {
// Y is always 0 since this is a vertical flyout.
return 0;
}
/**
* Move the flyout to the edge of the workspace.
*/
position() {
if (!this.isVisible() || !this.targetWorkspace.isVisible()) {
/** Move the flyout to the edge of the workspace. */
override position() {
if (!this.isVisible() || !this.targetWorkspace!.isVisible()) {
return;
}
const metricsManager = this.targetWorkspace.getMetricsManager();
const metricsManager = this.targetWorkspace!.getMetricsManager();
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
// Record the height for workspace metrics.
this.height_ = targetWorkspaceViewMetrics.height;
const edgeWidth = this.width_ - this.CORNER_RADIUS;
@@ -160,20 +155,18 @@ class VerticalFlyout extends Flyout {
/**
* Create and set the path for the visible boundaries of the flyout.
* @param {number} width The width of the flyout, not including the
* rounded corners.
* @param {number} height The height of the flyout, not including
* rounded corners.
* @private
* @param width The width of the flyout, not including the rounded corners.
* @param height The height of the flyout, not including rounded corners.
*/
setBackgroundPath_(width, height) {
private setBackgroundPath_(width: number, height: number) {
const atRight = this.toolboxPosition_ === toolbox.Position.RIGHT;
const totalWidth = width + this.CORNER_RADIUS;
// Decide whether to start on the left or right.
const path = ['M ' + (atRight ? totalWidth : 0) + ',0'];
const path: Array<string|number> =
['M ' + (atRight ? totalWidth : 0) + ',0'];
// Top.
path.push('h', atRight ? -width : width);
path.push('h', (atRight ? -width : width));
// Rounded corner.
path.push(
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
@@ -185,31 +178,28 @@ class VerticalFlyout extends Flyout {
'a', this.CORNER_RADIUS, this.CORNER_RADIUS, 0, 0, atRight ? 0 : 1,
atRight ? this.CORNER_RADIUS : -this.CORNER_RADIUS, this.CORNER_RADIUS);
// Bottom.
path.push('h', atRight ? width : -width);
path.push('h', (atRight ? width : -width));
path.push('z');
this.svgBackground_.setAttribute('d', path.join(' '));
this.svgBackground_!.setAttribute('d', path.join(' '));
}
/**
* Scroll the flyout to the top.
*/
scrollToStart() {
/** Scroll the flyout to the top. */
override scrollToStart() {
this.workspace_.scrollbar.setY(0);
}
/**
* Scroll the flyout.
* @param {!WheelEvent} e Mouse wheel scroll event.
* @protected
* @param e Mouse wheel scroll event.
*/
wheel_(e) {
protected override wheel_(e: WheelEvent) {
const scrollDelta = browserEvents.getScrollDeltaPixels(e);
if (scrollDelta.y) {
const metricsManager = this.workspace_.getMetricsManager();
const scrollMetrics = metricsManager.getScrollMetrics();
const viewMetrics = metricsManager.getViewMetrics();
const pos = (viewMetrics.top - scrollMetrics.top) + scrollDelta.y;
const pos = viewMetrics.top - scrollMetrics.top + scrollDelta.y;
this.workspace_.scrollbar.setY(pos);
// When the flyout moves from a wheel event, hide WidgetDiv and
@@ -217,7 +207,6 @@ class VerticalFlyout extends Flyout {
WidgetDiv.hide();
dropDownDiv.hideWithoutAnimation();
}
// Don't scroll the page.
e.preventDefault();
// Don't propagate mousewheel event (zooming).
@@ -226,43 +215,45 @@ class VerticalFlyout extends Flyout {
/**
* Lay out the blocks in the flyout.
* @param {!Array<!FlyoutItem>} contents The blocks and buttons to lay
* out.
* @param {!Array<number>} gaps The visible gaps between blocks.
* @protected
* @param contents The blocks and buttons to lay out.
* @param gaps The visible gaps between blocks.
*/
layout_(contents, gaps) {
this.workspace_.scale = this.targetWorkspace.scale;
protected override layout_(contents: FlyoutItem[], gaps: number[]) {
this.workspace_.scale = this.targetWorkspace!.scale;
const margin = this.MARGIN;
const cursorX = this.RTL ? margin : margin + this.tabWidth_;
let cursorY = margin;
for (let i = 0, item; (item = contents[i]); i++) {
for (let i = 0, item; item = contents[i]; i++) {
if (item.type === 'block') {
const block = item.block;
const allBlocks = block.getDescendants(false);
for (let j = 0, child; (child = allBlocks[j]); j++) {
const allBlocks = block!.getDescendants(false);
for (let j = 0, child; child = allBlocks[j]; j++) {
// Mark blocks as being inside a flyout. This is used to detect and
// prevent the closure of the flyout if the user right-clicks on such
// a block.
child.isInFlyout = true;
}
block.render();
const root = block.getSvgRoot();
const blockHW = block.getHeightWidth();
block!.render();
const root = block!.getSvgRoot();
const blockHW = block!.getHeightWidth();
const moveX =
block.outputConnection ? cursorX - this.tabWidth_ : cursorX;
block.moveBy(moveX, cursorY);
block!.outputConnection ? cursorX - this.tabWidth_ : cursorX;
block!.moveBy(moveX, cursorY);
// AnyDuringMigration because: Argument of type 'BlockSvg | undefined'
// is not assignable to parameter of type 'BlockSvg'.
const rect = this.createRect_(
block, this.RTL ? moveX - blockHW.width : moveX, cursorY, blockHW,
i);
block as AnyDuringMigration,
this.RTL ? moveX - blockHW.width : moveX, cursorY, blockHW, i);
this.addBlockListeners_(root, block, rect);
// AnyDuringMigration because: Argument of type 'BlockSvg | undefined'
// is not assignable to parameter of type 'BlockSvg'.
this.addBlockListeners_(root, block as AnyDuringMigration, rect);
cursorY += blockHW.height + gaps[i];
} else if (item.type === 'button') {
const button = /** @type {!FlyoutButton} */ (item.button);
const button = item.button as FlyoutButton;
this.initFlyoutButton_(button, cursorX, cursorY);
cursorY += button.height + gaps[i];
}
@@ -273,12 +264,12 @@ class VerticalFlyout extends Flyout {
* Determine if a drag delta is toward the workspace, based on the position
* and orientation of the flyout. This is used in determineDragIntention_ to
* determine if a new block should be created or if the flyout should scroll.
* @param {!Coordinate} currentDragDeltaXY How far the pointer has
* moved from the position at mouse down, in pixel units.
* @return {boolean} True if the drag is toward the workspace.
* @package
* @param currentDragDeltaXY How far the pointer has moved from the position
* at mouse down, in pixel units.
* @return True if the drag is toward the workspace.
* @internal
*/
isDragTowardWorkspace(currentDragDeltaXY) {
override isDragTowardWorkspace(currentDragDeltaXY: Coordinate): boolean {
const dx = currentDragDeltaXY.x;
const dy = currentDragDeltaXY.y;
// Direction goes from -180 to 180, with 0 toward the right and 90 on top.
@@ -286,7 +277,7 @@ class VerticalFlyout extends Flyout {
const range = this.dragAngleRange_;
// Check for left or right dragging.
if ((dragDirection < range && dragDirection > -range) ||
if (dragDirection < range && dragDirection > -range ||
(dragDirection < -180 + range || dragDirection > 180 - range)) {
return true;
}
@@ -296,10 +287,10 @@ class VerticalFlyout extends Flyout {
/**
* Returns the bounding rectangle of the drag target area in pixel units
* relative to viewport.
* @return {?Rect} The component's bounding box. Null if drag
* target area should be ignored.
* @return The component's bounding box. Null if drag target area should be
* ignored.
*/
getClientRect() {
override getClientRect(): Rect|null {
if (!this.svgGroup_ || this.autoClose || !this.isVisible()) {
// The bounding rectangle won't compute correctly if the flyout is closed
// and auto-close flyouts aren't valid drag targets (or delete areas).
@@ -325,20 +316,19 @@ class VerticalFlyout extends Flyout {
/**
* Compute width of flyout. toolbox.Position mat under each block.
* For RTL: Lay out the blocks and buttons to be right-aligned.
* @protected
*/
reflowInternal_() {
protected override reflowInternal_() {
this.workspace_.scale = this.getFlyoutScale();
let flyoutWidth = 0;
const blocks = this.workspace_.getTopBlocks(false);
for (let i = 0, block; (block = blocks[i]); i++) {
for (let i = 0, block; block = blocks[i]; i++) {
let width = block.getHeightWidth().width;
if (block.outputConnection) {
width -= this.tabWidth_;
}
flyoutWidth = Math.max(flyoutWidth, width);
}
for (let i = 0, button; (button = this.buttons_[i]); i++) {
for (let i = 0, button; button = this.buttons_[i]; i++) {
flyoutWidth = Math.max(flyoutWidth, button.width);
}
flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_;
@@ -346,7 +336,7 @@ class VerticalFlyout extends Flyout {
flyoutWidth += Scrollbar.scrollbarThickness;
if (this.width_ !== flyoutWidth) {
for (let i = 0, block; (block = blocks[i]); i++) {
for (let i = 0, block; block = blocks[i]; i++) {
if (this.RTL) {
// With the flyoutWidth known, right-align the blocks.
const oldX = block.getRelativeToSurfaceXY().x;
@@ -357,12 +347,15 @@ class VerticalFlyout extends Flyout {
block.moveBy(newX - oldX, 0);
}
if (this.rectMap_.has(block)) {
this.moveRectToBlock_(this.rectMap_.get(block), block);
// AnyDuringMigration because: Argument of type 'SVGElement |
// undefined' is not assignable to parameter of type 'SVGElement'.
this.moveRectToBlock_(
this.rectMap_.get(block) as AnyDuringMigration, block);
}
}
if (this.RTL) {
// With the flyoutWidth known, right-align the buttons.
for (let i = 0, button; (button = this.buttons_[i]); i++) {
for (let i = 0, button; button = this.buttons_[i]; i++) {
const y = button.getPosition().y;
const x = flyoutWidth / this.workspace_.scale - button.width -
this.MARGIN - this.tabWidth_;
@@ -370,32 +363,22 @@ class VerticalFlyout extends Flyout {
}
}
if (this.targetWorkspace.toolboxPosition === this.toolboxPosition_ &&
if (this.targetWorkspace!.toolboxPosition === this.toolboxPosition_ &&
this.toolboxPosition_ === toolbox.Position.LEFT &&
!this.targetWorkspace.getToolbox()) {
!this.targetWorkspace!.getToolbox()) {
// This flyout is a simple toolbox. Reposition the workspace so that
// (0,0) is in the correct position relative to the new absolute edge
// (ie toolbox edge).
this.targetWorkspace.translate(
this.targetWorkspace.scrollX + flyoutWidth,
this.targetWorkspace.scrollY);
this.targetWorkspace!.translate(
this.targetWorkspace!.scrollX + flyoutWidth,
this.targetWorkspace!.scrollY);
}
// Record the width for workspace metrics and .position.
this.width_ = flyoutWidth;
this.position();
this.targetWorkspace.recordDragTargets();
this.targetWorkspace!.recordDragTargets();
}
}
}
/**
* The name of the vertical flyout in the registry.
* @type {string}
*/
VerticalFlyout.registryName = 'verticalFlyout';
registry.register(
registry.Type.FLYOUTS_VERTICAL_TOOLBOX, registry.DEFAULT, VerticalFlyout);
exports.VerticalFlyout = VerticalFlyout;

View File

@@ -8,23 +8,20 @@
* @fileoverview Utility functions for generating executable code from
* Blockly code.
*/
'use strict';
/**
* Utility functions for generating executable code from
* Blockly code.
* @class
*/
goog.module('Blockly.Generator');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Generator');
const common = goog.require('Blockly.common');
const deprecation = goog.require('Blockly.utils.deprecation');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {Names, NameType} = goog.require('Blockly.Names');
/* eslint-disable-next-line no-unused-vars */
const {Workspace} = goog.requireType('Blockly.Workspace');
import type {Block} from './block.js';
import * as common from './common.js';
import {Names, NameType} from './names.js';
import * as deprecation from './utils/deprecation.js';
import type {Workspace} from './workspace.js';
/**
@@ -32,115 +29,91 @@ const {Workspace} = goog.requireType('Blockly.Workspace');
* @unrestricted
* @alias Blockly.Generator
*/
class Generator {
/**
* @param {string} name Language name of this generator.
*/
constructor(name) {
this.name_ = name;
export class Generator {
name_: AnyDuringMigration;
/**
* This is used as a placeholder in functions defined using
* Generator.provideFunction_. It must not be legal code that could
* legitimately appear in a function definition (or comment), and it must
* not confuse the regular expression parser.
* @type {string}
* @protected
*/
this.FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}';
/**
* This is used as a placeholder in functions defined using
* Generator.provideFunction_. It must not be legal code that could
* legitimately appear in a function definition (or comment), and it must
* not confuse the regular expression parser.
*/
protected FUNCTION_NAME_PLACEHOLDER_ = '{leCUI8hutHZI4480Dc}';
FUNCTION_NAME_PLACEHOLDER_REGEXP_: AnyDuringMigration;
/**
* Arbitrary code to inject into locations that risk causing infinite loops.
* Any instances of '%1' will be replaced by the block ID that failed.
* E.g. ' checkTimeout(%1);\n'
*/
INFINITE_LOOP_TRAP: string|null = null;
/**
* Arbitrary code to inject before every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
*/
STATEMENT_PREFIX: string|null = null;
/**
* Arbitrary code to inject after every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
*/
STATEMENT_SUFFIX: string|null = null;
/**
* The method of indenting. Defaults to two spaces, but language generators
* may override this to increase indent or change to tabs.
*/
INDENT = ' ';
/**
* Maximum length for a comment before wrapping. Does not account for
* indenting level.
*/
COMMENT_WRAP = 60;
/** List of outer-inner pairings that do NOT require parentheses. */
ORDER_OVERRIDES: number[][] = [];
/**
* Whether the init method has been called.
* Generators that set this flag to false after creation and true in init
* will cause blockToCode to emit a warning if the generator has not been
* initialized. If this flag is untouched, it will have no effect.
*/
isInitialized: boolean|null = null;
/** Comma-separated list of reserved words. */
protected RESERVED_WORDS_ = '';
/** A dictionary of definitions to be printed before the code. */
protected definitions_?: AnyDuringMigration = undefined;
/**
* A dictionary mapping desired function names in definitions_ to actual
* function names (to avoid collisions with user functions).
*/
protected functionNames_?: AnyDuringMigration = undefined;
/** A database of variable and procedure names. */
protected nameDB_?: Names = undefined;
/** @param name Language name of this generator. */
constructor(name: string) {
this.name_ = name;
this.FUNCTION_NAME_PLACEHOLDER_REGEXP_ =
new RegExp(this.FUNCTION_NAME_PLACEHOLDER_, 'g');
/**
* Arbitrary code to inject into locations that risk causing infinite loops.
* Any instances of '%1' will be replaced by the block ID that failed.
* E.g. ' checkTimeout(%1);\n'
* @type {?string}
*/
this.INFINITE_LOOP_TRAP = null;
/**
* Arbitrary code to inject before every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
* @type {?string}
*/
this.STATEMENT_PREFIX = null;
/**
* Arbitrary code to inject after every statement.
* Any instances of '%1' will be replaced by the block ID of the statement.
* E.g. 'highlight(%1);\n'
* @type {?string}
*/
this.STATEMENT_SUFFIX = null;
/**
* The method of indenting. Defaults to two spaces, but language generators
* may override this to increase indent or change to tabs.
* @type {string}
*/
this.INDENT = ' ';
/**
* Maximum length for a comment before wrapping. Does not account for
* indenting level.
* @type {number}
*/
this.COMMENT_WRAP = 60;
/**
* List of outer-inner pairings that do NOT require parentheses.
* @type {!Array<!Array<number>>}
*/
this.ORDER_OVERRIDES = [];
/**
* Whether the init method has been called.
* Generators that set this flag to false after creation and true in init
* will cause blockToCode to emit a warning if the generator has not been
* initialized. If this flag is untouched, it will have no effect.
* @type {?boolean}
*/
this.isInitialized = null;
/**
* Comma-separated list of reserved words.
* @type {string}
* @protected
*/
this.RESERVED_WORDS_ = '';
/**
* A dictionary of definitions to be printed before the code.
* @type {!Object|undefined}
* @protected
*/
this.definitions_ = undefined;
/**
* A dictionary mapping desired function names in definitions_ to actual
* function names (to avoid collisions with user functions).
* @type {!Object|undefined}
* @protected
*/
this.functionNames_ = undefined;
/**
* A database of variable and procedure names.
* @type {!Names|undefined}
* @protected
*/
this.nameDB_ = undefined;
}
/**
* Generate code for all blocks in the workspace to the specified language.
* @param {!Workspace=} workspace Workspace to generate code from.
* @return {string} Generated code.
* @param workspace Workspace to generate code from.
* @return Generated code.
*/
workspaceToCode(workspace) {
workspaceToCode(workspace?: Workspace): string {
if (!workspace) {
// Backwards compatibility from before there could be multiple workspaces.
console.warn(
@@ -150,7 +123,7 @@ class Generator {
let code = [];
this.init(workspace);
const blocks = workspace.getTopBlocks(true);
for (let i = 0, block; (block = blocks[i]); i++) {
for (let i = 0, block; block = blocks[i]; i++) {
let line = this.blockToCode(block);
if (Array.isArray(line)) {
// Value blocks return tuples of code and operator order.
@@ -161,7 +134,9 @@ class Generator {
if (block.outputConnection) {
// This block is a naked value. Ask the language's code generator if
// it wants to append a semicolon, or something.
line = this.scrubNakedValue(line);
// AnyDuringMigration because: Argument of type 'string | any[]' is
// not assignable to parameter of type 'string'.
line = this.scrubNakedValue(line as AnyDuringMigration);
if (this.STATEMENT_PREFIX && !block.suppressPrefixSuffix) {
line = this.injectId(this.STATEMENT_PREFIX, block) + line;
}
@@ -172,10 +147,18 @@ class Generator {
code.push(line);
}
}
code = code.join('\n'); // Blank line between each section.
code = this.finish(code);
// AnyDuringMigration because: Type 'string' is not assignable to type
// 'any[]'.
// Blank line between each section.
code = code.join('\n') as AnyDuringMigration;
// AnyDuringMigration because: Argument of type 'any[]' is not assignable
// to parameter of type 'string'. AnyDuringMigration because: Type 'string'
// is not assignable to type 'any[]'.
code = this.finish(code as AnyDuringMigration) as AnyDuringMigration;
// Final scrubbing of whitespace.
code = code.replace(/^\s+\n/, '');
// AnyDuringMigration because: Property 'replace' does not exist on type
// 'any[]'.
code = (code as AnyDuringMigration).replace(/^\s+\n/, '');
code = code.replace(/\n\s+$/, '\n');
code = code.replace(/[ \t]+\n/g, '\n');
return code;
@@ -188,20 +171,20 @@ class Generator {
/**
* Prepend a common prefix onto each line of code.
* Intended for indenting code or adding comment markers.
* @param {string} text The lines of code.
* @param {string} prefix The common prefix.
* @return {string} The prefixed lines of code.
* @param text The lines of code.
* @param prefix The common prefix.
* @return The prefixed lines of code.
*/
prefixLines(text, prefix) {
prefixLines(text: string, prefix: string): string {
return prefix + text.replace(/(?!\n$)\n/g, '\n' + prefix);
}
/**
* Recursively spider a tree of blocks, returning all their comments.
* @param {!Block} block The block from which to start spidering.
* @return {string} Concatenated list of comments.
* @param block The block from which to start spidering.
* @return Concatenated list of comments.
*/
allNestedComments(block) {
allNestedComments(block: Block): string {
const comments = [];
const blocks = block.getDescendants(true);
for (let i = 0; i < blocks.length; i++) {
@@ -220,14 +203,14 @@ class Generator {
/**
* Generate code for the specified block (and attached blocks).
* The generator must be initialized before calling this function.
* @param {?Block} block The block to generate code for.
* @param {boolean=} opt_thisOnly True to generate code for only this
* statement.
* @return {string|!Array} For statement blocks, the generated code.
* @param block The block to generate code for.
* @param opt_thisOnly True to generate code for only this statement.
* @return For statement blocks, the generated code.
* For value blocks, an array containing the generated code and an
* operator order value. Returns '' if block is null.
* operator order value. Returns '' if block is null.
*/
blockToCode(block, opt_thisOnly) {
blockToCode(block: Block|null, opt_thisOnly?: boolean): string
|AnyDuringMigration[] {
if (this.isInitialized === false) {
console.warn(
'Generator init was not called before blockToCode was called.');
@@ -244,7 +227,7 @@ class Generator {
return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]);
}
const func = this[block.type];
const func = (this as AnyDuringMigration)[block.type];
if (typeof func !== 'function') {
throw Error(
'Language "' + this.name_ + '" does not know how to generate ' +
@@ -279,14 +262,14 @@ class Generator {
/**
* Generate code representing the specified value input.
* @param {!Block} block The block containing the input.
* @param {string} name The name of the input.
* @param {number} outerOrder The maximum binding strength (minimum order
* value) of any operators adjacent to "block".
* @return {string} Generated code or '' if no blocks are connected or the
* specified input does not exist.
* @param block The block containing the input.
* @param name The name of the input.
* @param outerOrder The maximum binding strength (minimum order value) of any
* operators adjacent to "block".
* @return Generated code or '' if no blocks are connected or the specified
* input does not exist.
*/
valueToCode(block, name, outerOrder) {
valueToCode(block: Block, name: string, outerOrder: number): string {
if (isNaN(outerOrder)) {
throw TypeError('Expecting valid order from block: ' + block.type);
}
@@ -353,11 +336,11 @@ class Generator {
* statement input. Indent the code.
* This is mainly used in generators. When trying to generate code to evaluate
* look at using workspaceToCode or blockToCode.
* @param {!Block} block The block containing the input.
* @param {string} name The name of the input.
* @return {string} Generated code or '' if no blocks are connected.
* @param block The block containing the input.
* @param name The name of the input.
* @return Generated code or '' if no blocks are connected.
*/
statementToCode(block, name) {
statementToCode(block: Block, name: string): string {
const targetBlock = block.getInputTargetBlock(name);
let code = this.blockToCode(targetBlock);
// Value blocks must return code and order of operations info.
@@ -368,7 +351,7 @@ class Generator {
(targetBlock && targetBlock.type));
}
if (code) {
code = this.prefixLines(/** @type {string} */ (code), this.INDENT);
code = this.prefixLines((code), this.INDENT);
}
return code;
}
@@ -378,11 +361,11 @@ class Generator {
* Add statement suffix at the start of the loop block (right after the loop
* statement executes), and a statement prefix to the end of the loop block
* (right before the loop statement executes).
* @param {string} branch Code for loop contents.
* @param {!Block} block Enclosing block.
* @return {string} Loop contents, with infinite loop trap added.
* @param branch Code for loop contents.
* @param block Enclosing block.
* @return Loop contents, with infinite loop trap added.
*/
addLoopTrap(branch, block) {
addLoopTrap(branch: string, block: Block): string {
if (this.INFINITE_LOOP_TRAP) {
branch = this.prefixLines(
this.injectId(this.INFINITE_LOOP_TRAP, block), this.INDENT) +
@@ -404,21 +387,21 @@ class Generator {
/**
* Inject a block ID into a message to replace '%1'.
* Used for STATEMENT_PREFIX, STATEMENT_SUFFIX, and INFINITE_LOOP_TRAP.
* @param {string} msg Code snippet with '%1'.
* @param {!Block} block Block which has an ID.
* @return {string} Code snippet with ID.
* @param msg Code snippet with '%1'.
* @param block Block which has an ID.
* @return Code snippet with ID.
*/
injectId(msg, block) {
injectId(msg: string, block: Block): string {
const id = block.id.replace(/\$/g, '$$$$'); // Issue 251.
return msg.replace(/%1/g, '\'' + id + '\'');
}
/**
* Add one or more words to the list of reserved words for this language.
* @param {string} words Comma-separated list of words to add to the list.
* @param words Comma-separated list of words to add to the list.
* No spaces. Duplicates are ok.
*/
addReservedWords(words) {
addReservedWords(words: string) {
this.RESERVED_WORDS_ += words + ',';
}
@@ -436,18 +419,17 @@ class Generator {
*
* The code gets output when Generator.finish() is called.
*
* @param {string} desiredName The desired name of the function
* (e.g. mathIsPrime).
* @param {!Array<string>|string} code A list of statements or one multi-line
* code string. Use ' ' for indents (they will be replaced).
* @return {string} The actual name of the new function. This may differ
* from desiredName if the former has already been taken by the user.
* @protected
* @param desiredName The desired name of the function (e.g. mathIsPrime).
* @param code A list of statements or one multi-line code string. Use ' '
* for indents (they will be replaced).
* @return The actual name of the new function. This may differ from
* desiredName if the former has already been taken by the user.
*/
provideFunction_(desiredName, code) {
protected provideFunction_(desiredName: string, code: string[]|string):
string {
if (!this.definitions_[desiredName]) {
const functionName =
this.nameDB_.getDistinctName(desiredName, NameType.PROCEDURE);
this.nameDB_!.getDistinctName(desiredName, NameType.PROCEDURE);
this.functionNames_[desiredName] = functionName;
if (Array.isArray(code)) {
code = code.join('\n');
@@ -473,9 +455,9 @@ class Generator {
* Hook for code to run before code generation starts.
* Subclasses may override this, e.g. to initialise the database of variable
* names.
* @param {!Workspace} _workspace Workspace to generate code from.
* @param _workspace Workspace to generate code from.
*/
init(_workspace) {
init(_workspace: Workspace) {
// Optionally override
// Create a dictionary of definitions to be printed before the code.
this.definitions_ = Object.create(null);
@@ -491,14 +473,13 @@ class Generator {
* Subclasses may override this, e.g. to generate code for statements
* following the block, or to handle comments for the specified block and any
* connected value blocks.
* @param {!Block} _block The current block.
* @param {string} code The code created for this block.
* @param {boolean=} _opt_thisOnly True to generate code for only this
* statement.
* @return {string} Code with comments and subsequent blocks added.
* @protected
* @param _block The current block.
* @param code The code created for this block.
* @param _opt_thisOnly True to generate code for only this statement.
* @return Code with comments and subsequent blocks added.
*/
scrub_(_block, code, _opt_thisOnly) {
protected scrub_(_block: Block, code: string, _opt_thisOnly?: boolean):
string {
// Optionally override
return code;
}
@@ -507,10 +488,10 @@ class Generator {
* Hook for code to run at end of code generation.
* Subclasses may override this, e.g. to prepend the generated code with
* import statements or variable definitions.
* @param {string} code Generated code.
* @return {string} Completed code.
* @param code Generated code.
* @return Completed code.
*/
finish(code) {
finish(code: string): string {
// Optionally override
// Clean up temporary data.
delete this.definitions_;
@@ -523,10 +504,10 @@ class Generator {
* anything.
* Subclasses may override this, e.g. if their language does not allow
* naked values.
* @param {string} line Line of generated code.
* @return {string} Legal line of code.
* @param line Line of generated code.
* @return Legal line of code.
*/
scrubNakedValue(line) {
scrubNakedValue(line: string): string {
// Optionally override
return line;
}
@@ -536,29 +517,23 @@ Object.defineProperties(Generator.prototype, {
/**
* A database of variable names.
* @name Blockly.Generator.prototype.variableDB_
* @type {!Names|undefined}
* @protected
* @deprecated 'variableDB_' was renamed to 'nameDB_' (May 2021).
* @suppress {checkTypes}
*/
variableDB_: {
/**
* @this {Generator}
* @return {!Names|undefined} Name database.
*/
get: function() {
deprecation.warn('variableDB_', 'May 2021', 'September 2022', 'nameDB_');
return this.nameDB_;
},
/**
* @this {Generator}
* @param {!Names|undefined} nameDb New name database.
*/
set: function(nameDb) {
deprecation.warn('variableDB_', 'May 2021', 'September 2022', 'nameDB_');
// AnyDuringMigration because: Type 'Names | undefined' is not assignable to
// type 'PropertyDescriptor'.
variableDB_: ({
/** @return Name database. */
get(this: Generator): Names |
undefined {
deprecation.warn(
'variableDB_', 'May 2021', 'September 2022', 'nameDB_');
return this.nameDB_;
},
/** @param nameDb New name database. */
set(this: Generator, nameDb: Names|undefined) {
deprecation.warn('variableDB_', 'May 2021', 'September2022', 'nameDB_');
this.nameDB_ = nameDb;
},
},
}),
});
exports.Generator = Generator;

File diff suppressed because it is too large Load Diff

View File

@@ -8,134 +8,114 @@
* @fileoverview Object for configuring and updating a workspace grid in
* Blockly.
*/
'use strict';
/**
* Object for configuring and updating a workspace grid in
* Blockly.
* @class
*/
goog.module('Blockly.Grid');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Grid');
const dom = goog.require('Blockly.utils.dom');
const userAgent = goog.require('Blockly.utils.userAgent');
const {Svg} = goog.require('Blockly.utils.Svg');
import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
/**
* Class for a workspace's grid.
* @alias Blockly.Grid
*/
class Grid {
export class Grid {
/**
* @param {!SVGElement} pattern The grid's SVG pattern, created during
* injection.
* @param {!Object} options A dictionary of normalized options for the grid.
* The scale of the grid, used to set stroke width on grid lines.
* This should always be the same as the workspace scale.
*/
private scale_ = 1;
private readonly spacing_: number;
private readonly length_: number;
private readonly line1_: SVGElement;
private readonly line2_: SVGElement;
private readonly snapToGrid_: boolean;
/**
* @param pattern The grid's SVG pattern, created during injection.
* @param options A dictionary of normalized options for the grid.
* See grid documentation:
* https://developers.google.com/blockly/guides/configure/web/grid
*/
constructor(pattern, options) {
/**
* The scale of the grid, used to set stroke width on grid lines.
* This should always be the same as the workspace scale.
* @type {number}
* @private
*/
this.scale_ = 1;
/**
* The grid's SVG pattern, created during injection.
* @type {!SVGElement}
* @private
*/
this.gridPattern_ = pattern;
/**
* The spacing of the grid lines (in px).
* @type {number}
* @private
*/
constructor(private pattern: SVGElement, options: AnyDuringMigration) {
/** The spacing of the grid lines (in px). */
this.spacing_ = options['spacing'];
/**
* How long the grid lines should be (in px).
* @type {number}
* @private
*/
/** How long the grid lines should be (in px). */
this.length_ = options['length'];
/**
* The horizontal grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line1_ = /** @type {SVGElement} */ (pattern.firstChild);
/** The horizontal grid line, if it exists. */
this.line1_ = pattern.firstChild as SVGElement;
/**
* The vertical grid line, if it exists.
* @type {SVGElement}
* @private
*/
this.line2_ =
this.line1_ && (/** @type {SVGElement} */ (this.line1_.nextSibling));
/** The vertical grid line, if it exists. */
this.line2_ = this.line1_ && this.line1_.nextSibling as SVGElement;
/**
* Whether blocks should snap to the grid.
* @type {boolean}
* @private
*/
/** Whether blocks should snap to the grid. */
this.snapToGrid_ = options['snap'];
}
/**
* Dispose of this grid and unlink from the DOM.
* @package
* @suppress {checkTypes}
* @internal
*/
dispose() {
this.gridPattern_ = null;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'SVGElement'.
this.pattern = null as AnyDuringMigration;
}
/**
* Whether blocks should snap to the grid, based on the initial configuration.
* @return {boolean} True if blocks should snap, false otherwise.
* @package
* @return True if blocks should snap, false otherwise.
* @internal
*/
shouldSnap() {
shouldSnap(): boolean {
return this.snapToGrid_;
}
/**
* Get the spacing of the grid points (in px).
* @return {number} The spacing of the grid points.
* @package
* @return The spacing of the grid points.
* @internal
*/
getSpacing() {
getSpacing(): number {
return this.spacing_;
}
/**
* Get the ID of the pattern element, which should be randomized to avoid
* conflicts with other Blockly instances on the page.
* @return {string} The pattern ID.
* @package
* @return The pattern ID.
* @internal
*/
getPatternId() {
return this.gridPattern_.id;
getPatternId(): string {
return this.pattern.id;
}
/**
* Update the grid with a new scale.
* @param {number} scale The new workspace scale.
* @package
* @param scale The new workspace scale.
* @internal
*/
update(scale) {
update(scale: number) {
this.scale_ = scale;
// MSIE freaks if it sees a 0x0 pattern, so set empty patterns to 100x100.
const safeSpacing = (this.spacing_ * scale) || 100;
const safeSpacing = this.spacing_ * scale || 100;
this.gridPattern_.setAttribute('width', safeSpacing);
this.gridPattern_.setAttribute('height', safeSpacing);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.pattern.setAttribute('width', safeSpacing as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.pattern.setAttribute('height', safeSpacing as AnyDuringMigration);
let half = Math.floor(this.spacing_ / 2) + 0.5;
let start = half - this.length_ / 2;
@@ -152,34 +132,49 @@ class Grid {
/**
* Set the attributes on one of the lines in the grid. Use this to update the
* length and stroke width of the grid lines.
* @param {SVGElement} line Which line to update.
* @param {number} width The new stroke size (in px).
* @param {number} x1 The new x start position of the line (in px).
* @param {number} x2 The new x end position of the line (in px).
* @param {number} y1 The new y start position of the line (in px).
* @param {number} y2 The new y end position of the line (in px).
* @private
* @param line Which line to update.
* @param width The new stroke size (in px).
* @param x1 The new x start position of the line (in px).
* @param x2 The new x end position of the line (in px).
* @param y1 The new y start position of the line (in px).
* @param y2 The new y end position of the line (in px).
*/
setLineAttributes_(line, width, x1, x2, y1, y2) {
private setLineAttributes_(
line: SVGElement, width: number, x1: number, x2: number, y1: number,
y2: number) {
if (line) {
line.setAttribute('stroke-width', width);
line.setAttribute('x1', x1);
line.setAttribute('y1', y1);
line.setAttribute('x2', x2);
line.setAttribute('y2', y2);
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
line.setAttribute('stroke-width', width as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
line.setAttribute('x1', x1 as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
line.setAttribute('y1', y1 as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
line.setAttribute('x2', x2 as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not
// assignable to parameter of type 'string'.
line.setAttribute('y2', y2 as AnyDuringMigration);
}
}
/**
* Move the grid to a new x and y position, and make sure that change is
* visible.
* @param {number} x The new x position of the grid (in px).
* @param {number} y The new y position of the grid (in px).
* @package
* @param x The new x position of the grid (in px).
* @param y The new y position of the grid (in px).
* @internal
*/
moveTo(x, y) {
this.gridPattern_.setAttribute('x', x);
this.gridPattern_.setAttribute('y', y);
moveTo(x: number, y: number) {
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.pattern.setAttribute('x', x as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'number' is not assignable
// to parameter of type 'string'.
this.pattern.setAttribute('y', y as AnyDuringMigration);
if (userAgent.IE || userAgent.EDGE) {
// IE/Edge doesn't notice that the x/y offsets have changed.
@@ -190,23 +185,26 @@ class Grid {
/**
* Create the DOM for the grid described by options.
* @param {string} rnd A random ID to append to the pattern's ID.
* @param {!Object} gridOptions The object containing grid configuration.
* @param {!SVGElement} defs The root SVG element for this workspace's defs.
* @return {!SVGElement} The SVG element for the grid pattern.
* @package
* @param rnd A random ID to append to the pattern's ID.
* @param gridOptions The object containing grid configuration.
* @param defs The root SVG element for this workspace's defs.
* @return The SVG element for the grid pattern.
* @internal
*/
static createDom(rnd, gridOptions, defs) {
static createDom(
rnd: string, gridOptions: AnyDuringMigration,
defs: SVGElement): SVGElement {
/*
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
<rect stroke="#888" />
<rect stroke="#888" />
</pattern>
*/
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
<rect stroke="#888" />
<rect stroke="#888" />
</pattern>
*/
const gridPattern = dom.createSvgElement(
Svg.PATTERN,
{'id': 'blocklyGridPattern' + rnd, 'patternUnits': 'userSpaceOnUse'},
defs);
// x1, y1, x1, x2 properties will be set later in update.
if (gridOptions['length'] > 0 && gridOptions['spacing'] > 0) {
dom.createSvgElement(
Svg.LINE, {'stroke': gridOptions['colour']}, gridPattern);
@@ -214,7 +212,6 @@ class Grid {
dom.createSvgElement(
Svg.LINE, {'stroke': gridOptions['colour']}, gridPattern);
}
// x1, y1, x1, x2 properties will be set later in update.
} else {
// Edge 16 doesn't handle empty patterns
dom.createSvgElement(Svg.LINE, {}, gridPattern);
@@ -222,5 +219,3 @@ class Grid {
return gridPattern;
}
}
exports.Grid = Grid;

View File

@@ -7,138 +7,111 @@
/**
* @fileoverview Object representing an icon on a block.
*/
'use strict';
/**
* Object representing an icon on a block.
* @class
*/
goog.module('Blockly.Icon');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Icon');
const browserEvents = goog.require('Blockly.browserEvents');
const dom = goog.require('Blockly.utils.dom');
const svgMath = goog.require('Blockly.utils.svgMath');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {Bubble} = goog.requireType('Blockly.Bubble');
const {Coordinate} = goog.require('Blockly.utils.Coordinate');
const {Size} = goog.require('Blockly.utils.Size');
const {Svg} = goog.require('Blockly.utils.Svg');
import type {BlockSvg} from './block_svg.js';
import * as browserEvents from './browser_events.js';
import type {Bubble} from './bubble.js';
import {Coordinate} from './utils/coordinate.js';
import * as dom from './utils/dom.js';
import {Size} from './utils/size.js';
import {Svg} from './utils/svg.js';
import * as svgMath from './utils/svg_math.js';
/**
* Class for an icon.
* @abstract
* @alias Blockly.Icon
*/
class Icon {
/**
* @param {BlockSvg} block The block associated with this icon.
*/
constructor(block) {
/**
* The block this icon is attached to.
* @type {BlockSvg}
* @protected
*/
export abstract class Icon {
protected block_: BlockSvg;
/** The icon SVG group. */
iconGroup_: SVGGElement|null = null;
/** Whether this icon gets hidden when the block is collapsed. */
collapseHidden = true;
/** Height and width of icons. */
readonly SIZE = 17;
/** Bubble UI (if visible). */
protected bubble_: Bubble|null = null;
/** Absolute coordinate of icon's center. */
protected iconXY_: Coordinate|null = null;
/** @param block The block associated with this icon. */
constructor(block: BlockSvg) {
this.block_ = block;
/**
* The icon SVG group.
* @type {?SVGGElement}
*/
this.iconGroup_ = null;
/**
* Whether this icon gets hidden when the block is collapsed.
* @type {boolean}
*/
this.collapseHidden = true;
/**
* Height and width of icons.
* @const
*/
this.SIZE = 17;
/**
* Bubble UI (if visible).
* @type {?Bubble}
* @protected
*/
this.bubble_ = null;
/**
* Absolute coordinate of icon's center.
* @type {?Coordinate}
* @protected
*/
this.iconXY_ = null;
}
/**
* Create the icon on the block.
*/
/** Create the icon on the block. */
createIcon() {
if (this.iconGroup_) {
// Icon already exists.
return;
}
/* Here's the markup that will be generated:
<g class="blocklyIconGroup">
...
</g>
*/
<g class="blocklyIconGroup">
...
</g>
*/
this.iconGroup_ =
dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'}, null);
dom.createSvgElement(Svg.G, {'class': 'blocklyIconGroup'});
if (this.block_.isInFlyout) {
dom.addClass(
/** @type {!Element} */ (this.iconGroup_),
'blocklyIconGroupReadonly');
dom.addClass(this.iconGroup_ as Element, 'blocklyIconGroupReadonly');
}
this.drawIcon_(this.iconGroup_);
// AnyDuringMigration because: Argument of type 'SVGGElement | null' is not
// assignable to parameter of type 'Element'.
this.drawIcon_(this.iconGroup_ as AnyDuringMigration);
this.block_.getSvgRoot().appendChild(this.iconGroup_);
// AnyDuringMigration because: Argument of type 'SVGGElement | null' is not
// assignable to parameter of type 'Node'.
this.block_.getSvgRoot().appendChild(this.iconGroup_ as AnyDuringMigration);
// AnyDuringMigration because: Argument of type 'SVGGElement | null' is not
// assignable to parameter of type 'EventTarget'.
browserEvents.conditionalBind(
this.iconGroup_, 'mouseup', this, this.iconClick_);
this.iconGroup_ as AnyDuringMigration, 'mouseup', this,
this.iconClick_);
this.updateEditable();
}
/**
* Dispose of this icon.
*/
/** Dispose of this icon. */
dispose() {
// Dispose of and unlink the icon.
dom.removeNode(this.iconGroup_);
this.iconGroup_ = null;
// Dispose of and unlink the bubble.
this.setVisible(false);
this.block_ = null;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'BlockSvg'.
this.block_ = null as AnyDuringMigration;
}
/**
* Add or remove the UI indicating if this icon may be clicked or not.
*/
updateEditable() {
// No-op on the base class.
}
/** Add or remove the UI indicating if this icon may be clicked or not. */
updateEditable() {}
// No-op on the base class.
/**
* Is the associated bubble visible?
* @return {boolean} True if the bubble is visible.
* @return True if the bubble is visible.
*/
isVisible() {
isVisible(): boolean {
return !!this.bubble_;
}
/**
* Clicking on the icon toggles if the bubble is visible.
* @param {!Event} e Mouse click event.
* @protected
* @param e Mouse click event.
*/
iconClick_(e) {
if (this.block_.workspace.isDragging()) {
protected iconClick_(e: Event) {
if (this.block_.workspace!.isDragging()) {
// Drag operation is concluding. Don't open the editor.
return;
}
@@ -147,23 +120,21 @@ class Icon {
}
}
/**
* Change the colour of the associated bubble to match its block.
*/
/** Change the colour of the associated bubble to match its block. */
applyColour() {
if (this.isVisible()) {
this.bubble_.setColour(this.block_.style.colourPrimary);
this.bubble_!.setColour(this.block_.style.colourPrimary);
}
}
/**
* Notification that the icon has moved. Update the arrow accordingly.
* @param {!Coordinate} xy Absolute location in workspace coordinates.
* @param xy Absolute location in workspace coordinates.
*/
setIconLocation(xy) {
setIconLocation(xy: Coordinate) {
this.iconXY_ = xy;
if (this.isVisible()) {
this.bubble_.setAnchorLocation(xy);
this.bubble_!.setAnchorLocation(xy);
}
}
@@ -174,8 +145,7 @@ class Icon {
computeIconLocation() {
// Find coordinates for the centre of the icon and update the arrow.
const blockXY = this.block_.getRelativeToSurfaceXY();
const iconXY = svgMath.getRelativeXY(
/** @type {!SVGElement} */ (this.iconGroup_));
const iconXY = svgMath.getRelativeXY(this.iconGroup_ as SVGElement);
const newXY = new Coordinate(
blockXY.x + iconXY.x + this.SIZE / 2,
blockXY.y + iconXY.y + this.SIZE / 2);
@@ -186,10 +156,9 @@ class Icon {
/**
* Returns the center of the block's icon relative to the surface.
* @return {?Coordinate} Object with x and y properties in
* workspace coordinates.
* @return Object with x and y properties in workspace coordinates.
*/
getIconLocation() {
getIconLocation(): Coordinate|null {
return this.iconXY_;
}
@@ -197,29 +166,24 @@ class Icon {
* Get the size of the icon as used for rendering.
* This differs from the actual size of the icon, because it bulges slightly
* out of its row rather than increasing the height of its row.
* @return {!Size} Height and width.
* @return Height and width.
*/
getCorrectedSize() {
getCorrectedSize(): Size {
// TODO (#2562): Remove getCorrectedSize.
return new Size(this.SIZE, this.SIZE - 2);
}
/**
* Draw the icon.
* @param {!Element} _group The icon group.
* @protected
* @param _group The icon group.
*/
drawIcon_(_group) {
// No-op on base class.
}
protected drawIcon_(_group: Element) {}
// No-op on base class.
/**
* Show or hide the icon.
* @param {boolean} _visible True if the icon should be visible.
* @param _visible True if the icon should be visible.
*/
setVisible(_visible) {
// No-op on base class
}
setVisible(_visible: boolean) {}
}
exports.Icon = Icon;
// No-op on base class

View File

@@ -7,65 +7,69 @@
/**
* @fileoverview Functions for injecting Blockly into a web page.
*/
'use strict';
/**
* Functions for injecting Blockly into a web page.
* @namespace Blockly.inject
*/
goog.module('Blockly.inject');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.inject');
const Css = goog.require('Blockly.Css');
const Tooltip = goog.require('Blockly.Tooltip');
const Touch = goog.require('Blockly.Touch');
const WidgetDiv = goog.require('Blockly.WidgetDiv');
const aria = goog.require('Blockly.utils.aria');
const browserEvents = goog.require('Blockly.browserEvents');
const bumpObjects = goog.require('Blockly.bumpObjects');
const common = goog.require('Blockly.common');
const dom = goog.require('Blockly.utils.dom');
const dropDownDiv = goog.require('Blockly.dropDownDiv');
const userAgent = goog.require('Blockly.utils.userAgent');
const {BlockDragSurfaceSvg} = goog.require('Blockly.BlockDragSurfaceSvg');
/* eslint-disable-next-line no-unused-vars */
const {BlocklyOptions} = goog.requireType('Blockly.BlocklyOptions');
const {Grid} = goog.require('Blockly.Grid');
const {Msg} = goog.require('Blockly.Msg');
const {Options} = goog.require('Blockly.Options');
const {ScrollbarPair} = goog.require('Blockly.ScrollbarPair');
const {ShortcutRegistry} = goog.require('Blockly.ShortcutRegistry');
const {Svg} = goog.require('Blockly.utils.Svg');
const {WorkspaceDragSurfaceSvg} = goog.require('Blockly.WorkspaceDragSurfaceSvg');
const {WorkspaceSvg} = goog.require('Blockly.WorkspaceSvg');
const {Workspace} = goog.require('Blockly.Workspace');
import {BlockDragSurfaceSvg} from './block_drag_surface.js';
import type {BlocklyOptions} from './blockly_options.js';
import * as browserEvents from './browser_events.js';
import * as bumpObjects from './bump_objects.js';
import * as common from './common.js';
import * as Css from './css.js';
import * as dropDownDiv from './dropdowndiv.js';
import {Grid} from './grid.js';
import {Msg} from './msg.js';
import {Options} from './options.js';
import {ScrollbarPair} from './scrollbar_pair.js';
import {ShortcutRegistry} from './shortcut_registry.js';
import * as Tooltip from './tooltip.js';
import * as Touch from './touch.js';
import * as aria from './utils/aria.js';
import * as dom from './utils/dom.js';
import {Svg} from './utils/svg.js';
import * as userAgent from './utils/useragent.js';
import * as WidgetDiv from './widgetdiv.js';
import {Workspace} from './workspace.js';
import {WorkspaceDragSurfaceSvg} from './workspace_drag_surface_svg.js';
import {WorkspaceSvg} from './workspace_svg.js';
/**
* Inject a Blockly editor into the specified container element (usually a div).
* @param {Element|string} container Containing element, or its ID,
* or a CSS selector.
* @param {BlocklyOptions=} opt_options Optional dictionary of options.
* @return {!WorkspaceSvg} Newly created main workspace.
* @param container Containing element, or its ID, or a CSS selector.
* @param opt_options Optional dictionary of options.
* @return Newly created main workspace.
* @alias Blockly.inject
*/
const inject = function(container, opt_options) {
export function inject(
container: Element|string, opt_options?: BlocklyOptions): WorkspaceSvg {
if (typeof container === 'string') {
container =
document.getElementById(container) || document.querySelector(container);
// AnyDuringMigration because: Type 'Element | null' is not assignable to
// type 'string | Element'.
container = (document.getElementById(container) ||
document.querySelector(container)) as AnyDuringMigration;
}
// Verify that the container is in document.
if (!container || !dom.containsNode(document, container)) {
// AnyDuringMigration because: Argument of type 'string | Element' is not
// assignable to parameter of type 'Node'.
if (!container ||
!dom.containsNode(document, container as AnyDuringMigration)) {
throw Error('Error: container is not in current document.');
}
const options =
new Options(opt_options || (/** @type {!BlocklyOptions} */ ({})));
const subContainer =
/** @type {!HTMLDivElement} */ (document.createElement('div'));
const options = new Options(opt_options || {} as BlocklyOptions);
const subContainer = (document.createElement('div'));
subContainer.className = 'injectionDiv';
subContainer.tabIndex = 0;
aria.setState(subContainer, aria.State.LABEL, Msg['WORKSPACE_ARIA_LABEL']);
container.appendChild(subContainer);
// AnyDuringMigration because: Property 'appendChild' does not exist on type
// 'string | Element'.
(container as AnyDuringMigration).appendChild(subContainer);
const svg = createDom(subContainer, options);
// Create surfaces for dragging things. These are optimizations
@@ -81,24 +85,28 @@ const inject = function(container, opt_options) {
// Keep focus on the first workspace so entering keyboard navigation looks
// correct.
common.setMainWorkspace(workspace);
// AnyDuringMigration because: Argument of type 'WorkspaceSvg' is not
// assignable to parameter of type 'Workspace'.
common.setMainWorkspace(workspace as AnyDuringMigration);
common.svgResize(workspace);
subContainer.addEventListener('focusin', function() {
common.setMainWorkspace(workspace);
// AnyDuringMigration because: Argument of type 'WorkspaceSvg' is not
// assignable to parameter of type 'Workspace'.
common.setMainWorkspace(workspace as AnyDuringMigration);
});
return workspace;
};
}
/**
* Create the SVG image.
* @param {!Element} container Containing element.
* @param {!Options} options Dictionary of options.
* @return {!Element} Newly created SVG image.
* @param container Containing element.
* @param options Dictionary of options.
* @return Newly created SVG image.
*/
const createDom = function(container, options) {
function createDom(container: Element, options: Options): Element {
// Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying
// out content in RTL mode. Therefore Blockly forces the use of LTR,
// then manually positions content in RTL as needed.
@@ -109,15 +117,15 @@ const createDom = function(container, options) {
// Build the SVG DOM.
/*
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
class="blocklySvg">
...
</svg>
*/
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
class="blocklySvg">
...
</svg>
*/
const svg = dom.createSvgElement(
Svg.SVG, {
'xmlns': dom.SVG_NS,
@@ -129,10 +137,10 @@ const createDom = function(container, options) {
},
container);
/*
<defs>
... filters go here ...
</defs>
*/
<defs>
... filters go here ...
</defs>
*/
const defs = dom.createSvgElement(Svg.DEFS, {}, svg);
// Each filter/pattern needs a unique ID for the case of multiple Blockly
// instances on a page. Browser behaviour becomes undefined otherwise.
@@ -141,20 +149,19 @@ const createDom = function(container, options) {
options.gridPattern = Grid.createDom(rnd, options.gridOptions, defs);
return svg;
};
}
/**
* Create a main workspace and add it to the SVG.
* @param {!Element} svg SVG element with pattern defined.
* @param {!Options} options Dictionary of options.
* @param {!BlockDragSurfaceSvg} blockDragSurface Drag surface SVG
* for the blocks.
* @param {!WorkspaceDragSurfaceSvg} workspaceDragSurface Drag surface
* SVG for the workspace.
* @return {!WorkspaceSvg} Newly created main workspace.
* @param svg SVG element with pattern defined.
* @param options Dictionary of options.
* @param blockDragSurface Drag surface SVG for the blocks.
* @param workspaceDragSurface Drag surface SVG for the workspace.
* @return Newly created main workspace.
*/
const createMainWorkspace = function(
svg, options, blockDragSurface, workspaceDragSurface) {
function createMainWorkspace(
svg: Element, options: Options, blockDragSurface: BlockDragSurfaceSvg,
workspaceDragSurface: WorkspaceDragSurfaceSvg): WorkspaceSvg {
options.parentWorkspace = null;
const mainWorkspace =
new WorkspaceSvg(options, blockDragSurface, workspaceDragSurface);
@@ -196,20 +203,20 @@ const createMainWorkspace = function(
dropDownDiv.createDom();
Tooltip.createDom();
return mainWorkspace;
};
}
/**
* Initialize Blockly with various handlers.
* @param {!WorkspaceSvg} mainWorkspace Newly created main workspace.
* @param mainWorkspace Newly created main workspace.
*/
const init = function(mainWorkspace) {
function init(mainWorkspace: WorkspaceSvg) {
const options = mainWorkspace.options;
const svg = mainWorkspace.getParentSvg();
// Suppress the browser's context menu.
browserEvents.conditionalBind(
/** @type {!Element} */ (svg.parentNode), 'contextmenu', null,
function(e) {
svg.parentNode as Element, 'contextmenu', null,
function(e: AnyDuringMigration) {
if (!browserEvents.isTargetInput(e)) {
e.preventDefault();
}
@@ -264,36 +271,34 @@ const init = function(mainWorkspace) {
if (options.hasSounds) {
loadSounds(options.pathToMedia, mainWorkspace);
}
};
}
/**
* Handle a key-down on SVG drawing surface. Does nothing if the main workspace
* is not visible.
* @param {!KeyboardEvent} e Key down event.
* @param e Key down event.
*/
// TODO (https://github.com/google/blockly/issues/1998) handle cases where there
// are multiple workspaces and non-main workspaces are able to accept input.
const onKeyDown = function(e) {
const mainWorkspace =
/** @type {!WorkspaceSvg} */ (common.getMainWorkspace());
function onKeyDown(e: KeyboardEvent) {
const mainWorkspace = common.getMainWorkspace() as WorkspaceSvg;
if (!mainWorkspace) {
return;
}
if (browserEvents.isTargetInput(e) ||
(mainWorkspace.rendered && !mainWorkspace.isVisible())) {
mainWorkspace.rendered && !mainWorkspace.isVisible()) {
// When focused on an HTML text input widget, don't trap any keys.
// Ignore keypresses on rendered workspaces that have been explicitly
// hidden.
return;
}
ShortcutRegistry.registry.onKeyDown(mainWorkspace, e);
};
}
/**
* Whether event handlers have been bound. Document event handlers will only
* be bound once, even if Blockly is destroyed and reinjected.
* @type {boolean}
*/
let documentEventsBound = false;
@@ -307,12 +312,12 @@ let documentEventsBound = false;
* Also, 'keydown' has to be on the whole document since the browser doesn't
* understand a concept of focus on the SVG image.
*/
const bindDocumentEvents = function() {
function bindDocumentEvents() {
if (!documentEventsBound) {
browserEvents.conditionalBind(document, 'scroll', null, function() {
const workspaces = Workspace.getAll();
for (let i = 0, workspace; (workspace = workspaces[i]); i++) {
if (workspace.updateInverseScreenCTM) {
const workspaces = common.getAllWorkspaces();
for (let i = 0, workspace; workspace = workspaces[i]; i++) {
if (workspace instanceof WorkspaceSvg) {
workspace.updateInverseScreenCTM();
}
}
@@ -327,20 +332,19 @@ const bindDocumentEvents = function() {
browserEvents.conditionalBind(
window, 'orientationchange', document, function() {
// TODO (#397): Fix for multiple Blockly workspaces.
common.svgResize(/** @type {!WorkspaceSvg} */
(common.getMainWorkspace()));
common.svgResize(common.getMainWorkspace() as WorkspaceSvg);
});
}
}
documentEventsBound = true;
};
}
/**
* Load sounds for the given workspace.
* @param {string} pathToMedia The path to the media directory.
* @param {!WorkspaceSvg} workspace The workspace to load sounds for.
* @param pathToMedia The path to the media directory.
* @param workspace The workspace to load sounds for.
*/
const loadSounds = function(pathToMedia, workspace) {
function loadSounds(pathToMedia: string, workspace: WorkspaceSvg) {
const audioMgr = workspace.getAudioManager();
audioMgr.load(
[
@@ -365,13 +369,13 @@ const loadSounds = function(pathToMedia, workspace) {
'delete');
// Bind temporary hooks that preload the sounds.
const soundBinds = [];
const unbindSounds = function() {
const soundBinds: AnyDuringMigration[] = [];
function unbindSounds() {
while (soundBinds.length) {
browserEvents.unbind(soundBinds.pop());
}
audioMgr.preload();
};
}
// These are bound on mouse/touch events with
// Blockly.browserEvents.conditionalBind, so they restrict the touch
@@ -383,6 +387,4 @@ const loadSounds = function(pathToMedia, workspace) {
document, 'mousemove', null, unbindSounds, true));
soundBinds.push(browserEvents.conditionalBind(
document, 'touchstart', null, unbindSounds, true));
};
exports.inject = inject;
}

View File

@@ -7,92 +7,75 @@
/**
* @fileoverview Object representing an input (value, statement, or dummy).
*/
'use strict';
/**
* Object representing an input (value, statement, or dummy).
* @class
*/
goog.module('Blockly.Input');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.Input');
const fieldRegistry = goog.require('Blockly.fieldRegistry');
/* eslint-disable-next-line no-unused-vars */
const {BlockSvg} = goog.requireType('Blockly.BlockSvg');
/* eslint-disable-next-line no-unused-vars */
const {Block} = goog.requireType('Blockly.Block');
/* eslint-disable-next-line no-unused-vars */
const {Connection} = goog.requireType('Blockly.Connection');
/* eslint-disable-next-line no-unused-vars */
const {Field} = goog.requireType('Blockly.Field');
/* eslint-disable-next-line no-unused-vars */
const {RenderedConnection} = goog.requireType('Blockly.RenderedConnection');
const {inputTypes} = goog.require('Blockly.inputTypes');
/** @suppress {extraRequire} */
goog.require('Blockly.FieldLabel');
// Unused import preserved for side-effects. Remove if unneeded.
import './field_label.js';
import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
import type {Connection} from './connection.js';
import type {Field} from './field.js';
import * as fieldRegistry from './field_registry.js';
import {inputTypes} from './input_types.js';
import type {RenderedConnection} from './rendered_connection.js';
/**
* Class for an input with an optional field.
* @alias Blockly.Input
*/
class Input {
export class Input {
private sourceBlock_: Block;
fieldRow: Field[] = [];
align: Align;
/** Is the input visible? */
private visible_ = true;
/**
* @param {number} type The type of the input.
* @param {string} name Language-neutral identifier which may used to find
* this input again.
* @param {!Block} block The block containing this input.
* @param {Connection} connection Optional connection for this input.
* @param type The type of the input.
* @param name Language-neutral identifier which may used to find this input
* again.
* @param block The block containing this input.
* @param connection Optional connection for this input.
*/
constructor(type, name, block, connection) {
constructor(
public type: number, public name: string, block: Block,
public connection: Connection|null) {
if (type !== inputTypes.DUMMY && !name) {
throw Error(
'Value inputs and statement inputs must have non-empty name.');
}
/** @type {number} */
this.type = type;
/** @type {string} */
this.name = name;
/**
* @type {!Block}
* @private
*/
this.sourceBlock_ = block;
/** @type {Connection} */
this.connection = connection;
/** @type {!Array<!Field>} */
this.fieldRow = [];
/**
* Alignment of input's fields (left, right or centre).
* @type {number}
*/
/** Alignment of input's fields (left, right or centre). */
this.align = Align.LEFT;
/**
* Is the input visible?
* @type {boolean}
* @private
*/
this.visible_ = true;
}
/**
* Get the source block for this input.
* @return {?Block} The source block, or null if there is none.
* @return The source block, or null if there is none.
*/
getSourceBlock() {
getSourceBlock(): Block|null {
return this.sourceBlock_;
}
/**
* Add a field (or label from string), and all prefix and suffix fields, to
* the end of the input's field row.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to
* find this field again. Should be unique to the host block.
* @return {!Input} The input being append to (to allow chaining).
* @param field Something to add as a field.
* @param opt_name Language-neutral identifier which may used to find this
* field again. Should be unique to the host block.
* @return The input being append to (to allow chaining).
*/
appendField(field, opt_name) {
appendField(field: string|Field, opt_name?: string): Input {
this.insertFieldAt(this.fieldRow.length, field, opt_name);
return this;
}
@@ -100,13 +83,13 @@ class Input {
/**
* Inserts a field (or label from string), and all prefix and suffix fields,
* at the location of the input's field row.
* @param {number} index The index at which to insert field.
* @param {string|!Field} field Something to add as a field.
* @param {string=} opt_name Language-neutral identifier which may used to
* find this field again. Should be unique to the host block.
* @return {number} The index following the last inserted field.
* @param index The index at which to insert field.
* @param field Something to add as a field.
* @param opt_name Language-neutral identifier which may used to find this
* field again. Should be unique to the host block.
* @return The index following the last inserted field.
*/
insertFieldAt(index, field, opt_name) {
insertFieldAt(index: number, field: string|Field, opt_name?: string): number {
if (index < 0 || index > this.fieldRow.length) {
throw Error('index ' + index + ' out of bounds.');
}
@@ -118,10 +101,10 @@ class Input {
// Generate a FieldLabel when given a plain text field.
if (typeof field === 'string') {
field = /** @type {!Field} **/ (fieldRegistry.fromJson({
field = fieldRegistry.fromJson({
'type': 'field_label',
'text': field,
}));
}) as Field;
}
field.setSourceBlock(this.sourceBlock_);
@@ -145,8 +128,7 @@ class Input {
}
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
(this.sourceBlock_ as BlockSvg).render();
// Adding a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
}
@@ -155,21 +137,19 @@ class Input {
/**
* Remove a field from this input.
* @param {string} name The name of the field.
* @param {boolean=} opt_quiet True to prevent an error if field is not
* present.
* @return {boolean} True if operation succeeds, false if field is not present
* and opt_quiet is true.
* @param name The name of the field.
* @param opt_quiet True to prevent an error if field is not present.
* @return True if operation succeeds, false if field is not present and
* opt_quiet is true.
* @throws {Error} if the field is not present and opt_quiet is false.
*/
removeField(name, opt_quiet) {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
removeField(name: string, opt_quiet?: boolean): boolean {
for (let i = 0, field; field = this.fieldRow[i]; i++) {
if (field.name === name) {
field.dispose();
this.fieldRow.splice(i, 1);
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
(this.sourceBlock_ as BlockSvg).render();
// Removing a field will cause the block to change shape.
this.sourceBlock_.bumpNeighbours();
}
@@ -184,42 +164,41 @@ class Input {
/**
* Gets whether this input is visible or not.
* @return {boolean} True if visible.
* @return True if visible.
*/
isVisible() {
isVisible(): boolean {
return this.visible_;
}
/**
* Sets whether this input is visible or not.
* Should only be used to collapse/uncollapse a block.
* @param {boolean} visible True if visible.
* @return {!Array<!BlockSvg>} List of blocks to render.
* @package
* @param visible True if visible.
* @return List of blocks to render.
* @internal
*/
setVisible(visible) {
setVisible(visible: boolean): BlockSvg[] {
// Note: Currently there are only unit tests for block.setCollapsed()
// because this function is package. If this function goes back to being a
// public API tests (lots of tests) should be added.
let renderList = [];
let renderList: AnyDuringMigration[] = [];
if (this.visible_ === visible) {
return renderList;
}
this.visible_ = visible;
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
for (let y = 0, field; field = this.fieldRow[y]; y++) {
field.setVisible(visible);
}
if (this.connection) {
this.connection =
/** @type {!RenderedConnection} */ (this.connection);
const renderedConnection = this.connection as RenderedConnection;
// Has a connection.
if (visible) {
renderList = this.connection.startTrackingAll();
renderList = renderedConnection.startTrackingAll();
} else {
this.connection.stopTrackingAll();
renderedConnection.stopTrackingAll();
}
const child = this.connection.targetBlock();
const child = renderedConnection.targetBlock();
if (child) {
child.getSvgRoot().style.display = visible ? 'block' : 'none';
}
@@ -229,21 +208,21 @@ class Input {
/**
* Mark all fields on this input as dirty.
* @package
* @internal
*/
markDirty() {
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
for (let y = 0, field; field = this.fieldRow[y]; y++) {
field.markDirty();
}
}
/**
* Change a connection's compatibility.
* @param {string|Array<string>|null} check Compatible value type or
* list of value types. Null if all types are compatible.
* @return {!Input} The input being modified (to allow chaining).
* @param check Compatible value type or list of value types. Null if all
* types are compatible.
* @return The input being modified (to allow chaining).
*/
setCheck(check) {
setCheck(check: string|string[]|null): Input {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
@@ -253,25 +232,25 @@ class Input {
/**
* Change the alignment of the connection's field(s).
* @param {number} align One of the values of Align
* In RTL mode directions are reversed, and Align.RIGHT aligns to the left.
* @return {!Input} The input being modified (to allow chaining).
* @param align One of the values of Align. In RTL mode directions
* are reversed, and Align.RIGHT aligns to the left.
* @return The input being modified (to allow chaining).
*/
setAlign(align) {
setAlign(align: Align): Input {
this.align = align;
if (this.sourceBlock_.rendered) {
this.sourceBlock_ = /** @type {!BlockSvg} */ (this.sourceBlock_);
this.sourceBlock_.render();
const sourceBlock = this.sourceBlock_ as BlockSvg;
sourceBlock.render();
}
return this;
}
/**
* Changes the connection's shadow block.
* @param {?Element} shadow DOM representation of a block or null.
* @return {!Input} The input being modified (to allow chaining).
* @param shadow DOM representation of a block or null.
* @return The input being modified (to allow chaining).
*/
setShadowDom(shadow) {
setShadowDom(shadow: Element|null): Input {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
@@ -281,20 +260,18 @@ class Input {
/**
* Returns the XML representation of the connection's shadow block.
* @return {?Element} Shadow DOM representation of a block or null.
* @return Shadow DOM representation of a block or null.
*/
getShadowDom() {
getShadowDom(): Element|null {
if (!this.connection) {
throw Error('This input does not have a connection.');
}
return this.connection.getShadowDom();
}
/**
* Initialize the fields on this input.
*/
/** Initialize the fields on this input. */
init() {
if (!this.sourceBlock_.workspace.rendered) {
if (!this.sourceBlock_.workspace!.rendered) {
return; // Headless blocks don't need fields initialized.
}
for (let i = 0; i < this.fieldRow.length; i++) {
@@ -307,29 +284,29 @@ class Input {
* @suppress {checkTypes}
*/
dispose() {
for (let i = 0, field; (field = this.fieldRow[i]); i++) {
for (let i = 0, field; field = this.fieldRow[i]; i++) {
field.dispose();
}
if (this.connection) {
this.connection.dispose();
}
this.sourceBlock_ = null;
// AnyDuringMigration because: Type 'null' is not assignable to type
// 'Block'.
this.sourceBlock_ = null as AnyDuringMigration;
}
}
/**
* Enum for alignment of inputs.
* @enum {number}
* @alias Blockly.Input.Align
*/
const Align = {
LEFT: -1,
CENTRE: 0,
RIGHT: 1,
};
exports.Align = Align;
export namespace Input {
/**
* Enum for alignment of inputs.
* @alias Blockly.Input.Align
*/
export enum Align {
LEFT = -1,
CENTRE = 0,
RIGHT = 1,
}
}
// Add Align to Input so that `Blockly.Input.Align` is publicly accessible.
Input.Align = Align;
exports.Input = Input;
export type Align = Input.Align;
export const Align = Input.Align;

View File

@@ -8,29 +8,25 @@
* @fileoverview An enum for the possible types of inputs.
*/
'use strict';
/**
* An enum for the possible types of inputs.
* @namespace Blockly.inputTypes
*/
goog.module('Blockly.inputTypes');
import * as goog from '../closure/goog/goog.js';
goog.declareModuleId('Blockly.inputTypes');
const {ConnectionType} = goog.require('Blockly.ConnectionType');
import {ConnectionType} from './connection_type.js';
/**
* Enum for the type of a connection or input.
* @enum {number}
* @alias Blockly.inputTypes
*/
const inputTypes = {
export enum inputTypes {
// A right-facing value input. E.g. 'set item to' or 'return'.
VALUE: ConnectionType.INPUT_VALUE,
VALUE = ConnectionType.INPUT_VALUE,
// A down-facing block stack. E.g. 'if-do' or 'else'.
STATEMENT: ConnectionType.NEXT_STATEMENT,
STATEMENT = ConnectionType.NEXT_STATEMENT,
// A dummy input. Used to add field(s) with no input.
DUMMY: 5,
};
exports.inputTypes = inputTypes;
DUMMY = 5
}

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