mirror of
https://github.com/google/blockly.git
synced 2025-12-16 06:10:12 +01:00
Merge branch 'rc/v12.0.0' into develop-v12-merge
This commit is contained in:
@@ -352,6 +352,11 @@
|
|||||||
// Needs investigation.
|
// Needs investigation.
|
||||||
"ae-forgotten-export": {
|
"ae-forgotten-export": {
|
||||||
"logLevel": "none"
|
"logLevel": "none"
|
||||||
|
},
|
||||||
|
|
||||||
|
// We don't prefix our internal APIs with underscores.
|
||||||
|
"ae-internal-missing-underscore": {
|
||||||
|
"logLevel": "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ const CUSTOM_CONTEXT_MENU_CREATE_VARIABLES_GET_MIXIN = {
|
|||||||
}
|
}
|
||||||
const varField = this.getField('VAR') as FieldVariable;
|
const varField = this.getField('VAR') as FieldVariable;
|
||||||
const variable = varField.getVariable()!;
|
const variable = varField.getVariable()!;
|
||||||
const varName = variable.name;
|
const varName = variable.getName();
|
||||||
if (!this.isCollapsed() && varName !== null) {
|
if (!this.isCollapsed() && varName !== null) {
|
||||||
const getVarBlockState = {
|
const getVarBlockState = {
|
||||||
type: 'variables_get',
|
type: 'variables_get',
|
||||||
|
|||||||
@@ -31,11 +31,14 @@ import '../core/icons/comment_icon.js';
|
|||||||
import {MutatorIcon as Mutator} from '../core/icons/mutator_icon.js';
|
import {MutatorIcon as Mutator} from '../core/icons/mutator_icon.js';
|
||||||
import '../core/icons/warning_icon.js';
|
import '../core/icons/warning_icon.js';
|
||||||
import {Align} from '../core/inputs/align.js';
|
import {Align} from '../core/inputs/align.js';
|
||||||
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
} from '../core/interfaces/i_variable_model.js';
|
||||||
import {Msg} from '../core/msg.js';
|
import {Msg} from '../core/msg.js';
|
||||||
import {Names} from '../core/names.js';
|
import {Names} from '../core/names.js';
|
||||||
import * as Procedures from '../core/procedures.js';
|
import * as Procedures from '../core/procedures.js';
|
||||||
import * as xmlUtils from '../core/utils/xml.js';
|
import * as xmlUtils from '../core/utils/xml.js';
|
||||||
import type {VariableModel} from '../core/variable_model.js';
|
|
||||||
import * as Variables from '../core/variables.js';
|
import * as Variables from '../core/variables.js';
|
||||||
import type {Workspace} from '../core/workspace.js';
|
import type {Workspace} from '../core/workspace.js';
|
||||||
import type {WorkspaceSvg} from '../core/workspace_svg.js';
|
import type {WorkspaceSvg} from '../core/workspace_svg.js';
|
||||||
@@ -48,7 +51,7 @@ export const blocks: {[key: string]: BlockDefinition} = {};
|
|||||||
type ProcedureBlock = Block & ProcedureMixin;
|
type ProcedureBlock = Block & ProcedureMixin;
|
||||||
interface ProcedureMixin extends ProcedureMixinType {
|
interface ProcedureMixin extends ProcedureMixinType {
|
||||||
arguments_: string[];
|
arguments_: string[];
|
||||||
argumentVarModels_: VariableModel[];
|
argumentVarModels_: IVariableModel<IVariableState>[];
|
||||||
callType_: string;
|
callType_: string;
|
||||||
paramIds_: string[];
|
paramIds_: string[];
|
||||||
hasStatements_: boolean;
|
hasStatements_: boolean;
|
||||||
@@ -128,7 +131,7 @@ const PROCEDURE_DEF_COMMON = {
|
|||||||
for (let i = 0; i < this.argumentVarModels_.length; i++) {
|
for (let i = 0; i < this.argumentVarModels_.length; i++) {
|
||||||
const parameter = xmlUtils.createElement('arg');
|
const parameter = xmlUtils.createElement('arg');
|
||||||
const argModel = this.argumentVarModels_[i];
|
const argModel = this.argumentVarModels_[i];
|
||||||
parameter.setAttribute('name', argModel.name);
|
parameter.setAttribute('name', argModel.getName());
|
||||||
parameter.setAttribute('varid', argModel.getId());
|
parameter.setAttribute('varid', argModel.getId());
|
||||||
if (opt_paramIds && this.paramIds_) {
|
if (opt_paramIds && this.paramIds_) {
|
||||||
parameter.setAttribute('paramId', this.paramIds_[i]);
|
parameter.setAttribute('paramId', this.paramIds_[i]);
|
||||||
@@ -196,7 +199,7 @@ const PROCEDURE_DEF_COMMON = {
|
|||||||
state['params'].push({
|
state['params'].push({
|
||||||
// We don't need to serialize the name, but just in case we decide
|
// We don't need to serialize the name, but just in case we decide
|
||||||
// to separate params from variables.
|
// to separate params from variables.
|
||||||
'name': this.argumentVarModels_[i].name,
|
'name': this.argumentVarModels_[i].getName(),
|
||||||
'id': this.argumentVarModels_[i].getId(),
|
'id': this.argumentVarModels_[i].getId(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -224,7 +227,7 @@ const PROCEDURE_DEF_COMMON = {
|
|||||||
param['name'],
|
param['name'],
|
||||||
'',
|
'',
|
||||||
);
|
);
|
||||||
this.arguments_.push(variable.name);
|
this.arguments_.push(variable.getName());
|
||||||
this.argumentVarModels_.push(variable);
|
this.argumentVarModels_.push(variable);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -352,7 +355,9 @@ const PROCEDURE_DEF_COMMON = {
|
|||||||
*
|
*
|
||||||
* @returns List of variable models.
|
* @returns List of variable models.
|
||||||
*/
|
*/
|
||||||
getVarModels: function (this: ProcedureBlock): VariableModel[] {
|
getVarModels: function (
|
||||||
|
this: ProcedureBlock,
|
||||||
|
): IVariableModel<IVariableState>[] {
|
||||||
return this.argumentVarModels_;
|
return this.argumentVarModels_;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
@@ -370,23 +375,23 @@ const PROCEDURE_DEF_COMMON = {
|
|||||||
newId: string,
|
newId: string,
|
||||||
) {
|
) {
|
||||||
const oldVariable = this.workspace.getVariableById(oldId)!;
|
const oldVariable = this.workspace.getVariableById(oldId)!;
|
||||||
if (oldVariable.type !== '') {
|
if (oldVariable.getType() !== '') {
|
||||||
// Procedure arguments always have the empty type.
|
// Procedure arguments always have the empty type.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const oldName = oldVariable.name;
|
const oldName = oldVariable.getName();
|
||||||
const newVar = this.workspace.getVariableById(newId)!;
|
const newVar = this.workspace.getVariableById(newId)!;
|
||||||
|
|
||||||
let change = false;
|
let change = false;
|
||||||
for (let i = 0; i < this.argumentVarModels_.length; i++) {
|
for (let i = 0; i < this.argumentVarModels_.length; i++) {
|
||||||
if (this.argumentVarModels_[i].getId() === oldId) {
|
if (this.argumentVarModels_[i].getId() === oldId) {
|
||||||
this.arguments_[i] = newVar.name;
|
this.arguments_[i] = newVar.getName();
|
||||||
this.argumentVarModels_[i] = newVar;
|
this.argumentVarModels_[i] = newVar;
|
||||||
change = true;
|
change = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (change) {
|
if (change) {
|
||||||
this.displayRenamedVar_(oldName, newVar.name);
|
this.displayRenamedVar_(oldName, newVar.getName());
|
||||||
Procedures.mutateCallers(this);
|
Procedures.mutateCallers(this);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -398,9 +403,9 @@ const PROCEDURE_DEF_COMMON = {
|
|||||||
*/
|
*/
|
||||||
updateVarName: function (
|
updateVarName: function (
|
||||||
this: ProcedureBlock & BlockSvg,
|
this: ProcedureBlock & BlockSvg,
|
||||||
variable: VariableModel,
|
variable: IVariableModel<IVariableState>,
|
||||||
) {
|
) {
|
||||||
const newName = variable.name;
|
const newName = variable.getName();
|
||||||
let change = false;
|
let change = false;
|
||||||
let oldName;
|
let oldName;
|
||||||
for (let i = 0; i < this.argumentVarModels_.length; i++) {
|
for (let i = 0; i < this.argumentVarModels_.length; i++) {
|
||||||
@@ -473,12 +478,16 @@ const PROCEDURE_DEF_COMMON = {
|
|||||||
const getVarBlockState = {
|
const getVarBlockState = {
|
||||||
type: 'variables_get',
|
type: 'variables_get',
|
||||||
fields: {
|
fields: {
|
||||||
VAR: {name: argVar.name, id: argVar.getId(), type: argVar.type},
|
VAR: {
|
||||||
|
name: argVar.getName(),
|
||||||
|
id: argVar.getId(),
|
||||||
|
type: argVar.getType(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
options.push({
|
options.push({
|
||||||
enabled: true,
|
enabled: true,
|
||||||
text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.name),
|
text: Msg['VARIABLES_SET_CREATE_GET'].replace('%1', argVar.getName()),
|
||||||
callback: ContextMenu.callbackFactory(this, getVarBlockState),
|
callback: ContextMenu.callbackFactory(this, getVarBlockState),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -620,30 +629,49 @@ type ArgumentBlock = Block & ArgumentMixin;
|
|||||||
interface ArgumentMixin extends ArgumentMixinType {}
|
interface ArgumentMixin extends ArgumentMixinType {}
|
||||||
type ArgumentMixinType = typeof PROCEDURES_MUTATORARGUMENT;
|
type ArgumentMixinType = typeof PROCEDURES_MUTATORARGUMENT;
|
||||||
|
|
||||||
// TODO(#6920): This is kludgy.
|
/**
|
||||||
type FieldTextInputForArgument = FieldTextInput & {
|
* Field responsible for editing procedure argument names.
|
||||||
oldShowEditorFn_(_e?: Event, quietInput?: boolean): void;
|
*/
|
||||||
createdVariables_: VariableModel[];
|
class ProcedureArgumentField extends FieldTextInput {
|
||||||
};
|
/**
|
||||||
|
* Whether or not this field is currently being edited interactively.
|
||||||
|
*/
|
||||||
|
editingInteractively = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The procedure argument variable whose name is being interactively edited.
|
||||||
|
*/
|
||||||
|
editingVariable?: IVariableModel<IVariableState>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the field editor.
|
||||||
|
*
|
||||||
|
* @param e The event that triggered display of the field editor.
|
||||||
|
*/
|
||||||
|
protected override showEditor_(e?: Event) {
|
||||||
|
super.showEditor_(e);
|
||||||
|
this.editingInteractively = true;
|
||||||
|
this.editingVariable = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles cleanup when the field editor is dismissed.
|
||||||
|
*/
|
||||||
|
override onFinishEditing_(value: string) {
|
||||||
|
super.onFinishEditing_(value);
|
||||||
|
this.editingInteractively = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const PROCEDURES_MUTATORARGUMENT = {
|
const PROCEDURES_MUTATORARGUMENT = {
|
||||||
/**
|
/**
|
||||||
* Mutator block for procedure argument.
|
* Mutator block for procedure argument.
|
||||||
*/
|
*/
|
||||||
init: function (this: ArgumentBlock) {
|
init: function (this: ArgumentBlock) {
|
||||||
const field = fieldRegistry.fromJson({
|
const field = new ProcedureArgumentField(
|
||||||
type: 'field_input',
|
Procedures.DEFAULT_ARG,
|
||||||
text: Procedures.DEFAULT_ARG,
|
this.validator_,
|
||||||
}) as FieldTextInputForArgument;
|
);
|
||||||
field.setValidator(this.validator_);
|
|
||||||
// Hack: override showEditor to do just a little bit more work.
|
|
||||||
// We don't have a good place to hook into the start of a text edit.
|
|
||||||
field.oldShowEditorFn_ = (field as AnyDuringMigration).showEditor_;
|
|
||||||
const newShowEditorFn = function (this: typeof field) {
|
|
||||||
this.createdVariables_ = [];
|
|
||||||
this.oldShowEditorFn_();
|
|
||||||
};
|
|
||||||
(field as AnyDuringMigration).showEditor_ = newShowEditorFn;
|
|
||||||
|
|
||||||
this.appendDummyInput()
|
this.appendDummyInput()
|
||||||
.appendField(Msg['PROCEDURES_MUTATORARG_TITLE'])
|
.appendField(Msg['PROCEDURES_MUTATORARG_TITLE'])
|
||||||
@@ -653,14 +681,6 @@ const PROCEDURES_MUTATORARGUMENT = {
|
|||||||
this.setStyle('procedure_blocks');
|
this.setStyle('procedure_blocks');
|
||||||
this.setTooltip(Msg['PROCEDURES_MUTATORARG_TOOLTIP']);
|
this.setTooltip(Msg['PROCEDURES_MUTATORARG_TOOLTIP']);
|
||||||
this.contextMenu = false;
|
this.contextMenu = false;
|
||||||
|
|
||||||
// Create the default variable when we drag the block in from the flyout.
|
|
||||||
// Have to do this after installing the field on the block.
|
|
||||||
field.onFinishEditing_ = this.deleteIntermediateVars_;
|
|
||||||
// Create an empty list so onFinishEditing_ has something to look at, even
|
|
||||||
// though the editor was never opened.
|
|
||||||
field.createdVariables_ = [];
|
|
||||||
field.onFinishEditing_('x');
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -674,11 +694,11 @@ const PROCEDURES_MUTATORARGUMENT = {
|
|||||||
* @returns Valid name, or null if a name was not specified.
|
* @returns Valid name, or null if a name was not specified.
|
||||||
*/
|
*/
|
||||||
validator_: function (
|
validator_: function (
|
||||||
this: FieldTextInputForArgument,
|
this: ProcedureArgumentField,
|
||||||
varName: string,
|
varName: string,
|
||||||
): string | null {
|
): string | null {
|
||||||
const sourceBlock = this.getSourceBlock()!;
|
const sourceBlock = this.getSourceBlock()!;
|
||||||
const outerWs = sourceBlock!.workspace.getRootWorkspace()!;
|
const outerWs = sourceBlock.workspace.getRootWorkspace()!;
|
||||||
varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, '');
|
varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, '');
|
||||||
if (!varName) {
|
if (!varName) {
|
||||||
return null;
|
return null;
|
||||||
@@ -707,50 +727,31 @@ const PROCEDURES_MUTATORARGUMENT = {
|
|||||||
return varName;
|
return varName;
|
||||||
}
|
}
|
||||||
|
|
||||||
let model = outerWs.getVariable(varName, '');
|
const model = outerWs.getVariable(varName, '');
|
||||||
if (model && model.name !== varName) {
|
if (model && model.getName() !== varName) {
|
||||||
// Rename the variable (case change)
|
// Rename the variable (case change)
|
||||||
outerWs.renameVariableById(model.getId(), varName);
|
outerWs.renameVariableById(model.getId(), varName);
|
||||||
}
|
}
|
||||||
if (!model) {
|
if (!model) {
|
||||||
model = outerWs.createVariable(varName, '');
|
if (this.editingInteractively) {
|
||||||
if (model && this.createdVariables_) {
|
if (!this.editingVariable) {
|
||||||
this.createdVariables_.push(model);
|
this.editingVariable = outerWs.createVariable(varName, '');
|
||||||
|
} else {
|
||||||
|
outerWs.renameVariableById(this.editingVariable.getId(), varName);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
outerWs.createVariable(varName, '');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return varName;
|
return varName;
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* Called when focusing away from the text field.
|
|
||||||
* Deletes all variables that were created as the user typed their intended
|
|
||||||
* variable name.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
* @param newText The new variable name.
|
|
||||||
*/
|
|
||||||
deleteIntermediateVars_: function (
|
|
||||||
this: FieldTextInputForArgument,
|
|
||||||
newText: string,
|
|
||||||
) {
|
|
||||||
const outerWs = this.getSourceBlock()!.workspace.getRootWorkspace();
|
|
||||||
if (!outerWs) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (let i = 0; i < this.createdVariables_.length; i++) {
|
|
||||||
const model = this.createdVariables_[i];
|
|
||||||
if (model.name !== newText) {
|
|
||||||
outerWs.deleteVariableById(model.getId());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
blocks['procedures_mutatorarg'] = PROCEDURES_MUTATORARGUMENT;
|
blocks['procedures_mutatorarg'] = PROCEDURES_MUTATORARGUMENT;
|
||||||
|
|
||||||
/** Type of a block using the PROCEDURE_CALL_COMMON mixin. */
|
/** Type of a block using the PROCEDURE_CALL_COMMON mixin. */
|
||||||
type CallBlock = Block & CallMixin;
|
type CallBlock = Block & CallMixin;
|
||||||
interface CallMixin extends CallMixinType {
|
interface CallMixin extends CallMixinType {
|
||||||
argumentVarModels_: VariableModel[];
|
argumentVarModels_: IVariableModel<IVariableState>[];
|
||||||
arguments_: string[];
|
arguments_: string[];
|
||||||
defType_: string;
|
defType_: string;
|
||||||
quarkIds_: string[] | null;
|
quarkIds_: string[] | null;
|
||||||
@@ -1029,7 +1030,7 @@ const PROCEDURE_CALL_COMMON = {
|
|||||||
*
|
*
|
||||||
* @returns List of variable models.
|
* @returns List of variable models.
|
||||||
*/
|
*/
|
||||||
getVarModels: function (this: CallBlock): VariableModel[] {
|
getVarModels: function (this: CallBlock): IVariableModel<IVariableState>[] {
|
||||||
return this.argumentVarModels_;
|
return this.argumentVarModels_;
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -165,11 +165,12 @@ const deleteOptionCallbackFactory = function (
|
|||||||
block: VariableBlock,
|
block: VariableBlock,
|
||||||
): () => void {
|
): () => void {
|
||||||
return function () {
|
return function () {
|
||||||
const workspace = block.workspace;
|
|
||||||
const variableField = block.getField('VAR') as FieldVariable;
|
const variableField = block.getField('VAR') as FieldVariable;
|
||||||
const variable = variableField.getVariable()!;
|
const variable = variableField.getVariable();
|
||||||
workspace.deleteVariableById(variable.getId());
|
if (variable) {
|
||||||
(workspace as WorkspaceSvg).refreshToolboxSelection();
|
Variables.deleteVariable(variable.getWorkspace(), variable, block);
|
||||||
|
}
|
||||||
|
(block.workspace as WorkspaceSvg).refreshToolboxSelection();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -144,9 +144,9 @@ const CUSTOM_CONTEXT_MENU_VARIABLE_GETTER_SETTER_MIXIN = {
|
|||||||
const id = this.getFieldValue('VAR');
|
const id = this.getFieldValue('VAR');
|
||||||
const variableModel = Variables.getVariable(this.workspace, id)!;
|
const variableModel = Variables.getVariable(this.workspace, id)!;
|
||||||
if (this.type === 'variables_get_dynamic') {
|
if (this.type === 'variables_get_dynamic') {
|
||||||
this.outputConnection!.setCheck(variableModel.type);
|
this.outputConnection!.setCheck(variableModel.getType());
|
||||||
} else {
|
} else {
|
||||||
this.getInput('VALUE')!.connection!.setCheck(variableModel.type);
|
this.getInput('VALUE')!.connection!.setCheck(variableModel.getType());
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -176,11 +176,12 @@ const renameOptionCallbackFactory = function (block: VariableBlock) {
|
|||||||
*/
|
*/
|
||||||
const deleteOptionCallbackFactory = function (block: VariableBlock) {
|
const deleteOptionCallbackFactory = function (block: VariableBlock) {
|
||||||
return function () {
|
return function () {
|
||||||
const workspace = block.workspace;
|
|
||||||
const variableField = block.getField('VAR') as FieldVariable;
|
const variableField = block.getField('VAR') as FieldVariable;
|
||||||
const variable = variableField.getVariable()!;
|
const variable = variableField.getVariable();
|
||||||
workspace.deleteVariableById(variable.getId());
|
if (variable) {
|
||||||
(workspace as WorkspaceSvg).refreshToolboxSelection();
|
Variables.deleteVariable(variable.getWorkspace(), variable, block);
|
||||||
|
}
|
||||||
|
(block.workspace as WorkspaceSvg).refreshToolboxSelection();
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ import {ValueInput} from './inputs/value_input.js';
|
|||||||
import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
import type {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
||||||
import {isCommentIcon} from './interfaces/i_comment_icon.js';
|
import {isCommentIcon} from './interfaces/i_comment_icon.js';
|
||||||
import {type IIcon} from './interfaces/i_icon.js';
|
import {type IIcon} from './interfaces/i_icon.js';
|
||||||
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
} from './interfaces/i_variable_model.js';
|
||||||
import * as registry from './registry.js';
|
import * as registry from './registry.js';
|
||||||
import * as Tooltip from './tooltip.js';
|
import * as Tooltip from './tooltip.js';
|
||||||
import * as arrayUtils from './utils/array.js';
|
import * as arrayUtils from './utils/array.js';
|
||||||
@@ -51,7 +55,6 @@ import * as deprecation from './utils/deprecation.js';
|
|||||||
import * as idGenerator from './utils/idgenerator.js';
|
import * as idGenerator from './utils/idgenerator.js';
|
||||||
import * as parsing from './utils/parsing.js';
|
import * as parsing from './utils/parsing.js';
|
||||||
import {Size} from './utils/size.js';
|
import {Size} from './utils/size.js';
|
||||||
import type {VariableModel} from './variable_model.js';
|
|
||||||
import type {Workspace} from './workspace.js';
|
import type {Workspace} from './workspace.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -792,7 +795,7 @@ export class Block implements IASTNodeLocation {
|
|||||||
this.deletable &&
|
this.deletable &&
|
||||||
!this.shadow &&
|
!this.shadow &&
|
||||||
!this.isDeadOrDying() &&
|
!this.isDeadOrDying() &&
|
||||||
!this.workspace.options.readOnly
|
!this.workspace.isReadOnly()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +828,7 @@ export class Block implements IASTNodeLocation {
|
|||||||
this.movable &&
|
this.movable &&
|
||||||
!this.shadow &&
|
!this.shadow &&
|
||||||
!this.isDeadOrDying() &&
|
!this.isDeadOrDying() &&
|
||||||
!this.workspace.options.readOnly
|
!this.workspace.isReadOnly()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -914,7 +917,7 @@ export class Block implements IASTNodeLocation {
|
|||||||
*/
|
*/
|
||||||
isEditable(): boolean {
|
isEditable(): boolean {
|
||||||
return (
|
return (
|
||||||
this.editable && !this.isDeadOrDying() && !this.workspace.options.readOnly
|
this.editable && !this.isDeadOrDying() && !this.workspace.isReadOnly()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,10 +937,8 @@ export class Block implements IASTNodeLocation {
|
|||||||
*/
|
*/
|
||||||
setEditable(editable: boolean) {
|
setEditable(editable: boolean) {
|
||||||
this.editable = editable;
|
this.editable = editable;
|
||||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
for (const field of this.getFields()) {
|
||||||
for (let j = 0, field; (field = input.fieldRow[j]); j++) {
|
field.updateEditable();
|
||||||
field.updateEditable();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1104,16 +1105,27 @@ export class Block implements IASTNodeLocation {
|
|||||||
' instead',
|
' instead',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
for (const field of this.getFields()) {
|
||||||
for (let j = 0, field; (field = input.fieldRow[j]); j++) {
|
if (field.name === name) {
|
||||||
if (field.name === name) {
|
return field;
|
||||||
return field;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a generator that provides every field on the block.
|
||||||
|
*
|
||||||
|
* @yields A generator that can be used to iterate the fields on the block.
|
||||||
|
*/
|
||||||
|
*getFields(): Generator<Field> {
|
||||||
|
for (const input of this.inputList) {
|
||||||
|
for (const field of input.fieldRow) {
|
||||||
|
yield field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return all variables referenced by this block.
|
* Return all variables referenced by this block.
|
||||||
*
|
*
|
||||||
@@ -1121,12 +1133,9 @@ export class Block implements IASTNodeLocation {
|
|||||||
*/
|
*/
|
||||||
getVars(): string[] {
|
getVars(): string[] {
|
||||||
const vars: string[] = [];
|
const vars: string[] = [];
|
||||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
for (const field of this.getFields()) {
|
||||||
for (let j = 0, field; (field = input.fieldRow[j]); j++) {
|
if (field.referencesVariables()) {
|
||||||
if (field.referencesVariables()) {
|
vars.push(field.getValue());
|
||||||
// NOTE: This only applies to `FieldVariable`, a `Field<string>`
|
|
||||||
vars.push(field.getValue() as string);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return vars;
|
return vars;
|
||||||
@@ -1138,19 +1147,17 @@ export class Block implements IASTNodeLocation {
|
|||||||
* @returns List of variable models.
|
* @returns List of variable models.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
getVarModels(): VariableModel[] {
|
getVarModels(): IVariableModel<IVariableState>[] {
|
||||||
const vars = [];
|
const vars = [];
|
||||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
for (const field of this.getFields()) {
|
||||||
for (let j = 0, field; (field = input.fieldRow[j]); j++) {
|
if (field.referencesVariables()) {
|
||||||
if (field.referencesVariables()) {
|
const model = this.workspace.getVariableById(
|
||||||
const model = this.workspace.getVariableById(
|
field.getValue() as string,
|
||||||
field.getValue() as string,
|
);
|
||||||
);
|
// Check if the variable actually exists (and isn't just a potential
|
||||||
// Check if the variable actually exists (and isn't just a potential
|
// variable).
|
||||||
// variable).
|
if (model) {
|
||||||
if (model) {
|
vars.push(model);
|
||||||
vars.push(model);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1164,15 +1171,13 @@ export class Block implements IASTNodeLocation {
|
|||||||
* @param variable The variable being renamed.
|
* @param variable The variable being renamed.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
updateVarName(variable: VariableModel) {
|
updateVarName(variable: IVariableModel<IVariableState>) {
|
||||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
for (const field of this.getFields()) {
|
||||||
for (let j = 0, field; (field = input.fieldRow[j]); j++) {
|
if (
|
||||||
if (
|
field.referencesVariables() &&
|
||||||
field.referencesVariables() &&
|
variable.getId() === field.getValue()
|
||||||
variable.getId() === field.getValue()
|
) {
|
||||||
) {
|
field.refreshVariableName();
|
||||||
field.refreshVariableName();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1186,11 +1191,9 @@ export class Block implements IASTNodeLocation {
|
|||||||
* updated name.
|
* updated name.
|
||||||
*/
|
*/
|
||||||
renameVarById(oldId: string, newId: string) {
|
renameVarById(oldId: string, newId: string) {
|
||||||
for (let i = 0, input; (input = this.inputList[i]); i++) {
|
for (const field of this.getFields()) {
|
||||||
for (let j = 0, field; (field = input.fieldRow[j]); j++) {
|
if (field.referencesVariables() && oldId === field.getValue()) {
|
||||||
if (field.referencesVariables() && oldId === field.getValue()) {
|
field.setValue(newId);
|
||||||
field.setValue(newId);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1408,7 +1411,7 @@ export class Block implements IASTNodeLocation {
|
|||||||
return this.disabledReasons.size === 0;
|
return this.disabledReasons.size === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated v11 - Get whether the block is manually disabled. */
|
/** @deprecated v11 - Get or sets whether the block is manually disabled. */
|
||||||
private get disabled(): boolean {
|
private get disabled(): boolean {
|
||||||
deprecation.warn(
|
deprecation.warn(
|
||||||
'disabled',
|
'disabled',
|
||||||
@@ -1419,7 +1422,6 @@ export class Block implements IASTNodeLocation {
|
|||||||
return this.hasDisabledReason(constants.MANUALLY_DISABLED);
|
return this.hasDisabledReason(constants.MANUALLY_DISABLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @deprecated v11 - Set whether the block is manually disabled. */
|
|
||||||
private set disabled(value: boolean) {
|
private set disabled(value: boolean) {
|
||||||
deprecation.warn(
|
deprecation.warn(
|
||||||
'disabled',
|
'disabled',
|
||||||
@@ -2516,7 +2518,7 @@ export class Block implements IASTNodeLocation {
|
|||||||
*
|
*
|
||||||
* Intended to on be used in console logs and errors. If you need a string
|
* Intended to on be used in console logs and errors. If you need a string
|
||||||
* that uses the user's native language (including block text, field values,
|
* that uses the user's native language (including block text, field values,
|
||||||
* and child blocks), use [toString()]{@link Block#toString}.
|
* and child blocks), use {@link (Block:class).toString | toString()}.
|
||||||
*
|
*
|
||||||
* @returns The description.
|
* @returns The description.
|
||||||
*/
|
*/
|
||||||
|
|||||||
283
core/block_flyout_inflater.ts
Normal file
283
core/block_flyout_inflater.ts
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {BlockSvg} from './block_svg.js';
|
||||||
|
import * as browserEvents from './browser_events.js';
|
||||||
|
import * as common from './common.js';
|
||||||
|
import {MANUALLY_DISABLED} from './constants.js';
|
||||||
|
import type {Abstract as AbstractEvent} from './events/events_abstract.js';
|
||||||
|
import {EventType} from './events/type.js';
|
||||||
|
import {FlyoutItem} from './flyout_item.js';
|
||||||
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||||
|
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||||
|
import * as registry from './registry.js';
|
||||||
|
import * as blocks from './serialization/blocks.js';
|
||||||
|
import type {BlockInfo} from './utils/toolbox.js';
|
||||||
|
import * as utilsXml from './utils/xml.js';
|
||||||
|
import type {WorkspaceSvg} from './workspace_svg.js';
|
||||||
|
import * as Xml from './xml.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The language-neutral ID for when the reason why a block is disabled is
|
||||||
|
* because the workspace is at block capacity.
|
||||||
|
*/
|
||||||
|
const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON =
|
||||||
|
'WORKSPACE_AT_BLOCK_CAPACITY';
|
||||||
|
|
||||||
|
const BLOCK_TYPE = 'block';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class responsible for creating blocks for flyouts.
|
||||||
|
*/
|
||||||
|
export class BlockFlyoutInflater implements IFlyoutInflater {
|
||||||
|
protected permanentlyDisabledBlocks = new Set<BlockSvg>();
|
||||||
|
protected listeners = new Map<string, browserEvents.Data[]>();
|
||||||
|
protected flyout?: IFlyout;
|
||||||
|
private capacityWrapper: (event: AbstractEvent) => void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new BlockFlyoutInflater instance.
|
||||||
|
*/
|
||||||
|
constructor() {
|
||||||
|
this.capacityWrapper = this.filterFlyoutBasedOnCapacity.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inflates a flyout block from the given state and adds it to the flyout.
|
||||||
|
*
|
||||||
|
* @param state A JSON representation of a flyout block.
|
||||||
|
* @param flyout The flyout to create the block on.
|
||||||
|
* @returns A newly created block.
|
||||||
|
*/
|
||||||
|
load(state: object, flyout: IFlyout): FlyoutItem {
|
||||||
|
this.setFlyout(flyout);
|
||||||
|
const block = this.createBlock(state as BlockInfo, flyout.getWorkspace());
|
||||||
|
|
||||||
|
if (!block.isEnabled()) {
|
||||||
|
// Record blocks that were initially disabled.
|
||||||
|
// Do not enable these blocks as a result of capacity filtering.
|
||||||
|
this.permanentlyDisabledBlocks.add(block);
|
||||||
|
} else {
|
||||||
|
this.updateStateBasedOnCapacity(block);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
block.getDescendants(false).forEach((b) => (b.isInFlyout = true));
|
||||||
|
this.addBlockListeners(block);
|
||||||
|
|
||||||
|
return new FlyoutItem(block, BLOCK_TYPE, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a block on the given workspace.
|
||||||
|
*
|
||||||
|
* @param blockDefinition A JSON representation of the block to create.
|
||||||
|
* @param workspace The workspace to create the block on.
|
||||||
|
* @returns The newly created block.
|
||||||
|
*/
|
||||||
|
createBlock(blockDefinition: BlockInfo, workspace: WorkspaceSvg): BlockSvg {
|
||||||
|
let block;
|
||||||
|
if (blockDefinition['blockxml']) {
|
||||||
|
const xml = (
|
||||||
|
typeof blockDefinition['blockxml'] === 'string'
|
||||||
|
? utilsXml.textToDom(blockDefinition['blockxml'])
|
||||||
|
: blockDefinition['blockxml']
|
||||||
|
) as Element;
|
||||||
|
block = Xml.domToBlockInternal(xml, workspace);
|
||||||
|
} else {
|
||||||
|
if (blockDefinition['enabled'] === undefined) {
|
||||||
|
blockDefinition['enabled'] =
|
||||||
|
blockDefinition['disabled'] !== 'true' &&
|
||||||
|
blockDefinition['disabled'] !== true;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
blockDefinition['disabledReasons'] === undefined &&
|
||||||
|
blockDefinition['enabled'] === false
|
||||||
|
) {
|
||||||
|
blockDefinition['disabledReasons'] = [MANUALLY_DISABLED];
|
||||||
|
}
|
||||||
|
// These fields used to be allowed and may still be present, but are
|
||||||
|
// ignored here since everything in the flyout should always be laid out
|
||||||
|
// linearly.
|
||||||
|
if ('x' in blockDefinition) {
|
||||||
|
delete blockDefinition['x'];
|
||||||
|
}
|
||||||
|
if ('y' in blockDefinition) {
|
||||||
|
delete blockDefinition['y'];
|
||||||
|
}
|
||||||
|
block = blocks.appendInternal(blockDefinition as blocks.State, workspace);
|
||||||
|
}
|
||||||
|
|
||||||
|
return block as BlockSvg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of space that should follow this block.
|
||||||
|
*
|
||||||
|
* @param state A JSON representation of a flyout block.
|
||||||
|
* @param defaultGap The default spacing for flyout items.
|
||||||
|
* @returns The amount of space that should follow this block.
|
||||||
|
*/
|
||||||
|
gapForItem(state: object, defaultGap: number): number {
|
||||||
|
const blockState = state as BlockInfo;
|
||||||
|
let gap;
|
||||||
|
if (blockState['gap']) {
|
||||||
|
gap = parseInt(String(blockState['gap']));
|
||||||
|
} else if (blockState['blockxml']) {
|
||||||
|
const xml = (
|
||||||
|
typeof blockState['blockxml'] === 'string'
|
||||||
|
? utilsXml.textToDom(blockState['blockxml'])
|
||||||
|
: blockState['blockxml']
|
||||||
|
) as Element;
|
||||||
|
gap = parseInt(xml.getAttribute('gap')!);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !gap || isNaN(gap) ? defaultGap : gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the given block.
|
||||||
|
*
|
||||||
|
* @param item The flyout block to dispose of.
|
||||||
|
*/
|
||||||
|
disposeItem(item: FlyoutItem): void {
|
||||||
|
const element = item.getElement();
|
||||||
|
if (!(element instanceof BlockSvg)) return;
|
||||||
|
this.removeListeners(element.id);
|
||||||
|
element.dispose(false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes event listeners for the block with the given ID.
|
||||||
|
*
|
||||||
|
* @param blockId The ID of the block to remove event listeners from.
|
||||||
|
*/
|
||||||
|
protected removeListeners(blockId: string) {
|
||||||
|
const blockListeners = this.listeners.get(blockId) ?? [];
|
||||||
|
blockListeners.forEach((l) => browserEvents.unbind(l));
|
||||||
|
this.listeners.delete(blockId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates this inflater's flyout.
|
||||||
|
*
|
||||||
|
* @param flyout The flyout that owns this inflater.
|
||||||
|
*/
|
||||||
|
protected setFlyout(flyout: IFlyout) {
|
||||||
|
if (this.flyout === flyout) return;
|
||||||
|
|
||||||
|
if (this.flyout) {
|
||||||
|
this.flyout.targetWorkspace?.removeChangeListener(this.capacityWrapper);
|
||||||
|
}
|
||||||
|
this.flyout = flyout;
|
||||||
|
this.flyout.targetWorkspace?.addChangeListener(this.capacityWrapper);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the enabled state of the given block based on the capacity of the
|
||||||
|
* workspace.
|
||||||
|
*
|
||||||
|
* @param block The block to update the enabled/disabled state of.
|
||||||
|
*/
|
||||||
|
private updateStateBasedOnCapacity(block: BlockSvg) {
|
||||||
|
const enable = this.flyout?.targetWorkspace?.isCapacityAvailable(
|
||||||
|
common.getBlockTypeCounts(block),
|
||||||
|
);
|
||||||
|
let currentBlock: BlockSvg | null = block;
|
||||||
|
while (currentBlock) {
|
||||||
|
currentBlock.setDisabledReason(
|
||||||
|
!enable,
|
||||||
|
WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON,
|
||||||
|
);
|
||||||
|
currentBlock = currentBlock.getNextBlock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add listeners to a block that has been added to the flyout.
|
||||||
|
*
|
||||||
|
* @param block The block to add listeners for.
|
||||||
|
*/
|
||||||
|
protected addBlockListeners(block: BlockSvg) {
|
||||||
|
const blockListeners = [];
|
||||||
|
|
||||||
|
blockListeners.push(
|
||||||
|
browserEvents.conditionalBind(
|
||||||
|
block.getSvgRoot(),
|
||||||
|
'pointerdown',
|
||||||
|
block,
|
||||||
|
(e: PointerEvent) => {
|
||||||
|
const gesture = this.flyout?.targetWorkspace?.getGesture(e);
|
||||||
|
if (gesture && this.flyout) {
|
||||||
|
gesture.setStartBlock(block);
|
||||||
|
gesture.handleFlyoutStart(e, this.flyout);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
blockListeners.push(
|
||||||
|
browserEvents.bind(block.getSvgRoot(), 'pointerenter', null, () => {
|
||||||
|
if (!this.flyout?.targetWorkspace?.isDragging()) {
|
||||||
|
block.addSelect();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
blockListeners.push(
|
||||||
|
browserEvents.bind(block.getSvgRoot(), 'pointerleave', null, () => {
|
||||||
|
if (!this.flyout?.targetWorkspace?.isDragging()) {
|
||||||
|
block.removeSelect();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.listeners.set(block.id, blockListeners);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the state of blocks in our owning flyout to be disabled/enabled
|
||||||
|
* based on the capacity of the workspace for more blocks of that type.
|
||||||
|
*
|
||||||
|
* @param event The event that triggered this update.
|
||||||
|
*/
|
||||||
|
private filterFlyoutBasedOnCapacity(event: AbstractEvent) {
|
||||||
|
if (
|
||||||
|
!this.flyout ||
|
||||||
|
(event &&
|
||||||
|
!(
|
||||||
|
event.type === EventType.BLOCK_CREATE ||
|
||||||
|
event.type === EventType.BLOCK_DELETE
|
||||||
|
))
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
|
||||||
|
this.flyout
|
||||||
|
.getWorkspace()
|
||||||
|
.getTopBlocks(false)
|
||||||
|
.forEach((block) => {
|
||||||
|
if (!this.permanentlyDisabledBlocks.has(block)) {
|
||||||
|
this.updateStateBasedOnCapacity(block);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of items this inflater is responsible for creating.
|
||||||
|
*
|
||||||
|
* @returns An identifier for the type of items this inflater creates.
|
||||||
|
*/
|
||||||
|
getType() {
|
||||||
|
return BLOCK_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
registry.Type.FLYOUT_INFLATER,
|
||||||
|
BLOCK_TYPE,
|
||||||
|
BlockFlyoutInflater,
|
||||||
|
);
|
||||||
@@ -194,6 +194,9 @@ export class BlockSvg
|
|||||||
this.workspace = workspace;
|
this.workspace = workspace;
|
||||||
this.svgGroup = dom.createSvgElement(Svg.G, {});
|
this.svgGroup = dom.createSvgElement(Svg.G, {});
|
||||||
|
|
||||||
|
if (prototypeName) {
|
||||||
|
dom.addClass(this.svgGroup, prototypeName);
|
||||||
|
}
|
||||||
/** A block style object. */
|
/** A block style object. */
|
||||||
this.style = workspace.getRenderer().getConstants().getBlockStyle(null);
|
this.style = workspace.getRenderer().getConstants().getBlockStyle(null);
|
||||||
|
|
||||||
@@ -228,7 +231,7 @@ export class BlockSvg
|
|||||||
this.applyColour();
|
this.applyColour();
|
||||||
this.pathObject.updateMovable(this.isMovable() || this.isInFlyout);
|
this.pathObject.updateMovable(this.isMovable() || this.isInFlyout);
|
||||||
const svg = this.getSvgRoot();
|
const svg = this.getSvgRoot();
|
||||||
if (!this.workspace.options.readOnly && svg) {
|
if (svg) {
|
||||||
browserEvents.conditionalBind(svg, 'pointerdown', this, this.onMouseDown);
|
browserEvents.conditionalBind(svg, 'pointerdown', this, this.onMouseDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,9 +532,12 @@ export class BlockSvg
|
|||||||
if (!collapsed) {
|
if (!collapsed) {
|
||||||
this.updateDisabled();
|
this.updateDisabled();
|
||||||
this.removeInput(collapsedInputName);
|
this.removeInput(collapsedInputName);
|
||||||
|
dom.removeClass(this.svgGroup, 'blocklyCollapsed');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dom.addClass(this.svgGroup, 'blocklyCollapsed');
|
||||||
|
|
||||||
const text = this.toString(internalConstants.COLLAPSE_CHARS);
|
const text = this.toString(internalConstants.COLLAPSE_CHARS);
|
||||||
const field = this.getField(collapsedFieldName);
|
const field = this.getField(collapsedFieldName);
|
||||||
if (field) {
|
if (field) {
|
||||||
@@ -579,6 +585,8 @@ export class BlockSvg
|
|||||||
* @param e Pointer down event.
|
* @param e Pointer down event.
|
||||||
*/
|
*/
|
||||||
private onMouseDown(e: PointerEvent) {
|
private onMouseDown(e: PointerEvent) {
|
||||||
|
if (this.workspace.isReadOnly()) return;
|
||||||
|
|
||||||
const gesture = this.workspace.getGesture(e);
|
const gesture = this.workspace.getGesture(e);
|
||||||
if (gesture) {
|
if (gesture) {
|
||||||
gesture.handleBlockStart(e, this);
|
gesture.handleBlockStart(e, this);
|
||||||
@@ -606,7 +614,7 @@ export class BlockSvg
|
|||||||
protected generateContextMenu(): Array<
|
protected generateContextMenu(): Array<
|
||||||
ContextMenuOption | LegacyContextMenuOption
|
ContextMenuOption | LegacyContextMenuOption
|
||||||
> | null {
|
> | null {
|
||||||
if (this.workspace.options.readOnly || !this.contextMenu) {
|
if (this.workspace.isReadOnly() || !this.contextMenu) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
|
const menuOptions = ContextMenuRegistry.registry.getContextMenuOptions(
|
||||||
@@ -676,6 +684,24 @@ export class BlockSvg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a CSS class to the SVG group of this block.
|
||||||
|
*
|
||||||
|
* @param className
|
||||||
|
*/
|
||||||
|
addClass(className: string) {
|
||||||
|
dom.addClass(this.svgGroup, className);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a CSS class from the SVG group of this block.
|
||||||
|
*
|
||||||
|
* @param className
|
||||||
|
*/
|
||||||
|
removeClass(className: string) {
|
||||||
|
dom.removeClass(this.svgGroup, className);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recursively adds or removes the dragging class to this node and its
|
* Recursively adds or removes the dragging class to this node and its
|
||||||
* children.
|
* children.
|
||||||
@@ -688,10 +714,10 @@ export class BlockSvg
|
|||||||
if (adding) {
|
if (adding) {
|
||||||
this.translation = '';
|
this.translation = '';
|
||||||
common.draggingConnections.push(...this.getConnections_(true));
|
common.draggingConnections.push(...this.getConnections_(true));
|
||||||
dom.addClass(this.svgGroup, 'blocklyDragging');
|
this.addClass('blocklyDragging');
|
||||||
} else {
|
} else {
|
||||||
common.draggingConnections.length = 0;
|
common.draggingConnections.length = 0;
|
||||||
dom.removeClass(this.svgGroup, 'blocklyDragging');
|
this.removeClass('blocklyDragging');
|
||||||
}
|
}
|
||||||
// Recurse through all blocks attached under this one.
|
// Recurse through all blocks attached under this one.
|
||||||
for (let i = 0; i < this.childBlocks_.length; i++) {
|
for (let i = 0; i < this.childBlocks_.length; i++) {
|
||||||
@@ -716,6 +742,13 @@ export class BlockSvg
|
|||||||
*/
|
*/
|
||||||
override setEditable(editable: boolean) {
|
override setEditable(editable: boolean) {
|
||||||
super.setEditable(editable);
|
super.setEditable(editable);
|
||||||
|
|
||||||
|
if (editable) {
|
||||||
|
dom.removeClass(this.svgGroup, 'blocklyNotEditable');
|
||||||
|
} else {
|
||||||
|
dom.addClass(this.svgGroup, 'blocklyNotEditable');
|
||||||
|
}
|
||||||
|
|
||||||
const icons = this.getIcons();
|
const icons = this.getIcons();
|
||||||
for (let i = 0; i < icons.length; i++) {
|
for (let i = 0; i < icons.length; i++) {
|
||||||
icons[i].updateEditable();
|
icons[i].updateEditable();
|
||||||
@@ -873,17 +906,15 @@ export class BlockSvg
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
applyColour() {
|
applyColour() {
|
||||||
this.pathObject.applyColour(this);
|
this.pathObject.applyColour?.(this);
|
||||||
|
|
||||||
const icons = this.getIcons();
|
const icons = this.getIcons();
|
||||||
for (let i = 0; i < icons.length; i++) {
|
for (let i = 0; i < icons.length; i++) {
|
||||||
icons[i].applyColour();
|
icons[i].applyColour();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let x = 0, input; (input = this.inputList[x]); x++) {
|
for (const field of this.getFields()) {
|
||||||
for (let y = 0, field; (field = input.fieldRow[y]); y++) {
|
field.applyColour();
|
||||||
field.applyColour();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1075,6 +1106,20 @@ export class BlockSvg
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add blocklyNotDeletable class when block is not deletable
|
||||||
|
* Or remove class when block is deletable
|
||||||
|
*/
|
||||||
|
override setDeletable(deletable: boolean) {
|
||||||
|
super.setDeletable(deletable);
|
||||||
|
|
||||||
|
if (deletable) {
|
||||||
|
dom.removeClass(this.svgGroup, 'blocklyNotDeletable');
|
||||||
|
} else {
|
||||||
|
dom.addClass(this.svgGroup, 'blocklyNotDeletable');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set whether the block is highlighted or not. Block highlighting is
|
* Set whether the block is highlighted or not. Block highlighting is
|
||||||
* often used to visually mark blocks currently being executed.
|
* often used to visually mark blocks currently being executed.
|
||||||
@@ -1139,7 +1184,7 @@ export class BlockSvg
|
|||||||
.getConstants()
|
.getConstants()
|
||||||
.getBlockStyleForColour(this.colour_);
|
.getBlockStyleForColour(this.colour_);
|
||||||
|
|
||||||
this.pathObject.setStyle(styleObj.style);
|
this.pathObject.setStyle?.(styleObj.style);
|
||||||
this.style = styleObj.style;
|
this.style = styleObj.style;
|
||||||
this.styleName_ = styleObj.name;
|
this.styleName_ = styleObj.name;
|
||||||
|
|
||||||
@@ -1157,16 +1202,22 @@ export class BlockSvg
|
|||||||
.getRenderer()
|
.getRenderer()
|
||||||
.getConstants()
|
.getConstants()
|
||||||
.getBlockStyle(blockStyleName);
|
.getBlockStyle(blockStyleName);
|
||||||
this.styleName_ = blockStyleName;
|
|
||||||
|
if (this.styleName_) {
|
||||||
|
dom.removeClass(this.svgGroup, this.styleName_);
|
||||||
|
}
|
||||||
|
|
||||||
if (blockStyle) {
|
if (blockStyle) {
|
||||||
this.hat = blockStyle.hat;
|
this.hat = blockStyle.hat;
|
||||||
this.pathObject.setStyle(blockStyle);
|
this.pathObject.setStyle?.(blockStyle);
|
||||||
// Set colour to match Block.
|
// Set colour to match Block.
|
||||||
this.colour_ = blockStyle.colourPrimary;
|
this.colour_ = blockStyle.colourPrimary;
|
||||||
this.style = blockStyle;
|
this.style = blockStyle;
|
||||||
|
|
||||||
this.applyColour();
|
this.applyColour();
|
||||||
|
|
||||||
|
dom.addClass(this.svgGroup, blockStyleName);
|
||||||
|
this.styleName_ = blockStyleName;
|
||||||
} else {
|
} else {
|
||||||
throw Error('Invalid style name: ' + blockStyleName);
|
throw Error('Invalid style name: ' + blockStyleName);
|
||||||
}
|
}
|
||||||
@@ -1736,4 +1787,16 @@ export class BlockSvg
|
|||||||
traverseJson(json as unknown as {[key: string]: unknown});
|
traverseJson(json as unknown as {[key: string]: unknown});
|
||||||
return [json];
|
return [json];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override jsonInit(json: AnyDuringMigration): void {
|
||||||
|
super.jsonInit(json);
|
||||||
|
|
||||||
|
if (json['classes']) {
|
||||||
|
this.addClass(
|
||||||
|
Array.isArray(json['classes'])
|
||||||
|
? json['classes'].join(' ')
|
||||||
|
: json['classes'],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import './events/events_var_create.js';
|
|||||||
|
|
||||||
import {Block} from './block.js';
|
import {Block} from './block.js';
|
||||||
import * as blockAnimations from './block_animations.js';
|
import * as blockAnimations from './block_animations.js';
|
||||||
|
import {BlockFlyoutInflater} from './block_flyout_inflater.js';
|
||||||
import {BlockSvg} from './block_svg.js';
|
import {BlockSvg} from './block_svg.js';
|
||||||
import {BlocklyOptions} from './blockly_options.js';
|
import {BlocklyOptions} from './blockly_options.js';
|
||||||
import {Blocks} from './blocks.js';
|
import {Blocks} from './blocks.js';
|
||||||
@@ -24,6 +25,7 @@ import * as browserEvents from './browser_events.js';
|
|||||||
import * as bubbles from './bubbles.js';
|
import * as bubbles from './bubbles.js';
|
||||||
import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js';
|
import {MiniWorkspaceBubble} from './bubbles/mini_workspace_bubble.js';
|
||||||
import * as bumpObjects from './bump_objects.js';
|
import * as bumpObjects from './bump_objects.js';
|
||||||
|
import {ButtonFlyoutInflater} from './button_flyout_inflater.js';
|
||||||
import * as clipboard from './clipboard.js';
|
import * as clipboard from './clipboard.js';
|
||||||
import * as comments from './comments.js';
|
import * as comments from './comments.js';
|
||||||
import * as common from './common.js';
|
import * as common from './common.js';
|
||||||
@@ -62,6 +64,7 @@ import {
|
|||||||
FieldDropdownConfig,
|
FieldDropdownConfig,
|
||||||
FieldDropdownFromJsonConfig,
|
FieldDropdownFromJsonConfig,
|
||||||
FieldDropdownValidator,
|
FieldDropdownValidator,
|
||||||
|
ImageProperties,
|
||||||
MenuGenerator,
|
MenuGenerator,
|
||||||
MenuGeneratorFunction,
|
MenuGeneratorFunction,
|
||||||
MenuOption,
|
MenuOption,
|
||||||
@@ -99,7 +102,9 @@ import {
|
|||||||
import {Flyout} from './flyout_base.js';
|
import {Flyout} from './flyout_base.js';
|
||||||
import {FlyoutButton} from './flyout_button.js';
|
import {FlyoutButton} from './flyout_button.js';
|
||||||
import {HorizontalFlyout} from './flyout_horizontal.js';
|
import {HorizontalFlyout} from './flyout_horizontal.js';
|
||||||
|
import {FlyoutItem} from './flyout_item.js';
|
||||||
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
|
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
|
||||||
|
import {FlyoutSeparator} from './flyout_separator.js';
|
||||||
import {VerticalFlyout} from './flyout_vertical.js';
|
import {VerticalFlyout} from './flyout_vertical.js';
|
||||||
import {CodeGenerator} from './generator.js';
|
import {CodeGenerator} from './generator.js';
|
||||||
import {Gesture} from './gesture.js';
|
import {Gesture} from './gesture.js';
|
||||||
@@ -107,8 +112,11 @@ import {Grid} from './grid.js';
|
|||||||
import * as icons from './icons.js';
|
import * as icons from './icons.js';
|
||||||
import {inject} from './inject.js';
|
import {inject} from './inject.js';
|
||||||
import * as inputs from './inputs.js';
|
import * as inputs from './inputs.js';
|
||||||
|
import {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||||
|
import {LabelFlyoutInflater} from './label_flyout_inflater.js';
|
||||||
|
import {SeparatorFlyoutInflater} from './separator_flyout_inflater.js';
|
||||||
|
|
||||||
import {Input} from './inputs/input.js';
|
import {Input} from './inputs/input.js';
|
||||||
import {InsertionMarkerManager} from './insertion_marker_manager.js';
|
|
||||||
import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js';
|
import {InsertionMarkerPreviewer} from './insertion_marker_previewer.js';
|
||||||
import {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
import {IASTNodeLocation} from './interfaces/i_ast_node_location.js';
|
||||||
import {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
import {IASTNodeLocationSvg} from './interfaces/i_ast_node_location_svg.js';
|
||||||
@@ -132,6 +140,8 @@ import {
|
|||||||
} from './interfaces/i_draggable.js';
|
} from './interfaces/i_draggable.js';
|
||||||
import {IDragger} from './interfaces/i_dragger.js';
|
import {IDragger} from './interfaces/i_dragger.js';
|
||||||
import {IFlyout} from './interfaces/i_flyout.js';
|
import {IFlyout} from './interfaces/i_flyout.js';
|
||||||
|
import {IFocusableNode} from './interfaces/i_focusable_node.js';
|
||||||
|
import {IFocusableTree} from './interfaces/i_focusable_tree.js';
|
||||||
import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js';
|
import {IHasBubble, hasBubble} from './interfaces/i_has_bubble.js';
|
||||||
import {IIcon, isIcon} from './interfaces/i_icon.js';
|
import {IIcon, isIcon} from './interfaces/i_icon.js';
|
||||||
import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
|
import {IKeyboardAccessible} from './interfaces/i_keyboard_accessible.js';
|
||||||
@@ -155,6 +165,8 @@ import {
|
|||||||
IVariableBackedParameterModel,
|
IVariableBackedParameterModel,
|
||||||
isVariableBackedParameterModel,
|
isVariableBackedParameterModel,
|
||||||
} from './interfaces/i_variable_backed_parameter_model.js';
|
} from './interfaces/i_variable_backed_parameter_model.js';
|
||||||
|
import {IVariableMap} from './interfaces/i_variable_map.js';
|
||||||
|
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
|
||||||
import * as internalConstants from './internal_constants.js';
|
import * as internalConstants from './internal_constants.js';
|
||||||
import {ASTNode} from './keyboard_nav/ast_node.js';
|
import {ASTNode} from './keyboard_nav/ast_node.js';
|
||||||
import {BasicCursor} from './keyboard_nav/basic_cursor.js';
|
import {BasicCursor} from './keyboard_nav/basic_cursor.js';
|
||||||
@@ -471,6 +483,8 @@ export {
|
|||||||
};
|
};
|
||||||
export const DropDownDiv = dropDownDiv;
|
export const DropDownDiv = dropDownDiv;
|
||||||
export {
|
export {
|
||||||
|
BlockFlyoutInflater,
|
||||||
|
ButtonFlyoutInflater,
|
||||||
CodeGenerator,
|
CodeGenerator,
|
||||||
Field,
|
Field,
|
||||||
FieldCheckbox,
|
FieldCheckbox,
|
||||||
@@ -504,7 +518,9 @@ export {
|
|||||||
FieldVariableValidator,
|
FieldVariableValidator,
|
||||||
Flyout,
|
Flyout,
|
||||||
FlyoutButton,
|
FlyoutButton,
|
||||||
|
FlyoutItem,
|
||||||
FlyoutMetricsManager,
|
FlyoutMetricsManager,
|
||||||
|
FlyoutSeparator,
|
||||||
CodeGenerator as Generator,
|
CodeGenerator as Generator,
|
||||||
Gesture,
|
Gesture,
|
||||||
Grid,
|
Grid,
|
||||||
@@ -529,6 +545,9 @@ export {
|
|||||||
IDraggable,
|
IDraggable,
|
||||||
IDragger,
|
IDragger,
|
||||||
IFlyout,
|
IFlyout,
|
||||||
|
IFlyoutInflater,
|
||||||
|
IFocusableNode,
|
||||||
|
IFocusableTree,
|
||||||
IHasBubble,
|
IHasBubble,
|
||||||
IIcon,
|
IIcon,
|
||||||
IKeyboardAccessible,
|
IKeyboardAccessible,
|
||||||
@@ -546,9 +565,13 @@ export {
|
|||||||
IToolbox,
|
IToolbox,
|
||||||
IToolboxItem,
|
IToolboxItem,
|
||||||
IVariableBackedParameterModel,
|
IVariableBackedParameterModel,
|
||||||
|
IVariableMap,
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
ImageProperties,
|
||||||
Input,
|
Input,
|
||||||
InsertionMarkerManager,
|
|
||||||
InsertionMarkerPreviewer,
|
InsertionMarkerPreviewer,
|
||||||
|
LabelFlyoutInflater,
|
||||||
LayerManager,
|
LayerManager,
|
||||||
Marker,
|
Marker,
|
||||||
MarkerManager,
|
MarkerManager,
|
||||||
@@ -564,6 +587,7 @@ export {
|
|||||||
RenderedConnection,
|
RenderedConnection,
|
||||||
Scrollbar,
|
Scrollbar,
|
||||||
ScrollbarPair,
|
ScrollbarPair,
|
||||||
|
SeparatorFlyoutInflater,
|
||||||
ShortcutRegistry,
|
ShortcutRegistry,
|
||||||
TabNavigateCursor,
|
TabNavigateCursor,
|
||||||
Theme,
|
Theme,
|
||||||
|
|||||||
@@ -106,11 +106,7 @@ export abstract class Bubble implements IBubble, ISelectable {
|
|||||||
);
|
);
|
||||||
const embossGroup = dom.createSvgElement(
|
const embossGroup = dom.createSvgElement(
|
||||||
Svg.G,
|
Svg.G,
|
||||||
{
|
{'class': 'blocklyEmboss'},
|
||||||
'filter': `url(#${
|
|
||||||
this.workspace.getRenderer().getConstants().embossFilterId
|
|
||||||
})`,
|
|
||||||
},
|
|
||||||
this.svgRoot,
|
this.svgRoot,
|
||||||
);
|
);
|
||||||
this.tail = dom.createSvgElement(
|
this.tail = dom.createSvgElement(
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export class MiniWorkspaceBubble extends Bubble {
|
|||||||
flyout?.show(options.languageTree);
|
flyout?.show(options.languageTree);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
dom.addClass(this.svgRoot, 'blocklyMiniWorkspaceBubble');
|
||||||
this.miniWorkspace.addChangeListener(this.onWorkspaceChange.bind(this));
|
this.miniWorkspace.addChangeListener(this.onWorkspaceChange.bind(this));
|
||||||
this.miniWorkspace
|
this.miniWorkspace
|
||||||
.getFlyout()
|
.getFlyout()
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export class TextBubble extends Bubble {
|
|||||||
super(workspace, anchor, ownerRect);
|
super(workspace, anchor, ownerRect);
|
||||||
this.paragraph = this.stringToSvg(text, this.contentContainer);
|
this.paragraph = this.stringToSvg(text, this.contentContainer);
|
||||||
this.updateBubbleSize();
|
this.updateBubbleSize();
|
||||||
|
dom.addClass(this.svgRoot, 'blocklyTextBubble');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @returns the current text of this text bubble. */
|
/** @returns the current text of this text bubble. */
|
||||||
|
|||||||
@@ -48,6 +48,9 @@ export class TextInputBubble extends Bubble {
|
|||||||
/** Functions listening for changes to the size of this bubble. */
|
/** Functions listening for changes to the size of this bubble. */
|
||||||
private sizeChangeListeners: (() => void)[] = [];
|
private sizeChangeListeners: (() => void)[] = [];
|
||||||
|
|
||||||
|
/** Functions listening for changes to the location of this bubble. */
|
||||||
|
private locationChangeListeners: (() => void)[] = [];
|
||||||
|
|
||||||
/** The text of this bubble. */
|
/** The text of this bubble. */
|
||||||
private text = '';
|
private text = '';
|
||||||
|
|
||||||
@@ -123,6 +126,11 @@ export class TextInputBubble extends Bubble {
|
|||||||
this.sizeChangeListeners.push(listener);
|
this.sizeChangeListeners.push(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Adds a change listener to be notified when this bubble's location changes. */
|
||||||
|
addLocationChangeListener(listener: () => void) {
|
||||||
|
this.locationChangeListeners.push(listener);
|
||||||
|
}
|
||||||
|
|
||||||
/** Creates the editor UI for this bubble. */
|
/** Creates the editor UI for this bubble. */
|
||||||
private createEditor(container: SVGGElement): {
|
private createEditor(container: SVGGElement): {
|
||||||
inputRoot: SVGForeignObjectElement;
|
inputRoot: SVGForeignObjectElement;
|
||||||
@@ -230,10 +238,25 @@ export class TextInputBubble extends Bubble {
|
|||||||
|
|
||||||
/** @returns the size of this bubble. */
|
/** @returns the size of this bubble. */
|
||||||
getSize(): Size {
|
getSize(): Size {
|
||||||
// Overriden to be public.
|
// Overridden to be public.
|
||||||
return super.getSize();
|
return super.getSize();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override moveDuringDrag(newLoc: Coordinate) {
|
||||||
|
super.moveDuringDrag(newLoc);
|
||||||
|
this.onLocationChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
override setPositionRelativeToAnchor(left: number, top: number) {
|
||||||
|
super.setPositionRelativeToAnchor(left, top);
|
||||||
|
this.onLocationChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override positionByRect(rect = new Rect(0, 0, 0, 0)) {
|
||||||
|
super.positionByRect(rect);
|
||||||
|
this.onLocationChange();
|
||||||
|
}
|
||||||
|
|
||||||
/** Handles mouse down events on the resize target. */
|
/** Handles mouse down events on the resize target. */
|
||||||
private onResizePointerDown(e: PointerEvent) {
|
private onResizePointerDown(e: PointerEvent) {
|
||||||
this.bringToFront();
|
this.bringToFront();
|
||||||
@@ -316,6 +339,13 @@ export class TextInputBubble extends Bubble {
|
|||||||
listener();
|
listener();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Handles a location change event for the text area. Calls event listeners. */
|
||||||
|
private onLocationChange() {
|
||||||
|
for (const listener of this.locationChangeListeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Css.register(`
|
Css.register(`
|
||||||
|
|||||||
76
core/button_flyout_inflater.ts
Normal file
76
core/button_flyout_inflater.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {FlyoutButton} from './flyout_button.js';
|
||||||
|
import {FlyoutItem} from './flyout_item.js';
|
||||||
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||||
|
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||||
|
import * as registry from './registry.js';
|
||||||
|
import {ButtonOrLabelInfo} from './utils/toolbox.js';
|
||||||
|
|
||||||
|
const BUTTON_TYPE = 'button';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class responsible for creating buttons for flyouts.
|
||||||
|
*/
|
||||||
|
export class ButtonFlyoutInflater implements IFlyoutInflater {
|
||||||
|
/**
|
||||||
|
* Inflates a flyout button from the given state and adds it to the flyout.
|
||||||
|
*
|
||||||
|
* @param state A JSON representation of a flyout button.
|
||||||
|
* @param flyout The flyout to create the button on.
|
||||||
|
* @returns A newly created FlyoutButton.
|
||||||
|
*/
|
||||||
|
load(state: object, flyout: IFlyout): FlyoutItem {
|
||||||
|
const button = new FlyoutButton(
|
||||||
|
flyout.getWorkspace(),
|
||||||
|
flyout.targetWorkspace!,
|
||||||
|
state as ButtonOrLabelInfo,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
button.show();
|
||||||
|
|
||||||
|
return new FlyoutItem(button, BUTTON_TYPE, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of space that should follow this button.
|
||||||
|
*
|
||||||
|
* @param state A JSON representation of a flyout button.
|
||||||
|
* @param defaultGap The default spacing for flyout items.
|
||||||
|
* @returns The amount of space that should follow this button.
|
||||||
|
*/
|
||||||
|
gapForItem(state: object, defaultGap: number): number {
|
||||||
|
return defaultGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the given button.
|
||||||
|
*
|
||||||
|
* @param item The flyout button to dispose of.
|
||||||
|
*/
|
||||||
|
disposeItem(item: FlyoutItem): void {
|
||||||
|
const element = item.getElement();
|
||||||
|
if (element instanceof FlyoutButton) {
|
||||||
|
element.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of items this inflater is responsible for creating.
|
||||||
|
*
|
||||||
|
* @returns An identifier for the type of items this inflater creates.
|
||||||
|
*/
|
||||||
|
getType() {
|
||||||
|
return BUTTON_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
registry.Type.FLYOUT_INFLATER,
|
||||||
|
BUTTON_TYPE,
|
||||||
|
ButtonFlyoutInflater,
|
||||||
|
);
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
// Former goog.module ID: Blockly.clipboard
|
// Former goog.module ID: Blockly.clipboard
|
||||||
|
|
||||||
import {BlockPaster} from './clipboard/block_paster.js';
|
import {BlockCopyData, BlockPaster} from './clipboard/block_paster.js';
|
||||||
import * as registry from './clipboard/registry.js';
|
import * as registry from './clipboard/registry.js';
|
||||||
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
||||||
import * as globalRegistry from './registry.js';
|
import * as globalRegistry from './registry.js';
|
||||||
@@ -112,4 +112,4 @@ export const TEST_ONLY = {
|
|||||||
copyInternal,
|
copyInternal,
|
||||||
};
|
};
|
||||||
|
|
||||||
export {BlockPaster, registry};
|
export {BlockCopyData, BlockPaster, registry};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export class CommentView implements IRenderedElement {
|
|||||||
private textArea: HTMLTextAreaElement;
|
private textArea: HTMLTextAreaElement;
|
||||||
|
|
||||||
/** The current size of the comment in workspace units. */
|
/** The current size of the comment in workspace units. */
|
||||||
private size: Size = new Size(120, 100);
|
private size: Size;
|
||||||
|
|
||||||
/** Whether the comment is collapsed or not. */
|
/** Whether the comment is collapsed or not. */
|
||||||
private collapsed: boolean = false;
|
private collapsed: boolean = false;
|
||||||
@@ -95,15 +95,18 @@ export class CommentView implements IRenderedElement {
|
|||||||
private resizePointerMoveListener: browserEvents.Data | null = null;
|
private resizePointerMoveListener: browserEvents.Data | null = null;
|
||||||
|
|
||||||
/** Whether this comment view is currently being disposed or not. */
|
/** Whether this comment view is currently being disposed or not. */
|
||||||
private disposing = false;
|
protected disposing = false;
|
||||||
|
|
||||||
/** Whether this comment view has been disposed or not. */
|
/** Whether this comment view has been disposed or not. */
|
||||||
private disposed = false;
|
protected disposed = false;
|
||||||
|
|
||||||
/** Size of this comment when the resize drag was initiated. */
|
/** Size of this comment when the resize drag was initiated. */
|
||||||
private preResizeSize?: Size;
|
private preResizeSize?: Size;
|
||||||
|
|
||||||
constructor(private readonly workspace: WorkspaceSvg) {
|
/** The default size of newly created comments. */
|
||||||
|
static defaultCommentSize = new Size(120, 100);
|
||||||
|
|
||||||
|
constructor(readonly workspace: WorkspaceSvg) {
|
||||||
this.svgRoot = dom.createSvgElement(Svg.G, {
|
this.svgRoot = dom.createSvgElement(Svg.G, {
|
||||||
'class': 'blocklyComment blocklyEditable blocklyDraggable',
|
'class': 'blocklyComment blocklyEditable blocklyDraggable',
|
||||||
});
|
});
|
||||||
@@ -129,6 +132,7 @@ export class CommentView implements IRenderedElement {
|
|||||||
workspace.getLayerManager()?.append(this, layers.BLOCK);
|
workspace.getLayerManager()?.append(this, layers.BLOCK);
|
||||||
|
|
||||||
// Set size to the default size.
|
// Set size to the default size.
|
||||||
|
this.size = CommentView.defaultCommentSize;
|
||||||
this.setSizeWithoutFiringEvents(this.size);
|
this.setSizeWithoutFiringEvents(this.size);
|
||||||
|
|
||||||
// Set default transform (including inverted scale for RTL).
|
// Set default transform (including inverted scale for RTL).
|
||||||
@@ -685,6 +689,11 @@ export class CommentView implements IRenderedElement {
|
|||||||
this.onTextChange();
|
this.onTextChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the placeholder text displayed for an empty comment. */
|
||||||
|
setPlaceholderText(text: string) {
|
||||||
|
this.textArea.placeholder = text;
|
||||||
|
}
|
||||||
|
|
||||||
/** Registers a callback that listens for text changes. */
|
/** Registers a callback that listens for text changes. */
|
||||||
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
|
addTextChangeListener(listener: (oldText: string, newText: string) => void) {
|
||||||
this.textChangeListeners.push(listener);
|
this.textChangeListeners.push(listener);
|
||||||
|
|||||||
@@ -105,6 +105,11 @@ export class RenderedWorkspaceComment
|
|||||||
this.view.setText(text);
|
this.view.setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the placeholder text displayed if the comment is empty. */
|
||||||
|
setPlaceholderText(text: string): void {
|
||||||
|
this.view.setPlaceholderText(text);
|
||||||
|
}
|
||||||
|
|
||||||
/** Sets the size of the comment. */
|
/** Sets the size of the comment. */
|
||||||
override setSize(size: Size) {
|
override setSize(size: Size) {
|
||||||
// setSize will trigger the change listener that updates
|
// setSize will trigger the change listener that updates
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {Coordinate} from '../utils/coordinate.js';
|
|||||||
import * as idGenerator from '../utils/idgenerator.js';
|
import * as idGenerator from '../utils/idgenerator.js';
|
||||||
import {Size} from '../utils/size.js';
|
import {Size} from '../utils/size.js';
|
||||||
import {Workspace} from '../workspace.js';
|
import {Workspace} from '../workspace.js';
|
||||||
|
import {CommentView} from './comment_view.js';
|
||||||
|
|
||||||
export class WorkspaceComment {
|
export class WorkspaceComment {
|
||||||
/** The unique identifier for this comment. */
|
/** The unique identifier for this comment. */
|
||||||
@@ -21,7 +22,7 @@ export class WorkspaceComment {
|
|||||||
private text = '';
|
private text = '';
|
||||||
|
|
||||||
/** The size of the comment in workspace units. */
|
/** The size of the comment in workspace units. */
|
||||||
private size = new Size(120, 100);
|
private size: Size;
|
||||||
|
|
||||||
/** Whether the comment is collapsed or not. */
|
/** Whether the comment is collapsed or not. */
|
||||||
private collapsed = false;
|
private collapsed = false;
|
||||||
@@ -56,6 +57,7 @@ export class WorkspaceComment {
|
|||||||
id?: string,
|
id?: string,
|
||||||
) {
|
) {
|
||||||
this.id = id && !workspace.getCommentById(id) ? id : idGenerator.genUid();
|
this.id = id && !workspace.getCommentById(id) ? id : idGenerator.genUid();
|
||||||
|
this.size = CommentView.defaultCommentSize;
|
||||||
|
|
||||||
workspace.addTopComment(this);
|
workspace.addTopComment(this);
|
||||||
|
|
||||||
@@ -142,7 +144,7 @@ export class WorkspaceComment {
|
|||||||
* workspace is read-only.
|
* workspace is read-only.
|
||||||
*/
|
*/
|
||||||
isEditable(): boolean {
|
isEditable(): boolean {
|
||||||
return this.isOwnEditable() && !this.workspace.options.readOnly;
|
return this.isOwnEditable() && !this.workspace.isReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,7 +165,7 @@ export class WorkspaceComment {
|
|||||||
* workspace is read-only.
|
* workspace is read-only.
|
||||||
*/
|
*/
|
||||||
isMovable() {
|
isMovable() {
|
||||||
return this.isOwnMovable() && !this.workspace.options.readOnly;
|
return this.isOwnMovable() && !this.workspace.isReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -187,7 +189,7 @@ export class WorkspaceComment {
|
|||||||
return (
|
return (
|
||||||
this.isOwnDeletable() &&
|
this.isOwnDeletable() &&
|
||||||
!this.isDeadOrDying() &&
|
!this.isDeadOrDying() &&
|
||||||
!this.workspace.options.readOnly
|
!this.workspace.isReadOnly()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -485,7 +485,7 @@ export class Connection implements IASTNodeLocationWithBlock {
|
|||||||
*
|
*
|
||||||
* Headless configurations (the default) do not have neighboring connection,
|
* Headless configurations (the default) do not have neighboring connection,
|
||||||
* and always return an empty list (the default).
|
* and always return an empty list (the default).
|
||||||
* {@link RenderedConnection#neighbours} overrides this behavior with a list
|
* {@link (RenderedConnection:class).neighbours} overrides this behavior with a list
|
||||||
* computed from the rendered positioning.
|
* computed from the rendered positioning.
|
||||||
*
|
*
|
||||||
* @param _maxLimit The maximum radius to another connection.
|
* @param _maxLimit The maximum radius to another connection.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import type {
|
|||||||
import {EventType} from './events/type.js';
|
import {EventType} from './events/type.js';
|
||||||
import * as eventUtils from './events/utils.js';
|
import * as eventUtils from './events/utils.js';
|
||||||
import {Menu} from './menu.js';
|
import {Menu} from './menu.js';
|
||||||
|
import {MenuSeparator} from './menu_separator.js';
|
||||||
import {MenuItem} from './menuitem.js';
|
import {MenuItem} from './menuitem.js';
|
||||||
import * as serializationBlocks from './serialization/blocks.js';
|
import * as serializationBlocks from './serialization/blocks.js';
|
||||||
import * as aria from './utils/aria.js';
|
import * as aria from './utils/aria.js';
|
||||||
@@ -111,6 +112,11 @@ function populate_(
|
|||||||
menu.setRole(aria.Role.MENU);
|
menu.setRole(aria.Role.MENU);
|
||||||
for (let i = 0; i < options.length; i++) {
|
for (let i = 0; i < options.length; i++) {
|
||||||
const option = options[i];
|
const option = options[i];
|
||||||
|
if (option.separator) {
|
||||||
|
menu.addChild(new MenuSeparator());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const menuItem = new MenuItem(option.text);
|
const menuItem = new MenuItem(option.text);
|
||||||
menuItem.setRightToLeft(rtl);
|
menuItem.setRightToLeft(rtl);
|
||||||
menuItem.setRole(aria.Role.MENUITEM);
|
menuItem.setRole(aria.Role.MENUITEM);
|
||||||
|
|||||||
@@ -619,7 +619,7 @@ export function registerCommentCreate() {
|
|||||||
if (!workspace) return;
|
if (!workspace) return;
|
||||||
eventUtils.setGroup(true);
|
eventUtils.setGroup(true);
|
||||||
const comment = new RenderedWorkspaceComment(workspace);
|
const comment = new RenderedWorkspaceComment(workspace);
|
||||||
comment.setText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']);
|
comment.setPlaceholderText(Msg['WORKSPACE_COMMENT_DEFAULT_TEXT']);
|
||||||
comment.moveTo(
|
comment.moveTo(
|
||||||
pixelsToWorkspaceCoords(
|
pixelsToWorkspaceCoords(
|
||||||
new Coordinate(e.clientX, e.clientY),
|
new Coordinate(e.clientX, e.clientY),
|
||||||
|
|||||||
@@ -87,21 +87,37 @@ export class ContextMenuRegistry {
|
|||||||
const menuOptions: ContextMenuOption[] = [];
|
const menuOptions: ContextMenuOption[] = [];
|
||||||
for (const item of this.registeredItems.values()) {
|
for (const item of this.registeredItems.values()) {
|
||||||
if (scopeType === item.scopeType) {
|
if (scopeType === item.scopeType) {
|
||||||
const precondition = item.preconditionFn(scope);
|
let menuOption:
|
||||||
if (precondition !== 'hidden') {
|
| ContextMenuRegistry.CoreContextMenuOption
|
||||||
|
| ContextMenuRegistry.SeparatorContextMenuOption
|
||||||
|
| ContextMenuRegistry.ActionContextMenuOption;
|
||||||
|
menuOption = {
|
||||||
|
scope,
|
||||||
|
weight: item.weight,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (item.separator) {
|
||||||
|
menuOption = {
|
||||||
|
...menuOption,
|
||||||
|
separator: true,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const precondition = item.preconditionFn(scope);
|
||||||
|
if (precondition === 'hidden') continue;
|
||||||
|
|
||||||
const displayText =
|
const displayText =
|
||||||
typeof item.displayText === 'function'
|
typeof item.displayText === 'function'
|
||||||
? item.displayText(scope)
|
? item.displayText(scope)
|
||||||
: item.displayText;
|
: item.displayText;
|
||||||
const menuOption: ContextMenuOption = {
|
menuOption = {
|
||||||
|
...menuOption,
|
||||||
text: displayText,
|
text: displayText,
|
||||||
enabled: precondition === 'enabled',
|
|
||||||
callback: item.callback,
|
callback: item.callback,
|
||||||
scope,
|
enabled: precondition === 'enabled',
|
||||||
weight: item.weight,
|
|
||||||
};
|
};
|
||||||
menuOptions.push(menuOption);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
menuOptions.push(menuOption);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
menuOptions.sort(function (a, b) {
|
menuOptions.sort(function (a, b) {
|
||||||
@@ -134,9 +150,18 @@ export namespace ContextMenuRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A menu item as entered in the registry.
|
* Fields common to all context menu registry items.
|
||||||
*/
|
*/
|
||||||
export interface RegistryItem {
|
interface CoreRegistryItem {
|
||||||
|
scopeType: ScopeType;
|
||||||
|
weight: number;
|
||||||
|
id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of a normal, clickable menu item in the registry.
|
||||||
|
*/
|
||||||
|
interface ActionRegistryItem extends CoreRegistryItem {
|
||||||
/**
|
/**
|
||||||
* @param scope Object that provides a reference to the thing that had its
|
* @param scope Object that provides a reference to the thing that had its
|
||||||
* context menu opened.
|
* context menu opened.
|
||||||
@@ -144,17 +169,38 @@ export namespace ContextMenuRegistry {
|
|||||||
* the event that triggered the click on the option.
|
* the event that triggered the click on the option.
|
||||||
*/
|
*/
|
||||||
callback: (scope: Scope, e: PointerEvent) => void;
|
callback: (scope: Scope, e: PointerEvent) => void;
|
||||||
scopeType: ScopeType;
|
|
||||||
displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement;
|
displayText: ((p1: Scope) => string | HTMLElement) | string | HTMLElement;
|
||||||
preconditionFn: (p1: Scope) => string;
|
preconditionFn: (p1: Scope) => string;
|
||||||
weight: number;
|
separator?: never;
|
||||||
id: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A menu item as presented to contextmenu.js.
|
* A representation of a menu separator item in the registry.
|
||||||
*/
|
*/
|
||||||
export interface ContextMenuOption {
|
interface SeparatorRegistryItem extends CoreRegistryItem {
|
||||||
|
separator: true;
|
||||||
|
callback?: never;
|
||||||
|
displayText?: never;
|
||||||
|
preconditionFn?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A menu item as entered in the registry.
|
||||||
|
*/
|
||||||
|
export type RegistryItem = ActionRegistryItem | SeparatorRegistryItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fields common to all context menu items as used by contextmenu.ts.
|
||||||
|
*/
|
||||||
|
export interface CoreContextMenuOption {
|
||||||
|
scope: Scope;
|
||||||
|
weight: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of a normal, clickable menu item in contextmenu.ts.
|
||||||
|
*/
|
||||||
|
export interface ActionContextMenuOption extends CoreContextMenuOption {
|
||||||
text: string | HTMLElement;
|
text: string | HTMLElement;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
/**
|
/**
|
||||||
@@ -164,10 +210,26 @@ export namespace ContextMenuRegistry {
|
|||||||
* the event that triggered the click on the option.
|
* the event that triggered the click on the option.
|
||||||
*/
|
*/
|
||||||
callback: (scope: Scope, e: PointerEvent) => void;
|
callback: (scope: Scope, e: PointerEvent) => void;
|
||||||
scope: Scope;
|
separator?: never;
|
||||||
weight: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A representation of a menu separator item in contextmenu.ts.
|
||||||
|
*/
|
||||||
|
export interface SeparatorContextMenuOption extends CoreContextMenuOption {
|
||||||
|
separator: true;
|
||||||
|
text?: never;
|
||||||
|
enabled?: never;
|
||||||
|
callback?: never;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A menu item as presented to contextmenu.ts.
|
||||||
|
*/
|
||||||
|
export type ContextMenuOption =
|
||||||
|
| ActionContextMenuOption
|
||||||
|
| SeparatorContextMenuOption;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A subset of ContextMenuOption corresponding to what was publicly
|
* A subset of ContextMenuOption corresponding to what was publicly
|
||||||
* documented. ContextMenuOption should be preferred for new code.
|
* documented. ContextMenuOption should be preferred for new code.
|
||||||
@@ -176,6 +238,7 @@ export namespace ContextMenuRegistry {
|
|||||||
text: string;
|
text: string;
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
callback: (p1: Scope) => void;
|
callback: (p1: Scope) => void;
|
||||||
|
separator?: never;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
100
core/css.ts
100
core/css.ts
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
// Former goog.module ID: Blockly.Css
|
// Former goog.module ID: Blockly.Css
|
||||||
|
|
||||||
/** Has CSS already been injected? */
|
/** Has CSS already been injected? */
|
||||||
let injected = false;
|
let injected = false;
|
||||||
|
|
||||||
@@ -83,17 +82,15 @@ let content = `
|
|||||||
-webkit-user-select: none;
|
-webkit-user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyNonSelectable {
|
|
||||||
user-select: none;
|
|
||||||
-ms-user-select: none;
|
|
||||||
-webkit-user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklyBlockCanvas.blocklyCanvasTransitioning,
|
.blocklyBlockCanvas.blocklyCanvasTransitioning,
|
||||||
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
|
.blocklyBubbleCanvas.blocklyCanvasTransitioning {
|
||||||
transition: transform .5s;
|
transition: transform .5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blocklyEmboss {
|
||||||
|
filter: var(--blocklyEmbossFilter);
|
||||||
|
}
|
||||||
|
|
||||||
.blocklyTooltipDiv {
|
.blocklyTooltipDiv {
|
||||||
background-color: #ffffc7;
|
background-color: #ffffc7;
|
||||||
border: 1px solid #ddc;
|
border: 1px solid #ddc;
|
||||||
@@ -121,7 +118,7 @@ let content = `
|
|||||||
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
|
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyDropDownDiv.blocklyFocused {
|
.blocklyDropDownDiv:focus {
|
||||||
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
|
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -141,47 +138,14 @@ let content = `
|
|||||||
z-index: -1;
|
z-index: -1;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
border-color: inherit;
|
border-color: inherit;
|
||||||
}
|
|
||||||
|
|
||||||
.blocklyDropDownButton {
|
|
||||||
display: inline-block;
|
|
||||||
float: left;
|
|
||||||
padding: 0;
|
|
||||||
margin: 4px;
|
|
||||||
border-radius: 4px;
|
|
||||||
outline: none;
|
|
||||||
border: 1px solid;
|
|
||||||
transition: box-shadow .1s;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklyArrowTop {
|
|
||||||
border-top: 1px solid;
|
border-top: 1px solid;
|
||||||
border-left: 1px solid;
|
border-left: 1px solid;
|
||||||
border-top-left-radius: 4px;
|
border-top-left-radius: 4px;
|
||||||
border-color: inherit;
|
border-color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyArrowBottom {
|
.blocklyHighlighted>.blocklyPath {
|
||||||
border-bottom: 1px solid;
|
filter: var(--blocklyEmbossFilter);
|
||||||
border-right: 1px solid;
|
|
||||||
border-bottom-right-radius: 4px;
|
|
||||||
border-color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklyResizeSE {
|
|
||||||
cursor: se-resize;
|
|
||||||
fill: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklyResizeSW {
|
|
||||||
cursor: sw-resize;
|
|
||||||
fill: #aaa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.blocklyResizeLine {
|
|
||||||
stroke: #515A5A;
|
|
||||||
stroke-width: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyHighlightedConnectionPath {
|
.blocklyHighlightedConnectionPath {
|
||||||
@@ -235,6 +199,7 @@ let content = `
|
|||||||
}
|
}
|
||||||
|
|
||||||
.blocklyDisabled>.blocklyPath {
|
.blocklyDisabled>.blocklyPath {
|
||||||
|
fill: var(--blocklyDisabledPattern);
|
||||||
fill-opacity: .5;
|
fill-opacity: .5;
|
||||||
stroke-opacity: .5;
|
stroke-opacity: .5;
|
||||||
}
|
}
|
||||||
@@ -251,7 +216,7 @@ let content = `
|
|||||||
stroke: none;
|
stroke: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyNonEditableText>text {
|
.blocklyNonEditableField>text {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -264,12 +229,15 @@ let content = `
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyHidden {
|
/*
|
||||||
display: none;
|
Don't allow users to select text. It gets annoying when trying to
|
||||||
}
|
drag a block and selected text moves instead.
|
||||||
|
*/
|
||||||
.blocklyFieldDropdown:not(.blocklyHidden) {
|
.blocklySvg text {
|
||||||
display: block;
|
user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
|
cursor: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyIconGroup {
|
.blocklyIconGroup {
|
||||||
@@ -419,6 +387,9 @@ input[type=number] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.blocklyWidgetDiv .blocklyMenu {
|
.blocklyWidgetDiv .blocklyMenu {
|
||||||
|
user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
|
box-shadow: 0 0 3px 1px rgba(0,0,0,.3);
|
||||||
@@ -433,11 +404,14 @@ input[type=number] {
|
|||||||
z-index: 20000; /* Arbitrary, but some apps depend on it... */
|
z-index: 20000; /* Arbitrary, but some apps depend on it... */
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyWidgetDiv .blocklyMenu.blocklyFocused {
|
.blocklyWidgetDiv .blocklyMenu:focus {
|
||||||
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
|
box-shadow: 0 0 6px 1px rgba(0,0,0,.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.blocklyDropDownDiv .blocklyMenu {
|
.blocklyDropDownDiv .blocklyMenu {
|
||||||
|
user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
-webkit-user-select: none;
|
||||||
background: inherit; /* Compatibility with gapi, reset from goog-menu */
|
background: inherit; /* Compatibility with gapi, reset from goog-menu */
|
||||||
border: inherit; /* Compatibility with gapi, reset from goog-menu */
|
border: inherit; /* Compatibility with gapi, reset from goog-menu */
|
||||||
font: normal 13px "Helvetica Neue", Helvetica, sans-serif;
|
font: normal 13px "Helvetica Neue", Helvetica, sans-serif;
|
||||||
@@ -465,8 +439,7 @@ input[type=number] {
|
|||||||
cursor: inherit;
|
cursor: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* State: hover. */
|
.blocklyMenuItem:hover {
|
||||||
.blocklyMenuItemHighlight {
|
|
||||||
background-color: rgba(0,0,0,.1);
|
background-color: rgba(0,0,0,.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,6 +462,14 @@ input[type=number] {
|
|||||||
margin-right: -24px;
|
margin-right: -24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blocklyMenuSeparator {
|
||||||
|
background-color: #ccc;
|
||||||
|
height: 1px;
|
||||||
|
border: 0;
|
||||||
|
margin-left: 4px;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.blocklyBlockDragSurface, .blocklyAnimationLayer {
|
.blocklyBlockDragSurface, .blocklyAnimationLayer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -499,4 +480,17 @@ input[type=number] {
|
|||||||
z-index: 80;
|
z-index: 80;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.blocklyField {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyInputField {
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blocklyDragging .blocklyField,
|
||||||
|
.blocklyDragging .blocklyIconGroup {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -62,8 +62,8 @@ export class BlockDragStrategy implements IDragStrategy {
|
|||||||
*/
|
*/
|
||||||
private dragOffset = new Coordinate(0, 0);
|
private dragOffset = new Coordinate(0, 0);
|
||||||
|
|
||||||
/** Was there already an event group in progress when the drag started? */
|
/** Used to persist an event group when snapping is done async. */
|
||||||
private inGroup: boolean = false;
|
private originalEventGroup = '';
|
||||||
|
|
||||||
constructor(private block: BlockSvg) {
|
constructor(private block: BlockSvg) {
|
||||||
this.workspace = block.workspace;
|
this.workspace = block.workspace;
|
||||||
@@ -78,7 +78,7 @@ export class BlockDragStrategy implements IDragStrategy {
|
|||||||
return (
|
return (
|
||||||
this.block.isOwnMovable() &&
|
this.block.isOwnMovable() &&
|
||||||
!this.block.isDeadOrDying() &&
|
!this.block.isDeadOrDying() &&
|
||||||
!this.workspace.options.readOnly &&
|
!this.workspace.isReadOnly() &&
|
||||||
// We never drag blocks in the flyout, only create new blocks that are
|
// We never drag blocks in the flyout, only create new blocks that are
|
||||||
// dragged.
|
// dragged.
|
||||||
!this.block.isInFlyout
|
!this.block.isInFlyout
|
||||||
@@ -96,10 +96,6 @@ export class BlockDragStrategy implements IDragStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.dragging = true;
|
this.dragging = true;
|
||||||
this.inGroup = !!eventUtils.getGroup();
|
|
||||||
if (!this.inGroup) {
|
|
||||||
eventUtils.setGroup(true);
|
|
||||||
}
|
|
||||||
this.fireDragStartEvent();
|
this.fireDragStartEvent();
|
||||||
|
|
||||||
this.startLoc = this.block.getRelativeToSurfaceXY();
|
this.startLoc = this.block.getRelativeToSurfaceXY();
|
||||||
@@ -363,6 +359,7 @@ export class BlockDragStrategy implements IDragStrategy {
|
|||||||
this.block.getParent()?.endDrag(e);
|
this.block.getParent()?.endDrag(e);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this.originalEventGroup = eventUtils.getGroup();
|
||||||
|
|
||||||
this.fireDragEndEvent();
|
this.fireDragEndEvent();
|
||||||
this.fireMoveEvent();
|
this.fireMoveEvent();
|
||||||
@@ -388,20 +385,19 @@ export class BlockDragStrategy implements IDragStrategy {
|
|||||||
} else {
|
} else {
|
||||||
this.block.queueRender().then(() => this.disposeStep());
|
this.block.queueRender().then(() => this.disposeStep());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.inGroup) {
|
|
||||||
eventUtils.setGroup(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Disposes of any state at the end of the drag. */
|
/** Disposes of any state at the end of the drag. */
|
||||||
private disposeStep() {
|
private disposeStep() {
|
||||||
|
const newGroup = eventUtils.getGroup();
|
||||||
|
eventUtils.setGroup(this.originalEventGroup);
|
||||||
this.block.snapToGrid();
|
this.block.snapToGrid();
|
||||||
|
|
||||||
// Must dispose after connections are applied to not break the dynamic
|
// Must dispose after connections are applied to not break the dynamic
|
||||||
// connections plugin. See #7859
|
// connections plugin. See #7859
|
||||||
this.connectionPreviewer!.dispose();
|
this.connectionPreviewer!.dispose();
|
||||||
this.workspace.setResizesEnabled(true);
|
this.workspace.setResizesEnabled(true);
|
||||||
|
eventUtils.setGroup(newGroup);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Connects the given candidate connections. */
|
/** Connects the given candidate connections. */
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {IBubble, WorkspaceSvg} from '../blockly.js';
|
import {IBubble, WorkspaceSvg} from '../blockly.js';
|
||||||
import * as eventUtils from '../events/utils.js';
|
|
||||||
import {IDragStrategy} from '../interfaces/i_draggable.js';
|
import {IDragStrategy} from '../interfaces/i_draggable.js';
|
||||||
import * as layers from '../layers.js';
|
import * as layers from '../layers.js';
|
||||||
import {Coordinate} from '../utils.js';
|
import {Coordinate} from '../utils.js';
|
||||||
@@ -13,9 +12,6 @@ import {Coordinate} from '../utils.js';
|
|||||||
export class BubbleDragStrategy implements IDragStrategy {
|
export class BubbleDragStrategy implements IDragStrategy {
|
||||||
private startLoc: Coordinate | null = null;
|
private startLoc: Coordinate | null = null;
|
||||||
|
|
||||||
/** Was there already an event group in progress when the drag started? */
|
|
||||||
private inGroup: boolean = false;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private bubble: IBubble,
|
private bubble: IBubble,
|
||||||
private workspace: WorkspaceSvg,
|
private workspace: WorkspaceSvg,
|
||||||
@@ -26,10 +22,6 @@ export class BubbleDragStrategy implements IDragStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startDrag(): void {
|
startDrag(): void {
|
||||||
this.inGroup = !!eventUtils.getGroup();
|
|
||||||
if (!this.inGroup) {
|
|
||||||
eventUtils.setGroup(true);
|
|
||||||
}
|
|
||||||
this.startLoc = this.bubble.getRelativeToSurfaceXY();
|
this.startLoc = this.bubble.getRelativeToSurfaceXY();
|
||||||
this.workspace.setResizesEnabled(false);
|
this.workspace.setResizesEnabled(false);
|
||||||
this.workspace.getLayerManager()?.moveToDragLayer(this.bubble);
|
this.workspace.getLayerManager()?.moveToDragLayer(this.bubble);
|
||||||
@@ -44,9 +36,6 @@ export class BubbleDragStrategy implements IDragStrategy {
|
|||||||
|
|
||||||
endDrag(): void {
|
endDrag(): void {
|
||||||
this.workspace.setResizesEnabled(true);
|
this.workspace.setResizesEnabled(true);
|
||||||
if (!this.inGroup) {
|
|
||||||
eventUtils.setGroup(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.workspace
|
this.workspace
|
||||||
.getLayerManager()
|
.getLayerManager()
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ export class CommentDragStrategy implements IDragStrategy {
|
|||||||
|
|
||||||
private workspace: WorkspaceSvg;
|
private workspace: WorkspaceSvg;
|
||||||
|
|
||||||
/** Was there already an event group in progress when the drag started? */
|
|
||||||
private inGroup: boolean = false;
|
|
||||||
|
|
||||||
constructor(private comment: RenderedWorkspaceComment) {
|
constructor(private comment: RenderedWorkspaceComment) {
|
||||||
this.workspace = comment.workspace;
|
this.workspace = comment.workspace;
|
||||||
}
|
}
|
||||||
@@ -29,15 +26,11 @@ export class CommentDragStrategy implements IDragStrategy {
|
|||||||
return (
|
return (
|
||||||
this.comment.isOwnMovable() &&
|
this.comment.isOwnMovable() &&
|
||||||
!this.comment.isDeadOrDying() &&
|
!this.comment.isDeadOrDying() &&
|
||||||
!this.workspace.options.readOnly
|
!this.workspace.isReadOnly()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
startDrag(): void {
|
startDrag(): void {
|
||||||
this.inGroup = !!eventUtils.getGroup();
|
|
||||||
if (!this.inGroup) {
|
|
||||||
eventUtils.setGroup(true);
|
|
||||||
}
|
|
||||||
this.fireDragStartEvent();
|
this.fireDragStartEvent();
|
||||||
this.startLoc = this.comment.getRelativeToSurfaceXY();
|
this.startLoc = this.comment.getRelativeToSurfaceXY();
|
||||||
this.workspace.setResizesEnabled(false);
|
this.workspace.setResizesEnabled(false);
|
||||||
@@ -61,9 +54,6 @@ export class CommentDragStrategy implements IDragStrategy {
|
|||||||
this.comment.snapToGrid();
|
this.comment.snapToGrid();
|
||||||
|
|
||||||
this.workspace.setResizesEnabled(true);
|
this.workspace.setResizesEnabled(true);
|
||||||
if (!this.inGroup) {
|
|
||||||
eventUtils.setGroup(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fire a UI event at the start of a comment drag. */
|
/** Fire a UI event at the start of a comment drag. */
|
||||||
|
|||||||
@@ -31,6 +31,9 @@ export class Dragger implements IDragger {
|
|||||||
|
|
||||||
/** Handles any drag startup. */
|
/** Handles any drag startup. */
|
||||||
onDragStart(e: PointerEvent) {
|
onDragStart(e: PointerEvent) {
|
||||||
|
if (!eventUtils.getGroup()) {
|
||||||
|
eventUtils.setGroup(true);
|
||||||
|
}
|
||||||
this.draggable.startDrag(e);
|
this.draggable.startDrag(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,13 +122,13 @@ export class Dragger implements IDragger {
|
|||||||
this.draggable.endDrag(e);
|
this.draggable.endDrag(e);
|
||||||
|
|
||||||
if (wouldDelete && isDeletable(root)) {
|
if (wouldDelete && isDeletable(root)) {
|
||||||
// We want to make sure the delete gets grouped with any possible
|
// We want to make sure the delete gets grouped with any possible move
|
||||||
// move event.
|
// event. In core Blockly this shouldn't happen, but due to a change
|
||||||
const newGroup = eventUtils.getGroup();
|
// in behavior older custom draggables might still clear the group.
|
||||||
eventUtils.setGroup(origGroup);
|
eventUtils.setGroup(origGroup);
|
||||||
root.dispose();
|
root.dispose();
|
||||||
eventUtils.setGroup(newGroup);
|
|
||||||
}
|
}
|
||||||
|
eventUtils.setGroup(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need to special case blocks for now so that we look at the root block
|
// We need to special case blocks for now so that we look at the root block
|
||||||
|
|||||||
@@ -136,12 +136,6 @@ export function createDom() {
|
|||||||
|
|
||||||
// Handle focusin/out events to add a visual indicator when
|
// Handle focusin/out events to add a visual indicator when
|
||||||
// a child is focused or blurred.
|
// a child is focused or blurred.
|
||||||
div.addEventListener('focusin', function () {
|
|
||||||
dom.addClass(div, 'blocklyFocused');
|
|
||||||
});
|
|
||||||
div.addEventListener('focusout', function () {
|
|
||||||
dom.removeClass(div, 'blocklyFocused');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -166,7 +160,7 @@ export function getOwner(): Field | null {
|
|||||||
*
|
*
|
||||||
* @returns Div to populate with content.
|
* @returns Div to populate with content.
|
||||||
*/
|
*/
|
||||||
export function getContentDiv(): Element {
|
export function getContentDiv(): HTMLDivElement {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,19 +697,12 @@ function positionInternal(
|
|||||||
|
|
||||||
// Update arrow CSS.
|
// Update arrow CSS.
|
||||||
if (metrics.arrowVisible) {
|
if (metrics.arrowVisible) {
|
||||||
|
const x = metrics.arrowX;
|
||||||
|
const y = metrics.arrowY;
|
||||||
|
const rotation = metrics.arrowAtTop ? 45 : 225;
|
||||||
arrow.style.display = '';
|
arrow.style.display = '';
|
||||||
arrow.style.transform =
|
arrow.style.transform = `translate(${x}px, ${y}px) rotate(${rotation}deg)`;
|
||||||
'translate(' +
|
arrow.setAttribute('class', 'blocklyDropDownArrow');
|
||||||
metrics.arrowX +
|
|
||||||
'px,' +
|
|
||||||
metrics.arrowY +
|
|
||||||
'px) rotate(45deg)';
|
|
||||||
arrow.setAttribute(
|
|
||||||
'class',
|
|
||||||
metrics.arrowAtTop
|
|
||||||
? 'blocklyDropDownArrow blocklyArrowTop'
|
|
||||||
: 'blocklyDropDownArrow blocklyArrowBottom',
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
arrow.style.display = 'none';
|
arrow.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,12 +40,15 @@ export {
|
|||||||
ToolboxItemSelect,
|
ToolboxItemSelect,
|
||||||
ToolboxItemSelectJson,
|
ToolboxItemSelectJson,
|
||||||
} from './events_toolbox_item_select.js';
|
} from './events_toolbox_item_select.js';
|
||||||
|
|
||||||
|
// Events.
|
||||||
export {TrashcanOpen, TrashcanOpenJson} from './events_trashcan_open.js';
|
export {TrashcanOpen, TrashcanOpenJson} from './events_trashcan_open.js';
|
||||||
export {UiBase} from './events_ui_base.js';
|
export {UiBase} from './events_ui_base.js';
|
||||||
export {VarBase, VarBaseJson} from './events_var_base.js';
|
export {VarBase, VarBaseJson} from './events_var_base.js';
|
||||||
export {VarCreate, VarCreateJson} from './events_var_create.js';
|
export {VarCreate, VarCreateJson} from './events_var_create.js';
|
||||||
export {VarDelete, VarDeleteJson} from './events_var_delete.js';
|
export {VarDelete, VarDeleteJson} from './events_var_delete.js';
|
||||||
export {VarRename, VarRenameJson} from './events_var_rename.js';
|
export {VarRename, VarRenameJson} from './events_var_rename.js';
|
||||||
|
export {VarTypeChange, VarTypeChangeJson} from './events_var_type_change.js';
|
||||||
export {ViewportChange, ViewportChangeJson} from './events_viewport.js';
|
export {ViewportChange, ViewportChangeJson} from './events_viewport.js';
|
||||||
export {FinishedLoading} from './workspace_events.js';
|
export {FinishedLoading} from './workspace_events.js';
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,10 @@
|
|||||||
*/
|
*/
|
||||||
// Former goog.module ID: Blockly.Events.VarBase
|
// Former goog.module ID: Blockly.Events.VarBase
|
||||||
|
|
||||||
import type {VariableModel} from '../variable_model.js';
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
} from '../interfaces/i_variable_model.js';
|
||||||
import type {Workspace} from '../workspace.js';
|
import type {Workspace} from '../workspace.js';
|
||||||
import {
|
import {
|
||||||
Abstract as AbstractEvent,
|
Abstract as AbstractEvent,
|
||||||
@@ -30,13 +33,13 @@ export class VarBase extends AbstractEvent {
|
|||||||
* @param opt_variable The variable this event corresponds to. Undefined for
|
* @param opt_variable The variable this event corresponds to. Undefined for
|
||||||
* a blank event.
|
* a blank event.
|
||||||
*/
|
*/
|
||||||
constructor(opt_variable?: VariableModel) {
|
constructor(opt_variable?: IVariableModel<IVariableState>) {
|
||||||
super();
|
super();
|
||||||
this.isBlank = typeof opt_variable === 'undefined';
|
this.isBlank = typeof opt_variable === 'undefined';
|
||||||
if (!opt_variable) return;
|
if (!opt_variable) return;
|
||||||
|
|
||||||
this.varId = opt_variable.getId();
|
this.varId = opt_variable.getId();
|
||||||
this.workspaceId = opt_variable.workspace.id;
|
this.workspaceId = opt_variable.getWorkspace().id;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -11,8 +11,12 @@
|
|||||||
*/
|
*/
|
||||||
// Former goog.module ID: Blockly.Events.VarCreate
|
// Former goog.module ID: Blockly.Events.VarCreate
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
} from '../interfaces/i_variable_model.js';
|
||||||
import * as registry from '../registry.js';
|
import * as registry from '../registry.js';
|
||||||
import type {VariableModel} from '../variable_model.js';
|
|
||||||
import type {Workspace} from '../workspace.js';
|
import type {Workspace} from '../workspace.js';
|
||||||
import {VarBase, VarBaseJson} from './events_var_base.js';
|
import {VarBase, VarBaseJson} from './events_var_base.js';
|
||||||
import {EventType} from './type.js';
|
import {EventType} from './type.js';
|
||||||
@@ -32,14 +36,14 @@ export class VarCreate extends VarBase {
|
|||||||
/**
|
/**
|
||||||
* @param opt_variable The created variable. Undefined for a blank event.
|
* @param opt_variable The created variable. Undefined for a blank event.
|
||||||
*/
|
*/
|
||||||
constructor(opt_variable?: VariableModel) {
|
constructor(opt_variable?: IVariableModel<IVariableState>) {
|
||||||
super(opt_variable);
|
super(opt_variable);
|
||||||
|
|
||||||
if (!opt_variable) {
|
if (!opt_variable) {
|
||||||
return; // Blank event to be populated by fromJson.
|
return; // Blank event to be populated by fromJson.
|
||||||
}
|
}
|
||||||
this.varType = opt_variable.type;
|
this.varType = opt_variable.getType();
|
||||||
this.varName = opt_variable.name;
|
this.varName = opt_variable.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,16 +6,18 @@
|
|||||||
|
|
||||||
// Former goog.module ID: Blockly.Events.VarDelete
|
// Former goog.module ID: Blockly.Events.VarDelete
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
} from '../interfaces/i_variable_model.js';
|
||||||
import * as registry from '../registry.js';
|
import * as registry from '../registry.js';
|
||||||
import type {VariableModel} from '../variable_model.js';
|
|
||||||
import type {Workspace} from '../workspace.js';
|
import type {Workspace} from '../workspace.js';
|
||||||
import {VarBase, VarBaseJson} from './events_var_base.js';
|
import {VarBase, VarBaseJson} from './events_var_base.js';
|
||||||
import {EventType} from './type.js';
|
import {EventType} from './type.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies listeners that a variable model has been deleted.
|
* Notifies listeners that a variable model has been deleted.
|
||||||
*
|
|
||||||
* @class
|
|
||||||
*/
|
*/
|
||||||
export class VarDelete extends VarBase {
|
export class VarDelete extends VarBase {
|
||||||
override type = EventType.VAR_DELETE;
|
override type = EventType.VAR_DELETE;
|
||||||
@@ -27,14 +29,14 @@ export class VarDelete extends VarBase {
|
|||||||
/**
|
/**
|
||||||
* @param opt_variable The deleted variable. Undefined for a blank event.
|
* @param opt_variable The deleted variable. Undefined for a blank event.
|
||||||
*/
|
*/
|
||||||
constructor(opt_variable?: VariableModel) {
|
constructor(opt_variable?: IVariableModel<IVariableState>) {
|
||||||
super(opt_variable);
|
super(opt_variable);
|
||||||
|
|
||||||
if (!opt_variable) {
|
if (!opt_variable) {
|
||||||
return; // Blank event to be populated by fromJson.
|
return; // Blank event to be populated by fromJson.
|
||||||
}
|
}
|
||||||
this.varType = opt_variable.type;
|
this.varType = opt_variable.getType();
|
||||||
this.varName = opt_variable.name;
|
this.varName = opt_variable.getName();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -6,16 +6,18 @@
|
|||||||
|
|
||||||
// Former goog.module ID: Blockly.Events.VarRename
|
// Former goog.module ID: Blockly.Events.VarRename
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
} from '../interfaces/i_variable_model.js';
|
||||||
import * as registry from '../registry.js';
|
import * as registry from '../registry.js';
|
||||||
import type {VariableModel} from '../variable_model.js';
|
|
||||||
import type {Workspace} from '../workspace.js';
|
import type {Workspace} from '../workspace.js';
|
||||||
import {VarBase, VarBaseJson} from './events_var_base.js';
|
import {VarBase, VarBaseJson} from './events_var_base.js';
|
||||||
import {EventType} from './type.js';
|
import {EventType} from './type.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notifies listeners that a variable model was renamed.
|
* Notifies listeners that a variable model was renamed.
|
||||||
*
|
|
||||||
* @class
|
|
||||||
*/
|
*/
|
||||||
export class VarRename extends VarBase {
|
export class VarRename extends VarBase {
|
||||||
override type = EventType.VAR_RENAME;
|
override type = EventType.VAR_RENAME;
|
||||||
@@ -30,13 +32,13 @@ export class VarRename extends VarBase {
|
|||||||
* @param opt_variable The renamed variable. Undefined for a blank event.
|
* @param opt_variable The renamed variable. Undefined for a blank event.
|
||||||
* @param newName The new name the variable will be changed to.
|
* @param newName The new name the variable will be changed to.
|
||||||
*/
|
*/
|
||||||
constructor(opt_variable?: VariableModel, newName?: string) {
|
constructor(opt_variable?: IVariableModel<IVariableState>, newName?: string) {
|
||||||
super(opt_variable);
|
super(opt_variable);
|
||||||
|
|
||||||
if (!opt_variable) {
|
if (!opt_variable) {
|
||||||
return; // Blank event to be populated by fromJson.
|
return; // Blank event to be populated by fromJson.
|
||||||
}
|
}
|
||||||
this.oldName = opt_variable.name;
|
this.oldName = opt_variable.getName();
|
||||||
this.newName = typeof newName === 'undefined' ? '' : newName;
|
this.newName = typeof newName === 'undefined' ? '' : newName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
122
core/events/events_var_type_change.ts
Normal file
122
core/events/events_var_type_change.ts
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class for a variable type change event.
|
||||||
|
*
|
||||||
|
* @class
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
} from '../interfaces/i_variable_model.js';
|
||||||
|
import * as registry from '../registry.js';
|
||||||
|
|
||||||
|
import type {Workspace} from '../workspace.js';
|
||||||
|
import {VarBase, VarBaseJson} from './events_var_base.js';
|
||||||
|
import {EventType} from './type.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notifies listeners that a variable's type has changed.
|
||||||
|
*/
|
||||||
|
export class VarTypeChange extends VarBase {
|
||||||
|
override type = EventType.VAR_TYPE_CHANGE;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param variable The variable whose type changed. Undefined for a blank event.
|
||||||
|
* @param oldType The old type of the variable. Undefined for a blank event.
|
||||||
|
* @param newType The new type of the variable. Undefined for a blank event.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
variable?: IVariableModel<IVariableState>,
|
||||||
|
public oldType?: string,
|
||||||
|
public newType?: string,
|
||||||
|
) {
|
||||||
|
super(variable);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encode the event as JSON.
|
||||||
|
*
|
||||||
|
* @returns JSON representation.
|
||||||
|
*/
|
||||||
|
override toJson(): VarTypeChangeJson {
|
||||||
|
const json = super.toJson() as VarTypeChangeJson;
|
||||||
|
if (!this.oldType || !this.newType) {
|
||||||
|
throw new Error(
|
||||||
|
"The variable's types are undefined. Either pass them to " +
|
||||||
|
'the constructor, or call fromJson',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
json['oldType'] = this.oldType;
|
||||||
|
json['newType'] = this.newType;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deserializes the JSON event.
|
||||||
|
*
|
||||||
|
* @param event The event to append new properties to. Should be a subclass
|
||||||
|
* of VarTypeChange, but we can't specify that due to the fact that
|
||||||
|
* parameters to static methods in subclasses must be supertypes of
|
||||||
|
* parameters to static methods in superclasses.
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
static fromJson(
|
||||||
|
json: VarTypeChangeJson,
|
||||||
|
workspace: Workspace,
|
||||||
|
event?: any,
|
||||||
|
): VarTypeChange {
|
||||||
|
const newEvent = super.fromJson(
|
||||||
|
json,
|
||||||
|
workspace,
|
||||||
|
event ?? new VarTypeChange(),
|
||||||
|
) as VarTypeChange;
|
||||||
|
newEvent.oldType = json['oldType'];
|
||||||
|
newEvent.newType = json['newType'];
|
||||||
|
return newEvent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run a variable type change event.
|
||||||
|
*
|
||||||
|
* @param forward True if run forward, false if run backward (undo).
|
||||||
|
*/
|
||||||
|
override run(forward: boolean) {
|
||||||
|
const workspace = this.getEventWorkspace_();
|
||||||
|
if (!this.varId) {
|
||||||
|
throw new Error(
|
||||||
|
'The var ID is undefined. Either pass a variable to ' +
|
||||||
|
'the constructor, or call fromJson',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this.oldType || !this.newType) {
|
||||||
|
throw new Error(
|
||||||
|
"The variable's types are undefined. Either pass them to " +
|
||||||
|
'the constructor, or call fromJson',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const variable = workspace.getVariableMap().getVariableById(this.varId);
|
||||||
|
if (!variable) return;
|
||||||
|
if (forward) {
|
||||||
|
workspace.getVariableMap().changeVariableType(variable, this.newType);
|
||||||
|
} else {
|
||||||
|
workspace.getVariableMap().changeVariableType(variable, this.oldType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VarTypeChangeJson extends VarBaseJson {
|
||||||
|
oldType: string;
|
||||||
|
newType: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
registry.Type.EVENT,
|
||||||
|
EventType.VAR_TYPE_CHANGE,
|
||||||
|
VarTypeChange,
|
||||||
|
);
|
||||||
@@ -28,6 +28,8 @@ export enum EventType {
|
|||||||
VAR_DELETE = 'var_delete',
|
VAR_DELETE = 'var_delete',
|
||||||
/** Type of event that renames a variable. */
|
/** Type of event that renames a variable. */
|
||||||
VAR_RENAME = 'var_rename',
|
VAR_RENAME = 'var_rename',
|
||||||
|
/** Type of event that changes the type of a variable. */
|
||||||
|
VAR_TYPE_CHANGE = 'var_type_change',
|
||||||
/**
|
/**
|
||||||
* Type of generic event that records a UI change.
|
* Type of generic event that records a UI change.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -437,7 +437,10 @@ function checkDropdownOptionsInTable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const options = dropdown.getOptions();
|
const options = dropdown.getOptions();
|
||||||
for (const [, key] of options) {
|
for (const option of options) {
|
||||||
|
if (option === FieldDropdown.SEPARATOR) continue;
|
||||||
|
|
||||||
|
const [, key] = option;
|
||||||
if (lookupTable[key] === undefined) {
|
if (lookupTable[key] === undefined) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`No tooltip mapping for value ${key} of field ` +
|
`No tooltip mapping for value ${key} of field ` +
|
||||||
|
|||||||
@@ -83,9 +83,6 @@ export abstract class Field<T = any>
|
|||||||
*/
|
*/
|
||||||
DEFAULT_VALUE: T | null = null;
|
DEFAULT_VALUE: T | null = null;
|
||||||
|
|
||||||
/** Non-breaking space. */
|
|
||||||
static readonly NBSP = '\u00A0';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A value used to signal when a field's constructor should *not* set the
|
* A value used to signal when a field's constructor should *not* set the
|
||||||
* field's value or run configure_, and should allow a subclass to do that
|
* field's value or run configure_, and should allow a subclass to do that
|
||||||
@@ -194,9 +191,6 @@ export abstract class Field<T = any>
|
|||||||
*/
|
*/
|
||||||
SERIALIZABLE = false;
|
SERIALIZABLE = false;
|
||||||
|
|
||||||
/** Mouse cursor style when over the hotspot that initiates the editor. */
|
|
||||||
CURSOR = '';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param value The initial value of the field.
|
* @param value The initial value of the field.
|
||||||
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
|
* Also accepts Field.SKIP_SETUP if you wish to skip setup (only used by
|
||||||
@@ -324,6 +318,9 @@ export abstract class Field<T = any>
|
|||||||
protected initView() {
|
protected initView() {
|
||||||
this.createBorderRect_();
|
this.createBorderRect_();
|
||||||
this.createTextElement_();
|
this.createTextElement_();
|
||||||
|
if (this.fieldGroup_) {
|
||||||
|
dom.addClass(this.fieldGroup_, 'blocklyField');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -374,7 +371,7 @@ export abstract class Field<T = any>
|
|||||||
this.textElement_ = dom.createSvgElement(
|
this.textElement_ = dom.createSvgElement(
|
||||||
Svg.TEXT,
|
Svg.TEXT,
|
||||||
{
|
{
|
||||||
'class': 'blocklyText',
|
'class': 'blocklyText blocklyFieldText',
|
||||||
},
|
},
|
||||||
this.fieldGroup_,
|
this.fieldGroup_,
|
||||||
);
|
);
|
||||||
@@ -406,7 +403,6 @@ export abstract class Field<T = any>
|
|||||||
* called by Blockly.Xml.
|
* called by Blockly.Xml.
|
||||||
*
|
*
|
||||||
* @param fieldElement The element containing info about the field's state.
|
* @param fieldElement The element containing info about the field's state.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
fromXml(fieldElement: Element) {
|
fromXml(fieldElement: Element) {
|
||||||
// Any because gremlins live here. No touchie!
|
// Any because gremlins live here. No touchie!
|
||||||
@@ -419,7 +415,6 @@ export abstract class Field<T = any>
|
|||||||
* @param fieldElement The element to populate with info about the field's
|
* @param fieldElement The element to populate with info about the field's
|
||||||
* state.
|
* state.
|
||||||
* @returns The element containing info about the field's state.
|
* @returns The element containing info about the field's state.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
toXml(fieldElement: Element): Element {
|
toXml(fieldElement: Element): Element {
|
||||||
// Any because gremlins live here. No touchie!
|
// Any because gremlins live here. No touchie!
|
||||||
@@ -438,7 +433,6 @@ export abstract class Field<T = any>
|
|||||||
* {@link https://developers.devsite.google.com/blockly/guides/create-custom-blocks/fields/customizing-fields/creating#full_serialization_and_backing_data | field serialization docs}
|
* {@link https://developers.devsite.google.com/blockly/guides/create-custom-blocks/fields/customizing-fields/creating#full_serialization_and_backing_data | field serialization docs}
|
||||||
* for more information.
|
* for more information.
|
||||||
* @returns JSON serializable state.
|
* @returns JSON serializable state.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
saveState(_doFullSerialization?: boolean): AnyDuringMigration {
|
saveState(_doFullSerialization?: boolean): AnyDuringMigration {
|
||||||
const legacyState = this.saveLegacyState(Field);
|
const legacyState = this.saveLegacyState(Field);
|
||||||
@@ -453,7 +447,6 @@ export abstract class Field<T = any>
|
|||||||
* called by the serialization system.
|
* called by the serialization system.
|
||||||
*
|
*
|
||||||
* @param state The state we want to apply to the field.
|
* @param state The state we want to apply to the field.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
loadState(state: AnyDuringMigration) {
|
loadState(state: AnyDuringMigration) {
|
||||||
if (this.loadLegacyState(Field, state)) {
|
if (this.loadLegacyState(Field, state)) {
|
||||||
@@ -516,8 +509,6 @@ export abstract class Field<T = any>
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispose of all DOM objects and events belonging to this editable field.
|
* Dispose of all DOM objects and events belonging to this editable field.
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
dispose() {
|
dispose() {
|
||||||
dropDownDiv.hideIfOwner(this);
|
dropDownDiv.hideIfOwner(this);
|
||||||
@@ -538,13 +529,11 @@ export abstract class Field<T = any>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.enabled_ && block.isEditable()) {
|
if (this.enabled_ && block.isEditable()) {
|
||||||
dom.addClass(group, 'blocklyEditableText');
|
dom.addClass(group, 'blocklyEditableField');
|
||||||
dom.removeClass(group, 'blocklyNonEditableText');
|
dom.removeClass(group, 'blocklyNonEditableField');
|
||||||
group.style.cursor = this.CURSOR;
|
|
||||||
} else {
|
} else {
|
||||||
dom.addClass(group, 'blocklyNonEditableText');
|
dom.addClass(group, 'blocklyNonEditableField');
|
||||||
dom.removeClass(group, 'blocklyEditableText');
|
dom.removeClass(group, 'blocklyEditableField');
|
||||||
group.style.cursor = '';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,12 +822,7 @@ export abstract class Field<T = any>
|
|||||||
|
|
||||||
let contentWidth = 0;
|
let contentWidth = 0;
|
||||||
if (this.textElement_) {
|
if (this.textElement_) {
|
||||||
contentWidth = dom.getFastTextWidth(
|
contentWidth = dom.getTextWidth(this.textElement_);
|
||||||
this.textElement_,
|
|
||||||
constants!.FIELD_TEXT_FONTSIZE,
|
|
||||||
constants!.FIELD_TEXT_FONTWEIGHT,
|
|
||||||
constants!.FIELD_TEXT_FONTFAMILY,
|
|
||||||
);
|
|
||||||
totalWidth += contentWidth;
|
totalWidth += contentWidth;
|
||||||
}
|
}
|
||||||
if (!this.isFullBlockField()) {
|
if (!this.isFullBlockField()) {
|
||||||
@@ -918,17 +902,6 @@ export abstract class Field<T = any>
|
|||||||
if (this.isDirty_) {
|
if (this.isDirty_) {
|
||||||
this.render_();
|
this.render_();
|
||||||
this.isDirty_ = false;
|
this.isDirty_ = false;
|
||||||
} else if (this.visible_ && this.size_.width === 0) {
|
|
||||||
// If the field is not visible the width will be 0 as well, one of the
|
|
||||||
// problems with the old system.
|
|
||||||
this.render_();
|
|
||||||
// Don't issue a warning if the field is actually zero width.
|
|
||||||
if (this.size_.width !== 0) {
|
|
||||||
console.warn(
|
|
||||||
'Deprecated use of setting size_.width to 0 to rerender a' +
|
|
||||||
' field. Set field.isDirty_ to true instead.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return this.size_;
|
return this.size_;
|
||||||
}
|
}
|
||||||
@@ -992,16 +965,10 @@ export abstract class Field<T = any>
|
|||||||
*/
|
*/
|
||||||
protected getDisplayText_(): string {
|
protected getDisplayText_(): string {
|
||||||
let text = this.getText();
|
let text = this.getText();
|
||||||
if (!text) {
|
|
||||||
// Prevent the field from disappearing if empty.
|
|
||||||
return Field.NBSP;
|
|
||||||
}
|
|
||||||
if (text.length > this.maxDisplayLength) {
|
if (text.length > this.maxDisplayLength) {
|
||||||
// Truncate displayed string and add an ellipsis ('...').
|
// Truncate displayed string and add an ellipsis ('...').
|
||||||
text = text.substring(0, this.maxDisplayLength - 2) + '…';
|
text = text.substring(0, this.maxDisplayLength - 2) + '…';
|
||||||
}
|
}
|
||||||
// Replace whitespace with non-breaking spaces so the text doesn't collapse.
|
|
||||||
text = text.replace(/\s/g, Field.NBSP);
|
|
||||||
if (this.sourceBlock_ && this.sourceBlock_.RTL) {
|
if (this.sourceBlock_ && this.sourceBlock_.RTL) {
|
||||||
// The SVG is LTR, force text to be RTL by adding an RLM.
|
// The SVG is LTR, force text to be RTL by adding an RLM.
|
||||||
text += '\u200F';
|
text += '\u200F';
|
||||||
@@ -1057,8 +1024,6 @@ export abstract class Field<T = any>
|
|||||||
* rerender this field and adjust for any sizing changes.
|
* rerender this field and adjust for any sizing changes.
|
||||||
* Other fields on the same block will not rerender, because their sizes have
|
* Other fields on the same block will not rerender, because their sizes have
|
||||||
* already been recorded.
|
* already been recorded.
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
forceRerender() {
|
forceRerender() {
|
||||||
this.isDirty_ = true;
|
this.isDirty_ = true;
|
||||||
@@ -1317,7 +1282,6 @@ export abstract class Field<T = any>
|
|||||||
* Subclasses may override this.
|
* Subclasses may override this.
|
||||||
*
|
*
|
||||||
* @returns True if this field has any variable references.
|
* @returns True if this field has any variable references.
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
referencesVariables(): boolean {
|
referencesVariables(): boolean {
|
||||||
return false;
|
return false;
|
||||||
@@ -1326,8 +1290,6 @@ export abstract class Field<T = any>
|
|||||||
/**
|
/**
|
||||||
* Refresh the variable name referenced by this field if this field references
|
* Refresh the variable name referenced by this field if this field references
|
||||||
* variables.
|
* variables.
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
refreshVariableName() {}
|
refreshVariableName() {}
|
||||||
// NOP
|
// NOP
|
||||||
|
|||||||
@@ -35,11 +35,6 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
|||||||
*/
|
*/
|
||||||
override SERIALIZABLE = true;
|
override SERIALIZABLE = true;
|
||||||
|
|
||||||
/**
|
|
||||||
* Mouse cursor style when over the hotspot that initiates editability.
|
|
||||||
*/
|
|
||||||
override CURSOR = 'default';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NOTE: The default value is set in `Field`, so maintain that value instead
|
* NOTE: The default value is set in `Field`, so maintain that value instead
|
||||||
* of overwriting it here or in the constructor.
|
* of overwriting it here or in the constructor.
|
||||||
@@ -114,7 +109,7 @@ export class FieldCheckbox extends Field<CheckboxBool> {
|
|||||||
super.initView();
|
super.initView();
|
||||||
|
|
||||||
const textElement = this.getTextElement();
|
const textElement = this.getTextElement();
|
||||||
dom.addClass(textElement, 'blocklyCheckbox');
|
dom.addClass(this.fieldGroup_!, 'blocklyCheckboxField');
|
||||||
textElement.style.display = this.value_ ? 'block' : 'none';
|
textElement.style.display = this.value_ ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from './field.js';
|
} from './field.js';
|
||||||
import * as fieldRegistry from './field_registry.js';
|
import * as fieldRegistry from './field_registry.js';
|
||||||
import {Menu} from './menu.js';
|
import {Menu} from './menu.js';
|
||||||
|
import {MenuSeparator} from './menu_separator.js';
|
||||||
import {MenuItem} from './menuitem.js';
|
import {MenuItem} from './menuitem.js';
|
||||||
import * as aria from './utils/aria.js';
|
import * as aria from './utils/aria.js';
|
||||||
import {Coordinate} from './utils/coordinate.js';
|
import {Coordinate} from './utils/coordinate.js';
|
||||||
@@ -36,14 +37,10 @@ import {Svg} from './utils/svg.js';
|
|||||||
* Class for an editable dropdown field.
|
* Class for an editable dropdown field.
|
||||||
*/
|
*/
|
||||||
export class FieldDropdown extends Field<string> {
|
export class FieldDropdown extends Field<string> {
|
||||||
/** Horizontal distance that a checkmark overhangs the dropdown. */
|
|
||||||
static CHECKMARK_OVERHANG = 25;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maximum height of the dropdown menu, as a percentage of the viewport
|
* Magic constant used to represent a separator in a list of dropdown items.
|
||||||
* height.
|
|
||||||
*/
|
*/
|
||||||
static MAX_MENU_HEIGHT_VH = 0.45;
|
static readonly SEPARATOR = 'separator';
|
||||||
|
|
||||||
static ARROW_CHAR = '▾';
|
static ARROW_CHAR = '▾';
|
||||||
|
|
||||||
@@ -70,9 +67,6 @@ export class FieldDropdown extends Field<string> {
|
|||||||
*/
|
*/
|
||||||
override SERIALIZABLE = true;
|
override SERIALIZABLE = true;
|
||||||
|
|
||||||
/** Mouse cursor style when over the hotspot that initiates the editor. */
|
|
||||||
override CURSOR = 'default';
|
|
||||||
|
|
||||||
protected menuGenerator_?: MenuGenerator;
|
protected menuGenerator_?: MenuGenerator;
|
||||||
|
|
||||||
/** A cache of the most recently generated options. */
|
/** A cache of the most recently generated options. */
|
||||||
@@ -213,6 +207,11 @@ export class FieldDropdown extends Field<string> {
|
|||||||
if (this.borderRect_) {
|
if (this.borderRect_) {
|
||||||
dom.addClass(this.borderRect_, 'blocklyDropdownRect');
|
dom.addClass(this.borderRect_, 'blocklyDropdownRect');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.fieldGroup_) {
|
||||||
|
dom.addClass(this.fieldGroup_, 'blocklyField');
|
||||||
|
dom.addClass(this.fieldGroup_, 'blocklyDropdownField');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -327,7 +326,13 @@ export class FieldDropdown extends Field<string> {
|
|||||||
const options = this.getOptions(false);
|
const options = this.getOptions(false);
|
||||||
this.selectedMenuItem = null;
|
this.selectedMenuItem = null;
|
||||||
for (let i = 0; i < options.length; i++) {
|
for (let i = 0; i < options.length; i++) {
|
||||||
const [label, value] = options[i];
|
const option = options[i];
|
||||||
|
if (option === FieldDropdown.SEPARATOR) {
|
||||||
|
menu.addChild(new MenuSeparator());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [label, value] = option;
|
||||||
const content = (() => {
|
const content = (() => {
|
||||||
if (typeof label === 'object') {
|
if (typeof label === 'object') {
|
||||||
// Convert ImageProperties to an HTMLImageElement.
|
// Convert ImageProperties to an HTMLImageElement.
|
||||||
@@ -541,12 +546,7 @@ export class FieldDropdown extends Field<string> {
|
|||||||
height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2,
|
height / 2 - this.getConstants()!.FIELD_DROPDOWN_SVG_ARROW_SIZE / 2,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
arrowWidth = dom.getFastTextWidth(
|
arrowWidth = dom.getTextWidth(this.arrow as SVGTSpanElement);
|
||||||
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_.width = imageWidth + arrowWidth + xPadding * 2;
|
||||||
this.size_.height = height;
|
this.size_.height = height;
|
||||||
@@ -579,12 +579,7 @@ export class FieldDropdown extends Field<string> {
|
|||||||
hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
|
hasBorder ? this.getConstants()!.FIELD_DROPDOWN_BORDER_RECT_HEIGHT : 0,
|
||||||
this.getConstants()!.FIELD_TEXT_HEIGHT,
|
this.getConstants()!.FIELD_TEXT_HEIGHT,
|
||||||
);
|
);
|
||||||
const textWidth = dom.getFastTextWidth(
|
const textWidth = dom.getTextWidth(this.getTextElement());
|
||||||
this.getTextElement(),
|
|
||||||
this.getConstants()!.FIELD_TEXT_FONTSIZE,
|
|
||||||
this.getConstants()!.FIELD_TEXT_FONTWEIGHT,
|
|
||||||
this.getConstants()!.FIELD_TEXT_FONTFAMILY,
|
|
||||||
);
|
|
||||||
const xPadding = hasBorder
|
const xPadding = hasBorder
|
||||||
? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
|
? this.getConstants()!.FIELD_BORDER_RECT_X_PADDING
|
||||||
: 0;
|
: 0;
|
||||||
@@ -681,7 +676,10 @@ export class FieldDropdown extends Field<string> {
|
|||||||
suffix?: string;
|
suffix?: string;
|
||||||
} {
|
} {
|
||||||
let hasImages = false;
|
let hasImages = false;
|
||||||
const trimmedOptions = options.map(([label, value]): MenuOption => {
|
const trimmedOptions = options.map((option): MenuOption => {
|
||||||
|
if (option === FieldDropdown.SEPARATOR) return option;
|
||||||
|
|
||||||
|
const [label, value] = option;
|
||||||
if (typeof label === 'string') {
|
if (typeof label === 'string') {
|
||||||
return [parsing.replaceMessageReferences(label), value];
|
return [parsing.replaceMessageReferences(label), value];
|
||||||
}
|
}
|
||||||
@@ -762,28 +760,28 @@ export class FieldDropdown extends Field<string> {
|
|||||||
}
|
}
|
||||||
let foundError = false;
|
let foundError = false;
|
||||||
for (let i = 0; i < options.length; i++) {
|
for (let i = 0; i < options.length; i++) {
|
||||||
const tuple = options[i];
|
const option = options[i];
|
||||||
if (!Array.isArray(tuple)) {
|
if (!Array.isArray(option) && option !== FieldDropdown.SEPARATOR) {
|
||||||
foundError = true;
|
foundError = true;
|
||||||
console.error(
|
console.error(
|
||||||
`Invalid option[${i}]: Each FieldDropdown option must be an array.
|
`Invalid option[${i}]: Each FieldDropdown option must be an array or
|
||||||
Found: ${tuple}`,
|
the string literal 'separator'. Found: ${option}`,
|
||||||
);
|
);
|
||||||
} else if (typeof tuple[1] !== 'string') {
|
} else if (typeof option[1] !== 'string') {
|
||||||
foundError = true;
|
foundError = true;
|
||||||
console.error(
|
console.error(
|
||||||
`Invalid option[${i}]: Each FieldDropdown option id must be a string.
|
`Invalid option[${i}]: Each FieldDropdown option id must be a string.
|
||||||
Found ${tuple[1]} in: ${tuple}`,
|
Found ${option[1]} in: ${option}`,
|
||||||
);
|
);
|
||||||
} else if (
|
} else if (
|
||||||
tuple[0] &&
|
option[0] &&
|
||||||
typeof tuple[0] !== 'string' &&
|
typeof option[0] !== 'string' &&
|
||||||
typeof tuple[0].src !== 'string'
|
typeof option[0].src !== 'string'
|
||||||
) {
|
) {
|
||||||
foundError = true;
|
foundError = true;
|
||||||
console.error(
|
console.error(
|
||||||
`Invalid option[${i}]: Each FieldDropdown option must have a string
|
`Invalid option[${i}]: Each FieldDropdown option must have a string
|
||||||
label or image description. Found ${tuple[0]} in: ${tuple}`,
|
label or image description. Found ${option[0]} in: ${option}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -804,11 +802,12 @@ export interface ImageProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An individual option in the dropdown menu. The first element is the human-
|
* An individual option in the dropdown menu. Can be either the string literal
|
||||||
* readable value (text or image), and the second element is the language-
|
* `separator` for a menu separator item, or an array for normal action menu
|
||||||
* neutral value.
|
* items. In the latter case, the first element is the human-readable value
|
||||||
|
* (text or image), and the second element is the language-neutral value.
|
||||||
*/
|
*/
|
||||||
export type MenuOption = [string | ImageProperties, string];
|
export type MenuOption = [string | ImageProperties, string] | 'separator';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A function that generates an array of menu options for FieldDropdown
|
* A function that generates an array of menu options for FieldDropdown
|
||||||
|
|||||||
@@ -151,6 +151,10 @@ export class FieldImage extends Field<string> {
|
|||||||
this.value_ as string,
|
this.value_ as string,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (this.fieldGroup_) {
|
||||||
|
dom.addClass(this.fieldGroup_, 'blocklyImageField');
|
||||||
|
}
|
||||||
|
|
||||||
if (this.clickHandler) {
|
if (this.clickHandler) {
|
||||||
this.imageElement.style.cursor = 'pointer';
|
this.imageElement.style.cursor = 'pointer';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -100,9 +100,6 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
|||||||
*/
|
*/
|
||||||
override SERIALIZABLE = true;
|
override SERIALIZABLE = true;
|
||||||
|
|
||||||
/** Mouse cursor style when over the hotspot that initiates the editor. */
|
|
||||||
override CURSOR = 'text';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param value The initial value of the field. Should cast to a string.
|
* @param value The initial value of the field. Should cast to a string.
|
||||||
* Defaults to an empty string if null or undefined. Also accepts
|
* Defaults to an empty string if null or undefined. Also accepts
|
||||||
@@ -149,6 +146,10 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
|||||||
if (this.isFullBlockField()) {
|
if (this.isFullBlockField()) {
|
||||||
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
|
this.clickTarget_ = (this.sourceBlock_ as BlockSvg).getSvgRoot();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.fieldGroup_) {
|
||||||
|
dom.addClass(this.fieldGroup_, 'blocklyInputField');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override isFullBlockField(): boolean {
|
protected override isFullBlockField(): boolean {
|
||||||
@@ -406,7 +407,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
|||||||
|
|
||||||
const clickTarget = this.getClickTarget_();
|
const clickTarget = this.getClickTarget_();
|
||||||
if (!clickTarget) throw new Error('A click target has not been set.');
|
if (!clickTarget) throw new Error('A click target has not been set.');
|
||||||
dom.addClass(clickTarget, 'editing');
|
dom.addClass(clickTarget, 'blocklyEditing');
|
||||||
|
|
||||||
const htmlInput = document.createElement('input');
|
const htmlInput = document.createElement('input');
|
||||||
htmlInput.className = 'blocklyHtmlInput';
|
htmlInput.className = 'blocklyHtmlInput';
|
||||||
@@ -416,7 +417,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
|||||||
'spellcheck',
|
'spellcheck',
|
||||||
this.spellcheck_ as AnyDuringMigration,
|
this.spellcheck_ as AnyDuringMigration,
|
||||||
);
|
);
|
||||||
const scale = this.workspace_!.getScale();
|
const scale = this.workspace_!.getAbsoluteScale();
|
||||||
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
|
const fontSize = this.getConstants()!.FIELD_TEXT_FONTSIZE * scale + 'pt';
|
||||||
div!.style.fontSize = fontSize;
|
div!.style.fontSize = fontSize;
|
||||||
htmlInput.style.fontSize = fontSize;
|
htmlInput.style.fontSize = fontSize;
|
||||||
@@ -501,7 +502,7 @@ export abstract class FieldInput<T extends InputTypes> extends Field<
|
|||||||
|
|
||||||
const clickTarget = this.getClickTarget_();
|
const clickTarget = this.getClickTarget_();
|
||||||
if (!clickTarget) throw new Error('A click target has not been set.');
|
if (!clickTarget) throw new Error('A click target has not been set.');
|
||||||
dom.removeClass(clickTarget, 'editing');
|
dom.removeClass(clickTarget, 'blocklyEditing');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -74,6 +74,9 @@ export class FieldLabel extends Field<string> {
|
|||||||
if (this.class) {
|
if (this.class) {
|
||||||
dom.addClass(this.getTextElement(), this.class);
|
dom.addClass(this.getTextElement(), this.class);
|
||||||
}
|
}
|
||||||
|
if (this.fieldGroup_) {
|
||||||
|
dom.addClass(this.fieldGroup_, 'blocklyLabelField');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
} from './field_input.js';
|
} from './field_input.js';
|
||||||
import * as fieldRegistry from './field_registry.js';
|
import * as fieldRegistry from './field_registry.js';
|
||||||
import * as aria from './utils/aria.js';
|
import * as aria from './utils/aria.js';
|
||||||
|
import * as dom from './utils/dom.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for an editable number field.
|
* Class for an editable number field.
|
||||||
@@ -307,6 +308,19 @@ export class FieldNumber extends FieldInput<number> {
|
|||||||
return htmlInput;
|
return htmlInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the field's DOM.
|
||||||
|
*
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
|
||||||
|
public override initView() {
|
||||||
|
super.initView();
|
||||||
|
if (this.fieldGroup_) {
|
||||||
|
dom.addClass(this.fieldGroup_, 'blocklyNumberField');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Construct a FieldNumber from a JSON arg object.
|
* Construct a FieldNumber from a JSON arg object.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
FieldInputValidator,
|
FieldInputValidator,
|
||||||
} from './field_input.js';
|
} from './field_input.js';
|
||||||
import * as fieldRegistry from './field_registry.js';
|
import * as fieldRegistry from './field_registry.js';
|
||||||
|
import * as dom from './utils/dom.js';
|
||||||
import * as parsing from './utils/parsing.js';
|
import * as parsing from './utils/parsing.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,6 +50,13 @@ export class FieldTextInput extends FieldInput<string> {
|
|||||||
super(value, validator, config);
|
super(value, validator, config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override initView() {
|
||||||
|
super.initView();
|
||||||
|
if (this.fieldGroup_) {
|
||||||
|
dom.addClass(this.fieldGroup_, 'blocklyTextInputField');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ensure that the input value casts to a valid string.
|
* Ensure that the input value casts to a valid string.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -23,14 +23,16 @@ import {
|
|||||||
MenuOption,
|
MenuOption,
|
||||||
} from './field_dropdown.js';
|
} from './field_dropdown.js';
|
||||||
import * as fieldRegistry from './field_registry.js';
|
import * as fieldRegistry from './field_registry.js';
|
||||||
|
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
|
||||||
import * as internalConstants from './internal_constants.js';
|
import * as internalConstants from './internal_constants.js';
|
||||||
import type {Menu} from './menu.js';
|
import type {Menu} from './menu.js';
|
||||||
import type {MenuItem} from './menuitem.js';
|
import type {MenuItem} from './menuitem.js';
|
||||||
import {Msg} from './msg.js';
|
import {Msg} from './msg.js';
|
||||||
|
import * as dom from './utils/dom.js';
|
||||||
import * as parsing from './utils/parsing.js';
|
import * as parsing from './utils/parsing.js';
|
||||||
import {Size} from './utils/size.js';
|
import {Size} from './utils/size.js';
|
||||||
import {VariableModel} from './variable_model.js';
|
|
||||||
import * as Variables from './variables.js';
|
import * as Variables from './variables.js';
|
||||||
|
import {WorkspaceSvg} from './workspace_svg.js';
|
||||||
import * as Xml from './xml.js';
|
import * as Xml from './xml.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +53,7 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
protected override size_: Size;
|
protected override size_: Size;
|
||||||
|
|
||||||
/** The variable model associated with this field. */
|
/** The variable model associated with this field. */
|
||||||
private variable: VariableModel | null = null;
|
private variable: IVariableModel<IVariableState> | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializable fields are saved by the serializer, non-serializable fields
|
* Serializable fields are saved by the serializer, non-serializable fields
|
||||||
@@ -148,6 +150,11 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
this.doValueUpdate_(variable.getId());
|
this.doValueUpdate_(variable.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override initView() {
|
||||||
|
super.initView();
|
||||||
|
dom.addClass(this.fieldGroup_!, 'blocklyVariableField');
|
||||||
|
}
|
||||||
|
|
||||||
override shouldAddBorderRect_() {
|
override shouldAddBorderRect_() {
|
||||||
const block = this.getSourceBlock();
|
const block = this.getSourceBlock();
|
||||||
if (!block) {
|
if (!block) {
|
||||||
@@ -190,12 +197,12 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// This should never happen :)
|
// This should never happen :)
|
||||||
if (variableType !== null && variableType !== variable.type) {
|
if (variableType !== null && variableType !== variable.getType()) {
|
||||||
throw Error(
|
throw Error(
|
||||||
"Serialized variable type with id '" +
|
"Serialized variable type with id '" +
|
||||||
variable.getId() +
|
variable.getId() +
|
||||||
"' had type " +
|
"' had type " +
|
||||||
variable.type +
|
variable.getType() +
|
||||||
', and ' +
|
', and ' +
|
||||||
'does not match variable field that references it: ' +
|
'does not match variable field that references it: ' +
|
||||||
Xml.domToText(fieldElement) +
|
Xml.domToText(fieldElement) +
|
||||||
@@ -218,9 +225,9 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
this.initModel();
|
this.initModel();
|
||||||
|
|
||||||
fieldElement.id = this.variable!.getId();
|
fieldElement.id = this.variable!.getId();
|
||||||
fieldElement.textContent = this.variable!.name;
|
fieldElement.textContent = this.variable!.getName();
|
||||||
if (this.variable!.type) {
|
if (this.variable!.getType()) {
|
||||||
fieldElement.setAttribute('variabletype', this.variable!.type);
|
fieldElement.setAttribute('variabletype', this.variable!.getType());
|
||||||
}
|
}
|
||||||
return fieldElement;
|
return fieldElement;
|
||||||
}
|
}
|
||||||
@@ -243,8 +250,8 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
this.initModel();
|
this.initModel();
|
||||||
const state = {'id': this.variable!.getId()};
|
const state = {'id': this.variable!.getId()};
|
||||||
if (doFullSerialization) {
|
if (doFullSerialization) {
|
||||||
(state as AnyDuringMigration)['name'] = this.variable!.name;
|
(state as AnyDuringMigration)['name'] = this.variable!.getName();
|
||||||
(state as AnyDuringMigration)['type'] = this.variable!.type;
|
(state as AnyDuringMigration)['type'] = this.variable!.getType();
|
||||||
}
|
}
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -301,7 +308,7 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
* is selected.
|
* is selected.
|
||||||
*/
|
*/
|
||||||
override getText(): string {
|
override getText(): string {
|
||||||
return this.variable ? this.variable.name : '';
|
return this.variable ? this.variable.getName() : '';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -312,10 +319,19 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
* @returns The selected variable, or null if none was selected.
|
* @returns The selected variable, or null if none was selected.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
getVariable(): VariableModel | null {
|
getVariable(): IVariableModel<IVariableState> | null {
|
||||||
return this.variable;
|
return this.variable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the type of this field's default variable.
|
||||||
|
*
|
||||||
|
* @returns The default type for this variable field.
|
||||||
|
*/
|
||||||
|
protected getDefaultType(): string {
|
||||||
|
return this.defaultType;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the validation function for this field, or null if not set.
|
* Gets the validation function for this field, or null if not set.
|
||||||
* Returns null if the variable is not set, because validators should not
|
* Returns null if the variable is not set, because validators should not
|
||||||
@@ -359,7 +375,7 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Type Checks.
|
// Type Checks.
|
||||||
const type = variable.type;
|
const type = variable.getType();
|
||||||
if (!this.typeIsAllowed(type)) {
|
if (!this.typeIsAllowed(type)) {
|
||||||
console.warn("Variable type doesn't match this field! Type was " + type);
|
console.warn("Variable type doesn't match this field! Type was " + type);
|
||||||
return null;
|
return null;
|
||||||
@@ -414,7 +430,7 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
if (variableTypes === null) {
|
if (variableTypes === null) {
|
||||||
// If variableTypes is null, return all variable types.
|
// If variableTypes is null, return all variable types.
|
||||||
if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) {
|
if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) {
|
||||||
return this.sourceBlock_.workspace.getVariableTypes();
|
return this.sourceBlock_.workspace.getVariableMap().getTypes();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
variableTypes = variableTypes || [''];
|
variableTypes = variableTypes || [''];
|
||||||
@@ -493,16 +509,17 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
const id = menuItem.getValue();
|
const id = menuItem.getValue();
|
||||||
// Handle special cases.
|
// Handle special cases.
|
||||||
if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) {
|
if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) {
|
||||||
if (id === internalConstants.RENAME_VARIABLE_ID) {
|
if (id === internalConstants.RENAME_VARIABLE_ID && this.variable) {
|
||||||
// Rename variable.
|
// Rename variable.
|
||||||
Variables.renameVariable(
|
Variables.renameVariable(this.sourceBlock_.workspace, this.variable);
|
||||||
this.sourceBlock_.workspace,
|
|
||||||
this.variable as VariableModel,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
} else if (id === internalConstants.DELETE_VARIABLE_ID) {
|
} else if (id === internalConstants.DELETE_VARIABLE_ID && this.variable) {
|
||||||
// Delete variable.
|
// Delete variable.
|
||||||
this.sourceBlock_.workspace.deleteVariableById(this.variable!.getId());
|
const workspace = this.variable.getWorkspace();
|
||||||
|
Variables.deleteVariable(workspace, this.variable, this.sourceBlock_);
|
||||||
|
if (workspace instanceof WorkspaceSvg) {
|
||||||
|
workspace.refreshToolboxSelection();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -554,24 +571,35 @@ export class FieldVariable extends FieldDropdown {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
const name = this.getText();
|
const name = this.getText();
|
||||||
let variableModelList: VariableModel[] = [];
|
let variableModelList: IVariableModel<IVariableState>[] = [];
|
||||||
if (this.sourceBlock_ && !this.sourceBlock_.isDeadOrDying()) {
|
const sourceBlock = this.getSourceBlock();
|
||||||
|
if (sourceBlock && !sourceBlock.isDeadOrDying()) {
|
||||||
|
const workspace = sourceBlock.workspace;
|
||||||
const variableTypes = this.getVariableTypes();
|
const variableTypes = this.getVariableTypes();
|
||||||
// Get a copy of the list, so that adding rename and new variable options
|
// Get a copy of the list, so that adding rename and new variable options
|
||||||
// doesn't modify the workspace's list.
|
// doesn't modify the workspace's list.
|
||||||
for (let i = 0; i < variableTypes.length; i++) {
|
for (let i = 0; i < variableTypes.length; i++) {
|
||||||
const variableType = variableTypes[i];
|
const variableType = variableTypes[i];
|
||||||
const variables =
|
const variables = workspace.getVariablesOfType(variableType);
|
||||||
this.sourceBlock_.workspace.getVariablesOfType(variableType);
|
|
||||||
variableModelList = variableModelList.concat(variables);
|
variableModelList = variableModelList.concat(variables);
|
||||||
|
if (workspace.isFlyout) {
|
||||||
|
variableModelList = variableModelList.concat(
|
||||||
|
workspace
|
||||||
|
.getPotentialVariableMap()
|
||||||
|
?.getVariablesOfType(variableType) ?? [],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
variableModelList.sort(VariableModel.compareByName);
|
variableModelList.sort(Variables.compareByName);
|
||||||
|
|
||||||
const options: [string, string][] = [];
|
const options: [string, string][] = [];
|
||||||
for (let i = 0; i < variableModelList.length; i++) {
|
for (let i = 0; i < variableModelList.length; i++) {
|
||||||
// Set the UUID as the internal representation of the variable.
|
// Set the UUID as the internal representation of the variable.
|
||||||
options[i] = [variableModelList[i].name, variableModelList[i].getId()];
|
options[i] = [
|
||||||
|
variableModelList[i].getName(),
|
||||||
|
variableModelList[i].getId(),
|
||||||
|
];
|
||||||
}
|
}
|
||||||
options.push([
|
options.push([
|
||||||
Msg['RENAME_VARIABLE'],
|
Msg['RENAME_VARIABLE'],
|
||||||
|
|||||||
@@ -11,46 +11,32 @@
|
|||||||
*/
|
*/
|
||||||
// Former goog.module ID: Blockly.Flyout
|
// Former goog.module ID: Blockly.Flyout
|
||||||
|
|
||||||
import type {Block} from './block.js';
|
|
||||||
import {BlockSvg} from './block_svg.js';
|
import {BlockSvg} from './block_svg.js';
|
||||||
import * as browserEvents from './browser_events.js';
|
import * as browserEvents from './browser_events.js';
|
||||||
import * as common from './common.js';
|
|
||||||
import {ComponentManager} from './component_manager.js';
|
import {ComponentManager} from './component_manager.js';
|
||||||
import {MANUALLY_DISABLED} from './constants.js';
|
|
||||||
import {DeleteArea} from './delete_area.js';
|
import {DeleteArea} from './delete_area.js';
|
||||||
import type {Abstract as AbstractEvent} from './events/events_abstract.js';
|
import type {Abstract as AbstractEvent} from './events/events_abstract.js';
|
||||||
import {EventType} from './events/type.js';
|
import {EventType} from './events/type.js';
|
||||||
import * as eventUtils from './events/utils.js';
|
import * as eventUtils from './events/utils.js';
|
||||||
import {FlyoutButton} from './flyout_button.js';
|
import {FlyoutItem} from './flyout_item.js';
|
||||||
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
|
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
|
||||||
|
import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js';
|
||||||
import {IAutoHideable} from './interfaces/i_autohideable.js';
|
import {IAutoHideable} from './interfaces/i_autohideable.js';
|
||||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||||
|
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||||
import type {Options} from './options.js';
|
import type {Options} from './options.js';
|
||||||
|
import * as registry from './registry.js';
|
||||||
import * as renderManagement from './render_management.js';
|
import * as renderManagement from './render_management.js';
|
||||||
import {ScrollbarPair} from './scrollbar_pair.js';
|
import {ScrollbarPair} from './scrollbar_pair.js';
|
||||||
|
import {SEPARATOR_TYPE} from './separator_flyout_inflater.js';
|
||||||
import * as blocks from './serialization/blocks.js';
|
import * as blocks from './serialization/blocks.js';
|
||||||
import * as Tooltip from './tooltip.js';
|
|
||||||
import {Coordinate} from './utils/coordinate.js';
|
import {Coordinate} from './utils/coordinate.js';
|
||||||
import * as dom from './utils/dom.js';
|
import * as dom from './utils/dom.js';
|
||||||
import * as idGenerator from './utils/idgenerator.js';
|
import * as idGenerator from './utils/idgenerator.js';
|
||||||
import {Svg} from './utils/svg.js';
|
import {Svg} from './utils/svg.js';
|
||||||
import * as toolbox from './utils/toolbox.js';
|
import * as toolbox from './utils/toolbox.js';
|
||||||
import * as utilsXml from './utils/xml.js';
|
|
||||||
import * as Variables from './variables.js';
|
import * as Variables from './variables.js';
|
||||||
import {WorkspaceSvg} from './workspace_svg.js';
|
import {WorkspaceSvg} from './workspace_svg.js';
|
||||||
import * as Xml from './xml.js';
|
|
||||||
|
|
||||||
enum FlyoutItemType {
|
|
||||||
BLOCK = 'block',
|
|
||||||
BUTTON = 'button',
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The language-neutral ID for when the reason why a block is disabled is
|
|
||||||
* because the workspace is at block capacity.
|
|
||||||
*/
|
|
||||||
const WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON =
|
|
||||||
'WORKSPACE_AT_BLOCK_CAPACITY';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class for a flyout.
|
* Class for a flyout.
|
||||||
@@ -85,12 +71,11 @@ export abstract class Flyout
|
|||||||
protected abstract setMetrics_(xyRatio: {x?: number; y?: number}): void;
|
protected abstract setMetrics_(xyRatio: {x?: number; y?: number}): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lay out the blocks in the flyout.
|
* Lay out the elements in the flyout.
|
||||||
*
|
*
|
||||||
* @param contents The blocks and buttons to lay out.
|
* @param contents The flyout elements to lay out.
|
||||||
* @param gaps The visible gaps between blocks.
|
|
||||||
*/
|
*/
|
||||||
protected abstract layout_(contents: FlyoutItem[], gaps: number[]): void;
|
protected abstract layout_(contents: FlyoutItem[]): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Scroll the flyout.
|
* Scroll the flyout.
|
||||||
@@ -100,8 +85,8 @@ export abstract class Flyout
|
|||||||
protected abstract wheel_(e: WheelEvent): void;
|
protected abstract wheel_(e: WheelEvent): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute height of flyout. Position mat under each block.
|
* Compute bounds of flyout.
|
||||||
* For RTL: Lay out the blocks right-aligned.
|
* For RTL: Lay out the elements right-aligned.
|
||||||
*/
|
*/
|
||||||
protected abstract reflowInternal_(): void;
|
protected abstract reflowInternal_(): void;
|
||||||
|
|
||||||
@@ -124,11 +109,6 @@ export abstract class Flyout
|
|||||||
*/
|
*/
|
||||||
abstract scrollToStart(): void;
|
abstract scrollToStart(): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* The type of a flyout content item.
|
|
||||||
*/
|
|
||||||
static FlyoutItemType = FlyoutItemType;
|
|
||||||
|
|
||||||
protected workspace_: WorkspaceSvg;
|
protected workspace_: WorkspaceSvg;
|
||||||
RTL: boolean;
|
RTL: boolean;
|
||||||
/**
|
/**
|
||||||
@@ -148,43 +128,15 @@ export abstract class Flyout
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that will be registered as a change listener on the workspace
|
* Function that will be registered as a change listener on the workspace
|
||||||
* to reflow when blocks in the flyout workspace change.
|
* to reflow when elements in the flyout workspace change.
|
||||||
*/
|
*/
|
||||||
private reflowWrapper: ((e: AbstractEvent) => void) | null = null;
|
private reflowWrapper: ((e: AbstractEvent) => void) | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Function that disables blocks in the flyout based on max block counts
|
* List of flyout elements.
|
||||||
* allowed in the target workspace. Registered as a change listener on the
|
|
||||||
* target workspace.
|
|
||||||
*/
|
|
||||||
private filterWrapper: ((e: AbstractEvent) => void) | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of background mats that lurk behind each block to catch clicks
|
|
||||||
* landing in the blocks' lakes and bays.
|
|
||||||
*/
|
|
||||||
private mats: SVGElement[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of visible buttons.
|
|
||||||
*/
|
|
||||||
protected buttons_: FlyoutButton[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of visible buttons and blocks.
|
|
||||||
*/
|
*/
|
||||||
protected contents: FlyoutItem[] = [];
|
protected contents: FlyoutItem[] = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* List of event listeners.
|
|
||||||
*/
|
|
||||||
private listeners: browserEvents.Data[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* List of blocks that should always be disabled.
|
|
||||||
*/
|
|
||||||
private permanentlyDisabled: Block[] = [];
|
|
||||||
|
|
||||||
protected readonly tabWidth_: number;
|
protected readonly tabWidth_: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -194,11 +146,6 @@ export abstract class Flyout
|
|||||||
*/
|
*/
|
||||||
targetWorkspace!: WorkspaceSvg;
|
targetWorkspace!: WorkspaceSvg;
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of blocks that can be reused.
|
|
||||||
*/
|
|
||||||
private recycledBlocks: BlockSvg[] = [];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Does the flyout automatically close when a block is created?
|
* Does the flyout automatically close when a block is created?
|
||||||
*/
|
*/
|
||||||
@@ -213,7 +160,6 @@ export abstract class Flyout
|
|||||||
* Whether the workspace containing this flyout is visible.
|
* Whether the workspace containing this flyout is visible.
|
||||||
*/
|
*/
|
||||||
private containerVisible = true;
|
private containerVisible = true;
|
||||||
protected rectMap_: WeakMap<BlockSvg, SVGElement>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Corner radius of the flyout background.
|
* Corner radius of the flyout background.
|
||||||
@@ -271,6 +217,13 @@ export abstract class Flyout
|
|||||||
* The root SVG group for the button or label.
|
* The root SVG group for the button or label.
|
||||||
*/
|
*/
|
||||||
protected svgGroup_: SVGGElement | null = null;
|
protected svgGroup_: SVGGElement | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map from flyout content type to the corresponding inflater class
|
||||||
|
* responsible for creating concrete instances of the content type.
|
||||||
|
*/
|
||||||
|
protected inflaters = new Map<string, IFlyoutInflater>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param workspaceOptions Dictionary of options for the
|
* @param workspaceOptions Dictionary of options for the
|
||||||
* workspace.
|
* workspace.
|
||||||
@@ -310,15 +263,7 @@ export abstract class Flyout
|
|||||||
this.tabWidth_ = this.workspace_.getRenderer().getConstants().TAB_WIDTH;
|
this.tabWidth_ = this.workspace_.getRenderer().getConstants().TAB_WIDTH;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A map from blocks to the rects which are beneath them to act as input
|
* Margin around the edges of the elements in the flyout.
|
||||||
* targets.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
this.rectMap_ = new WeakMap();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Margin around the edges of the blocks in the flyout.
|
|
||||||
*/
|
*/
|
||||||
this.MARGIN = this.CORNER_RADIUS;
|
this.MARGIN = this.CORNER_RADIUS;
|
||||||
|
|
||||||
@@ -403,8 +348,6 @@ export abstract class Flyout
|
|||||||
this.wheel_,
|
this.wheel_,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
this.filterWrapper = this.filterForCapacity.bind(this);
|
|
||||||
this.targetWorkspace.addChangeListener(this.filterWrapper);
|
|
||||||
|
|
||||||
// Dragging the flyout up and down.
|
// Dragging the flyout up and down.
|
||||||
this.boundEvents.push(
|
this.boundEvents.push(
|
||||||
@@ -448,9 +391,6 @@ export abstract class Flyout
|
|||||||
browserEvents.unbind(event);
|
browserEvents.unbind(event);
|
||||||
}
|
}
|
||||||
this.boundEvents.length = 0;
|
this.boundEvents.length = 0;
|
||||||
if (this.filterWrapper) {
|
|
||||||
this.targetWorkspace.removeChangeListener(this.filterWrapper);
|
|
||||||
}
|
|
||||||
if (this.workspace_) {
|
if (this.workspace_) {
|
||||||
this.workspace_.getThemeManager().unsubscribe(this.svgBackground_!);
|
this.workspace_.getThemeManager().unsubscribe(this.svgBackground_!);
|
||||||
this.workspace_.dispose();
|
this.workspace_.dispose();
|
||||||
@@ -570,16 +510,16 @@ export abstract class Flyout
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the list of buttons and blocks of the current flyout.
|
* Get the list of elements of the current flyout.
|
||||||
*
|
*
|
||||||
* @returns The array of flyout buttons and blocks.
|
* @returns The array of flyout elements.
|
||||||
*/
|
*/
|
||||||
getContents(): FlyoutItem[] {
|
getContents(): FlyoutItem[] {
|
||||||
return this.contents;
|
return this.contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Store the list of buttons and blocks on the flyout.
|
* Store the list of elements on the flyout.
|
||||||
*
|
*
|
||||||
* @param contents - The array of items for the flyout.
|
* @param contents - The array of items for the flyout.
|
||||||
*/
|
*/
|
||||||
@@ -654,16 +594,11 @@ export abstract class Flyout
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.setVisible(false);
|
this.setVisible(false);
|
||||||
// Delete all the event listeners.
|
|
||||||
for (const listen of this.listeners) {
|
|
||||||
browserEvents.unbind(listen);
|
|
||||||
}
|
|
||||||
this.listeners.length = 0;
|
|
||||||
if (this.reflowWrapper) {
|
if (this.reflowWrapper) {
|
||||||
this.workspace_.removeChangeListener(this.reflowWrapper);
|
this.workspace_.removeChangeListener(this.reflowWrapper);
|
||||||
this.reflowWrapper = null;
|
this.reflowWrapper = null;
|
||||||
}
|
}
|
||||||
// Do NOT delete the blocks here. Wait until Flyout.show.
|
// Do NOT delete the flyout contents here. Wait until Flyout.show.
|
||||||
// https://neil.fraser.name/news/2014/08/09/
|
// https://neil.fraser.name/news/2014/08/09/
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -691,26 +626,30 @@ export abstract class Flyout
|
|||||||
|
|
||||||
renderManagement.triggerQueuedRenders(this.workspace_);
|
renderManagement.triggerQueuedRenders(this.workspace_);
|
||||||
|
|
||||||
this.setContents(flyoutInfo.contents);
|
this.setContents(flyoutInfo);
|
||||||
|
|
||||||
this.layout_(flyoutInfo.contents, flyoutInfo.gaps);
|
this.layout_(flyoutInfo);
|
||||||
|
|
||||||
if (this.horizontalLayout) {
|
if (this.horizontalLayout) {
|
||||||
this.height_ = 0;
|
this.height_ = 0;
|
||||||
} else {
|
} else {
|
||||||
this.width_ = 0;
|
this.width_ = 0;
|
||||||
}
|
}
|
||||||
this.workspace_.setResizesEnabled(true);
|
|
||||||
this.reflow();
|
this.reflow();
|
||||||
|
this.workspace_.setResizesEnabled(true);
|
||||||
|
|
||||||
this.filterForCapacity();
|
// Listen for block change events, and reflow the flyout in response. This
|
||||||
|
// accommodates e.g. resizing a non-autoclosing flyout in response to the
|
||||||
// Correctly position the flyout's scrollbar when it opens.
|
// user typing long strings into fields on the blocks in the flyout.
|
||||||
this.position();
|
this.reflowWrapper = (event) => {
|
||||||
|
if (
|
||||||
this.reflowWrapper = this.reflow.bind(this);
|
event.type === EventType.BLOCK_CHANGE ||
|
||||||
|
event.type === EventType.BLOCK_FIELD_INTERMEDIATE_CHANGE
|
||||||
|
) {
|
||||||
|
this.reflow();
|
||||||
|
}
|
||||||
|
};
|
||||||
this.workspace_.addChangeListener(this.reflowWrapper);
|
this.workspace_.addChangeListener(this.reflowWrapper);
|
||||||
this.emptyRecycledBlocks();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -719,15 +658,12 @@ export abstract class Flyout
|
|||||||
*
|
*
|
||||||
* @param parsedContent The array
|
* @param parsedContent The array
|
||||||
* of objects to show in the flyout.
|
* of objects to show in the flyout.
|
||||||
* @returns The list of contents and gaps needed to lay out the flyout.
|
* @returns The list of contents needed to lay out the flyout.
|
||||||
*/
|
*/
|
||||||
private createFlyoutInfo(parsedContent: toolbox.FlyoutItemInfoArray): {
|
private createFlyoutInfo(
|
||||||
contents: FlyoutItem[];
|
parsedContent: toolbox.FlyoutItemInfoArray,
|
||||||
gaps: number[];
|
): FlyoutItem[] {
|
||||||
} {
|
|
||||||
const contents: FlyoutItem[] = [];
|
const contents: FlyoutItem[] = [];
|
||||||
const gaps: number[] = [];
|
|
||||||
this.permanentlyDisabled.length = 0;
|
|
||||||
const defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y;
|
const defaultGap = this.horizontalLayout ? this.GAP_X : this.GAP_Y;
|
||||||
for (const info of parsedContent) {
|
for (const info of parsedContent) {
|
||||||
if ('custom' in info) {
|
if ('custom' in info) {
|
||||||
@@ -736,44 +672,60 @@ export abstract class Flyout
|
|||||||
const flyoutDef = this.getDynamicCategoryContents(categoryName);
|
const flyoutDef = this.getDynamicCategoryContents(categoryName);
|
||||||
const parsedDynamicContent =
|
const parsedDynamicContent =
|
||||||
toolbox.convertFlyoutDefToJsonArray(flyoutDef);
|
toolbox.convertFlyoutDefToJsonArray(flyoutDef);
|
||||||
const {contents: dynamicContents, gaps: dynamicGaps} =
|
contents.push(...this.createFlyoutInfo(parsedDynamicContent));
|
||||||
this.createFlyoutInfo(parsedDynamicContent);
|
|
||||||
contents.push(...dynamicContents);
|
|
||||||
gaps.push(...dynamicGaps);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (info['kind'].toUpperCase()) {
|
const type = info['kind'].toLowerCase();
|
||||||
case 'BLOCK': {
|
const inflater = this.getInflaterForType(type);
|
||||||
const blockInfo = info as toolbox.BlockInfo;
|
if (inflater) {
|
||||||
const block = this.createFlyoutBlock(blockInfo);
|
contents.push(inflater.load(info, this));
|
||||||
contents.push({type: FlyoutItemType.BLOCK, block: block});
|
const gap = inflater.gapForItem(info, defaultGap);
|
||||||
this.addBlockGap(blockInfo, gaps, defaultGap);
|
if (gap) {
|
||||||
break;
|
contents.push(
|
||||||
}
|
new FlyoutItem(
|
||||||
case 'SEP': {
|
new FlyoutSeparator(
|
||||||
const sepInfo = info as toolbox.SeparatorInfo;
|
gap,
|
||||||
this.addSeparatorGap(sepInfo, gaps, defaultGap);
|
this.horizontalLayout ? SeparatorAxis.X : SeparatorAxis.Y,
|
||||||
break;
|
),
|
||||||
}
|
SEPARATOR_TYPE,
|
||||||
case 'LABEL': {
|
false,
|
||||||
const labelInfo = info as toolbox.LabelInfo;
|
),
|
||||||
// A label is a button with different styling.
|
);
|
||||||
const label = this.createButton(labelInfo, /** isLabel */ true);
|
|
||||||
contents.push({type: FlyoutItemType.BUTTON, button: label});
|
|
||||||
gaps.push(defaultGap);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'BUTTON': {
|
|
||||||
const buttonInfo = info as toolbox.ButtonInfo;
|
|
||||||
const button = this.createButton(buttonInfo, /** isLabel */ false);
|
|
||||||
contents.push({type: FlyoutItemType.BUTTON, button: button});
|
|
||||||
gaps.push(defaultGap);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {contents: contents, gaps: gaps};
|
return this.normalizeSeparators(contents);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates and returns the provided list of flyout contents to flatten
|
||||||
|
* separators as needed.
|
||||||
|
*
|
||||||
|
* When multiple separators occur one after another, the value of the last one
|
||||||
|
* takes precedence and the earlier separators in the group are removed.
|
||||||
|
*
|
||||||
|
* @param contents The list of flyout contents to flatten separators in.
|
||||||
|
* @returns An updated list of flyout contents with only one separator between
|
||||||
|
* each non-separator item.
|
||||||
|
*/
|
||||||
|
protected normalizeSeparators(contents: FlyoutItem[]): FlyoutItem[] {
|
||||||
|
for (let i = contents.length - 1; i > 0; i--) {
|
||||||
|
const elementType = contents[i].getType().toLowerCase();
|
||||||
|
const previousElementType = contents[i - 1].getType().toLowerCase();
|
||||||
|
if (
|
||||||
|
elementType === SEPARATOR_TYPE &&
|
||||||
|
previousElementType === SEPARATOR_TYPE
|
||||||
|
) {
|
||||||
|
// Remove previousElement from the array, shifting the current element
|
||||||
|
// forward as a result. This preserves the behavior where explicit
|
||||||
|
// separator elements override the value of prior implicit (or explicit)
|
||||||
|
// separator elements.
|
||||||
|
contents.splice(i - 1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return contents;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -800,287 +752,18 @@ export abstract class Flyout
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a flyout button or a flyout label.
|
* Delete elements from a previous showing of the flyout.
|
||||||
*
|
|
||||||
* @param btnInfo The object holding information about a button or a label.
|
|
||||||
* @param isLabel True if the button is a label, false otherwise.
|
|
||||||
* @returns The object used to display the button in the
|
|
||||||
* flyout.
|
|
||||||
*/
|
|
||||||
private createButton(
|
|
||||||
btnInfo: toolbox.ButtonOrLabelInfo,
|
|
||||||
isLabel: boolean,
|
|
||||||
): FlyoutButton {
|
|
||||||
const curButton = new FlyoutButton(
|
|
||||||
this.workspace_,
|
|
||||||
this.targetWorkspace as WorkspaceSvg,
|
|
||||||
btnInfo,
|
|
||||||
isLabel,
|
|
||||||
);
|
|
||||||
return curButton;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a block from the xml and permanently disable any blocks that were
|
|
||||||
* defined as disabled.
|
|
||||||
*
|
|
||||||
* @param blockInfo The info of the block.
|
|
||||||
* @returns The block created from the blockInfo.
|
|
||||||
*/
|
|
||||||
private createFlyoutBlock(blockInfo: toolbox.BlockInfo): BlockSvg {
|
|
||||||
let block;
|
|
||||||
if (blockInfo['blockxml']) {
|
|
||||||
const xml = (
|
|
||||||
typeof blockInfo['blockxml'] === 'string'
|
|
||||||
? utilsXml.textToDom(blockInfo['blockxml'])
|
|
||||||
: blockInfo['blockxml']
|
|
||||||
) as Element;
|
|
||||||
block = this.getRecycledBlock(xml.getAttribute('type')!);
|
|
||||||
if (!block) {
|
|
||||||
block = Xml.domToBlockInternal(xml, this.workspace_);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
block = this.getRecycledBlock(blockInfo['type']!);
|
|
||||||
if (!block) {
|
|
||||||
if (blockInfo['enabled'] === undefined) {
|
|
||||||
blockInfo['enabled'] =
|
|
||||||
blockInfo['disabled'] !== 'true' && blockInfo['disabled'] !== true;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
blockInfo['disabledReasons'] === undefined &&
|
|
||||||
blockInfo['enabled'] === false
|
|
||||||
) {
|
|
||||||
blockInfo['disabledReasons'] = [MANUALLY_DISABLED];
|
|
||||||
}
|
|
||||||
block = blocks.appendInternal(
|
|
||||||
blockInfo as blocks.State,
|
|
||||||
this.workspace_,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!block.isEnabled()) {
|
|
||||||
// Record blocks that were initially disabled.
|
|
||||||
// Do not enable these blocks as a result of capacity filtering.
|
|
||||||
this.permanentlyDisabled.push(block);
|
|
||||||
}
|
|
||||||
return block as BlockSvg;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a block from the array of recycled blocks with the given type, or
|
|
||||||
* undefined if one cannot be found.
|
|
||||||
*
|
|
||||||
* @param blockType The type of the block to try to recycle.
|
|
||||||
* @returns The recycled block, or undefined if
|
|
||||||
* one could not be recycled.
|
|
||||||
*/
|
|
||||||
private getRecycledBlock(blockType: string): BlockSvg | undefined {
|
|
||||||
let index = -1;
|
|
||||||
for (let i = 0; i < this.recycledBlocks.length; i++) {
|
|
||||||
if (this.recycledBlocks[i].type === blockType) {
|
|
||||||
index = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return index === -1 ? undefined : this.recycledBlocks.splice(index, 1)[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a gap in the flyout based on block info.
|
|
||||||
*
|
|
||||||
* @param blockInfo Information about a block.
|
|
||||||
* @param gaps The list of gaps between items in the flyout.
|
|
||||||
* @param defaultGap The default gap between one element and the
|
|
||||||
* next.
|
|
||||||
*/
|
|
||||||
private addBlockGap(
|
|
||||||
blockInfo: toolbox.BlockInfo,
|
|
||||||
gaps: number[],
|
|
||||||
defaultGap: number,
|
|
||||||
) {
|
|
||||||
let gap;
|
|
||||||
if (blockInfo['gap']) {
|
|
||||||
gap = parseInt(String(blockInfo['gap']));
|
|
||||||
} else if (blockInfo['blockxml']) {
|
|
||||||
const xml = (
|
|
||||||
typeof blockInfo['blockxml'] === 'string'
|
|
||||||
? utilsXml.textToDom(blockInfo['blockxml'])
|
|
||||||
: blockInfo['blockxml']
|
|
||||||
) as Element;
|
|
||||||
gap = parseInt(xml.getAttribute('gap')!);
|
|
||||||
}
|
|
||||||
gaps.push(!gap || isNaN(gap) ? defaultGap : gap);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add the necessary gap in the flyout for a separator.
|
|
||||||
*
|
|
||||||
* @param sepInfo The object holding
|
|
||||||
* information about a separator.
|
|
||||||
* @param gaps The list gaps between items in the flyout.
|
|
||||||
* @param defaultGap The default gap between the button and next
|
|
||||||
* element.
|
|
||||||
*/
|
|
||||||
private addSeparatorGap(
|
|
||||||
sepInfo: toolbox.SeparatorInfo,
|
|
||||||
gaps: number[],
|
|
||||||
defaultGap: number,
|
|
||||||
) {
|
|
||||||
// Change the gap between two toolbox elements.
|
|
||||||
// <sep gap="36"></sep>
|
|
||||||
// The default gap is 24, can be set larger or smaller.
|
|
||||||
// This overwrites the gap attribute on the previous element.
|
|
||||||
const newGap = parseInt(String(sepInfo['gap']));
|
|
||||||
// Ignore gaps before the first block.
|
|
||||||
if (!isNaN(newGap) && gaps.length > 0) {
|
|
||||||
gaps[gaps.length - 1] = newGap;
|
|
||||||
} else {
|
|
||||||
gaps.push(defaultGap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete blocks, mats and buttons from a previous showing of the flyout.
|
|
||||||
*/
|
*/
|
||||||
private clearOldBlocks() {
|
private clearOldBlocks() {
|
||||||
// Delete any blocks from a previous showing.
|
this.getContents().forEach((item) => {
|
||||||
const oldBlocks = this.workspace_.getTopBlocks(false);
|
const inflater = this.getInflaterForType(item.getType());
|
||||||
for (let i = 0, block; (block = oldBlocks[i]); i++) {
|
inflater?.disposeItem(item);
|
||||||
if (this.blockIsRecyclable_(block)) {
|
});
|
||||||
this.recycleBlock(block);
|
|
||||||
} else {
|
|
||||||
block.dispose(false, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Delete any mats from a previous showing.
|
|
||||||
for (let j = 0; j < this.mats.length; j++) {
|
|
||||||
const rect = this.mats[j];
|
|
||||||
if (rect) {
|
|
||||||
Tooltip.unbindMouseEvents(rect);
|
|
||||||
dom.removeNode(rect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.mats.length = 0;
|
|
||||||
// Delete any buttons from a previous showing.
|
|
||||||
for (let i = 0, button; (button = this.buttons_[i]); i++) {
|
|
||||||
button.dispose();
|
|
||||||
}
|
|
||||||
this.buttons_.length = 0;
|
|
||||||
|
|
||||||
// Clear potential variables from the previous showing.
|
// Clear potential variables from the previous showing.
|
||||||
this.workspace_.getPotentialVariableMap()?.clear();
|
this.workspace_.getPotentialVariableMap()?.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Empties all of the recycled blocks, properly disposing of them.
|
|
||||||
*/
|
|
||||||
private emptyRecycledBlocks() {
|
|
||||||
for (let i = 0; i < this.recycledBlocks.length; i++) {
|
|
||||||
this.recycledBlocks[i].dispose();
|
|
||||||
}
|
|
||||||
this.recycledBlocks = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns whether the given block can be recycled or not.
|
|
||||||
*
|
|
||||||
* @param _block The block to check for recyclability.
|
|
||||||
* @returns True if the block can be recycled. False otherwise.
|
|
||||||
*/
|
|
||||||
protected blockIsRecyclable_(_block: BlockSvg): boolean {
|
|
||||||
// By default, recycling is disabled.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Puts a previously created block into the recycle bin and moves it to the
|
|
||||||
* top of the workspace. Used during large workspace swaps to limit the number
|
|
||||||
* of new DOM elements we need to create.
|
|
||||||
*
|
|
||||||
* @param block The block to recycle.
|
|
||||||
*/
|
|
||||||
private recycleBlock(block: BlockSvg) {
|
|
||||||
const xy = block.getRelativeToSurfaceXY();
|
|
||||||
block.moveBy(-xy.x, -xy.y);
|
|
||||||
this.recycledBlocks.push(block);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add listeners to a block that has been added to the flyout.
|
|
||||||
*
|
|
||||||
* @param root The root node of the SVG group the block is in.
|
|
||||||
* @param block The block to add listeners for.
|
|
||||||
* @param rect The invisible rectangle under the block that acts
|
|
||||||
* as a mat for that block.
|
|
||||||
*/
|
|
||||||
protected addBlockListeners_(
|
|
||||||
root: SVGElement,
|
|
||||||
block: BlockSvg,
|
|
||||||
rect: SVGElement,
|
|
||||||
) {
|
|
||||||
this.listeners.push(
|
|
||||||
browserEvents.conditionalBind(
|
|
||||||
root,
|
|
||||||
'pointerdown',
|
|
||||||
null,
|
|
||||||
this.blockMouseDown(block),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
this.listeners.push(
|
|
||||||
browserEvents.conditionalBind(
|
|
||||||
rect,
|
|
||||||
'pointerdown',
|
|
||||||
null,
|
|
||||||
this.blockMouseDown(block),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
this.listeners.push(
|
|
||||||
browserEvents.bind(root, 'pointerenter', block, () => {
|
|
||||||
if (!this.targetWorkspace.isDragging()) {
|
|
||||||
block.addSelect();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.listeners.push(
|
|
||||||
browserEvents.bind(root, 'pointerleave', block, () => {
|
|
||||||
if (!this.targetWorkspace.isDragging()) {
|
|
||||||
block.removeSelect();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.listeners.push(
|
|
||||||
browserEvents.bind(rect, 'pointerenter', block, () => {
|
|
||||||
if (!this.targetWorkspace.isDragging()) {
|
|
||||||
block.addSelect();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.listeners.push(
|
|
||||||
browserEvents.bind(rect, 'pointerleave', block, () => {
|
|
||||||
if (!this.targetWorkspace.isDragging()) {
|
|
||||||
block.removeSelect();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle a pointerdown on an SVG block in a non-closing flyout.
|
|
||||||
*
|
|
||||||
* @param block The flyout block to copy.
|
|
||||||
* @returns Function to call when block is clicked.
|
|
||||||
*/
|
|
||||||
private blockMouseDown(block: BlockSvg) {
|
|
||||||
return (e: PointerEvent) => {
|
|
||||||
const gesture = this.targetWorkspace.getGesture(e);
|
|
||||||
if (gesture) {
|
|
||||||
gesture.setStartBlock(block);
|
|
||||||
gesture.handleFlyoutStart(e, this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pointer down on the flyout background. Start a vertical scroll drag.
|
* Pointer down on the flyout background. Start a vertical scroll drag.
|
||||||
*
|
*
|
||||||
@@ -1103,7 +786,7 @@ export abstract class Flyout
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
isBlockCreatable(block: BlockSvg): boolean {
|
isBlockCreatable(block: BlockSvg): boolean {
|
||||||
return block.isEnabled();
|
return block.isEnabled() && !this.getTargetWorkspace().isReadOnly();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1149,123 +832,12 @@ export abstract class Flyout
|
|||||||
}
|
}
|
||||||
if (this.autoClose) {
|
if (this.autoClose) {
|
||||||
this.hide();
|
this.hide();
|
||||||
} else {
|
|
||||||
this.filterForCapacity();
|
|
||||||
}
|
}
|
||||||
return newBlock;
|
return newBlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the given button: move it to the correct location,
|
* Reflow flyout contents.
|
||||||
* add listeners, etc.
|
|
||||||
*
|
|
||||||
* @param button The button to initialize and place.
|
|
||||||
* @param x The x position of the cursor during this layout pass.
|
|
||||||
* @param y The y position of the cursor during this layout pass.
|
|
||||||
*/
|
|
||||||
protected initFlyoutButton_(button: FlyoutButton, x: number, y: number) {
|
|
||||||
const buttonSvg = button.createDom();
|
|
||||||
button.moveTo(x, y);
|
|
||||||
button.show();
|
|
||||||
// Clicking on a flyout button or label is a lot like clicking on the
|
|
||||||
// flyout background.
|
|
||||||
this.listeners.push(
|
|
||||||
browserEvents.conditionalBind(
|
|
||||||
buttonSvg,
|
|
||||||
'pointerdown',
|
|
||||||
this,
|
|
||||||
this.onMouseDown,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.buttons_.push(button);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and place a rectangle corresponding to the given block.
|
|
||||||
*
|
|
||||||
* @param block The block to associate the rect to.
|
|
||||||
* @param x The x position of the cursor during this layout pass.
|
|
||||||
* @param y The y position of the cursor during this layout pass.
|
|
||||||
* @param blockHW The height and width of
|
|
||||||
* the block.
|
|
||||||
* @param index The index into the mats list where this rect should
|
|
||||||
* be placed.
|
|
||||||
* @returns Newly created SVG element for the rectangle behind
|
|
||||||
* the block.
|
|
||||||
*/
|
|
||||||
protected createRect_(
|
|
||||||
block: BlockSvg,
|
|
||||||
x: number,
|
|
||||||
y: number,
|
|
||||||
blockHW: {height: number; width: number},
|
|
||||||
index: number,
|
|
||||||
): SVGElement {
|
|
||||||
// Create an invisible rectangle under the block to act as a button. Just
|
|
||||||
// using the block as a button is poor, since blocks have holes in them.
|
|
||||||
const rect = dom.createSvgElement(Svg.RECT, {
|
|
||||||
'fill-opacity': 0,
|
|
||||||
'x': x,
|
|
||||||
'y': y,
|
|
||||||
'height': blockHW.height,
|
|
||||||
'width': blockHW.width,
|
|
||||||
});
|
|
||||||
(rect as AnyDuringMigration).tooltip = block;
|
|
||||||
Tooltip.bindMouseEvents(rect);
|
|
||||||
// Add the rectangles under the blocks, so that the blocks' tooltips work.
|
|
||||||
this.workspace_.getCanvas().insertBefore(rect, block.getSvgRoot());
|
|
||||||
|
|
||||||
this.rectMap_.set(block, rect);
|
|
||||||
this.mats[index] = rect;
|
|
||||||
return rect;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move a rectangle to sit exactly behind a block, taking into account tabs,
|
|
||||||
* hats, and any other protrusions we invent.
|
|
||||||
*
|
|
||||||
* @param rect The rectangle to move directly behind the block.
|
|
||||||
* @param block The block the rectangle should be behind.
|
|
||||||
*/
|
|
||||||
protected moveRectToBlock_(rect: SVGElement, block: BlockSvg) {
|
|
||||||
const blockHW = block.getHeightWidth();
|
|
||||||
rect.setAttribute('width', String(blockHW.width));
|
|
||||||
rect.setAttribute('height', String(blockHW.height));
|
|
||||||
|
|
||||||
const blockXY = block.getRelativeToSurfaceXY();
|
|
||||||
rect.setAttribute('y', String(blockXY.y));
|
|
||||||
rect.setAttribute(
|
|
||||||
'x',
|
|
||||||
String(this.RTL ? blockXY.x - blockHW.width : blockXY.x),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the blocks on the flyout to disable the ones that are above the
|
|
||||||
* capacity limit. For instance, if the user may only place two more blocks
|
|
||||||
* on the workspace, an "a + b" block that has two shadow blocks would be
|
|
||||||
* disabled.
|
|
||||||
*/
|
|
||||||
private filterForCapacity() {
|
|
||||||
const blocks = this.workspace_.getTopBlocks(false);
|
|
||||||
for (let i = 0, block; (block = blocks[i]); i++) {
|
|
||||||
if (!this.permanentlyDisabled.includes(block)) {
|
|
||||||
const enable = this.targetWorkspace.isCapacityAvailable(
|
|
||||||
common.getBlockTypeCounts(block),
|
|
||||||
);
|
|
||||||
while (block) {
|
|
||||||
block.setDisabledReason(
|
|
||||||
!enable,
|
|
||||||
WORKSPACE_AT_BLOCK_CAPACITY_DISABLED_REASON,
|
|
||||||
);
|
|
||||||
block = block.getNextBlock();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reflow blocks and their mats.
|
|
||||||
*/
|
*/
|
||||||
reflow() {
|
reflow() {
|
||||||
if (this.reflowWrapper) {
|
if (this.reflowWrapper) {
|
||||||
@@ -1364,13 +936,29 @@ export abstract class Flyout
|
|||||||
// No 'reason' provided since events are disabled.
|
// No 'reason' provided since events are disabled.
|
||||||
block.moveTo(new Coordinate(finalOffset.x, finalOffset.y));
|
block.moveTo(new Coordinate(finalOffset.x, finalOffset.y));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A flyout content item.
|
* Returns the inflater responsible for constructing items of the given type.
|
||||||
*/
|
*
|
||||||
export interface FlyoutItem {
|
* @param type The type of flyout content item to provide an inflater for.
|
||||||
type: FlyoutItemType;
|
* @returns An inflater object for the given type, or null if no inflater
|
||||||
button?: FlyoutButton | undefined;
|
* is registered for that type.
|
||||||
block?: BlockSvg | undefined;
|
*/
|
||||||
|
protected getInflaterForType(type: string): IFlyoutInflater | null {
|
||||||
|
if (this.inflaters.has(type)) {
|
||||||
|
return this.inflaters.get(type) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InflaterClass = registry.getClass(
|
||||||
|
registry.Type.FLYOUT_INFLATER,
|
||||||
|
type,
|
||||||
|
);
|
||||||
|
if (InflaterClass) {
|
||||||
|
const inflater = new InflaterClass();
|
||||||
|
this.inflaters.set(type, inflater);
|
||||||
|
return inflater;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,12 @@
|
|||||||
import type {IASTNodeLocationSvg} from './blockly.js';
|
import type {IASTNodeLocationSvg} from './blockly.js';
|
||||||
import * as browserEvents from './browser_events.js';
|
import * as browserEvents from './browser_events.js';
|
||||||
import * as Css from './css.js';
|
import * as Css from './css.js';
|
||||||
|
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||||
|
import type {IRenderedElement} from './interfaces/i_rendered_element.js';
|
||||||
import {Coordinate} from './utils/coordinate.js';
|
import {Coordinate} from './utils/coordinate.js';
|
||||||
import * as dom from './utils/dom.js';
|
import * as dom from './utils/dom.js';
|
||||||
import * as parsing from './utils/parsing.js';
|
import * as parsing from './utils/parsing.js';
|
||||||
|
import {Rect} from './utils/rect.js';
|
||||||
import * as style from './utils/style.js';
|
import * as style from './utils/style.js';
|
||||||
import {Svg} from './utils/svg.js';
|
import {Svg} from './utils/svg.js';
|
||||||
import type * as toolbox from './utils/toolbox.js';
|
import type * as toolbox from './utils/toolbox.js';
|
||||||
@@ -25,7 +28,9 @@ import type {WorkspaceSvg} from './workspace_svg.js';
|
|||||||
/**
|
/**
|
||||||
* Class for a button or label in the flyout.
|
* Class for a button or label in the flyout.
|
||||||
*/
|
*/
|
||||||
export class FlyoutButton implements IASTNodeLocationSvg {
|
export class FlyoutButton
|
||||||
|
implements IASTNodeLocationSvg, IBoundedElement, IRenderedElement
|
||||||
|
{
|
||||||
/** The horizontal margin around the text in the button. */
|
/** The horizontal margin around the text in the button. */
|
||||||
static TEXT_MARGIN_X = 5;
|
static TEXT_MARGIN_X = 5;
|
||||||
|
|
||||||
@@ -41,7 +46,8 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
private readonly cssClass: string | null;
|
private readonly cssClass: string | null;
|
||||||
|
|
||||||
/** Mouse up event data. */
|
/** Mouse up event data. */
|
||||||
private onMouseUpWrapper: browserEvents.Data | null = null;
|
private onMouseDownWrapper: browserEvents.Data;
|
||||||
|
private onMouseUpWrapper: browserEvents.Data;
|
||||||
info: toolbox.ButtonOrLabelInfo;
|
info: toolbox.ButtonOrLabelInfo;
|
||||||
|
|
||||||
/** The width of the button's rect. */
|
/** The width of the button's rect. */
|
||||||
@@ -51,7 +57,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
height = 0;
|
height = 0;
|
||||||
|
|
||||||
/** The root SVG group for the button or label. */
|
/** The root SVG group for the button or label. */
|
||||||
private svgGroup: SVGGElement | null = null;
|
private svgGroup: SVGGElement;
|
||||||
|
|
||||||
/** The SVG element with the text of the label or button. */
|
/** The SVG element with the text of the label or button. */
|
||||||
private svgText: SVGTextElement | null = null;
|
private svgText: SVGTextElement | null = null;
|
||||||
@@ -92,14 +98,6 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
|
|
||||||
/** The JSON specifying the label / button. */
|
/** The JSON specifying the label / button. */
|
||||||
this.info = json;
|
this.info = json;
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the button elements.
|
|
||||||
*
|
|
||||||
* @returns The button's SVG group.
|
|
||||||
*/
|
|
||||||
createDom(): SVGElement {
|
|
||||||
let cssClass = this.isFlyoutLabel
|
let cssClass = this.isFlyoutLabel
|
||||||
? 'blocklyFlyoutLabel'
|
? 'blocklyFlyoutLabel'
|
||||||
: 'blocklyFlyoutButton';
|
: 'blocklyFlyoutButton';
|
||||||
@@ -179,7 +177,7 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
fontWeight,
|
fontWeight,
|
||||||
fontFamily,
|
fontFamily,
|
||||||
);
|
);
|
||||||
this.height = fontMetrics.height;
|
this.height = this.height || fontMetrics.height;
|
||||||
|
|
||||||
if (!this.isFlyoutLabel) {
|
if (!this.isFlyoutLabel) {
|
||||||
this.width += 2 * FlyoutButton.TEXT_MARGIN_X;
|
this.width += 2 * FlyoutButton.TEXT_MARGIN_X;
|
||||||
@@ -198,15 +196,24 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
|
|
||||||
this.updateTransform();
|
this.updateTransform();
|
||||||
|
|
||||||
// AnyDuringMigration because: Argument of type 'SVGGElement | null' is not
|
this.onMouseDownWrapper = browserEvents.conditionalBind(
|
||||||
// assignable to parameter of type 'EventTarget'.
|
this.svgGroup,
|
||||||
|
'pointerdown',
|
||||||
|
this,
|
||||||
|
this.onMouseDown,
|
||||||
|
);
|
||||||
this.onMouseUpWrapper = browserEvents.conditionalBind(
|
this.onMouseUpWrapper = browserEvents.conditionalBind(
|
||||||
this.svgGroup as AnyDuringMigration,
|
this.svgGroup,
|
||||||
'pointerup',
|
'pointerup',
|
||||||
this,
|
this,
|
||||||
this.onMouseUp,
|
this.onMouseUp,
|
||||||
);
|
);
|
||||||
return this.svgGroup!;
|
}
|
||||||
|
|
||||||
|
createDom(): SVGElement {
|
||||||
|
// No-op, now handled in constructor. Will be removed in followup refactor
|
||||||
|
// PR that updates the flyout classes to use inflaters.
|
||||||
|
return this.svgGroup;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Correctly position the flyout button and make it visible. */
|
/** Correctly position the flyout button and make it visible. */
|
||||||
@@ -235,6 +242,17 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
this.updateTransform();
|
this.updateTransform();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move the element by a relative offset.
|
||||||
|
*
|
||||||
|
* @param dx Horizontal offset in workspace units.
|
||||||
|
* @param dy Vertical offset in workspace units.
|
||||||
|
* @param _reason Why is this move happening? 'user', 'bump', 'snap'...
|
||||||
|
*/
|
||||||
|
moveBy(dx: number, dy: number, _reason?: string[]) {
|
||||||
|
this.moveTo(this.position.x + dx, this.position.y + dy);
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns Whether or not the button is a label. */
|
/** @returns Whether or not the button is a label. */
|
||||||
isLabel(): boolean {
|
isLabel(): boolean {
|
||||||
return this.isFlyoutLabel;
|
return this.isFlyoutLabel;
|
||||||
@@ -250,6 +268,21 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
return this.position;
|
return this.position;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the coordinates of a bounded element describing the dimensions of
|
||||||
|
* the element. Coordinate system: workspace coordinates.
|
||||||
|
*
|
||||||
|
* @returns Object with coordinates of the bounded element.
|
||||||
|
*/
|
||||||
|
getBoundingRectangle() {
|
||||||
|
return new Rect(
|
||||||
|
this.position.y,
|
||||||
|
this.position.y + this.height,
|
||||||
|
this.position.x,
|
||||||
|
this.position.x + this.width,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/** @returns Text of the button. */
|
/** @returns Text of the button. */
|
||||||
getButtonText(): string {
|
getButtonText(): string {
|
||||||
return this.text;
|
return this.text;
|
||||||
@@ -275,9 +308,8 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
|
|
||||||
/** Dispose of this button. */
|
/** Dispose of this button. */
|
||||||
dispose() {
|
dispose() {
|
||||||
if (this.onMouseUpWrapper) {
|
browserEvents.unbind(this.onMouseDownWrapper);
|
||||||
browserEvents.unbind(this.onMouseUpWrapper);
|
browserEvents.unbind(this.onMouseUpWrapper);
|
||||||
}
|
|
||||||
if (this.svgGroup) {
|
if (this.svgGroup) {
|
||||||
dom.removeNode(this.svgGroup);
|
dom.removeNode(this.svgGroup);
|
||||||
}
|
}
|
||||||
@@ -342,6 +374,21 @@ export class FlyoutButton implements IASTNodeLocationSvg {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onMouseDown(e: PointerEvent) {
|
||||||
|
const gesture = this.targetWorkspace.getGesture(e);
|
||||||
|
const flyout = this.targetWorkspace.getFlyout();
|
||||||
|
if (gesture && flyout) {
|
||||||
|
gesture.handleFlyoutStart(e, flyout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns The root SVG element of this rendered element.
|
||||||
|
*/
|
||||||
|
getSvgRoot() {
|
||||||
|
return this.svgGroup;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CSS for buttons and labels. See css.js for use. */
|
/** CSS for buttons and labels. See css.js for use. */
|
||||||
|
|||||||
@@ -13,8 +13,8 @@
|
|||||||
|
|
||||||
import * as browserEvents from './browser_events.js';
|
import * as browserEvents from './browser_events.js';
|
||||||
import * as dropDownDiv from './dropdowndiv.js';
|
import * as dropDownDiv from './dropdowndiv.js';
|
||||||
import {Flyout, FlyoutItem} from './flyout_base.js';
|
import {Flyout} from './flyout_base.js';
|
||||||
import type {FlyoutButton} from './flyout_button.js';
|
import type {FlyoutItem} from './flyout_item.js';
|
||||||
import type {Options} from './options.js';
|
import type {Options} from './options.js';
|
||||||
import * as registry from './registry.js';
|
import * as registry from './registry.js';
|
||||||
import {Scrollbar} from './scrollbar.js';
|
import {Scrollbar} from './scrollbar.js';
|
||||||
@@ -98,7 +98,7 @@ export class HorizontalFlyout extends Flyout {
|
|||||||
if (atTop) {
|
if (atTop) {
|
||||||
y = toolboxMetrics.height;
|
y = toolboxMetrics.height;
|
||||||
} else {
|
} else {
|
||||||
y = viewMetrics.height - this.height_;
|
y = viewMetrics.height - this.getHeight();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (atTop) {
|
if (atTop) {
|
||||||
@@ -116,7 +116,7 @@ export class HorizontalFlyout extends Flyout {
|
|||||||
// to align the bottom edge of the flyout with the bottom edge of the
|
// to align the bottom edge of the flyout with the bottom edge of the
|
||||||
// blocklyDiv, we calculate the full height of the div minus the height
|
// blocklyDiv, we calculate the full height of the div minus the height
|
||||||
// of the flyout.
|
// of the flyout.
|
||||||
y = viewMetrics.height + absoluteMetrics.top - this.height_;
|
y = viewMetrics.height + absoluteMetrics.top - this.getHeight();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,13 +133,13 @@ export class HorizontalFlyout extends Flyout {
|
|||||||
this.width_ = targetWorkspaceViewMetrics.width;
|
this.width_ = targetWorkspaceViewMetrics.width;
|
||||||
|
|
||||||
const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS;
|
const edgeWidth = targetWorkspaceViewMetrics.width - 2 * this.CORNER_RADIUS;
|
||||||
const edgeHeight = this.height_ - this.CORNER_RADIUS;
|
const edgeHeight = this.getHeight() - this.CORNER_RADIUS;
|
||||||
this.setBackgroundPath(edgeWidth, edgeHeight);
|
this.setBackgroundPath(edgeWidth, edgeHeight);
|
||||||
|
|
||||||
const x = this.getX();
|
const x = this.getX();
|
||||||
const y = this.getY();
|
const y = this.getY();
|
||||||
|
|
||||||
this.positionAt_(this.width_, this.height_, x, y);
|
this.positionAt_(this.getWidth(), this.getHeight(), x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,10 +252,9 @@ export class HorizontalFlyout extends Flyout {
|
|||||||
/**
|
/**
|
||||||
* Lay out the blocks in the flyout.
|
* Lay out the blocks in the flyout.
|
||||||
*
|
*
|
||||||
* @param contents The blocks and buttons to lay out.
|
* @param contents The flyout items to lay out.
|
||||||
* @param gaps The visible gaps between blocks.
|
|
||||||
*/
|
*/
|
||||||
protected override layout_(contents: FlyoutItem[], gaps: number[]) {
|
protected override layout_(contents: FlyoutItem[]) {
|
||||||
this.workspace_.scale = this.targetWorkspace!.scale;
|
this.workspace_.scale = this.targetWorkspace!.scale;
|
||||||
const margin = this.MARGIN;
|
const margin = this.MARGIN;
|
||||||
let cursorX = margin + this.tabWidth_;
|
let cursorX = margin + this.tabWidth_;
|
||||||
@@ -264,43 +263,11 @@ export class HorizontalFlyout extends Flyout {
|
|||||||
contents = contents.reverse();
|
contents = contents.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let i = 0, item; (item = contents[i]); i++) {
|
for (const item of contents) {
|
||||||
if (item.type === 'block') {
|
const rect = item.getElement().getBoundingRectangle();
|
||||||
const block = item.block;
|
const moveX = this.RTL ? cursorX + rect.getWidth() : cursorX;
|
||||||
|
item.getElement().moveBy(moveX, cursorY);
|
||||||
if (block === undefined || block === null) {
|
cursorX += item.getElement().getBoundingRectangle().getWidth();
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const root = block.getSvgRoot();
|
|
||||||
const blockHW = block.getHeightWidth();
|
|
||||||
// Figure out where to place the block.
|
|
||||||
const tab = block.outputConnection ? this.tabWidth_ : 0;
|
|
||||||
let moveX;
|
|
||||||
if (this.RTL) {
|
|
||||||
moveX = cursorX + blockHW.width;
|
|
||||||
} else {
|
|
||||||
moveX = cursorX - tab;
|
|
||||||
}
|
|
||||||
block.moveBy(moveX, cursorY);
|
|
||||||
|
|
||||||
const rect = this.createRect_(block, moveX, cursorY, blockHW, i);
|
|
||||||
cursorX += blockHW.width + gaps[i];
|
|
||||||
|
|
||||||
this.addBlockListeners_(root, block, rect);
|
|
||||||
} else if (item.type === 'button') {
|
|
||||||
const button = item.button as FlyoutButton;
|
|
||||||
this.initFlyoutButton_(button, cursorX, cursorY);
|
|
||||||
cursorX += button.width + gaps[i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,26 +334,17 @@ export class HorizontalFlyout extends Flyout {
|
|||||||
*/
|
*/
|
||||||
protected override reflowInternal_() {
|
protected override reflowInternal_() {
|
||||||
this.workspace_.scale = this.getFlyoutScale();
|
this.workspace_.scale = this.getFlyoutScale();
|
||||||
let flyoutHeight = 0;
|
let flyoutHeight = this.getContents().reduce((maxHeightSoFar, item) => {
|
||||||
const blocks = this.workspace_.getTopBlocks(false);
|
return Math.max(
|
||||||
for (let i = 0, block; (block = blocks[i]); i++) {
|
maxHeightSoFar,
|
||||||
flyoutHeight = Math.max(flyoutHeight, block.getHeightWidth().height);
|
item.getElement().getBoundingRectangle().getHeight(),
|
||||||
}
|
);
|
||||||
const buttons = this.buttons_;
|
}, 0);
|
||||||
for (let i = 0, button; (button = buttons[i]); i++) {
|
|
||||||
flyoutHeight = Math.max(flyoutHeight, button.height);
|
|
||||||
}
|
|
||||||
flyoutHeight += this.MARGIN * 1.5;
|
flyoutHeight += this.MARGIN * 1.5;
|
||||||
flyoutHeight *= this.workspace_.scale;
|
flyoutHeight *= this.workspace_.scale;
|
||||||
flyoutHeight += Scrollbar.scrollbarThickness;
|
flyoutHeight += Scrollbar.scrollbarThickness;
|
||||||
|
|
||||||
if (this.height_ !== flyoutHeight) {
|
if (this.getHeight() !== flyoutHeight) {
|
||||||
for (let i = 0, block; (block = blocks[i]); i++) {
|
|
||||||
if (this.rectMap_.has(block)) {
|
|
||||||
this.moveRectToBlock_(this.rectMap_.get(block)!, block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(#7689): Remove this.
|
// TODO(#7689): Remove this.
|
||||||
// Workspace with no scrollbars where this is permanently open on the top.
|
// Workspace with no scrollbars where this is permanently open on the top.
|
||||||
// If scrollbars exist they properly update the metrics.
|
// If scrollbars exist they properly update the metrics.
|
||||||
|
|||||||
42
core/flyout_item.ts
Normal file
42
core/flyout_item.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of an item displayed in a flyout.
|
||||||
|
*/
|
||||||
|
export class FlyoutItem {
|
||||||
|
/**
|
||||||
|
* Creates a new FlyoutItem.
|
||||||
|
*
|
||||||
|
* @param element The element that will be displayed in the flyout.
|
||||||
|
* @param type The type of element. Should correspond to the type of the
|
||||||
|
* flyout inflater that created this object.
|
||||||
|
* @param focusable True if the element should be allowed to be focused by
|
||||||
|
* e.g. keyboard navigation in the flyout.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private element: IBoundedElement,
|
||||||
|
private type: string,
|
||||||
|
private focusable: boolean,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the element displayed in the flyout.
|
||||||
|
*/
|
||||||
|
getElement() {
|
||||||
|
return this.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of flyout element this item represents.
|
||||||
|
*/
|
||||||
|
getType() {
|
||||||
|
return this.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns whether or not the flyout element can receive focus.
|
||||||
|
*/
|
||||||
|
isFocusable() {
|
||||||
|
return this.focusable;
|
||||||
|
}
|
||||||
|
}
|
||||||
61
core/flyout_separator.ts
Normal file
61
core/flyout_separator.ts
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {IBoundedElement} from './interfaces/i_bounded_element.js';
|
||||||
|
import {Rect} from './utils/rect.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a gap between elements in a flyout.
|
||||||
|
*/
|
||||||
|
export class FlyoutSeparator implements IBoundedElement {
|
||||||
|
private x = 0;
|
||||||
|
private y = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new separator.
|
||||||
|
*
|
||||||
|
* @param gap The amount of space this separator should occupy.
|
||||||
|
* @param axis The axis along which this separator occupies space.
|
||||||
|
*/
|
||||||
|
constructor(
|
||||||
|
private gap: number,
|
||||||
|
private axis: SeparatorAxis,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the bounding box of this separator.
|
||||||
|
*
|
||||||
|
* @returns The bounding box of this separator.
|
||||||
|
*/
|
||||||
|
getBoundingRectangle(): Rect {
|
||||||
|
switch (this.axis) {
|
||||||
|
case SeparatorAxis.X:
|
||||||
|
return new Rect(this.y, this.y, this.x, this.x + this.gap);
|
||||||
|
case SeparatorAxis.Y:
|
||||||
|
return new Rect(this.y, this.y + this.gap, this.x, this.x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repositions this separator.
|
||||||
|
*
|
||||||
|
* @param dx The distance to move this separator on the X axis.
|
||||||
|
* @param dy The distance to move this separator on the Y axis.
|
||||||
|
* @param _reason The reason this move was initiated.
|
||||||
|
*/
|
||||||
|
moveBy(dx: number, dy: number, _reason?: string[]) {
|
||||||
|
this.x += dx;
|
||||||
|
this.y += dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of an axis along which a separator occupies space.
|
||||||
|
*/
|
||||||
|
export const enum SeparatorAxis {
|
||||||
|
X = 'x',
|
||||||
|
Y = 'y',
|
||||||
|
}
|
||||||
@@ -13,8 +13,8 @@
|
|||||||
|
|
||||||
import * as browserEvents from './browser_events.js';
|
import * as browserEvents from './browser_events.js';
|
||||||
import * as dropDownDiv from './dropdowndiv.js';
|
import * as dropDownDiv from './dropdowndiv.js';
|
||||||
import {Flyout, FlyoutItem} from './flyout_base.js';
|
import {Flyout} from './flyout_base.js';
|
||||||
import type {FlyoutButton} from './flyout_button.js';
|
import type {FlyoutItem} from './flyout_item.js';
|
||||||
import type {Options} from './options.js';
|
import type {Options} from './options.js';
|
||||||
import * as registry from './registry.js';
|
import * as registry from './registry.js';
|
||||||
import {Scrollbar} from './scrollbar.js';
|
import {Scrollbar} from './scrollbar.js';
|
||||||
@@ -86,7 +86,7 @@ export class VerticalFlyout extends Flyout {
|
|||||||
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
|
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
|
||||||
x = toolboxMetrics.width;
|
x = toolboxMetrics.width;
|
||||||
} else {
|
} else {
|
||||||
x = viewMetrics.width - this.width_;
|
x = viewMetrics.width - this.getWidth();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
|
if (this.toolboxPosition_ === toolbox.Position.LEFT) {
|
||||||
@@ -104,7 +104,7 @@ export class VerticalFlyout extends Flyout {
|
|||||||
// to align the right edge of the flyout with the right edge of the
|
// to align the right edge of the flyout with the right edge of the
|
||||||
// blocklyDiv, we calculate the full width of the div minus the width
|
// blocklyDiv, we calculate the full width of the div minus the width
|
||||||
// of the flyout.
|
// of the flyout.
|
||||||
x = viewMetrics.width + absoluteMetrics.left - this.width_;
|
x = viewMetrics.width + absoluteMetrics.left - this.getWidth();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,7 +130,7 @@ export class VerticalFlyout extends Flyout {
|
|||||||
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
|
const targetWorkspaceViewMetrics = metricsManager.getViewMetrics();
|
||||||
this.height_ = targetWorkspaceViewMetrics.height;
|
this.height_ = targetWorkspaceViewMetrics.height;
|
||||||
|
|
||||||
const edgeWidth = this.width_ - this.CORNER_RADIUS;
|
const edgeWidth = this.getWidth() - this.CORNER_RADIUS;
|
||||||
const edgeHeight =
|
const edgeHeight =
|
||||||
targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS;
|
targetWorkspaceViewMetrics.height - 2 * this.CORNER_RADIUS;
|
||||||
this.setBackgroundPath(edgeWidth, edgeHeight);
|
this.setBackgroundPath(edgeWidth, edgeHeight);
|
||||||
@@ -138,7 +138,7 @@ export class VerticalFlyout extends Flyout {
|
|||||||
const x = this.getX();
|
const x = this.getX();
|
||||||
const y = this.getY();
|
const y = this.getY();
|
||||||
|
|
||||||
this.positionAt_(this.width_, this.height_, x, y);
|
this.positionAt_(this.getWidth(), this.getHeight(), x, y);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,51 +221,17 @@ export class VerticalFlyout extends Flyout {
|
|||||||
/**
|
/**
|
||||||
* Lay out the blocks in the flyout.
|
* Lay out the blocks in the flyout.
|
||||||
*
|
*
|
||||||
* @param contents The blocks and buttons to lay out.
|
* @param contents The flyout items to lay out.
|
||||||
* @param gaps The visible gaps between blocks.
|
|
||||||
*/
|
*/
|
||||||
protected override layout_(contents: FlyoutItem[], gaps: number[]) {
|
protected override layout_(contents: FlyoutItem[]) {
|
||||||
this.workspace_.scale = this.targetWorkspace!.scale;
|
this.workspace_.scale = this.targetWorkspace!.scale;
|
||||||
const margin = this.MARGIN;
|
const margin = this.MARGIN;
|
||||||
const cursorX = this.RTL ? margin : margin + this.tabWidth_;
|
const cursorX = this.RTL ? margin : margin + this.tabWidth_;
|
||||||
let cursorY = margin;
|
let cursorY = margin;
|
||||||
|
|
||||||
for (let i = 0, item; (item = contents[i]); i++) {
|
for (const item of contents) {
|
||||||
if (item.type === 'block') {
|
item.getElement().moveBy(cursorX, cursorY);
|
||||||
const block = item.block;
|
cursorY += item.getElement().getBoundingRectangle().getHeight();
|
||||||
if (!block) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
const root = block.getSvgRoot();
|
|
||||||
const blockHW = block.getHeightWidth();
|
|
||||||
const moveX = block.outputConnection
|
|
||||||
? cursorX - this.tabWidth_
|
|
||||||
: cursorX;
|
|
||||||
block.moveBy(moveX, cursorY);
|
|
||||||
|
|
||||||
const rect = this.createRect_(
|
|
||||||
block,
|
|
||||||
this.RTL ? moveX - blockHW.width : moveX,
|
|
||||||
cursorY,
|
|
||||||
blockHW,
|
|
||||||
i,
|
|
||||||
);
|
|
||||||
|
|
||||||
this.addBlockListeners_(root, block, rect);
|
|
||||||
|
|
||||||
cursorY += blockHW.height + gaps[i];
|
|
||||||
} else if (item.type === 'button') {
|
|
||||||
const button = item.button as FlyoutButton;
|
|
||||||
this.initFlyoutButton_(button, cursorX, cursorY);
|
|
||||||
cursorY += button.height + gaps[i];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -328,52 +294,32 @@ export class VerticalFlyout extends Flyout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compute width of flyout. toolbox.Position mat under each block.
|
* Compute width of flyout.
|
||||||
* For RTL: Lay out the blocks and buttons to be right-aligned.
|
* For RTL: Lay out the blocks and buttons to be right-aligned.
|
||||||
*/
|
*/
|
||||||
protected override reflowInternal_() {
|
protected override reflowInternal_() {
|
||||||
this.workspace_.scale = this.getFlyoutScale();
|
this.workspace_.scale = this.getFlyoutScale();
|
||||||
let flyoutWidth = 0;
|
let flyoutWidth = this.getContents().reduce((maxWidthSoFar, item) => {
|
||||||
const blocks = this.workspace_.getTopBlocks(false);
|
return Math.max(
|
||||||
for (let i = 0, block; (block = blocks[i]); i++) {
|
maxWidthSoFar,
|
||||||
let width = block.getHeightWidth().width;
|
item.getElement().getBoundingRectangle().getWidth(),
|
||||||
if (block.outputConnection) {
|
);
|
||||||
width -= this.tabWidth_;
|
}, 0);
|
||||||
}
|
|
||||||
flyoutWidth = Math.max(flyoutWidth, width);
|
|
||||||
}
|
|
||||||
for (let i = 0, button; (button = this.buttons_[i]); i++) {
|
|
||||||
flyoutWidth = Math.max(flyoutWidth, button.width);
|
|
||||||
}
|
|
||||||
flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_;
|
flyoutWidth += this.MARGIN * 1.5 + this.tabWidth_;
|
||||||
flyoutWidth *= this.workspace_.scale;
|
flyoutWidth *= this.workspace_.scale;
|
||||||
flyoutWidth += Scrollbar.scrollbarThickness;
|
flyoutWidth += Scrollbar.scrollbarThickness;
|
||||||
|
|
||||||
if (this.width_ !== flyoutWidth) {
|
if (this.getWidth() !== flyoutWidth) {
|
||||||
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;
|
|
||||||
let newX = flyoutWidth / this.workspace_.scale - this.MARGIN;
|
|
||||||
if (!block.outputConnection) {
|
|
||||||
newX -= this.tabWidth_;
|
|
||||||
}
|
|
||||||
block.moveBy(newX - oldX, 0);
|
|
||||||
}
|
|
||||||
if (this.rectMap_.has(block)) {
|
|
||||||
this.moveRectToBlock_(this.rectMap_.get(block)!, block);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.RTL) {
|
if (this.RTL) {
|
||||||
// With the flyoutWidth known, right-align the buttons.
|
// With the flyoutWidth known, right-align the flyout contents.
|
||||||
for (let i = 0, button; (button = this.buttons_[i]); i++) {
|
for (const item of this.getContents()) {
|
||||||
const y = button.getPosition().y;
|
const oldX = item.getElement().getBoundingRectangle().left;
|
||||||
const x =
|
const newX =
|
||||||
flyoutWidth / this.workspace_.scale -
|
flyoutWidth / this.workspace_.scale -
|
||||||
button.width -
|
item.getElement().getBoundingRectangle().getWidth() -
|
||||||
this.MARGIN -
|
this.MARGIN -
|
||||||
this.tabWidth_;
|
this.tabWidth_;
|
||||||
button.moveTo(x, y);
|
item.getElement().moveBy(newX - oldX, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -252,8 +252,7 @@ export class CodeGenerator {
|
|||||||
return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]);
|
return opt_thisOnly ? '' : this.blockToCode(block.getChildren(false)[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Look up block generator function in dictionary - but fall back
|
// Look up block generator function in dictionary.
|
||||||
// to looking up on this if not found, for backwards compatibility.
|
|
||||||
const func = this.forBlock[block.type];
|
const func = this.forBlock[block.type];
|
||||||
if (typeof func !== 'function') {
|
if (typeof func !== 'function') {
|
||||||
throw Error(
|
throw Error(
|
||||||
|
|||||||
@@ -894,7 +894,7 @@ export class Gesture {
|
|||||||
'Cannot do a block click because the target block is ' + 'undefined',
|
'Cannot do a block click because the target block is ' + 'undefined',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (this.targetBlock.isEnabled()) {
|
if (this.flyout.isBlockCreatable(this.targetBlock)) {
|
||||||
if (!eventUtils.getGroup()) {
|
if (!eventUtils.getGroup()) {
|
||||||
eventUtils.setGroup(true);
|
eventUtils.setGroup(true);
|
||||||
}
|
}
|
||||||
|
|||||||
15
core/grid.ts
15
core/grid.ts
@@ -210,6 +210,9 @@ export class Grid {
|
|||||||
* @param rnd A random ID to append to the pattern's ID.
|
* @param rnd A random ID to append to the pattern's ID.
|
||||||
* @param gridOptions The object containing grid configuration.
|
* @param gridOptions The object containing grid configuration.
|
||||||
* @param defs The root SVG element for this workspace's defs.
|
* @param defs The root SVG element for this workspace's defs.
|
||||||
|
* @param injectionDiv The div containing the parent workspace and all related
|
||||||
|
* workspaces and block containers. CSS variables representing SVG patterns
|
||||||
|
* will be scoped to this container.
|
||||||
* @returns The SVG element for the grid pattern.
|
* @returns The SVG element for the grid pattern.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@@ -217,6 +220,7 @@ export class Grid {
|
|||||||
rnd: string,
|
rnd: string,
|
||||||
gridOptions: GridOptions,
|
gridOptions: GridOptions,
|
||||||
defs: SVGElement,
|
defs: SVGElement,
|
||||||
|
injectionDiv?: HTMLElement,
|
||||||
): SVGElement {
|
): SVGElement {
|
||||||
/*
|
/*
|
||||||
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
|
<pattern id="blocklyGridPattern837493" patternUnits="userSpaceOnUse">
|
||||||
@@ -247,6 +251,17 @@ export class Grid {
|
|||||||
// Edge 16 doesn't handle empty patterns
|
// Edge 16 doesn't handle empty patterns
|
||||||
dom.createSvgElement(Svg.LINE, {}, gridPattern);
|
dom.createSvgElement(Svg.LINE, {}, gridPattern);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (injectionDiv) {
|
||||||
|
// Add CSS variables scoped to the injection div referencing the created
|
||||||
|
// patterns so that CSS can apply the patterns to any element in the
|
||||||
|
// injection div.
|
||||||
|
injectionDiv.style.setProperty(
|
||||||
|
'--blocklyGridPattern',
|
||||||
|
`url(#${gridPattern.id})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return gridPattern;
|
return gridPattern;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,6 +55,9 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
|||||||
/** The size of this comment (which is applied to the editable bubble). */
|
/** The size of this comment (which is applied to the editable bubble). */
|
||||||
private bubbleSize = new Size(DEFAULT_BUBBLE_WIDTH, DEFAULT_BUBBLE_HEIGHT);
|
private bubbleSize = new Size(DEFAULT_BUBBLE_WIDTH, DEFAULT_BUBBLE_HEIGHT);
|
||||||
|
|
||||||
|
/** The location of the comment bubble in workspace coordinates. */
|
||||||
|
private bubbleLocation?: Coordinate;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The visibility of the bubble for this comment.
|
* The visibility of the bubble for this comment.
|
||||||
*
|
*
|
||||||
@@ -108,7 +111,7 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
|||||||
},
|
},
|
||||||
this.svgRoot,
|
this.svgRoot,
|
||||||
);
|
);
|
||||||
dom.addClass(this.svgRoot!, 'blockly-icon-comment');
|
dom.addClass(this.svgRoot!, 'blocklyCommentIcon');
|
||||||
}
|
}
|
||||||
|
|
||||||
override dispose() {
|
override dispose() {
|
||||||
@@ -144,7 +147,13 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override onLocationChange(blockOrigin: Coordinate): void {
|
override onLocationChange(blockOrigin: Coordinate): void {
|
||||||
|
const oldLocation = this.workspaceLocation;
|
||||||
super.onLocationChange(blockOrigin);
|
super.onLocationChange(blockOrigin);
|
||||||
|
if (this.bubbleLocation) {
|
||||||
|
const newLocation = this.workspaceLocation;
|
||||||
|
const delta = Coordinate.difference(newLocation, oldLocation);
|
||||||
|
this.bubbleLocation = Coordinate.sum(this.bubbleLocation, delta);
|
||||||
|
}
|
||||||
const anchorLocation = this.getAnchorLocation();
|
const anchorLocation = this.getAnchorLocation();
|
||||||
this.textInputBubble?.setAnchorLocation(anchorLocation);
|
this.textInputBubble?.setAnchorLocation(anchorLocation);
|
||||||
}
|
}
|
||||||
@@ -184,18 +193,42 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
|||||||
return this.bubbleSize;
|
return this.bubbleSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the location of the comment bubble in the workspace.
|
||||||
|
*/
|
||||||
|
setBubbleLocation(location: Coordinate) {
|
||||||
|
this.bubbleLocation = location;
|
||||||
|
this.textInputBubble?.moveDuringDrag(location);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns the location of the comment bubble in the workspace.
|
||||||
|
*/
|
||||||
|
getBubbleLocation(): Coordinate | undefined {
|
||||||
|
return this.bubbleLocation;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns the state of the comment as a JSON serializable value if the
|
* @returns the state of the comment as a JSON serializable value if the
|
||||||
* comment has text. Otherwise returns null.
|
* comment has text. Otherwise returns null.
|
||||||
*/
|
*/
|
||||||
saveState(): CommentState | null {
|
saveState(): CommentState | null {
|
||||||
if (this.text) {
|
if (this.text) {
|
||||||
return {
|
const state: CommentState = {
|
||||||
'text': this.text,
|
'text': this.text,
|
||||||
'pinned': this.bubbleIsVisible(),
|
'pinned': this.bubbleIsVisible(),
|
||||||
'height': this.bubbleSize.height,
|
'height': this.bubbleSize.height,
|
||||||
'width': this.bubbleSize.width,
|
'width': this.bubbleSize.width,
|
||||||
};
|
};
|
||||||
|
const location = this.getBubbleLocation();
|
||||||
|
if (location) {
|
||||||
|
state['x'] = this.sourceBlock.workspace.RTL
|
||||||
|
? this.sourceBlock.workspace.getWidth() -
|
||||||
|
(location.x + this.bubbleSize.width)
|
||||||
|
: location.x;
|
||||||
|
state['y'] = location.y;
|
||||||
|
}
|
||||||
|
return state;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -209,6 +242,16 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
|||||||
);
|
);
|
||||||
this.bubbleVisiblity = state['pinned'] ?? false;
|
this.bubbleVisiblity = state['pinned'] ?? false;
|
||||||
this.setBubbleVisible(this.bubbleVisiblity);
|
this.setBubbleVisible(this.bubbleVisiblity);
|
||||||
|
let x = state['x'];
|
||||||
|
const y = state['y'];
|
||||||
|
renderManagement.finishQueuedRenders().then(() => {
|
||||||
|
if (x && y) {
|
||||||
|
x = this.sourceBlock.workspace.RTL
|
||||||
|
? this.sourceBlock.workspace.getWidth() - (x + this.bubbleSize.width)
|
||||||
|
: x;
|
||||||
|
this.setBubbleLocation(new Coordinate(x, y));
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
override onClick(): void {
|
override onClick(): void {
|
||||||
@@ -252,6 +295,12 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onBubbleLocationChange(): void {
|
||||||
|
if (this.textInputBubble) {
|
||||||
|
this.bubbleLocation = this.textInputBubble.getRelativeToSurfaceXY();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bubbleIsVisible(): boolean {
|
bubbleIsVisible(): boolean {
|
||||||
return this.bubbleVisiblity;
|
return this.bubbleVisiblity;
|
||||||
}
|
}
|
||||||
@@ -313,6 +362,14 @@ export class CommentIcon extends Icon implements IHasBubble, ISerializable {
|
|||||||
);
|
);
|
||||||
this.textInputBubble.setText(this.getText());
|
this.textInputBubble.setText(this.getText());
|
||||||
this.textInputBubble.setSize(this.bubbleSize, true);
|
this.textInputBubble.setSize(this.bubbleSize, true);
|
||||||
|
if (this.bubbleLocation) {
|
||||||
|
this.textInputBubble.moveDuringDrag(this.bubbleLocation);
|
||||||
|
}
|
||||||
|
this.textInputBubble.addTextChangeListener(() => this.onTextChange());
|
||||||
|
this.textInputBubble.addSizeChangeListener(() => this.onSizeChange());
|
||||||
|
this.textInputBubble.addLocationChangeListener(() =>
|
||||||
|
this.onBubbleLocationChange(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Hides any open bubbles owned by this comment. */
|
/** Hides any open bubbles owned by this comment. */
|
||||||
@@ -355,6 +412,12 @@ export interface CommentState {
|
|||||||
|
|
||||||
/** The width of the comment bubble. */
|
/** The width of the comment bubble. */
|
||||||
width?: number;
|
width?: number;
|
||||||
|
|
||||||
|
/** The X coordinate of the comment bubble. */
|
||||||
|
x?: number;
|
||||||
|
|
||||||
|
/** The Y coordinate of the comment bubble. */
|
||||||
|
y?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
registry.register(CommentIcon.TYPE, CommentIcon);
|
registry.register(CommentIcon.TYPE, CommentIcon);
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ export class MutatorIcon extends Icon implements IHasBubble {
|
|||||||
{'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'},
|
{'class': 'blocklyIconShape', 'r': '2.7', 'cx': '8', 'cy': '8'},
|
||||||
this.svgRoot,
|
this.svgRoot,
|
||||||
);
|
);
|
||||||
dom.addClass(this.svgRoot!, 'blockly-icon-mutator');
|
dom.addClass(this.svgRoot!, 'blocklyMutatorIcon');
|
||||||
}
|
}
|
||||||
|
|
||||||
override dispose(): void {
|
override dispose(): void {
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ export class WarningIcon extends Icon implements IHasBubble {
|
|||||||
},
|
},
|
||||||
this.svgRoot,
|
this.svgRoot,
|
||||||
);
|
);
|
||||||
dom.addClass(this.svgRoot!, 'blockly-icon-warning');
|
dom.addClass(this.svgRoot!, 'blocklyWarningIcon');
|
||||||
}
|
}
|
||||||
|
|
||||||
override dispose() {
|
override dispose() {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export function inject(
|
|||||||
* @param options Dictionary of options.
|
* @param options Dictionary of options.
|
||||||
* @returns Newly created SVG image.
|
* @returns Newly created SVG image.
|
||||||
*/
|
*/
|
||||||
function createDom(container: Element, options: Options): SVGElement {
|
function createDom(container: HTMLElement, options: Options): SVGElement {
|
||||||
// Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying
|
// Sadly browsers (Chrome vs Firefox) are currently inconsistent in laying
|
||||||
// out content in RTL mode. Therefore Blockly forces the use of LTR,
|
// out content in RTL mode. Therefore Blockly forces the use of LTR,
|
||||||
// then manually positions content in RTL as needed.
|
// then manually positions content in RTL as needed.
|
||||||
@@ -141,7 +141,12 @@ function createDom(container: Element, options: Options): SVGElement {
|
|||||||
// https://neil.fraser.name/news/2015/11/01/
|
// https://neil.fraser.name/news/2015/11/01/
|
||||||
const rnd = String(Math.random()).substring(2);
|
const rnd = String(Math.random()).substring(2);
|
||||||
|
|
||||||
options.gridPattern = Grid.createDom(rnd, options.gridOptions, defs);
|
options.gridPattern = Grid.createDom(
|
||||||
|
rnd,
|
||||||
|
options.gridOptions,
|
||||||
|
defs,
|
||||||
|
container,
|
||||||
|
);
|
||||||
return svg;
|
return svg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +158,7 @@ function createDom(container: Element, options: Options): SVGElement {
|
|||||||
* @returns Newly created main workspace.
|
* @returns Newly created main workspace.
|
||||||
*/
|
*/
|
||||||
function createMainWorkspace(
|
function createMainWorkspace(
|
||||||
injectionDiv: Element,
|
injectionDiv: HTMLElement,
|
||||||
svg: SVGElement,
|
svg: SVGElement,
|
||||||
options: Options,
|
options: Options,
|
||||||
): WorkspaceSvg {
|
): WorkspaceSvg {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import type {Connection} from '../connection.js';
|
|||||||
import type {ConnectionType} from '../connection_type.js';
|
import type {ConnectionType} from '../connection_type.js';
|
||||||
import type {Field} from '../field.js';
|
import type {Field} from '../field.js';
|
||||||
import * as fieldRegistry from '../field_registry.js';
|
import * as fieldRegistry from '../field_registry.js';
|
||||||
import type {RenderedConnection} from '../rendered_connection.js';
|
import {RenderedConnection} from '../rendered_connection.js';
|
||||||
import {Align} from './align.js';
|
import {Align} from './align.js';
|
||||||
import {inputTypes} from './input_types.js';
|
import {inputTypes} from './input_types.js';
|
||||||
|
|
||||||
@@ -181,15 +181,14 @@ export class Input {
|
|||||||
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
|
for (let y = 0, field; (field = this.fieldRow[y]); y++) {
|
||||||
field.setVisible(visible);
|
field.setVisible(visible);
|
||||||
}
|
}
|
||||||
if (this.connection) {
|
if (this.connection && this.connection instanceof RenderedConnection) {
|
||||||
const renderedConnection = this.connection as RenderedConnection;
|
|
||||||
// Has a connection.
|
// Has a connection.
|
||||||
if (visible) {
|
if (visible) {
|
||||||
renderList = renderedConnection.startTrackingAll();
|
renderList = this.connection.startTrackingAll();
|
||||||
} else {
|
} else {
|
||||||
renderedConnection.stopTrackingAll();
|
this.connection.stopTrackingAll();
|
||||||
}
|
}
|
||||||
const child = renderedConnection.targetBlock();
|
const child = this.connection.targetBlock();
|
||||||
if (child) {
|
if (child) {
|
||||||
child.getSvgRoot().style.display = visible ? 'block' : 'none';
|
child.getSvgRoot().style.display = visible ? 'block' : 'none';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,742 +0,0 @@
|
|||||||
/**
|
|
||||||
* @license
|
|
||||||
* Copyright 2017 Google LLC
|
|
||||||
* SPDX-License-Identifier: Apache-2.0
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that controls updates to connections during drags.
|
|
||||||
*
|
|
||||||
* @class
|
|
||||||
*/
|
|
||||||
// Former goog.module ID: Blockly.InsertionMarkerManager
|
|
||||||
|
|
||||||
import * as blockAnimations from './block_animations.js';
|
|
||||||
import type {BlockSvg} from './block_svg.js';
|
|
||||||
import * as common from './common.js';
|
|
||||||
import {ComponentManager} from './component_manager.js';
|
|
||||||
import {config} from './config.js';
|
|
||||||
import * as eventUtils from './events/utils.js';
|
|
||||||
import type {IDeleteArea} from './interfaces/i_delete_area.js';
|
|
||||||
import type {IDragTarget} from './interfaces/i_drag_target.js';
|
|
||||||
import * as renderManagement from './render_management.js';
|
|
||||||
import {finishQueuedRenders} from './render_management.js';
|
|
||||||
import type {RenderedConnection} from './rendered_connection.js';
|
|
||||||
import * as blocks from './serialization/blocks.js';
|
|
||||||
import type {Coordinate} from './utils/coordinate.js';
|
|
||||||
import type {WorkspaceSvg} from './workspace_svg.js';
|
|
||||||
|
|
||||||
/** Represents a nearby valid connection. */
|
|
||||||
interface CandidateConnection {
|
|
||||||
/**
|
|
||||||
* A nearby valid connection that is compatible with local.
|
|
||||||
* This is not on any of the blocks that are being dragged.
|
|
||||||
*/
|
|
||||||
closest: RenderedConnection;
|
|
||||||
/**
|
|
||||||
* A connection on the dragging stack that is compatible with closest. This is
|
|
||||||
* on the top block that is being dragged or the last block in the dragging
|
|
||||||
* stack.
|
|
||||||
*/
|
|
||||||
local: RenderedConnection;
|
|
||||||
radius: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class that controls updates to connections during drags. It is primarily
|
|
||||||
* responsible for finding the closest eligible connection and highlighting or
|
|
||||||
* unhighlighting it as needed during a drag.
|
|
||||||
*
|
|
||||||
* @deprecated v10 - Use an IConnectionPreviewer instead.
|
|
||||||
*/
|
|
||||||
export class InsertionMarkerManager {
|
|
||||||
/**
|
|
||||||
* The top block in the stack being dragged.
|
|
||||||
* Does not change during a drag.
|
|
||||||
*/
|
|
||||||
private readonly topBlock: BlockSvg;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The workspace on which these connections are being dragged.
|
|
||||||
* Does not change during a drag.
|
|
||||||
*/
|
|
||||||
private readonly workspace: WorkspaceSvg;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The last connection on the stack, if it's not the last connection on the
|
|
||||||
* first block.
|
|
||||||
* Set in initAvailableConnections, if at all.
|
|
||||||
*/
|
|
||||||
private lastOnStack: RenderedConnection | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The insertion marker corresponding to the last block in the stack, if
|
|
||||||
* that's not the same as the first block in the stack.
|
|
||||||
* Set in initAvailableConnections, if at all
|
|
||||||
*/
|
|
||||||
private lastMarker: BlockSvg | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The insertion marker that shows up between blocks to show where a block
|
|
||||||
* would go if dropped immediately.
|
|
||||||
*/
|
|
||||||
private firstMarker: BlockSvg;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Information about the connection that would be made if the dragging block
|
|
||||||
* were released immediately. Updated on every mouse move.
|
|
||||||
*/
|
|
||||||
private activeCandidate: CandidateConnection | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the block would be deleted if it were dropped immediately.
|
|
||||||
* Updated on every mouse move.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
public wouldDeleteBlock = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connection on the insertion marker block that corresponds to
|
|
||||||
* the active candidate's local connection on the currently dragged block.
|
|
||||||
*/
|
|
||||||
private markerConnection: RenderedConnection | null = null;
|
|
||||||
|
|
||||||
/** The block that currently has an input being highlighted, or null. */
|
|
||||||
private highlightedBlock: BlockSvg | null = null;
|
|
||||||
|
|
||||||
/** The block being faded to indicate replacement, or null. */
|
|
||||||
private fadedBlock: BlockSvg | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The connections on the dragging blocks that are available to connect to
|
|
||||||
* other blocks. This includes all open connections on the top block, as
|
|
||||||
* well as the last connection on the block stack.
|
|
||||||
*/
|
|
||||||
private availableConnections: RenderedConnection[];
|
|
||||||
|
|
||||||
/** @param block The top block in the stack being dragged. */
|
|
||||||
constructor(block: BlockSvg) {
|
|
||||||
common.setSelected(block);
|
|
||||||
this.topBlock = block;
|
|
||||||
|
|
||||||
this.workspace = block.workspace;
|
|
||||||
|
|
||||||
this.firstMarker = this.createMarkerBlock(this.topBlock);
|
|
||||||
|
|
||||||
this.availableConnections = this.initAvailableConnections();
|
|
||||||
|
|
||||||
if (this.lastOnStack) {
|
|
||||||
this.lastMarker = this.createMarkerBlock(
|
|
||||||
this.lastOnStack.getSourceBlock(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sever all links from this object.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
dispose() {
|
|
||||||
this.availableConnections.length = 0;
|
|
||||||
this.disposeInsertionMarker(this.firstMarker);
|
|
||||||
this.disposeInsertionMarker(this.lastMarker);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the available connections for the top block. These connections can
|
|
||||||
* change if a block is unplugged and the stack is healed.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
updateAvailableConnections() {
|
|
||||||
this.availableConnections = this.initAvailableConnections();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether the block would be connected if dropped immediately, based
|
|
||||||
* on information from the most recent move event.
|
|
||||||
*
|
|
||||||
* @returns True if the block would be connected if dropped immediately.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
wouldConnectBlock(): boolean {
|
|
||||||
return !!this.activeCandidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Connect to the closest connection and render the results.
|
|
||||||
* This should be called at the end of a drag.
|
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
applyConnections() {
|
|
||||||
if (!this.activeCandidate) return;
|
|
||||||
eventUtils.disable();
|
|
||||||
this.hidePreview();
|
|
||||||
eventUtils.enable();
|
|
||||||
const {local, closest} = this.activeCandidate;
|
|
||||||
local.connect(closest);
|
|
||||||
const inferiorConnection = local.isSuperior() ? closest : local;
|
|
||||||
const rootBlock = this.topBlock.getRootBlock();
|
|
||||||
|
|
||||||
finishQueuedRenders().then(() => {
|
|
||||||
blockAnimations.connectionUiEffect(inferiorConnection.getSourceBlock());
|
|
||||||
// bringToFront is incredibly expensive. Delay until the next frame.
|
|
||||||
setTimeout(() => {
|
|
||||||
rootBlock.bringToFront();
|
|
||||||
}, 0);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update connections based on the most recent move location.
|
|
||||||
*
|
|
||||||
* @param dxy Position relative to drag start, in workspace units.
|
|
||||||
* @param dragTarget The drag target that the block is currently over.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
update(dxy: Coordinate, dragTarget: IDragTarget | null) {
|
|
||||||
const newCandidate = this.getCandidate(dxy);
|
|
||||||
|
|
||||||
this.wouldDeleteBlock = this.shouldDelete(!!newCandidate, dragTarget);
|
|
||||||
|
|
||||||
const shouldUpdate =
|
|
||||||
this.wouldDeleteBlock || this.shouldUpdatePreviews(newCandidate, dxy);
|
|
||||||
|
|
||||||
if (shouldUpdate) {
|
|
||||||
// Don't fire events for insertion marker creation or movement.
|
|
||||||
eventUtils.disable();
|
|
||||||
this.maybeHidePreview(newCandidate);
|
|
||||||
this.maybeShowPreview(newCandidate);
|
|
||||||
eventUtils.enable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create an insertion marker that represents the given block.
|
|
||||||
*
|
|
||||||
* @param sourceBlock The block that the insertion marker will represent.
|
|
||||||
* @returns The insertion marker that represents the given block.
|
|
||||||
*/
|
|
||||||
private createMarkerBlock(sourceBlock: BlockSvg): BlockSvg {
|
|
||||||
eventUtils.disable();
|
|
||||||
let result: BlockSvg;
|
|
||||||
try {
|
|
||||||
const blockJson = blocks.save(sourceBlock, {
|
|
||||||
addCoordinates: false,
|
|
||||||
addInputBlocks: false,
|
|
||||||
addNextBlocks: false,
|
|
||||||
doFullSerialization: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!blockJson) {
|
|
||||||
throw new Error(
|
|
||||||
`Failed to serialize source block. ${sourceBlock.toDevString()}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
result = blocks.append(blockJson, this.workspace) as BlockSvg;
|
|
||||||
|
|
||||||
// Turn shadow blocks that are created programmatically during
|
|
||||||
// initalization to insertion markers too.
|
|
||||||
for (const block of result.getDescendants(false)) {
|
|
||||||
block.setInsertionMarker(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
result.initSvg();
|
|
||||||
result.getSvgRoot().setAttribute('visibility', 'hidden');
|
|
||||||
} finally {
|
|
||||||
eventUtils.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Populate the list of available connections on this block stack. If the
|
|
||||||
* stack has more than one block, this function will also update lastOnStack.
|
|
||||||
*
|
|
||||||
* @returns A list of available connections.
|
|
||||||
*/
|
|
||||||
private initAvailableConnections(): RenderedConnection[] {
|
|
||||||
const available = this.topBlock.getConnections_(false);
|
|
||||||
// Also check the last connection on this stack
|
|
||||||
const lastOnStack = this.topBlock.lastConnectionInStack(true);
|
|
||||||
if (lastOnStack && lastOnStack !== this.topBlock.nextConnection) {
|
|
||||||
available.push(lastOnStack);
|
|
||||||
this.lastOnStack = lastOnStack;
|
|
||||||
}
|
|
||||||
return available;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether the previews (insertion marker and replacement marker) should be
|
|
||||||
* updated based on the closest candidate and the current drag distance.
|
|
||||||
*
|
|
||||||
* @param newCandidate A new candidate connection that may replace the current
|
|
||||||
* best candidate.
|
|
||||||
* @param dxy Position relative to drag start, in workspace units.
|
|
||||||
* @returns Whether the preview should be updated.
|
|
||||||
*/
|
|
||||||
private shouldUpdatePreviews(
|
|
||||||
newCandidate: CandidateConnection | null,
|
|
||||||
dxy: Coordinate,
|
|
||||||
): boolean {
|
|
||||||
// Only need to update if we were showing a preview before.
|
|
||||||
if (!newCandidate) return !!this.activeCandidate;
|
|
||||||
|
|
||||||
// We weren't showing a preview before, but we should now.
|
|
||||||
if (!this.activeCandidate) return true;
|
|
||||||
|
|
||||||
// We're already showing an insertion marker.
|
|
||||||
// Decide whether the new connection has higher priority.
|
|
||||||
const {local: activeLocal, closest: activeClosest} = this.activeCandidate;
|
|
||||||
if (
|
|
||||||
activeClosest === newCandidate.closest &&
|
|
||||||
activeLocal === newCandidate.local
|
|
||||||
) {
|
|
||||||
// The connection was the same as the current connection.
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const xDiff = activeLocal.x + dxy.x - activeClosest.x;
|
|
||||||
const yDiff = activeLocal.y + dxy.y - activeClosest.y;
|
|
||||||
const curDistance = Math.sqrt(xDiff * xDiff + yDiff * yDiff);
|
|
||||||
// Slightly prefer the existing preview over a new preview.
|
|
||||||
return (
|
|
||||||
newCandidate.radius < curDistance - config.currentConnectionPreference
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Find the nearest valid connection, which may be the same as the current
|
|
||||||
* closest connection.
|
|
||||||
*
|
|
||||||
* @param dxy Position relative to drag start, in workspace units.
|
|
||||||
* @returns An object containing a local connection, a closest connection, and
|
|
||||||
* a radius.
|
|
||||||
*/
|
|
||||||
private getCandidate(dxy: Coordinate): CandidateConnection | null {
|
|
||||||
// It's possible that a block has added or removed connections during a
|
|
||||||
// drag, (e.g. in a drag/move event handler), so let's update the available
|
|
||||||
// connections. Note that this will be called on every move while dragging,
|
|
||||||
// so it might cause slowness, especially if the block stack is large. If
|
|
||||||
// so, maybe it could be made more efficient. Also note that we won't update
|
|
||||||
// the connections if we've already connected the insertion marker to a
|
|
||||||
// block.
|
|
||||||
if (!this.markerConnection || !this.markerConnection.isConnected()) {
|
|
||||||
this.updateAvailableConnections();
|
|
||||||
}
|
|
||||||
|
|
||||||
let radius = this.getStartRadius();
|
|
||||||
let candidate = null;
|
|
||||||
for (let i = 0; i < this.availableConnections.length; i++) {
|
|
||||||
const myConnection = this.availableConnections[i];
|
|
||||||
const neighbour = myConnection.closest(radius, dxy);
|
|
||||||
if (neighbour.connection) {
|
|
||||||
candidate = {
|
|
||||||
closest: neighbour.connection,
|
|
||||||
local: myConnection,
|
|
||||||
radius: neighbour.radius,
|
|
||||||
};
|
|
||||||
radius = neighbour.radius;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return candidate;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decide the radius at which to start searching for the closest connection.
|
|
||||||
*
|
|
||||||
* @returns The radius at which to start the search for the closest
|
|
||||||
* connection.
|
|
||||||
*/
|
|
||||||
private getStartRadius(): number {
|
|
||||||
// If there is already a connection highlighted,
|
|
||||||
// increase the radius we check for making new connections.
|
|
||||||
// When a connection is highlighted, blocks move around when the
|
|
||||||
// insertion marker is created, which could cause the connection became out
|
|
||||||
// of range. By increasing radiusConnection when a connection already
|
|
||||||
// exists, we never "lose" the connection from the offset.
|
|
||||||
return this.activeCandidate
|
|
||||||
? config.connectingSnapRadius
|
|
||||||
: config.snapRadius;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether ending the drag would delete the block.
|
|
||||||
*
|
|
||||||
* @param newCandidate Whether there is a candidate connection that the
|
|
||||||
* block could connect to if the drag ended immediately.
|
|
||||||
* @param dragTarget The drag target that the block is currently over.
|
|
||||||
* @returns Whether dropping the block immediately would delete the block.
|
|
||||||
*/
|
|
||||||
private shouldDelete(
|
|
||||||
newCandidate: boolean,
|
|
||||||
dragTarget: IDragTarget | null,
|
|
||||||
): boolean {
|
|
||||||
if (dragTarget) {
|
|
||||||
const componentManager = this.workspace.getComponentManager();
|
|
||||||
const isDeleteArea = componentManager.hasCapability(
|
|
||||||
dragTarget.id,
|
|
||||||
ComponentManager.Capability.DELETE_AREA,
|
|
||||||
);
|
|
||||||
if (isDeleteArea) {
|
|
||||||
return (dragTarget as IDeleteArea).wouldDelete(this.topBlock);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show an insertion marker or replacement highlighting during a drag, if
|
|
||||||
* needed.
|
|
||||||
* At the beginning of this function, this.activeConnection should be null.
|
|
||||||
*
|
|
||||||
* @param newCandidate A new candidate connection that may replace the current
|
|
||||||
* best candidate.
|
|
||||||
*/
|
|
||||||
private maybeShowPreview(newCandidate: CandidateConnection | null) {
|
|
||||||
if (this.wouldDeleteBlock) return; // Nope, don't add a marker.
|
|
||||||
if (!newCandidate) return; // Nothing to connect to.
|
|
||||||
|
|
||||||
const closest = newCandidate.closest;
|
|
||||||
|
|
||||||
// Something went wrong and we're trying to connect to an invalid
|
|
||||||
// connection.
|
|
||||||
if (
|
|
||||||
closest === this.activeCandidate?.closest ||
|
|
||||||
closest.getSourceBlock().isInsertionMarker()
|
|
||||||
) {
|
|
||||||
console.log('Trying to connect to an insertion marker');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.activeCandidate = newCandidate;
|
|
||||||
// Add an insertion marker or replacement marker.
|
|
||||||
this.showPreview(this.activeCandidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A preview should be shown. This function figures out if it should be a
|
|
||||||
* block highlight or an insertion marker, and shows the appropriate one.
|
|
||||||
*
|
|
||||||
* @param activeCandidate The connection that will be made if the drag ends
|
|
||||||
* immediately.
|
|
||||||
*/
|
|
||||||
private showPreview(activeCandidate: CandidateConnection) {
|
|
||||||
const renderer = this.workspace.getRenderer();
|
|
||||||
const method = renderer.getConnectionPreviewMethod(
|
|
||||||
activeCandidate.closest,
|
|
||||||
activeCandidate.local,
|
|
||||||
this.topBlock,
|
|
||||||
);
|
|
||||||
|
|
||||||
switch (method) {
|
|
||||||
case InsertionMarkerManager.PREVIEW_TYPE.INPUT_OUTLINE:
|
|
||||||
this.showInsertionInputOutline(activeCandidate);
|
|
||||||
break;
|
|
||||||
case InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER:
|
|
||||||
this.showInsertionMarker(activeCandidate);
|
|
||||||
break;
|
|
||||||
case InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE:
|
|
||||||
this.showReplacementFade(activeCandidate);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally highlight the actual connection, as a nod to previous
|
|
||||||
// behaviour.
|
|
||||||
if (renderer.shouldHighlightConnection(activeCandidate.closest)) {
|
|
||||||
activeCandidate.closest.highlight();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hide an insertion marker or replacement highlighting during a drag, if
|
|
||||||
* needed.
|
|
||||||
* At the end of this function, this.activeCandidate will be null.
|
|
||||||
*
|
|
||||||
* @param newCandidate A new candidate connection that may replace the current
|
|
||||||
* best candidate.
|
|
||||||
*/
|
|
||||||
private maybeHidePreview(newCandidate: CandidateConnection | null) {
|
|
||||||
// If there's no new preview, remove the old one but don't bother deleting
|
|
||||||
// it. We might need it later, and this saves disposing of it and recreating
|
|
||||||
// it.
|
|
||||||
if (!newCandidate) {
|
|
||||||
this.hidePreview();
|
|
||||||
} else {
|
|
||||||
if (this.activeCandidate) {
|
|
||||||
const closestChanged =
|
|
||||||
this.activeCandidate.closest !== newCandidate.closest;
|
|
||||||
const localChanged = this.activeCandidate.local !== newCandidate.local;
|
|
||||||
|
|
||||||
// If there's a new preview and there was a preview before, and either
|
|
||||||
// connection has changed, remove the old preview.
|
|
||||||
// Also hide if we had a preview before but now we're going to delete
|
|
||||||
// instead.
|
|
||||||
if (closestChanged || localChanged || this.wouldDeleteBlock) {
|
|
||||||
this.hidePreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either way, clear out old state.
|
|
||||||
this.markerConnection = null;
|
|
||||||
this.activeCandidate = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A preview should be hidden. Loop through all possible preview modes
|
|
||||||
* and hide everything.
|
|
||||||
*/
|
|
||||||
private hidePreview() {
|
|
||||||
const closest = this.activeCandidate?.closest;
|
|
||||||
if (
|
|
||||||
closest &&
|
|
||||||
closest.targetBlock() &&
|
|
||||||
this.workspace.getRenderer().shouldHighlightConnection(closest)
|
|
||||||
) {
|
|
||||||
closest.unhighlight();
|
|
||||||
}
|
|
||||||
this.hideReplacementFade();
|
|
||||||
this.hideInsertionInputOutline();
|
|
||||||
this.hideInsertionMarker();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows an insertion marker connected to the appropriate blocks (based on
|
|
||||||
* manager state).
|
|
||||||
*
|
|
||||||
* @param activeCandidate The connection that will be made if the drag ends
|
|
||||||
* immediately.
|
|
||||||
*/
|
|
||||||
private showInsertionMarker(activeCandidate: CandidateConnection) {
|
|
||||||
const {local, closest} = activeCandidate;
|
|
||||||
|
|
||||||
const isLastInStack = this.lastOnStack && local === this.lastOnStack;
|
|
||||||
let insertionMarker = isLastInStack ? this.lastMarker : this.firstMarker;
|
|
||||||
if (!insertionMarker) {
|
|
||||||
throw new Error(
|
|
||||||
'Cannot show the insertion marker because there is no insertion ' +
|
|
||||||
'marker block',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let imConn;
|
|
||||||
try {
|
|
||||||
imConn = insertionMarker.getMatchingConnection(
|
|
||||||
local.getSourceBlock(),
|
|
||||||
local,
|
|
||||||
);
|
|
||||||
} catch {
|
|
||||||
// It's possible that the number of connections on the local block has
|
|
||||||
// changed since the insertion marker was originally created. Let's
|
|
||||||
// recreate the insertion marker and try again. In theory we could
|
|
||||||
// probably recreate the marker block (e.g. in getCandidate_), which is
|
|
||||||
// called more often during the drag, but creating a block that often
|
|
||||||
// might be too slow, so we only do it if necessary.
|
|
||||||
if (isLastInStack && this.lastOnStack) {
|
|
||||||
this.disposeInsertionMarker(this.lastMarker);
|
|
||||||
this.lastMarker = this.createMarkerBlock(
|
|
||||||
this.lastOnStack.getSourceBlock(),
|
|
||||||
);
|
|
||||||
insertionMarker = this.lastMarker;
|
|
||||||
} else {
|
|
||||||
this.disposeInsertionMarker(this.firstMarker);
|
|
||||||
this.firstMarker = this.createMarkerBlock(this.topBlock);
|
|
||||||
insertionMarker = this.firstMarker;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!insertionMarker) {
|
|
||||||
throw new Error(
|
|
||||||
'Cannot show the insertion marker because there is no insertion ' +
|
|
||||||
'marker block',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
imConn = insertionMarker.getMatchingConnection(
|
|
||||||
local.getSourceBlock(),
|
|
||||||
local,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!imConn) {
|
|
||||||
throw new Error(
|
|
||||||
'Cannot show the insertion marker because there is no ' +
|
|
||||||
'associated connection',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (imConn === this.markerConnection) {
|
|
||||||
throw new Error(
|
|
||||||
"Made it to showInsertionMarker_ even though the marker isn't " +
|
|
||||||
'changing',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render disconnected from everything else so that we have a valid
|
|
||||||
// connection location.
|
|
||||||
insertionMarker.queueRender();
|
|
||||||
renderManagement.triggerQueuedRenders();
|
|
||||||
|
|
||||||
// Connect() also renders the insertion marker.
|
|
||||||
imConn.connect(closest);
|
|
||||||
|
|
||||||
const originalOffsetToTarget = {
|
|
||||||
x: closest.x - imConn.x,
|
|
||||||
y: closest.y - imConn.y,
|
|
||||||
};
|
|
||||||
const originalOffsetInBlock = imConn.getOffsetInBlock().clone();
|
|
||||||
const imConnConst = imConn;
|
|
||||||
renderManagement.finishQueuedRenders().then(() => {
|
|
||||||
// Position so that the existing block doesn't move.
|
|
||||||
insertionMarker?.positionNearConnection(
|
|
||||||
imConnConst,
|
|
||||||
originalOffsetToTarget,
|
|
||||||
originalOffsetInBlock,
|
|
||||||
);
|
|
||||||
insertionMarker?.getSvgRoot().setAttribute('visibility', 'visible');
|
|
||||||
});
|
|
||||||
|
|
||||||
this.markerConnection = imConn;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Disconnects and hides the current insertion marker. Should return the
|
|
||||||
* blocks to their original state.
|
|
||||||
*/
|
|
||||||
private hideInsertionMarker() {
|
|
||||||
if (!this.markerConnection) return;
|
|
||||||
|
|
||||||
const markerConn = this.markerConnection;
|
|
||||||
const imBlock = markerConn.getSourceBlock();
|
|
||||||
const markerPrev = imBlock.previousConnection;
|
|
||||||
const markerOutput = imBlock.outputConnection;
|
|
||||||
|
|
||||||
if (!markerPrev?.targetConnection && !markerOutput?.targetConnection) {
|
|
||||||
// If we are the top block, unplugging doesn't do anything.
|
|
||||||
// The marker connection may not have a target block if we are hiding
|
|
||||||
// as part of applying connections.
|
|
||||||
markerConn.targetBlock()?.unplug(false);
|
|
||||||
} else {
|
|
||||||
imBlock.unplug(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (markerConn.targetConnection) {
|
|
||||||
throw Error(
|
|
||||||
'markerConnection still connected at the end of ' +
|
|
||||||
'disconnectInsertionMarker',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.markerConnection = null;
|
|
||||||
const svg = imBlock.getSvgRoot();
|
|
||||||
if (svg) {
|
|
||||||
svg.setAttribute('visibility', 'hidden');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows an outline around the input the closest connection belongs to.
|
|
||||||
*
|
|
||||||
* @param activeCandidate The connection that will be made if the drag ends
|
|
||||||
* immediately.
|
|
||||||
*/
|
|
||||||
private showInsertionInputOutline(activeCandidate: CandidateConnection) {
|
|
||||||
const closest = activeCandidate.closest;
|
|
||||||
this.highlightedBlock = closest.getSourceBlock();
|
|
||||||
this.highlightedBlock.highlightShapeForInput(closest, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Hides any visible input outlines. */
|
|
||||||
private hideInsertionInputOutline() {
|
|
||||||
if (!this.highlightedBlock) return;
|
|
||||||
|
|
||||||
if (!this.activeCandidate) {
|
|
||||||
throw new Error(
|
|
||||||
'Cannot hide the insertion marker outline because ' +
|
|
||||||
'there is no active candidate',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.highlightedBlock.highlightShapeForInput(
|
|
||||||
this.activeCandidate.closest,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
this.highlightedBlock = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a replacement fade affect on the closest connection's target block
|
|
||||||
* (the block that is currently connected to it).
|
|
||||||
*
|
|
||||||
* @param activeCandidate The connection that will be made if the drag ends
|
|
||||||
* immediately.
|
|
||||||
*/
|
|
||||||
private showReplacementFade(activeCandidate: CandidateConnection) {
|
|
||||||
this.fadedBlock = activeCandidate.closest.targetBlock();
|
|
||||||
if (!this.fadedBlock) {
|
|
||||||
throw new Error(
|
|
||||||
'Cannot show the replacement fade because the ' +
|
|
||||||
'closest connection does not have a target block',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
this.fadedBlock.fadeForReplacement(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hides/Removes any visible fade affects.
|
|
||||||
*/
|
|
||||||
private hideReplacementFade() {
|
|
||||||
if (!this.fadedBlock) return;
|
|
||||||
|
|
||||||
this.fadedBlock.fadeForReplacement(false);
|
|
||||||
this.fadedBlock = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of the insertion markers that currently exist. Drags have 0, 1,
|
|
||||||
* or 2 insertion markers.
|
|
||||||
*
|
|
||||||
* @returns A possibly empty list of insertion marker blocks.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
getInsertionMarkers(): BlockSvg[] {
|
|
||||||
const result = [];
|
|
||||||
if (this.firstMarker) {
|
|
||||||
result.push(this.firstMarker);
|
|
||||||
}
|
|
||||||
if (this.lastMarker) {
|
|
||||||
result.push(this.lastMarker);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Safely disposes of an insertion marker.
|
|
||||||
*/
|
|
||||||
private disposeInsertionMarker(marker: BlockSvg | null) {
|
|
||||||
if (marker) {
|
|
||||||
eventUtils.disable();
|
|
||||||
try {
|
|
||||||
marker.dispose();
|
|
||||||
} finally {
|
|
||||||
eventUtils.enable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export namespace InsertionMarkerManager {
|
|
||||||
/**
|
|
||||||
* An enum describing different kinds of previews the InsertionMarkerManager
|
|
||||||
* could display.
|
|
||||||
*/
|
|
||||||
export enum PREVIEW_TYPE {
|
|
||||||
INSERTION_MARKER = 0,
|
|
||||||
INPUT_OUTLINE = 1,
|
|
||||||
REPLACEMENT_FADE = 2,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PreviewType = InsertionMarkerManager.PREVIEW_TYPE;
|
|
||||||
export const PreviewType = InsertionMarkerManager.PREVIEW_TYPE;
|
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import {CommentState} from '../icons/comment_icon.js';
|
import {CommentState} from '../icons/comment_icon.js';
|
||||||
import {IconType} from '../icons/icon_types.js';
|
import {IconType} from '../icons/icon_types.js';
|
||||||
|
import {Coordinate} from '../utils/coordinate.js';
|
||||||
import {Size} from '../utils/size.js';
|
import {Size} from '../utils/size.js';
|
||||||
import {IHasBubble, hasBubble} from './i_has_bubble.js';
|
import {IHasBubble, hasBubble} from './i_has_bubble.js';
|
||||||
import {IIcon, isIcon} from './i_icon.js';
|
import {IIcon, isIcon} from './i_icon.js';
|
||||||
@@ -20,6 +21,10 @@ export interface ICommentIcon extends IIcon, IHasBubble, ISerializable {
|
|||||||
|
|
||||||
getBubbleSize(): Size;
|
getBubbleSize(): Size;
|
||||||
|
|
||||||
|
setBubbleLocation(location: Coordinate): void;
|
||||||
|
|
||||||
|
getBubbleLocation(): Coordinate | undefined;
|
||||||
|
|
||||||
saveState(): CommentState;
|
saveState(): CommentState;
|
||||||
|
|
||||||
loadState(state: CommentState): void;
|
loadState(state: CommentState): void;
|
||||||
@@ -35,6 +40,8 @@ export function isCommentIcon(obj: object): obj is ICommentIcon {
|
|||||||
(obj as any)['getText'] !== undefined &&
|
(obj as any)['getText'] !== undefined &&
|
||||||
(obj as any)['setBubbleSize'] !== undefined &&
|
(obj as any)['setBubbleSize'] !== undefined &&
|
||||||
(obj as any)['getBubbleSize'] !== undefined &&
|
(obj as any)['getBubbleSize'] !== undefined &&
|
||||||
|
(obj as any)['setBubbleLocation'] !== undefined &&
|
||||||
|
(obj as any)['getBubbleLocation'] !== undefined &&
|
||||||
obj.getType() === IconType.COMMENT
|
obj.getType() === IconType.COMMENT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
// Former goog.module ID: Blockly.IFlyout
|
// Former goog.module ID: Blockly.IFlyout
|
||||||
|
|
||||||
import type {BlockSvg} from '../block_svg.js';
|
import type {BlockSvg} from '../block_svg.js';
|
||||||
import {FlyoutItem} from '../flyout_base.js';
|
import type {FlyoutItem} from '../flyout_item.js';
|
||||||
import type {Coordinate} from '../utils/coordinate.js';
|
import type {Coordinate} from '../utils/coordinate.js';
|
||||||
import type {Svg} from '../utils/svg.js';
|
import type {Svg} from '../utils/svg.js';
|
||||||
import type {FlyoutDefinition} from '../utils/toolbox.js';
|
import type {FlyoutDefinition} from '../utils/toolbox.js';
|
||||||
|
|||||||
51
core/interfaces/i_flyout_inflater.ts
Normal file
51
core/interfaces/i_flyout_inflater.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import type {FlyoutItem} from '../flyout_item.js';
|
||||||
|
import type {IFlyout} from './i_flyout.js';
|
||||||
|
|
||||||
|
export interface IFlyoutInflater {
|
||||||
|
/**
|
||||||
|
* Loads the object represented by the given state onto the workspace.
|
||||||
|
*
|
||||||
|
* Note that this method's interface is identical to that in ISerializer, to
|
||||||
|
* allow for code reuse.
|
||||||
|
*
|
||||||
|
* @param state A JSON representation of an element to inflate on the flyout.
|
||||||
|
* @param flyout The flyout on whose workspace the inflated element
|
||||||
|
* should be created. If the inflated element is an `IRenderedElement` it
|
||||||
|
* itself or the inflater should append it to the workspace; the flyout
|
||||||
|
* will not do so itself. The flyout is responsible for positioning the
|
||||||
|
* element, however.
|
||||||
|
* @returns The newly inflated flyout element.
|
||||||
|
*/
|
||||||
|
load(state: object, flyout: IFlyout): FlyoutItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of spacing that should follow the element corresponding
|
||||||
|
* to the given JSON representation.
|
||||||
|
*
|
||||||
|
* @param state A JSON representation of the element preceding the gap.
|
||||||
|
* @param defaultGap The default gap for elements in this flyout.
|
||||||
|
* @returns The gap that should follow the given element.
|
||||||
|
*/
|
||||||
|
gapForItem(state: object, defaultGap: number): number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the given element.
|
||||||
|
*
|
||||||
|
* If the element in question resides on the flyout workspace, it should remove
|
||||||
|
* itself. Implementers are not otherwise required to fully dispose of the
|
||||||
|
* element; it may be e.g. cached for performance purposes.
|
||||||
|
*
|
||||||
|
* @param element The flyout element to dispose of.
|
||||||
|
*/
|
||||||
|
disposeItem(item: FlyoutItem): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of items that this inflater is responsible for inflating.
|
||||||
|
* This should be the same as the name under which this inflater registers
|
||||||
|
* itself, as well as the value returned by `getType()` on the `FlyoutItem`
|
||||||
|
* objects returned by `load()`.
|
||||||
|
*
|
||||||
|
* @returns The type of items this inflater creates.
|
||||||
|
*/
|
||||||
|
getType(): string;
|
||||||
|
}
|
||||||
36
core/interfaces/i_focusable_node.ts
Normal file
36
core/interfaces/i_focusable_node.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {IFocusableTree} from './i_focusable_tree.js';
|
||||||
|
|
||||||
|
/** Represents anything that can have input focus. */
|
||||||
|
export interface IFocusableNode {
|
||||||
|
/**
|
||||||
|
* Returns the DOM element that can be explicitly requested to receive focus.
|
||||||
|
*
|
||||||
|
* IMPORTANT: Please note that this element is expected to have a visual
|
||||||
|
* presence on the page as it will both be explicitly focused and have its
|
||||||
|
* style changed depending on its current focus state (i.e. blurred, actively
|
||||||
|
* focused, and passively focused). The element will have one of two styles
|
||||||
|
* attached (where no style indicates blurred/not focused):
|
||||||
|
* - blocklyActiveFocus
|
||||||
|
* - blocklyPassiveFocus
|
||||||
|
*
|
||||||
|
* The returned element must also have a valid ID specified, and unique to the
|
||||||
|
* element relative to its nearest IFocusableTree parent.
|
||||||
|
*
|
||||||
|
* It's expected the return element will not change for the lifetime of the
|
||||||
|
* node.
|
||||||
|
*/
|
||||||
|
getFocusableElement(): HTMLElement | SVGElement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the closest parent tree of this node (in cases where a tree has
|
||||||
|
* distinct trees underneath it), which represents the tree to which this node
|
||||||
|
* belongs.
|
||||||
|
*/
|
||||||
|
getFocusableTree(): IFocusableTree;
|
||||||
|
}
|
||||||
53
core/interfaces/i_focusable_tree.ts
Normal file
53
core/interfaces/i_focusable_tree.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {IFocusableNode} from './i_focusable_node.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a tree of focusable elements with its own active/passive focus
|
||||||
|
* context.
|
||||||
|
*
|
||||||
|
* Note that focus is handled by FocusManager, and tree implementations can have
|
||||||
|
* at most one IFocusableNode focused at one time. If the tree itself has focus,
|
||||||
|
* then the tree's focused node is considered 'active' ('passive' if another
|
||||||
|
* tree has focus).
|
||||||
|
*
|
||||||
|
* Focus is shared between one or more trees, where each tree can have exactly
|
||||||
|
* one active or passive node (and only one active node can exist on the whole
|
||||||
|
* page at any given time). The idea of passive focus is to provide context to
|
||||||
|
* users on where their focus will be restored upon navigating back to a
|
||||||
|
* previously focused tree.
|
||||||
|
*/
|
||||||
|
export interface IFocusableTree {
|
||||||
|
/**
|
||||||
|
* Returns the current node with focus in this tree, or null if none (or if
|
||||||
|
* the root has focus).
|
||||||
|
*
|
||||||
|
* Note that this will never return a node from a nested sub-tree as that tree
|
||||||
|
* should specifically be called in order to retrieve its focused node.
|
||||||
|
*/
|
||||||
|
getFocusedNode(): IFocusableNode | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the top-level focusable node of the tree.
|
||||||
|
*
|
||||||
|
* It's expected that the returned node will be focused in cases where
|
||||||
|
* FocusManager wants to focus a tree in a situation where it does not
|
||||||
|
* currently have a focused node.
|
||||||
|
*/
|
||||||
|
getRootFocusableNode(): IFocusableNode;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the IFocusableNode corresponding to the select element, or null if
|
||||||
|
* the element does not have such a node.
|
||||||
|
*
|
||||||
|
* The provided element must have a non-null ID that conforms to the contract
|
||||||
|
* mentioned in IFocusableNode.
|
||||||
|
*/
|
||||||
|
findFocusableNodeFor(
|
||||||
|
element: HTMLElement | SVGElement,
|
||||||
|
): IFocusableNode | null;
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ export interface IMetricsManager {
|
|||||||
* Gets the width, height and position of the toolbox on the workspace in
|
* Gets the width, height and position of the toolbox on the workspace in
|
||||||
* pixel coordinates. Returns 0 for the width and height if the workspace has
|
* pixel coordinates. Returns 0 for the width and height if the workspace has
|
||||||
* a simple toolbox instead of a category toolbox. To get the width and height
|
* a simple toolbox instead of a category toolbox. To get the width and height
|
||||||
* of a simple toolbox, see {@link IMetricsManager#getFlyoutMetrics}.
|
* of a simple toolbox, see {@link IMetricsManager.getFlyoutMetrics}.
|
||||||
*
|
*
|
||||||
* @returns The object with the width, height and position of the toolbox.
|
* @returns The object with the width, height and position of the toolbox.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -4,18 +4,15 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/** @internal */
|
|
||||||
export interface IRenderedElement {
|
export interface IRenderedElement {
|
||||||
/**
|
/**
|
||||||
* @returns The root SVG element of htis rendered element.
|
* @returns The root SVG element of this rendered element.
|
||||||
*/
|
*/
|
||||||
getSvgRoot(): SVGElement;
|
getSvgRoot(): SVGElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns True if the given object is an IRenderedElement.
|
* @returns True if the given object is an IRenderedElement.
|
||||||
*
|
|
||||||
* @internal
|
|
||||||
*/
|
*/
|
||||||
export function isRenderedElement(obj: any): obj is IRenderedElement {
|
export function isRenderedElement(obj: any): obj is IRenderedElement {
|
||||||
return obj['getSvgRoot'] !== undefined;
|
return obj['getSvgRoot'] !== undefined;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ export interface IToolbox extends IRegistrable {
|
|||||||
setVisible(isVisible: boolean): void;
|
setVisible(isVisible: boolean): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects the toolbox item by it's position in the list of toolbox items.
|
* Selects the toolbox item by its position in the list of toolbox items.
|
||||||
*
|
*
|
||||||
* @param position The position of the item to select.
|
* @param position The position of the item to select.
|
||||||
*/
|
*/
|
||||||
@@ -107,6 +107,14 @@ export interface IToolbox extends IRegistrable {
|
|||||||
*/
|
*/
|
||||||
getSelectedItem(): IToolboxItem | null;
|
getSelectedItem(): IToolboxItem | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the selected item.
|
||||||
|
*
|
||||||
|
* @param item The toolbox item to select, or null to remove the current
|
||||||
|
* selection.
|
||||||
|
*/
|
||||||
|
setSelectedItem(item: IToolboxItem | null): void;
|
||||||
|
|
||||||
/** Disposes of this toolbox. */
|
/** Disposes of this toolbox. */
|
||||||
dispose(): void;
|
dispose(): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,13 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type {VariableModel} from '../variable_model.js';
|
|
||||||
import {IParameterModel} from './i_parameter_model.js';
|
import {IParameterModel} from './i_parameter_model.js';
|
||||||
|
import type {IVariableModel, IVariableState} from './i_variable_model.js';
|
||||||
|
|
||||||
/** Interface for a parameter model that holds a variable model. */
|
/** Interface for a parameter model that holds a variable model. */
|
||||||
export interface IVariableBackedParameterModel extends IParameterModel {
|
export interface IVariableBackedParameterModel extends IParameterModel {
|
||||||
/** Returns the variable model held by this type. */
|
/** Returns the variable model held by this type. */
|
||||||
getVariableModel(): VariableModel;
|
getVariableModel(): IVariableModel<IVariableState>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
65
core/interfaces/i_variable_map.ts
Normal file
65
core/interfaces/i_variable_map.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {IVariableModel, IVariableState} from './i_variable_model.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Variable maps are container objects responsible for storing and managing the
|
||||||
|
* set of variables referenced on a workspace.
|
||||||
|
*
|
||||||
|
* Any of these methods may define invariants about which names and types are
|
||||||
|
* legal, and throw if they are not met.
|
||||||
|
*/
|
||||||
|
export interface IVariableMap<T extends IVariableModel<IVariableState>> {
|
||||||
|
/* Returns the variable corresponding to the given ID, or null if none. */
|
||||||
|
getVariableById(id: string): T | null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the variable with the given name, or null if not found. If `type`
|
||||||
|
* is provided, the variable's type must also match, or null should be
|
||||||
|
* returned.
|
||||||
|
*/
|
||||||
|
getVariable(name: string, type?: string): T | null;
|
||||||
|
|
||||||
|
/* Returns a list of all variables managed by this variable map. */
|
||||||
|
getAllVariables(): T[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all of the variables of the given type managed by this
|
||||||
|
* variable map.
|
||||||
|
*/
|
||||||
|
getVariablesOfType(type: string): T[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of the set of types of the variables managed by this
|
||||||
|
* variable map.
|
||||||
|
*/
|
||||||
|
getTypes(): string[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new variable with the given name. If ID is not specified, the
|
||||||
|
* variable map should create one. Returns the new variable.
|
||||||
|
*/
|
||||||
|
createVariable(name: string, id?: string, type?: string | null): T;
|
||||||
|
|
||||||
|
/* Adds a variable to this variable map. */
|
||||||
|
addVariable(variable: T): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the name of the given variable to the name provided and returns the
|
||||||
|
* renamed variable.
|
||||||
|
*/
|
||||||
|
renameVariable(variable: T, newName: string): T;
|
||||||
|
|
||||||
|
/* Changes the type of the given variable and returns it. */
|
||||||
|
changeVariableType(variable: T, newType: string): T;
|
||||||
|
|
||||||
|
/* Deletes the given variable. */
|
||||||
|
deleteVariable(variable: T): void;
|
||||||
|
|
||||||
|
/* Removes all variables from this variable map. */
|
||||||
|
clear(): void;
|
||||||
|
}
|
||||||
57
core/interfaces/i_variable_model.ts
Normal file
57
core/interfaces/i_variable_model.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import type {Workspace} from '../workspace.js';
|
||||||
|
|
||||||
|
/* Representation of a variable. */
|
||||||
|
export interface IVariableModel<T extends IVariableState> {
|
||||||
|
/* Returns the unique ID of this variable. */
|
||||||
|
getId(): string;
|
||||||
|
|
||||||
|
/* Returns the user-visible name of this variable. */
|
||||||
|
getName(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of the variable like 'int' or 'string'. Does not need to be
|
||||||
|
* unique. This will default to '' which is a specific type.
|
||||||
|
*/
|
||||||
|
getType(): string;
|
||||||
|
|
||||||
|
/* Sets the user-visible name of this variable. */
|
||||||
|
setName(name: string): this;
|
||||||
|
|
||||||
|
/* Sets the type of this variable. */
|
||||||
|
setType(type: string): this;
|
||||||
|
|
||||||
|
getWorkspace(): Workspace;
|
||||||
|
|
||||||
|
/* Serializes this variable */
|
||||||
|
save(): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IVariableModelStatic<T extends IVariableState> {
|
||||||
|
new (
|
||||||
|
workspace: Workspace,
|
||||||
|
name: string,
|
||||||
|
type?: string,
|
||||||
|
id?: string,
|
||||||
|
): IVariableModel<T>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new IVariableModel corresponding to the given state on the
|
||||||
|
* specified workspace. This method must be static in your implementation.
|
||||||
|
*/
|
||||||
|
load(state: T, workspace: Workspace): IVariableModel<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the state of a given variable.
|
||||||
|
*/
|
||||||
|
export interface IVariableState {
|
||||||
|
name: string;
|
||||||
|
id: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
@@ -13,11 +13,12 @@
|
|||||||
// Former goog.module ID: Blockly.ASTNode
|
// Former goog.module ID: Blockly.ASTNode
|
||||||
|
|
||||||
import {Block} from '../block.js';
|
import {Block} from '../block.js';
|
||||||
|
import {BlockSvg} from '../block_svg.js';
|
||||||
import type {Connection} from '../connection.js';
|
import type {Connection} from '../connection.js';
|
||||||
import {ConnectionType} from '../connection_type.js';
|
import {ConnectionType} from '../connection_type.js';
|
||||||
import type {Field} from '../field.js';
|
import type {Field} from '../field.js';
|
||||||
import {FlyoutItem} from '../flyout_base.js';
|
|
||||||
import {FlyoutButton} from '../flyout_button.js';
|
import {FlyoutButton} from '../flyout_button.js';
|
||||||
|
import type {FlyoutItem} from '../flyout_item.js';
|
||||||
import type {Input} from '../inputs/input.js';
|
import type {Input} from '../inputs/input.js';
|
||||||
import type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js';
|
import type {IASTNodeLocation} from '../interfaces/i_ast_node_location.js';
|
||||||
import type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js';
|
import type {IASTNodeLocationWithBlock} from '../interfaces/i_ast_node_location_with_block.js';
|
||||||
@@ -347,10 +348,11 @@ export class ASTNode {
|
|||||||
);
|
);
|
||||||
if (!nextItem) return null;
|
if (!nextItem) return null;
|
||||||
|
|
||||||
if (nextItem.type === 'button' && nextItem.button) {
|
const element = nextItem.getElement();
|
||||||
return ASTNode.createButtonNode(nextItem.button);
|
if (element instanceof FlyoutButton) {
|
||||||
} else if (nextItem.type === 'block' && nextItem.block) {
|
return ASTNode.createButtonNode(element);
|
||||||
return ASTNode.createStackNode(nextItem.block);
|
} else if (element instanceof BlockSvg) {
|
||||||
|
return ASTNode.createStackNode(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
@@ -370,12 +372,15 @@ export class ASTNode {
|
|||||||
forward: boolean,
|
forward: boolean,
|
||||||
): FlyoutItem | null {
|
): FlyoutItem | null {
|
||||||
const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => {
|
const currentIndex = flyoutContents.findIndex((item: FlyoutItem) => {
|
||||||
if (currentLocation instanceof Block && item.block === currentLocation) {
|
if (
|
||||||
|
currentLocation instanceof BlockSvg &&
|
||||||
|
item.getElement() === currentLocation
|
||||||
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
currentLocation instanceof FlyoutButton &&
|
currentLocation instanceof FlyoutButton &&
|
||||||
item.button === currentLocation
|
item.getElement() === currentLocation
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -384,7 +389,17 @@ export class ASTNode {
|
|||||||
|
|
||||||
if (currentIndex < 0) return null;
|
if (currentIndex < 0) return null;
|
||||||
|
|
||||||
const resultIndex = forward ? currentIndex + 1 : currentIndex - 1;
|
let resultIndex = forward ? currentIndex + 1 : currentIndex - 1;
|
||||||
|
// The flyout may contain non-focusable elements like spacers or custom
|
||||||
|
// items. If the next/previous element is one of those, keep looking until a
|
||||||
|
// focusable element is encountered.
|
||||||
|
while (
|
||||||
|
resultIndex >= 0 &&
|
||||||
|
resultIndex < flyoutContents.length &&
|
||||||
|
!flyoutContents[resultIndex].isFocusable()
|
||||||
|
) {
|
||||||
|
resultIndex += forward ? 1 : -1;
|
||||||
|
}
|
||||||
if (resultIndex === -1 || resultIndex === flyoutContents.length) {
|
if (resultIndex === -1 || resultIndex === flyoutContents.length) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
75
core/label_flyout_inflater.ts
Normal file
75
core/label_flyout_inflater.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2024 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {FlyoutButton} from './flyout_button.js';
|
||||||
|
import {FlyoutItem} from './flyout_item.js';
|
||||||
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||||
|
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||||
|
import * as registry from './registry.js';
|
||||||
|
import {ButtonOrLabelInfo} from './utils/toolbox.js';
|
||||||
|
const LABEL_TYPE = 'label';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class responsible for creating labels for flyouts.
|
||||||
|
*/
|
||||||
|
export class LabelFlyoutInflater implements IFlyoutInflater {
|
||||||
|
/**
|
||||||
|
* Inflates a flyout label from the given state and adds it to the flyout.
|
||||||
|
*
|
||||||
|
* @param state A JSON representation of a flyout label.
|
||||||
|
* @param flyout The flyout to create the label on.
|
||||||
|
* @returns A FlyoutButton configured as a label.
|
||||||
|
*/
|
||||||
|
load(state: object, flyout: IFlyout): FlyoutItem {
|
||||||
|
const label = new FlyoutButton(
|
||||||
|
flyout.getWorkspace(),
|
||||||
|
flyout.targetWorkspace!,
|
||||||
|
state as ButtonOrLabelInfo,
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
label.show();
|
||||||
|
|
||||||
|
return new FlyoutItem(label, LABEL_TYPE, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the amount of space that should follow this label.
|
||||||
|
*
|
||||||
|
* @param state A JSON representation of a flyout label.
|
||||||
|
* @param defaultGap The default spacing for flyout items.
|
||||||
|
* @returns The amount of space that should follow this label.
|
||||||
|
*/
|
||||||
|
gapForItem(state: object, defaultGap: number): number {
|
||||||
|
return defaultGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of the given label.
|
||||||
|
*
|
||||||
|
* @param item The flyout label to dispose of.
|
||||||
|
*/
|
||||||
|
disposeItem(item: FlyoutItem): void {
|
||||||
|
const element = item.getElement();
|
||||||
|
if (element instanceof FlyoutButton) {
|
||||||
|
element.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the type of items this inflater is responsible for creating.
|
||||||
|
*
|
||||||
|
* @returns An identifier for the type of items this inflater creates.
|
||||||
|
*/
|
||||||
|
getType() {
|
||||||
|
return LABEL_TYPE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.register(
|
||||||
|
registry.Type.FLYOUT_INFLATER,
|
||||||
|
LABEL_TYPE,
|
||||||
|
LabelFlyoutInflater,
|
||||||
|
);
|
||||||
41
core/menu.ts
41
core/menu.ts
@@ -12,10 +12,10 @@
|
|||||||
// Former goog.module ID: Blockly.Menu
|
// Former goog.module ID: Blockly.Menu
|
||||||
|
|
||||||
import * as browserEvents from './browser_events.js';
|
import * as browserEvents from './browser_events.js';
|
||||||
import type {MenuItem} from './menuitem.js';
|
import type {MenuSeparator} from './menu_separator.js';
|
||||||
|
import {MenuItem} from './menuitem.js';
|
||||||
import * as aria from './utils/aria.js';
|
import * as aria from './utils/aria.js';
|
||||||
import {Coordinate} from './utils/coordinate.js';
|
import {Coordinate} from './utils/coordinate.js';
|
||||||
import * as dom from './utils/dom.js';
|
|
||||||
import type {Size} from './utils/size.js';
|
import type {Size} from './utils/size.js';
|
||||||
import * as style from './utils/style.js';
|
import * as style from './utils/style.js';
|
||||||
|
|
||||||
@@ -24,11 +24,9 @@ import * as style from './utils/style.js';
|
|||||||
*/
|
*/
|
||||||
export class Menu {
|
export class Menu {
|
||||||
/**
|
/**
|
||||||
* Array of menu items.
|
* Array of menu items and separators.
|
||||||
* (Nulls are never in the array, but typing the array as nullable prevents
|
|
||||||
* the compiler from objecting to .indexOf(null))
|
|
||||||
*/
|
*/
|
||||||
private readonly menuItems: MenuItem[] = [];
|
private readonly menuItems: Array<MenuItem | MenuSeparator> = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Coordinates of the pointerdown event that caused this menu to open. Used to
|
* Coordinates of the pointerdown event that caused this menu to open. Used to
|
||||||
@@ -70,10 +68,10 @@ export class Menu {
|
|||||||
/**
|
/**
|
||||||
* Add a new menu item to the bottom of this menu.
|
* Add a new menu item to the bottom of this menu.
|
||||||
*
|
*
|
||||||
* @param menuItem Menu item to append.
|
* @param menuItem Menu item or separator to append.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
addChild(menuItem: MenuItem) {
|
addChild(menuItem: MenuItem | MenuSeparator) {
|
||||||
this.menuItems.push(menuItem);
|
this.menuItems.push(menuItem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,10 +81,10 @@ export class Menu {
|
|||||||
* @param container Element upon which to append this menu.
|
* @param container Element upon which to append this menu.
|
||||||
* @returns The menu's root DOM element.
|
* @returns The menu's root DOM element.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
render(container: Element): HTMLDivElement {
|
render(container: Element): HTMLDivElement {
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
// goog-menu is deprecated, use blocklyMenu. May 2020.
|
element.className = 'blocklyMenu';
|
||||||
element.className = 'blocklyMenu goog-menu blocklyNonSelectable';
|
|
||||||
element.tabIndex = 0;
|
element.tabIndex = 0;
|
||||||
if (this.roleName) {
|
if (this.roleName) {
|
||||||
aria.setRole(element, this.roleName);
|
aria.setRole(element, this.roleName);
|
||||||
@@ -157,7 +155,6 @@ export class Menu {
|
|||||||
const el = this.getElement();
|
const el = this.getElement();
|
||||||
if (el) {
|
if (el) {
|
||||||
el.focus({preventScroll: true});
|
el.focus({preventScroll: true});
|
||||||
dom.addClass(el, 'blocklyFocused');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +163,6 @@ export class Menu {
|
|||||||
const el = this.getElement();
|
const el = this.getElement();
|
||||||
if (el) {
|
if (el) {
|
||||||
el.blur();
|
el.blur();
|
||||||
dom.removeClass(el, 'blocklyFocused');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,7 +226,8 @@ export class Menu {
|
|||||||
while (currentElement && currentElement !== menuElem) {
|
while (currentElement && currentElement !== menuElem) {
|
||||||
if (currentElement.classList.contains('blocklyMenuItem')) {
|
if (currentElement.classList.contains('blocklyMenuItem')) {
|
||||||
// Having found a menu item's div, locate that menu item in this menu.
|
// Having found a menu item's div, locate that menu item in this menu.
|
||||||
for (let i = 0, menuItem; (menuItem = this.menuItems[i]); i++) {
|
const items = this.getMenuItems();
|
||||||
|
for (let i = 0, menuItem; (menuItem = items[i]); i++) {
|
||||||
if (menuItem.getElement() === currentElement) {
|
if (menuItem.getElement() === currentElement) {
|
||||||
return menuItem;
|
return menuItem;
|
||||||
}
|
}
|
||||||
@@ -252,11 +249,9 @@ export class Menu {
|
|||||||
setHighlighted(item: MenuItem | null) {
|
setHighlighted(item: MenuItem | null) {
|
||||||
const currentHighlighted = this.highlightedItem;
|
const currentHighlighted = this.highlightedItem;
|
||||||
if (currentHighlighted) {
|
if (currentHighlighted) {
|
||||||
currentHighlighted.setHighlighted(false);
|
|
||||||
this.highlightedItem = null;
|
this.highlightedItem = null;
|
||||||
}
|
}
|
||||||
if (item) {
|
if (item) {
|
||||||
item.setHighlighted(true);
|
|
||||||
this.highlightedItem = item;
|
this.highlightedItem = item;
|
||||||
// Bring the highlighted item into view. This has no effect if the menu is
|
// Bring the highlighted item into view. This has no effect if the menu is
|
||||||
// not scrollable.
|
// not scrollable.
|
||||||
@@ -316,7 +311,8 @@ export class Menu {
|
|||||||
private highlightHelper(startIndex: number, delta: number) {
|
private highlightHelper(startIndex: number, delta: number) {
|
||||||
let index = startIndex + delta;
|
let index = startIndex + delta;
|
||||||
let menuItem;
|
let menuItem;
|
||||||
while ((menuItem = this.menuItems[index])) {
|
const items = this.getMenuItems();
|
||||||
|
while ((menuItem = items[index])) {
|
||||||
if (menuItem.isEnabled()) {
|
if (menuItem.isEnabled()) {
|
||||||
this.setHighlighted(menuItem);
|
this.setHighlighted(menuItem);
|
||||||
break;
|
break;
|
||||||
@@ -408,9 +404,7 @@ export class Menu {
|
|||||||
// Keyboard events.
|
// Keyboard events.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Attempts to handle a keyboard event, if the menu item is enabled, by
|
* Attempts to handle a keyboard event.
|
||||||
* calling
|
|
||||||
* {@link Menu#handleKeyEventInternal_}.
|
|
||||||
*
|
*
|
||||||
* @param e Key event to handle.
|
* @param e Key event to handle.
|
||||||
*/
|
*/
|
||||||
@@ -479,4 +473,13 @@ export class Menu {
|
|||||||
menuSize.height = menuDom.scrollHeight;
|
menuSize.height = menuDom.scrollHeight;
|
||||||
return menuSize;
|
return menuSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the action menu items (omitting separators) in this menu.
|
||||||
|
*
|
||||||
|
* @returns The MenuItem objects displayed in this menu.
|
||||||
|
*/
|
||||||
|
private getMenuItems(): MenuItem[] {
|
||||||
|
return this.menuItems.filter((item) => item instanceof MenuItem);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
38
core/menu_separator.ts
Normal file
38
core/menu_separator.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2025 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as aria from './utils/aria.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Representation of a section separator in a menu.
|
||||||
|
*/
|
||||||
|
export class MenuSeparator {
|
||||||
|
/**
|
||||||
|
* DOM element representing this separator in a menu.
|
||||||
|
*/
|
||||||
|
private element: HTMLHRElement | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the DOM representation of this separator.
|
||||||
|
*
|
||||||
|
* @returns An <hr> element.
|
||||||
|
*/
|
||||||
|
createDom(): HTMLHRElement {
|
||||||
|
this.element = document.createElement('hr');
|
||||||
|
this.element.className = 'blocklyMenuSeparator';
|
||||||
|
aria.setRole(this.element, aria.Role.SEPARATOR);
|
||||||
|
|
||||||
|
return this.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disposes of this separator.
|
||||||
|
*/
|
||||||
|
dispose() {
|
||||||
|
this.element?.remove();
|
||||||
|
this.element = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
// Former goog.module ID: Blockly.MenuItem
|
// Former goog.module ID: Blockly.MenuItem
|
||||||
|
|
||||||
import * as aria from './utils/aria.js';
|
import * as aria from './utils/aria.js';
|
||||||
import * as dom from './utils/dom.js';
|
|
||||||
import * as idGenerator from './utils/idgenerator.js';
|
import * as idGenerator from './utils/idgenerator.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -64,22 +63,18 @@ export class MenuItem {
|
|||||||
this.element = element;
|
this.element = element;
|
||||||
|
|
||||||
// Set class and style
|
// Set class and style
|
||||||
// goog-menuitem* is deprecated, use blocklyMenuItem*. May 2020.
|
|
||||||
element.className =
|
element.className =
|
||||||
'blocklyMenuItem goog-menuitem ' +
|
'blocklyMenuItem ' +
|
||||||
(this.enabled ? '' : 'blocklyMenuItemDisabled goog-menuitem-disabled ') +
|
(this.enabled ? '' : 'blocklyMenuItemDisabled ') +
|
||||||
(this.checked ? 'blocklyMenuItemSelected goog-option-selected ' : '') +
|
(this.checked ? 'blocklyMenuItemSelected ' : '') +
|
||||||
(this.highlight
|
(this.rightToLeft ? 'blocklyMenuItemRtl ' : '');
|
||||||
? 'blocklyMenuItemHighlight goog-menuitem-highlight '
|
|
||||||
: '') +
|
|
||||||
(this.rightToLeft ? 'blocklyMenuItemRtl goog-menuitem-rtl ' : '');
|
|
||||||
|
|
||||||
const content = document.createElement('div');
|
const content = document.createElement('div');
|
||||||
content.className = 'blocklyMenuItemContent goog-menuitem-content';
|
content.className = 'blocklyMenuItemContent';
|
||||||
// Add a checkbox for checkable menu items.
|
// Add a checkbox for checkable menu items.
|
||||||
if (this.checkable) {
|
if (this.checkable) {
|
||||||
const checkbox = document.createElement('div');
|
const checkbox = document.createElement('div');
|
||||||
checkbox.className = 'blocklyMenuItemCheckbox goog-menuitem-checkbox';
|
checkbox.className = 'blocklyMenuItemCheckbox ';
|
||||||
content.appendChild(checkbox);
|
content.appendChild(checkbox);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,31 +175,6 @@ export class MenuItem {
|
|||||||
this.checked = checked;
|
this.checked = checked;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Highlights or unhighlights the component.
|
|
||||||
*
|
|
||||||
* @param highlight Whether to highlight or unhighlight the component.
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
setHighlighted(highlight: boolean) {
|
|
||||||
this.highlight = highlight;
|
|
||||||
|
|
||||||
const el = this.getElement();
|
|
||||||
if (el && this.isEnabled()) {
|
|
||||||
// goog-menuitem-highlight is deprecated, use blocklyMenuItemHighlight.
|
|
||||||
// May 2020.
|
|
||||||
const name = 'blocklyMenuItemHighlight';
|
|
||||||
const nameDep = 'goog-menuitem-highlight';
|
|
||||||
if (highlight) {
|
|
||||||
dom.addClass(el, name);
|
|
||||||
dom.addClass(el, nameDep);
|
|
||||||
} else {
|
|
||||||
dom.removeClass(el, name);
|
|
||||||
dom.removeClass(el, nameDep);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns true if the menu item is enabled, false otherwise.
|
* Returns true if the menu item is enabled, false otherwise.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export class MetricsManager implements IMetricsManager {
|
|||||||
* Gets the width, height and position of the toolbox on the workspace in
|
* Gets the width, height and position of the toolbox on the workspace in
|
||||||
* pixel coordinates. Returns 0 for the width and height if the workspace has
|
* pixel coordinates. Returns 0 for the width and height if the workspace has
|
||||||
* a simple toolbox instead of a category toolbox. To get the width and height
|
* a simple toolbox instead of a category toolbox. To get the width and height
|
||||||
* of a simple toolbox, see {@link MetricsManager#getFlyoutMetrics}.
|
* of a simple toolbox, see {@link (MetricsManager:class).getFlyoutMetrics}.
|
||||||
*
|
*
|
||||||
* @returns The object with the width, height and position of the toolbox.
|
* @returns The object with the width, height and position of the toolbox.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -11,9 +11,12 @@
|
|||||||
*/
|
*/
|
||||||
// Former goog.module ID: Blockly.Names
|
// Former goog.module ID: Blockly.Names
|
||||||
|
|
||||||
|
import type {IVariableMap} from './interfaces/i_variable_map.js';
|
||||||
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableState,
|
||||||
|
} from './interfaces/i_variable_model.js';
|
||||||
import {Msg} from './msg.js';
|
import {Msg} from './msg.js';
|
||||||
// import * as Procedures from './procedures.js';
|
|
||||||
import type {VariableMap} from './variable_map.js';
|
|
||||||
import * as Variables from './variables.js';
|
import * as Variables from './variables.js';
|
||||||
import type {Workspace} from './workspace.js';
|
import type {Workspace} from './workspace.js';
|
||||||
|
|
||||||
@@ -39,7 +42,8 @@ export class Names {
|
|||||||
/**
|
/**
|
||||||
* The variable map from the workspace, containing Blockly variable models.
|
* The variable map from the workspace, containing Blockly variable models.
|
||||||
*/
|
*/
|
||||||
private variableMap: VariableMap | null = null;
|
private variableMap: IVariableMap<IVariableModel<IVariableState>> | null =
|
||||||
|
null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param reservedWordsList A comma-separated string of words that are illegal
|
* @param reservedWordsList A comma-separated string of words that are illegal
|
||||||
@@ -70,7 +74,7 @@ export class Names {
|
|||||||
*
|
*
|
||||||
* @param map The map to track.
|
* @param map The map to track.
|
||||||
*/
|
*/
|
||||||
setVariableMap(map: VariableMap) {
|
setVariableMap(map: IVariableMap<IVariableModel<IVariableState>>) {
|
||||||
this.variableMap = map;
|
this.variableMap = map;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +99,7 @@ export class Names {
|
|||||||
}
|
}
|
||||||
const variable = this.variableMap.getVariableById(id);
|
const variable = this.variableMap.getVariableById(id);
|
||||||
if (variable) {
|
if (variable) {
|
||||||
return variable.name;
|
return variable.getName();
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ import {IProcedureModel} from './interfaces/i_procedure_model.js';
|
|||||||
import {Msg} from './msg.js';
|
import {Msg} from './msg.js';
|
||||||
import {Names} from './names.js';
|
import {Names} from './names.js';
|
||||||
import {ObservableProcedureMap} from './observable_procedure_map.js';
|
import {ObservableProcedureMap} from './observable_procedure_map.js';
|
||||||
|
import * as deprecation from './utils/deprecation.js';
|
||||||
|
import type {FlyoutItemInfo} from './utils/toolbox.js';
|
||||||
import * as utilsXml from './utils/xml.js';
|
import * as utilsXml from './utils/xml.js';
|
||||||
import * as Variables from './variables.js';
|
import * as Variables from './variables.js';
|
||||||
import type {Workspace} from './workspace.js';
|
import type {Workspace} from './workspace.js';
|
||||||
@@ -238,7 +240,7 @@ export function rename(this: Field, name: string): string {
|
|||||||
* @param workspace The workspace containing procedures.
|
* @param workspace The workspace containing procedures.
|
||||||
* @returns Array of XML block elements.
|
* @returns Array of XML block elements.
|
||||||
*/
|
*/
|
||||||
export function flyoutCategory(workspace: WorkspaceSvg): Element[] {
|
function xmlFlyoutCategory(workspace: WorkspaceSvg): Element[] {
|
||||||
const xmlList = [];
|
const xmlList = [];
|
||||||
if (Blocks['procedures_defnoreturn']) {
|
if (Blocks['procedures_defnoreturn']) {
|
||||||
// <block type="procedures_defnoreturn" gap="16">
|
// <block type="procedures_defnoreturn" gap="16">
|
||||||
@@ -322,6 +324,109 @@ export function flyoutCategory(workspace: WorkspaceSvg): Element[] {
|
|||||||
return xmlList;
|
return xmlList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal wrapper that returns the contents of the procedure category.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @param workspace The workspace to populate procedure blocks for.
|
||||||
|
*/
|
||||||
|
export function internalFlyoutCategory(
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
): FlyoutItemInfo[] {
|
||||||
|
return flyoutCategory(workspace, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function flyoutCategory(
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
useXml: true,
|
||||||
|
): Element[];
|
||||||
|
export function flyoutCategory(
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
useXml: false,
|
||||||
|
): FlyoutItemInfo[];
|
||||||
|
/**
|
||||||
|
* Construct the blocks required by the flyout for the procedure category.
|
||||||
|
*
|
||||||
|
* @param workspace The workspace containing procedures.
|
||||||
|
* @param useXml True to return the contents as XML, false to use JSON.
|
||||||
|
* @returns List of flyout contents as either XML or JSON.
|
||||||
|
*/
|
||||||
|
export function flyoutCategory(
|
||||||
|
workspace: WorkspaceSvg,
|
||||||
|
useXml = true,
|
||||||
|
): Element[] | FlyoutItemInfo[] {
|
||||||
|
if (useXml) {
|
||||||
|
deprecation.warn(
|
||||||
|
'The XML return value of Blockly.Procedures.flyoutCategory()',
|
||||||
|
'v12',
|
||||||
|
'v13',
|
||||||
|
'the same method, but handle a return type of FlyoutItemInfo[] (JSON) instead.',
|
||||||
|
);
|
||||||
|
return xmlFlyoutCategory(workspace);
|
||||||
|
}
|
||||||
|
const blocks = [];
|
||||||
|
if (Blocks['procedures_defnoreturn']) {
|
||||||
|
blocks.push({
|
||||||
|
'kind': 'block',
|
||||||
|
'type': 'procedures_defnoreturn',
|
||||||
|
'gap': 16,
|
||||||
|
'fields': {
|
||||||
|
'NAME': Msg['PROCEDURES_DEFNORETURN_PROCEDURE'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Blocks['procedures_defreturn']) {
|
||||||
|
blocks.push({
|
||||||
|
'kind': 'block',
|
||||||
|
'type': 'procedures_defreturn',
|
||||||
|
'gap': 16,
|
||||||
|
'fields': {
|
||||||
|
'NAME': Msg['PROCEDURES_DEFRETURN_PROCEDURE'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (Blocks['procedures_ifreturn']) {
|
||||||
|
blocks.push({
|
||||||
|
'kind': 'block',
|
||||||
|
'type': 'procedures_ifreturn',
|
||||||
|
'gap': 16,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (blocks.length) {
|
||||||
|
// Add slightly larger gap between system blocks and user calls.
|
||||||
|
blocks[blocks.length - 1]['gap'] = 24;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates JSON block definitions for each of the given procedures.
|
||||||
|
*
|
||||||
|
* @param procedureList A list of procedures, each of which is defined by a
|
||||||
|
* three-element list of name, parameter list, and return value boolean.
|
||||||
|
* @param templateName The type of the block to generate.
|
||||||
|
*/
|
||||||
|
function populateProcedures(
|
||||||
|
procedureList: ProcedureTuple[],
|
||||||
|
templateName: string,
|
||||||
|
) {
|
||||||
|
for (const [name, args] of procedureList) {
|
||||||
|
blocks.push({
|
||||||
|
'kind': 'block',
|
||||||
|
'type': templateName,
|
||||||
|
'gap': 16,
|
||||||
|
'extraState': {
|
||||||
|
'name': name,
|
||||||
|
'params': args,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tuple = allProcedures(workspace);
|
||||||
|
populateProcedures(tuple[0], 'procedures_callnoreturn');
|
||||||
|
populateProcedures(tuple[1], 'procedures_callreturn');
|
||||||
|
return blocks;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the procedure mutator's flyout so that the arg block is not a
|
* Updates the procedure mutator's flyout so that the arg block is not a
|
||||||
* duplicate of another arg.
|
* duplicate of another arg.
|
||||||
|
|||||||
@@ -14,11 +14,18 @@ import type {IConnectionPreviewer} from './interfaces/i_connection_previewer.js'
|
|||||||
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
import type {ICopyData, ICopyable} from './interfaces/i_copyable.js';
|
||||||
import type {IDragger} from './interfaces/i_dragger.js';
|
import type {IDragger} from './interfaces/i_dragger.js';
|
||||||
import type {IFlyout} from './interfaces/i_flyout.js';
|
import type {IFlyout} from './interfaces/i_flyout.js';
|
||||||
|
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
|
||||||
import type {IIcon} from './interfaces/i_icon.js';
|
import type {IIcon} from './interfaces/i_icon.js';
|
||||||
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
import type {IMetricsManager} from './interfaces/i_metrics_manager.js';
|
||||||
import type {IPaster} from './interfaces/i_paster.js';
|
import type {IPaster} from './interfaces/i_paster.js';
|
||||||
import type {ISerializer} from './interfaces/i_serializer.js';
|
import type {ISerializer} from './interfaces/i_serializer.js';
|
||||||
import type {IToolbox} from './interfaces/i_toolbox.js';
|
import type {IToolbox} from './interfaces/i_toolbox.js';
|
||||||
|
import type {IVariableMap} from './interfaces/i_variable_map.js';
|
||||||
|
import type {
|
||||||
|
IVariableModel,
|
||||||
|
IVariableModelStatic,
|
||||||
|
IVariableState,
|
||||||
|
} from './interfaces/i_variable_model.js';
|
||||||
import type {Cursor} from './keyboard_nav/cursor.js';
|
import type {Cursor} from './keyboard_nav/cursor.js';
|
||||||
import type {Options} from './options.js';
|
import type {Options} from './options.js';
|
||||||
import type {Renderer} from './renderers/common/renderer.js';
|
import type {Renderer} from './renderers/common/renderer.js';
|
||||||
@@ -93,6 +100,8 @@ export class Type<_T> {
|
|||||||
'flyoutsHorizontalToolbox',
|
'flyoutsHorizontalToolbox',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
static FLYOUT_INFLATER = new Type<IFlyoutInflater>('flyoutInflater');
|
||||||
|
|
||||||
static METRICS_MANAGER = new Type<IMetricsManager>('metricsManager');
|
static METRICS_MANAGER = new Type<IMetricsManager>('metricsManager');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,6 +118,14 @@ export class Type<_T> {
|
|||||||
|
|
||||||
/** @internal */
|
/** @internal */
|
||||||
static PASTER = new Type<IPaster<ICopyData, ICopyable<ICopyData>>>('paster');
|
static PASTER = new Type<IPaster<ICopyData, ICopyable<ICopyData>>>('paster');
|
||||||
|
|
||||||
|
static VARIABLE_MODEL = new Type<IVariableModelStatic<IVariableState>>(
|
||||||
|
'variableModel',
|
||||||
|
);
|
||||||
|
|
||||||
|
static VARIABLE_MAP = new Type<IVariableMap<IVariableModel<IVariableState>>>(
|
||||||
|
'variableMap',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -727,7 +727,10 @@ export class ConstantProvider {
|
|||||||
svgPaths.point(70, -height),
|
svgPaths.point(70, -height),
|
||||||
svgPaths.point(width, 0),
|
svgPaths.point(width, 0),
|
||||||
]);
|
]);
|
||||||
return {height, width, path: mainPath};
|
// Height is actually the Y position of the control points defining the
|
||||||
|
// curve of the hat; the hat's actual rendered height is 3/4 of the control
|
||||||
|
// points' Y position, per https://stackoverflow.com/a/5327329
|
||||||
|
return {height: height * 0.75, width, path: mainPath};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -923,8 +926,18 @@ export class ConstantProvider {
|
|||||||
* @param svg The root of the workspace's SVG.
|
* @param svg The root of the workspace's SVG.
|
||||||
* @param tagName The name to use for the CSS style tag.
|
* @param tagName The name to use for the CSS style tag.
|
||||||
* @param selector The CSS selector to use.
|
* @param selector The CSS selector to use.
|
||||||
|
* @param injectionDivIfIsParent The div containing the parent workspace and
|
||||||
|
* all related workspaces and block containers, if this renderer is for the
|
||||||
|
* parent workspace. CSS variables representing SVG patterns will be scoped
|
||||||
|
* to this container. Child workspaces should not override the CSS variables
|
||||||
|
* created by the parent and thus do not need access to the injection div.
|
||||||
*/
|
*/
|
||||||
createDom(svg: SVGElement, tagName: string, selector: string) {
|
createDom(
|
||||||
|
svg: SVGElement,
|
||||||
|
tagName: string,
|
||||||
|
selector: string,
|
||||||
|
injectionDivIfIsParent?: HTMLElement,
|
||||||
|
) {
|
||||||
this.injectCSS_(tagName, selector);
|
this.injectCSS_(tagName, selector);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -1031,6 +1044,24 @@ export class ConstantProvider {
|
|||||||
this.disabledPattern = disabledPattern;
|
this.disabledPattern = disabledPattern;
|
||||||
|
|
||||||
this.createDebugFilter();
|
this.createDebugFilter();
|
||||||
|
|
||||||
|
if (injectionDivIfIsParent) {
|
||||||
|
// If this renderer is for the parent workspace, add CSS variables scoped
|
||||||
|
// to the injection div referencing the created patterns so that CSS can
|
||||||
|
// apply the patterns to any element in the injection div.
|
||||||
|
injectionDivIfIsParent.style.setProperty(
|
||||||
|
'--blocklyEmbossFilter',
|
||||||
|
`url(#${this.embossFilterId})`,
|
||||||
|
);
|
||||||
|
injectionDivIfIsParent.style.setProperty(
|
||||||
|
'--blocklyDisabledPattern',
|
||||||
|
`url(#${this.disabledPatternId})`,
|
||||||
|
);
|
||||||
|
injectionDivIfIsParent.style.setProperty(
|
||||||
|
'--blocklyDebugFilter',
|
||||||
|
`url(#${this.debugFilterId})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1132,14 +1163,14 @@ export class ConstantProvider {
|
|||||||
`${selector} .blocklyText {`,
|
`${selector} .blocklyText {`,
|
||||||
`fill: #fff;`,
|
`fill: #fff;`,
|
||||||
`}`,
|
`}`,
|
||||||
`${selector} .blocklyNonEditableText>rect,`,
|
`${selector} .blocklyNonEditableField>rect,`,
|
||||||
`${selector} .blocklyEditableText>rect {`,
|
`${selector} .blocklyEditableField>rect {`,
|
||||||
`fill: ${this.FIELD_BORDER_RECT_COLOUR};`,
|
`fill: ${this.FIELD_BORDER_RECT_COLOUR};`,
|
||||||
`fill-opacity: .6;`,
|
`fill-opacity: .6;`,
|
||||||
`stroke: none;`,
|
`stroke: none;`,
|
||||||
`}`,
|
`}`,
|
||||||
`${selector} .blocklyNonEditableText>text,`,
|
`${selector} .blocklyNonEditableField>text,`,
|
||||||
`${selector} .blocklyEditableText>text {`,
|
`${selector} .blocklyEditableField>text {`,
|
||||||
`fill: #000;`,
|
`fill: #000;`,
|
||||||
`}`,
|
`}`,
|
||||||
|
|
||||||
@@ -1154,7 +1185,7 @@ export class ConstantProvider {
|
|||||||
`}`,
|
`}`,
|
||||||
|
|
||||||
// Editable field hover.
|
// Editable field hover.
|
||||||
`${selector} .blocklyEditableText:not(.editing):hover>rect {`,
|
`${selector} .blocklyEditableField:not(.blocklyEditing):hover>rect {`,
|
||||||
`stroke: #fff;`,
|
`stroke: #fff;`,
|
||||||
`stroke-width: 2;`,
|
`stroke-width: 2;`,
|
||||||
`}`,
|
`}`,
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import type {ExternalValueInput} from '../measurables/external_value_input.js';
|
|||||||
import type {Field} from '../measurables/field.js';
|
import type {Field} from '../measurables/field.js';
|
||||||
import type {Icon} from '../measurables/icon.js';
|
import type {Icon} from '../measurables/icon.js';
|
||||||
import type {InlineInput} from '../measurables/inline_input.js';
|
import type {InlineInput} from '../measurables/inline_input.js';
|
||||||
import type {PreviousConnection} from '../measurables/previous_connection.js';
|
|
||||||
import type {Row} from '../measurables/row.js';
|
import type {Row} from '../measurables/row.js';
|
||||||
import {Types} from '../measurables/types.js';
|
import {Types} from '../measurables/types.js';
|
||||||
import type {ConstantProvider, Notch, PuzzleTab} from './constants.js';
|
import type {ConstantProvider, Notch, PuzzleTab} from './constants.js';
|
||||||
@@ -116,13 +115,8 @@ export class Drawer {
|
|||||||
this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topLeft;
|
this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topLeft;
|
||||||
} else if (Types.isRightRoundedCorner(elem)) {
|
} else if (Types.isRightRoundedCorner(elem)) {
|
||||||
this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topRight;
|
this.outlinePath_ += this.constants_.OUTSIDE_CORNERS.topRight;
|
||||||
} else if (
|
} else if (Types.isPreviousConnection(elem)) {
|
||||||
Types.isPreviousConnection(elem) &&
|
this.outlinePath_ += (elem.shape as Notch).pathLeft;
|
||||||
elem instanceof Connection
|
|
||||||
) {
|
|
||||||
this.outlinePath_ += (
|
|
||||||
(elem as PreviousConnection).shape as Notch
|
|
||||||
).pathLeft;
|
|
||||||
} else if (Types.isHat(elem)) {
|
} else if (Types.isHat(elem)) {
|
||||||
this.outlinePath_ += this.constants_.START_HAT.path;
|
this.outlinePath_ += this.constants_.START_HAT.path;
|
||||||
} else if (Types.isSpacer(elem)) {
|
} else if (Types.isSpacer(elem)) {
|
||||||
@@ -217,7 +211,7 @@ export class Drawer {
|
|||||||
let rightCornerYOffset = 0;
|
let rightCornerYOffset = 0;
|
||||||
let outlinePath = '';
|
let outlinePath = '';
|
||||||
for (let i = elems.length - 1, elem; (elem = elems[i]); i--) {
|
for (let i = elems.length - 1, elem; (elem = elems[i]); i--) {
|
||||||
if (Types.isNextConnection(elem) && elem instanceof Connection) {
|
if (Types.isNextConnection(elem)) {
|
||||||
outlinePath += (elem.shape as Notch).pathRight;
|
outlinePath += (elem.shape as Notch).pathRight;
|
||||||
} else if (Types.isLeftSquareCorner(elem)) {
|
} else if (Types.isLeftSquareCorner(elem)) {
|
||||||
outlinePath += svgPaths.lineOnAxis('H', bottomRow.xPos);
|
outlinePath += svgPaths.lineOnAxis('H', bottomRow.xPos);
|
||||||
@@ -269,9 +263,9 @@ export class Drawer {
|
|||||||
for (let i = 0, row; (row = this.info_.rows[i]); i++) {
|
for (let i = 0, row; (row = this.info_.rows[i]); i++) {
|
||||||
for (let j = 0, elem; (elem = row.elements[j]); j++) {
|
for (let j = 0, elem; (elem = row.elements[j]); j++) {
|
||||||
if (Types.isInlineInput(elem)) {
|
if (Types.isInlineInput(elem)) {
|
||||||
this.drawInlineInput_(elem as InlineInput);
|
this.drawInlineInput_(elem);
|
||||||
} else if (Types.isIcon(elem) || Types.isField(elem)) {
|
} else if (Types.isIcon(elem) || Types.isField(elem)) {
|
||||||
this.layoutField_(elem as Field | Icon);
|
this.layoutField_(elem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -295,13 +289,13 @@ export class Drawer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Types.isIcon(fieldInfo)) {
|
if (Types.isIcon(fieldInfo)) {
|
||||||
const icon = (fieldInfo as Icon).icon;
|
const icon = fieldInfo.icon;
|
||||||
icon.setOffsetInBlock(new Coordinate(xPos, yPos));
|
icon.setOffsetInBlock(new Coordinate(xPos, yPos));
|
||||||
if (this.info_.isInsertionMarker) {
|
if (this.info_.isInsertionMarker) {
|
||||||
icon.hideForInsertionMarker();
|
icon.hideForInsertionMarker();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const svgGroup = (fieldInfo as Field).field.getSvgRoot()!;
|
const svgGroup = fieldInfo.field.getSvgRoot()!;
|
||||||
svgGroup.setAttribute(
|
svgGroup.setAttribute(
|
||||||
'transform',
|
'transform',
|
||||||
'translate(' + xPos + ',' + yPos + ')' + scale,
|
'translate(' + xPos + ',' + yPos + ')' + scale,
|
||||||
|
|||||||
@@ -49,21 +49,6 @@ export interface IPathObject {
|
|||||||
*/
|
*/
|
||||||
setPath(pathString: string): void;
|
setPath(pathString: string): void;
|
||||||
|
|
||||||
/**
|
|
||||||
* Apply the stored colours to the block's path, taking into account whether
|
|
||||||
* the paths belong to a shadow block.
|
|
||||||
*
|
|
||||||
* @param block The source block.
|
|
||||||
*/
|
|
||||||
applyColour(block: BlockSvg): void;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the style.
|
|
||||||
*
|
|
||||||
* @param blockStyle The block style to use.
|
|
||||||
*/
|
|
||||||
setStyle(blockStyle: BlockStyle): void;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Flip the SVG paths in RTL.
|
* Flip the SVG paths in RTL.
|
||||||
*/
|
*/
|
||||||
@@ -130,8 +115,23 @@ export interface IPathObject {
|
|||||||
rtl: boolean,
|
rtl: boolean,
|
||||||
): void;
|
): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the stored colours to the block's path, taking into account whether
|
||||||
|
* the paths belong to a shadow block.
|
||||||
|
*
|
||||||
|
* @param block The source block.
|
||||||
|
*/
|
||||||
|
applyColour?(block: BlockSvg): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Removes any highlight associated with the given connection, if it exists.
|
* Removes any highlight associated with the given connection, if it exists.
|
||||||
*/
|
*/
|
||||||
removeConnectionHighlight?(connection: RenderedConnection): void;
|
removeConnectionHighlight?(connection: RenderedConnection): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the style.
|
||||||
|
*
|
||||||
|
* @param blockStyle The block style to use.
|
||||||
|
*/
|
||||||
|
setStyle?(blockStyle: BlockStyle): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -231,7 +231,6 @@ export class RenderInfo {
|
|||||||
if (hasHat) {
|
if (hasHat) {
|
||||||
const hat = new Hat(this.constants_);
|
const hat = new Hat(this.constants_);
|
||||||
this.topRow.elements.push(hat);
|
this.topRow.elements.push(hat);
|
||||||
this.topRow.capline = hat.ascenderHeight;
|
|
||||||
} else if (hasPrevious) {
|
} else if (hasPrevious) {
|
||||||
this.topRow.hasPreviousConnection = true;
|
this.topRow.hasPreviousConnection = true;
|
||||||
this.topRow.connection = new PreviousConnection(
|
this.topRow.connection = new PreviousConnection(
|
||||||
@@ -458,6 +457,11 @@ export class RenderInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't add padding after zero-width fields.
|
||||||
|
if (prev && Types.isField(prev) && prev.width === 0) {
|
||||||
|
return this.constants_.NO_PADDING;
|
||||||
|
}
|
||||||
|
|
||||||
return this.constants_.MEDIUM_PADDING;
|
return this.constants_.MEDIUM_PADDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -672,20 +676,17 @@ export class RenderInfo {
|
|||||||
return row.yPos + elem.height / 2;
|
return row.yPos + elem.height / 2;
|
||||||
}
|
}
|
||||||
if (Types.isBottomRow(row)) {
|
if (Types.isBottomRow(row)) {
|
||||||
const bottomRow = row as BottomRow;
|
const baseline = row.yPos + row.height - row.descenderHeight;
|
||||||
const baseline =
|
|
||||||
bottomRow.yPos + bottomRow.height - bottomRow.descenderHeight;
|
|
||||||
if (Types.isNextConnection(elem)) {
|
if (Types.isNextConnection(elem)) {
|
||||||
return baseline + elem.height / 2;
|
return baseline + elem.height / 2;
|
||||||
}
|
}
|
||||||
return baseline - elem.height / 2;
|
return baseline - elem.height / 2;
|
||||||
}
|
}
|
||||||
if (Types.isTopRow(row)) {
|
if (Types.isTopRow(row)) {
|
||||||
const topRow = row as TopRow;
|
|
||||||
if (Types.isHat(elem)) {
|
if (Types.isHat(elem)) {
|
||||||
return topRow.capline - elem.height / 2;
|
return row.capline - elem.height / 2;
|
||||||
}
|
}
|
||||||
return topRow.capline + elem.height / 2;
|
return row.capline + elem.height / 2;
|
||||||
}
|
}
|
||||||
return row.yPos + row.height / 2;
|
return row.yPos + row.height / 2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,6 +65,8 @@ export class PathObject implements IPathObject {
|
|||||||
{'class': 'blocklyPath'},
|
{'class': 'blocklyPath'},
|
||||||
this.svgRoot,
|
this.svgRoot,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.setClass_('blocklyBlock', true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -167,14 +169,12 @@ export class PathObject implements IPathObject {
|
|||||||
*
|
*
|
||||||
* @param enable True if highlighted.
|
* @param enable True if highlighted.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
updateHighlighted(enable: boolean) {
|
updateHighlighted(enable: boolean) {
|
||||||
if (enable) {
|
if (enable) {
|
||||||
this.svgPath.setAttribute(
|
this.setClass_('blocklyHighlighted', true);
|
||||||
'filter',
|
|
||||||
'url(#' + this.constants.embossFilterId + ')',
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
this.svgPath.setAttribute('filter', 'none');
|
this.setClass_('blocklyHighlighted', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,8 +185,11 @@ export class PathObject implements IPathObject {
|
|||||||
*/
|
*/
|
||||||
protected updateShadow_(shadow: boolean) {
|
protected updateShadow_(shadow: boolean) {
|
||||||
if (shadow) {
|
if (shadow) {
|
||||||
|
this.setClass_('blocklyShadow', true);
|
||||||
this.svgPath.setAttribute('stroke', 'none');
|
this.svgPath.setAttribute('stroke', 'none');
|
||||||
this.svgPath.setAttribute('fill', this.style.colourSecondary);
|
this.svgPath.setAttribute('fill', this.style.colourSecondary);
|
||||||
|
} else {
|
||||||
|
this.setClass_('blocklyShadow', false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,12 +200,6 @@ export class PathObject implements IPathObject {
|
|||||||
*/
|
*/
|
||||||
protected updateDisabled_(disabled: boolean) {
|
protected updateDisabled_(disabled: boolean) {
|
||||||
this.setClass_('blocklyDisabled', disabled);
|
this.setClass_('blocklyDisabled', disabled);
|
||||||
if (disabled) {
|
|
||||||
this.svgPath.setAttribute(
|
|
||||||
'fill',
|
|
||||||
'url(#' + this.constants.disabledPatternId + ')',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -10,15 +10,9 @@ import type {Block} from '../../block.js';
|
|||||||
import type {BlockSvg} from '../../block_svg.js';
|
import type {BlockSvg} from '../../block_svg.js';
|
||||||
import {Connection} from '../../connection.js';
|
import {Connection} from '../../connection.js';
|
||||||
import {ConnectionType} from '../../connection_type.js';
|
import {ConnectionType} from '../../connection_type.js';
|
||||||
import {
|
|
||||||
InsertionMarkerManager,
|
|
||||||
PreviewType,
|
|
||||||
} from '../../insertion_marker_manager.js';
|
|
||||||
import type {IRegistrable} from '../../interfaces/i_registrable.js';
|
import type {IRegistrable} from '../../interfaces/i_registrable.js';
|
||||||
import type {Marker} from '../../keyboard_nav/marker.js';
|
import type {Marker} from '../../keyboard_nav/marker.js';
|
||||||
import type {RenderedConnection} from '../../rendered_connection.js';
|
|
||||||
import type {BlockStyle, Theme} from '../../theme.js';
|
import type {BlockStyle, Theme} from '../../theme.js';
|
||||||
import * as deprecation from '../../utils/deprecation.js';
|
|
||||||
import type {WorkspaceSvg} from '../../workspace_svg.js';
|
import type {WorkspaceSvg} from '../../workspace_svg.js';
|
||||||
import {ConstantProvider} from './constants.js';
|
import {ConstantProvider} from './constants.js';
|
||||||
import {Drawer} from './drawer.js';
|
import {Drawer} from './drawer.js';
|
||||||
@@ -79,17 +73,27 @@ export class Renderer implements IRegistrable {
|
|||||||
/**
|
/**
|
||||||
* Create any DOM elements that this renderer needs.
|
* Create any DOM elements that this renderer needs.
|
||||||
* If you need to create additional DOM elements, override the
|
* If you need to create additional DOM elements, override the
|
||||||
* {@link ConstantProvider#createDom} method instead.
|
* {@link blockRendering#ConstantProvider.createDom} method instead.
|
||||||
*
|
*
|
||||||
* @param svg The root of the workspace's SVG.
|
* @param svg The root of the workspace's SVG.
|
||||||
* @param theme The workspace theme object.
|
* @param theme The workspace theme object.
|
||||||
|
* @param injectionDivIfIsParent The div containing the parent workspace and
|
||||||
|
* all related workspaces and block containers, if this renderer is for the
|
||||||
|
* parent workspace. CSS variables representing SVG patterns will be scoped
|
||||||
|
* to this container. Child workspaces should not override the CSS variables
|
||||||
|
* created by the parent and thus do not need access to the injection div.
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
createDom(svg: SVGElement, theme: Theme) {
|
createDom(
|
||||||
|
svg: SVGElement,
|
||||||
|
theme: Theme,
|
||||||
|
injectionDivIfIsParent?: HTMLElement,
|
||||||
|
) {
|
||||||
this.constants_.createDom(
|
this.constants_.createDom(
|
||||||
svg,
|
svg,
|
||||||
this.name + '-' + theme.name,
|
this.name + '-' + theme.name,
|
||||||
'.' + this.getClassName() + '.' + theme.getClassName(),
|
'.' + this.getClassName() + '.' + theme.getClassName(),
|
||||||
|
injectionDivIfIsParent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,8 +102,17 @@ export class Renderer implements IRegistrable {
|
|||||||
*
|
*
|
||||||
* @param svg The root of the workspace's SVG.
|
* @param svg The root of the workspace's SVG.
|
||||||
* @param theme The workspace theme object.
|
* @param theme The workspace theme object.
|
||||||
|
* @param injectionDivIfIsParent The div containing the parent workspace and
|
||||||
|
* all related workspaces and block containers, if this renderer is for the
|
||||||
|
* parent workspace. CSS variables representing SVG patterns will be scoped
|
||||||
|
* to this container. Child workspaces should not override the CSS variables
|
||||||
|
* created by the parent and thus do not need access to the injection div.
|
||||||
*/
|
*/
|
||||||
refreshDom(svg: SVGElement, theme: Theme) {
|
refreshDom(
|
||||||
|
svg: SVGElement,
|
||||||
|
theme: Theme,
|
||||||
|
injectionDivIfIsParent?: HTMLElement,
|
||||||
|
) {
|
||||||
const previousConstants = this.getConstants();
|
const previousConstants = this.getConstants();
|
||||||
previousConstants.dispose();
|
previousConstants.dispose();
|
||||||
this.constants_ = this.makeConstants_();
|
this.constants_ = this.makeConstants_();
|
||||||
@@ -110,7 +123,7 @@ export class Renderer implements IRegistrable {
|
|||||||
this.constants_.randomIdentifier = previousConstants.randomIdentifier;
|
this.constants_.randomIdentifier = previousConstants.randomIdentifier;
|
||||||
this.constants_.setTheme(theme);
|
this.constants_.setTheme(theme);
|
||||||
this.constants_.init();
|
this.constants_.init();
|
||||||
this.createDom(svg, theme);
|
this.createDom(svg, theme, injectionDivIfIsParent);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,49 +236,6 @@ export class Renderer implements IRegistrable {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Chooses a connection preview method based on the available connection, the
|
|
||||||
* current dragged connection, and the block being dragged.
|
|
||||||
*
|
|
||||||
* @param closest The available connection.
|
|
||||||
* @param local The connection currently being dragged.
|
|
||||||
* @param topBlock The block currently being dragged.
|
|
||||||
* @returns The preview type to display.
|
|
||||||
*
|
|
||||||
* @deprecated v10 - This function is no longer respected. A custom
|
|
||||||
* IConnectionPreviewer may be able to fulfill the functionality.
|
|
||||||
*/
|
|
||||||
getConnectionPreviewMethod(
|
|
||||||
closest: RenderedConnection,
|
|
||||||
local: RenderedConnection,
|
|
||||||
topBlock: BlockSvg,
|
|
||||||
): PreviewType {
|
|
||||||
deprecation.warn(
|
|
||||||
'getConnectionPreviewMethod',
|
|
||||||
'v10',
|
|
||||||
'v12',
|
|
||||||
'an IConnectionPreviewer, if it fulfills your use case.',
|
|
||||||
);
|
|
||||||
if (
|
|
||||||
local.type === ConnectionType.OUTPUT_VALUE ||
|
|
||||||
local.type === ConnectionType.PREVIOUS_STATEMENT
|
|
||||||
) {
|
|
||||||
if (
|
|
||||||
!closest.isConnected() ||
|
|
||||||
this.orphanCanConnectAtEnd(
|
|
||||||
topBlock,
|
|
||||||
closest.targetBlock() as BlockSvg,
|
|
||||||
local.type,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER;
|
|
||||||
}
|
|
||||||
return InsertionMarkerManager.PREVIEW_TYPE.REPLACEMENT_FADE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render the block.
|
* Render the block.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export class Drawer extends BaseDrawer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override drawInlineInput_(input: InlineInput) {
|
override drawInlineInput_(input: InlineInput) {
|
||||||
this.highlighter_.drawInlineInput(input as InlineInput);
|
this.highlighter_.drawInlineInput(input);
|
||||||
|
|
||||||
super.drawInlineInput_(input);
|
super.drawInlineInput_(input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,9 @@ import {StatementInput} from '../../inputs/statement_input.js';
|
|||||||
import {ValueInput} from '../../inputs/value_input.js';
|
import {ValueInput} from '../../inputs/value_input.js';
|
||||||
import {RenderInfo as BaseRenderInfo} from '../common/info.js';
|
import {RenderInfo as BaseRenderInfo} from '../common/info.js';
|
||||||
import type {Measurable} from '../measurables/base.js';
|
import type {Measurable} from '../measurables/base.js';
|
||||||
import type {BottomRow} from '../measurables/bottom_row.js';
|
|
||||||
import {ExternalValueInput} from '../measurables/external_value_input.js';
|
import {ExternalValueInput} from '../measurables/external_value_input.js';
|
||||||
import type {Field} from '../measurables/field.js';
|
|
||||||
import {InRowSpacer} from '../measurables/in_row_spacer.js';
|
import {InRowSpacer} from '../measurables/in_row_spacer.js';
|
||||||
import type {InputRow} from '../measurables/input_row.js';
|
|
||||||
import type {Row} from '../measurables/row.js';
|
import type {Row} from '../measurables/row.js';
|
||||||
import type {TopRow} from '../measurables/top_row.js';
|
|
||||||
import {Types} from '../measurables/types.js';
|
import {Types} from '../measurables/types.js';
|
||||||
import type {ConstantProvider} from './constants.js';
|
import type {ConstantProvider} from './constants.js';
|
||||||
import {InlineInput} from './measurables/inline_input.js';
|
import {InlineInput} from './measurables/inline_input.js';
|
||||||
@@ -150,7 +146,7 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
override getInRowSpacing_(prev: Measurable | null, next: Measurable | null) {
|
override getInRowSpacing_(prev: Measurable | null, next: Measurable | null) {
|
||||||
if (!prev) {
|
if (!prev) {
|
||||||
// Between an editable field and the beginning of the row.
|
// Between an editable field and the beginning of the row.
|
||||||
if (next && Types.isField(next) && (next as Field).isEditable) {
|
if (next && Types.isField(next) && next.isEditable) {
|
||||||
return this.constants_.MEDIUM_PADDING;
|
return this.constants_.MEDIUM_PADDING;
|
||||||
}
|
}
|
||||||
// Inline input at the beginning of the row.
|
// Inline input at the beginning of the row.
|
||||||
@@ -167,7 +163,10 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
// Spacing between a non-input and the end of the row or a statement input.
|
// Spacing between a non-input and the end of the row or a statement input.
|
||||||
if (!Types.isInput(prev) && (!next || Types.isStatementInput(next))) {
|
if (!Types.isInput(prev) && (!next || Types.isStatementInput(next))) {
|
||||||
// Between an editable field and the end of the row.
|
// Between an editable field and the end of the row.
|
||||||
if (Types.isField(prev) && (prev as Field).isEditable) {
|
if (Types.isField(prev) && prev.isEditable) {
|
||||||
|
if (prev.width === 0) {
|
||||||
|
return this.constants_.NO_PADDING;
|
||||||
|
}
|
||||||
return this.constants_.MEDIUM_PADDING;
|
return this.constants_.MEDIUM_PADDING;
|
||||||
}
|
}
|
||||||
// Padding at the end of an icon-only row to make the block shape clearer.
|
// Padding at the end of an icon-only row to make the block shape clearer.
|
||||||
@@ -208,7 +207,7 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
// Spacing between a non-input and an input.
|
// Spacing between a non-input and an input.
|
||||||
if (!Types.isInput(prev) && next && Types.isInput(next)) {
|
if (!Types.isInput(prev) && next && Types.isInput(next)) {
|
||||||
// Between an editable field and an input.
|
// Between an editable field and an input.
|
||||||
if (Types.isField(prev) && (prev as Field).isEditable) {
|
if (Types.isField(prev) && prev.isEditable) {
|
||||||
if (Types.isInlineInput(next)) {
|
if (Types.isInlineInput(next)) {
|
||||||
return this.constants_.SMALL_PADDING;
|
return this.constants_.SMALL_PADDING;
|
||||||
} else if (Types.isExternalInput(next)) {
|
} else if (Types.isExternalInput(next)) {
|
||||||
@@ -233,7 +232,7 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
// Spacing between an inline input and a field.
|
// Spacing between an inline input and a field.
|
||||||
if (Types.isInlineInput(prev) && next && Types.isField(next)) {
|
if (Types.isInlineInput(prev) && next && Types.isField(next)) {
|
||||||
// Editable field after inline input.
|
// Editable field after inline input.
|
||||||
if ((next as Field).isEditable) {
|
if (next.isEditable) {
|
||||||
return this.constants_.MEDIUM_PADDING;
|
return this.constants_.MEDIUM_PADDING;
|
||||||
} else {
|
} else {
|
||||||
// Noneditable field after inline input.
|
// Noneditable field after inline input.
|
||||||
@@ -278,8 +277,11 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
Types.isField(prev) &&
|
Types.isField(prev) &&
|
||||||
next &&
|
next &&
|
||||||
Types.isField(next) &&
|
Types.isField(next) &&
|
||||||
(prev as Field).isEditable === (next as Field).isEditable
|
prev.isEditable === next.isEditable
|
||||||
) {
|
) {
|
||||||
|
if (prev.width === 0) {
|
||||||
|
return this.constants_.NO_PADDING;
|
||||||
|
}
|
||||||
return this.constants_.LARGE_PADDING;
|
return this.constants_.LARGE_PADDING;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -323,20 +325,17 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
return row.yPos + elem.height / 2;
|
return row.yPos + elem.height / 2;
|
||||||
}
|
}
|
||||||
if (Types.isBottomRow(row)) {
|
if (Types.isBottomRow(row)) {
|
||||||
const bottomRow = row as BottomRow;
|
const baseline = row.yPos + row.height - row.descenderHeight;
|
||||||
const baseline =
|
|
||||||
bottomRow.yPos + bottomRow.height - bottomRow.descenderHeight;
|
|
||||||
if (Types.isNextConnection(elem)) {
|
if (Types.isNextConnection(elem)) {
|
||||||
return baseline + elem.height / 2;
|
return baseline + elem.height / 2;
|
||||||
}
|
}
|
||||||
return baseline - elem.height / 2;
|
return baseline - elem.height / 2;
|
||||||
}
|
}
|
||||||
if (Types.isTopRow(row)) {
|
if (Types.isTopRow(row)) {
|
||||||
const topRow = row as TopRow;
|
|
||||||
if (Types.isHat(elem)) {
|
if (Types.isHat(elem)) {
|
||||||
return topRow.capline - elem.height / 2;
|
return row.capline - elem.height / 2;
|
||||||
}
|
}
|
||||||
return topRow.capline + elem.height / 2;
|
return row.capline + elem.height / 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
let result = row.yPos;
|
let result = row.yPos;
|
||||||
@@ -370,7 +369,7 @@ export class RenderInfo extends BaseRenderInfo {
|
|||||||
rowNextRightEdges.set(row, nextRightEdge);
|
rowNextRightEdges.set(row, nextRightEdge);
|
||||||
if (Types.isInputRow(row)) {
|
if (Types.isInputRow(row)) {
|
||||||
if (row.hasStatement) {
|
if (row.hasStatement) {
|
||||||
this.alignStatementRow_(row as InputRow);
|
this.alignStatementRow_(row);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
prevInput &&
|
prevInput &&
|
||||||
|
|||||||
@@ -102,14 +102,10 @@ export class PathObject extends BasePathObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override updateHighlighted(highlighted: boolean) {
|
override updateHighlighted(highlighted: boolean) {
|
||||||
|
super.updateHighlighted(highlighted);
|
||||||
if (highlighted) {
|
if (highlighted) {
|
||||||
this.svgPath.setAttribute(
|
|
||||||
'filter',
|
|
||||||
'url(#' + this.constants.embossFilterId + ')',
|
|
||||||
);
|
|
||||||
this.svgPathLight.style.display = 'none';
|
this.svgPathLight.style.display = 'none';
|
||||||
} else {
|
} else {
|
||||||
this.svgPath.setAttribute('filter', 'none');
|
|
||||||
this.svgPathLight.style.display = 'inline';
|
this.svgPathLight.style.display = 'inline';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,8 +49,12 @@ export class Renderer extends BaseRenderer {
|
|||||||
this.highlightConstants.init();
|
this.highlightConstants.init();
|
||||||
}
|
}
|
||||||
|
|
||||||
override refreshDom(svg: SVGElement, theme: Theme) {
|
override refreshDom(
|
||||||
super.refreshDom(svg, theme);
|
svg: SVGElement,
|
||||||
|
theme: Theme,
|
||||||
|
injectionDiv: HTMLElement,
|
||||||
|
) {
|
||||||
|
super.refreshDom(svg, theme, injectionDiv);
|
||||||
this.getHighlightConstants().init();
|
this.getHighlightConstants().init();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ import {Types} from './types.js';
|
|||||||
* row.
|
* row.
|
||||||
*/
|
*/
|
||||||
export class InRowSpacer extends Measurable {
|
export class InRowSpacer extends Measurable {
|
||||||
|
// This field exists solely to structurally distinguish this type from other
|
||||||
|
// Measurable subclasses. Because this class otherwise has the same fields as
|
||||||
|
// Measurable, and Typescript doesn't support nominal typing, Typescript will
|
||||||
|
// consider it and other subclasses in the same situation as being of the same
|
||||||
|
// type, even if typeguards are used, which could result in Typescript typing
|
||||||
|
// objects of this class as `never`.
|
||||||
|
private inRowSpacer: undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param constants The rendering constants provider.
|
* @param constants The rendering constants provider.
|
||||||
* @param width The width of the spacer.
|
* @param width The width of the spacer.
|
||||||
|
|||||||
@@ -7,10 +7,7 @@
|
|||||||
// Former goog.module ID: Blockly.blockRendering.InputRow
|
// Former goog.module ID: Blockly.blockRendering.InputRow
|
||||||
|
|
||||||
import type {ConstantProvider} from '../common/constants.js';
|
import type {ConstantProvider} from '../common/constants.js';
|
||||||
import {ExternalValueInput} from './external_value_input.js';
|
|
||||||
import {InputConnection} from './input_connection.js';
|
|
||||||
import {Row} from './row.js';
|
import {Row} from './row.js';
|
||||||
import {StatementInput} from './statement_input.js';
|
|
||||||
import {Types} from './types.js';
|
import {Types} from './types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -40,12 +37,11 @@ export class InputRow extends Row {
|
|||||||
for (let i = 0; i < this.elements.length; i++) {
|
for (let i = 0; i < this.elements.length; i++) {
|
||||||
const elem = this.elements[i];
|
const elem = this.elements[i];
|
||||||
this.width += elem.width;
|
this.width += elem.width;
|
||||||
if (Types.isInput(elem) && elem instanceof InputConnection) {
|
if (Types.isInput(elem)) {
|
||||||
if (Types.isStatementInput(elem) && elem instanceof StatementInput) {
|
if (Types.isStatementInput(elem)) {
|
||||||
connectedBlockWidths += elem.connectedBlockWidth;
|
connectedBlockWidths += elem.connectedBlockWidth;
|
||||||
} else if (
|
} else if (
|
||||||
Types.isExternalInput(elem) &&
|
Types.isExternalInput(elem) &&
|
||||||
elem instanceof ExternalValueInput &&
|
|
||||||
elem.connectedBlockWidth !== 0
|
elem.connectedBlockWidth !== 0
|
||||||
) {
|
) {
|
||||||
connectedBlockWidths +=
|
connectedBlockWidths +=
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ import {Types} from './types.js';
|
|||||||
* collapsed block takes up during rendering.
|
* collapsed block takes up during rendering.
|
||||||
*/
|
*/
|
||||||
export class JaggedEdge extends Measurable {
|
export class JaggedEdge extends Measurable {
|
||||||
|
// This field exists solely to structurally distinguish this type from other
|
||||||
|
// Measurable subclasses. Because this class otherwise has the same fields as
|
||||||
|
// Measurable, and Typescript doesn't support nominal typing, Typescript will
|
||||||
|
// consider it and other subclasses in the same situation as being of the same
|
||||||
|
// type, even if typeguards are used, which could result in Typescript typing
|
||||||
|
// objects of this class as `never`.
|
||||||
|
private jaggedEdge: undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param constants The rendering constants provider.
|
* @param constants The rendering constants provider.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ import {Types} from './types.js';
|
|||||||
* up during rendering.
|
* up during rendering.
|
||||||
*/
|
*/
|
||||||
export class NextConnection extends Connection {
|
export class NextConnection extends Connection {
|
||||||
|
// This field exists solely to structurally distinguish this type from other
|
||||||
|
// Measurable subclasses. Because this class otherwise has the same fields as
|
||||||
|
// Measurable, and Typescript doesn't support nominal typing, Typescript will
|
||||||
|
// consider it and other subclasses in the same situation as being of the same
|
||||||
|
// type, even if typeguards are used, which could result in Typescript typing
|
||||||
|
// objects of this class as `never`.
|
||||||
|
private nextConnection: undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param constants The rendering constants provider.
|
* @param constants The rendering constants provider.
|
||||||
* @param connectionModel The connection object on the block that this
|
* @param connectionModel The connection object on the block that this
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ import {Types} from './types.js';
|
|||||||
* up during rendering.
|
* up during rendering.
|
||||||
*/
|
*/
|
||||||
export class PreviousConnection extends Connection {
|
export class PreviousConnection extends Connection {
|
||||||
|
// This field exists solely to structurally distinguish this type from other
|
||||||
|
// Measurable subclasses. Because this class otherwise has the same fields as
|
||||||
|
// Measurable, and Typescript doesn't support nominal typing, Typescript will
|
||||||
|
// consider it and other subclasses in the same situation as being of the same
|
||||||
|
// type, even if typeguards are used, which could result in Typescript typing
|
||||||
|
// objects of this class as `never`.
|
||||||
|
private previousConnection: undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param constants The rendering constants provider.
|
* @param constants The rendering constants provider.
|
||||||
* @param connectionModel The connection object on the block that this
|
* @param connectionModel The connection object on the block that this
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ import {Types} from './types.js';
|
|||||||
* during rendering.
|
* during rendering.
|
||||||
*/
|
*/
|
||||||
export class RoundCorner extends Measurable {
|
export class RoundCorner extends Measurable {
|
||||||
|
// This field exists solely to structurally distinguish this type from other
|
||||||
|
// Measurable subclasses. Because this class otherwise has the same fields as
|
||||||
|
// Measurable, and Typescript doesn't support nominal typing, Typescript will
|
||||||
|
// consider it and other subclasses in the same situation as being of the same
|
||||||
|
// type, even if typeguards are used, which could result in Typescript typing
|
||||||
|
// objects of this class as `never`.
|
||||||
|
private roundCorner: undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param constants The rendering constants provider.
|
* @param constants The rendering constants provider.
|
||||||
* @param opt_position The position of this corner.
|
* @param opt_position The position of this corner.
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export class Row {
|
|||||||
for (let i = this.elements.length - 1; i >= 0; i--) {
|
for (let i = this.elements.length - 1; i >= 0; i--) {
|
||||||
const elem = this.elements[i];
|
const elem = this.elements[i];
|
||||||
if (Types.isInput(elem)) {
|
if (Types.isInput(elem)) {
|
||||||
return elem as InputConnection;
|
return elem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -166,8 +166,8 @@ export class Row {
|
|||||||
getFirstSpacer(): InRowSpacer | null {
|
getFirstSpacer(): InRowSpacer | null {
|
||||||
for (let i = 0; i < this.elements.length; i++) {
|
for (let i = 0; i < this.elements.length; i++) {
|
||||||
const elem = this.elements[i];
|
const elem = this.elements[i];
|
||||||
if (Types.isSpacer(elem)) {
|
if (Types.isInRowSpacer(elem)) {
|
||||||
return elem as InRowSpacer;
|
return elem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -181,8 +181,8 @@ export class Row {
|
|||||||
getLastSpacer(): InRowSpacer | null {
|
getLastSpacer(): InRowSpacer | null {
|
||||||
for (let i = this.elements.length - 1; i >= 0; i--) {
|
for (let i = this.elements.length - 1; i >= 0; i--) {
|
||||||
const elem = this.elements[i];
|
const elem = this.elements[i];
|
||||||
if (Types.isSpacer(elem)) {
|
if (Types.isInRowSpacer(elem)) {
|
||||||
return elem as InRowSpacer;
|
return elem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -15,6 +15,14 @@ import {Types} from './types.js';
|
|||||||
* during rendering.
|
* during rendering.
|
||||||
*/
|
*/
|
||||||
export class SquareCorner extends Measurable {
|
export class SquareCorner extends Measurable {
|
||||||
|
// This field exists solely to structurally distinguish this type from other
|
||||||
|
// Measurable subclasses. Because this class otherwise has the same fields as
|
||||||
|
// Measurable, and Typescript doesn't support nominal typing, Typescript will
|
||||||
|
// consider it and other subclasses in the same situation as being of the same
|
||||||
|
// type, even if typeguards are used, which could result in Typescript typing
|
||||||
|
// objects of this class as `never`.
|
||||||
|
private squareCorner: undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param constants The rendering constants provider.
|
* @param constants The rendering constants provider.
|
||||||
* @param opt_position The position of this corner.
|
* @param opt_position The position of this corner.
|
||||||
|
|||||||
@@ -16,6 +16,14 @@ import {Types} from './types.js';
|
|||||||
* during rendering
|
* during rendering
|
||||||
*/
|
*/
|
||||||
export class StatementInput extends InputConnection {
|
export class StatementInput extends InputConnection {
|
||||||
|
// This field exists solely to structurally distinguish this type from other
|
||||||
|
// Measurable subclasses. Because this class otherwise has the same fields as
|
||||||
|
// Measurable, and Typescript doesn't support nominal typing, Typescript will
|
||||||
|
// consider it and other subclasses in the same situation as being of the same
|
||||||
|
// type, even if typeguards are used, which could result in Typescript typing
|
||||||
|
// objects of this class as `never`.
|
||||||
|
private statementInput: undefined;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param constants The rendering constants provider.
|
* @param constants The rendering constants provider.
|
||||||
* @param input The statement input to measure and store information for.
|
* @param input The statement input to measure and store information for.
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
|
|
||||||
import type {BlockSvg} from '../../block_svg.js';
|
import type {BlockSvg} from '../../block_svg.js';
|
||||||
import type {ConstantProvider} from '../common/constants.js';
|
import type {ConstantProvider} from '../common/constants.js';
|
||||||
import {Hat} from './hat.js';
|
|
||||||
import type {PreviousConnection} from './previous_connection.js';
|
import type {PreviousConnection} from './previous_connection.js';
|
||||||
import {Row} from './row.js';
|
import {Row} from './row.js';
|
||||||
import {Types} from './types.js';
|
import {Types} from './types.js';
|
||||||
@@ -85,7 +84,7 @@ export class TopRow extends Row {
|
|||||||
const elem = this.elements[i];
|
const elem = this.elements[i];
|
||||||
width += elem.width;
|
width += elem.width;
|
||||||
if (!Types.isSpacer(elem)) {
|
if (!Types.isSpacer(elem)) {
|
||||||
if (Types.isHat(elem) && elem instanceof Hat) {
|
if (Types.isHat(elem)) {
|
||||||
ascenderHeight = Math.max(ascenderHeight, elem.ascenderHeight);
|
ascenderHeight = Math.max(ascenderHeight, elem.ascenderHeight);
|
||||||
} else {
|
} else {
|
||||||
height = Math.max(height, elem.height);
|
height = Math.max(height, elem.height);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user