Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions packages/blockly/core/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
/** Has CSS already been injected? */
const injectionSites = new WeakSet<Document | ShadowRoot>();
const registeredStyleSheets: Array<CSSStyleSheet> = [];
import * as userAgent from './utils/useragent.js';

/**
* Add some CSS to the blob that will be injected later. Allows optional
Expand Down Expand Up @@ -659,4 +660,20 @@ input[type=number] {
outline: var(--blockly-selection-width) solid var(--blockly-active-node-color);
border-radius: 2px;
}
.blocklyDialog {
min-width: 300px;
border-radius: 16px;
box-shadow: 0 8px 8px rgba(0, 0, 0, 0.2);
border: 1px solid #999;
}
.blocklyDialogForm {
display: flex;
flex-direction: column;
row-gap: 8px;
}
.blocklyDialogButtonRow {
display: flex;
flex-direction: ${userAgent.MOBILE || userAgent.APPLE ? 'row-reverse' : 'row'};
column-gap: 8px;
}
`;
131 changes: 123 additions & 8 deletions packages/blockly/core/dialog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@

// Former goog.module ID: Blockly.dialog

import {getFocusManager} from './focus_manager.js';
import {Msg} from './msg.js';
import type {ToastOptions} from './toast.js';
import {Toast} from './toast.js';
import type {WorkspaceSvg} from './workspace_svg.js';

/** Supported types of dialogs. */
enum DialogType {
ALERT = 1,
CONFIRM,
PROMPT,
}

/** Tally of the number of dialogs currently on screen. */
let activeDialogCount = 0;

const defaultAlert = function (message: string, opt_callback?: () => void) {
window.alert(message);
if (opt_callback) {
opt_callback();
}
displayDialog(DialogType.ALERT, message, opt_callback, undefined);
};

let alertImplementation = defaultAlert;
Expand All @@ -23,7 +32,7 @@ const defaultConfirm = function (
message: string,
callback: (result: boolean) => void,
) {
callback(window.confirm(message));
displayDialog(DialogType.CONFIRM, message, callback, undefined);
};

let confirmImplementation = defaultConfirm;
Expand All @@ -33,9 +42,7 @@ const defaultPrompt = function (
defaultValue: string,
callback: (result: string | null) => void,
) {
// NOTE TO DEVELOPER: Ephemeral focus doesn't need to be taken for the native
// window prompt since it prevents focus from changing while open.
callback(window.prompt(message, defaultValue));
displayDialog(DialogType.PROMPT, message, callback, defaultValue);
};

let promptImplementation = defaultPrompt;
Expand Down Expand Up @@ -165,3 +172,111 @@ export function setToast(
) {
toastImplementation = toastFunction;
}

/**
* Displays a dialog, potentially prompting for user input, and invokes the
* provided callback with the response.
*/
function displayDialog(
...[type, message, callback, defaultValue]:
| [
type: DialogType.PROMPT,
message: string,
callback: (result: string | null) => void,
defaultValue: string | undefined,
]
| [
type: DialogType.CONFIRM,
message: string,
callback: (result: boolean) => void,
defaultValue: undefined,
]
| [
type: DialogType.ALERT,
message: string,
callback: (() => void) | undefined,
defaultValue: undefined,
]
): void {
const OK = 'ok';
const CANCEL = 'cancel';

const dialog = document.createElement('dialog');
const form = document.createElement('form');
const label = document.createElement('label');
const input = document.createElement('input');
const buttonRow = document.createElement('div');
const ok = document.createElement('button');

dialog.className = 'blocklyDialog';
form.className = 'blocklyDialogForm';
label.className = 'blocklyDialogPrompt';
buttonRow.className = 'blocklyDialogButtonRow';
ok.className = 'blocklyDialogConfirmButton';

form.setAttribute('method', 'dialog');

label.textContent = message;
label.setAttribute('for', 'blockly-form-input');

ok.textContent = Msg['DIALOG_OK'];
ok.value = OK;

dialog.appendChild(form);
form.appendChild(label);

if (type === DialogType.PROMPT) {
input.id = 'blockly-form-input';
input.className = 'blocklyDialogInput';
input.type = 'text';
input.name = 'input';
input.autofocus = true;
if (defaultValue) {
input.value = defaultValue;
}
form.appendChild(input);
}

buttonRow.appendChild(ok);

if (type === DialogType.CONFIRM || type === DialogType.PROMPT) {
const cancel = document.createElement('button');
cancel.className = 'blocklyDialogCancelButton';
cancel.textContent = Msg['DIALOG_CANCEL'];
cancel.value = CANCEL;
buttonRow.appendChild(cancel);
}

form.appendChild(buttonRow);

const focusedNode = getFocusManager().getFocusedNode();
let restoreFocus: (() => void) | undefined;
if (focusedNode && activeDialogCount === 0) {
restoreFocus = getFocusManager().takeEphemeralFocus(
focusedNode.getFocusableElement(),
);
}

activeDialogCount++;

dialog.addEventListener('close', () => {
activeDialogCount--;
if (!activeDialogCount) {
restoreFocus?.();
}
dialog.remove();
switch (type) {
case DialogType.CONFIRM:
callback(dialog.returnValue === OK);
break;
case DialogType.PROMPT:
callback(dialog.returnValue === OK ? input.value : null);
break;
case DialogType.ALERT:
callback?.();
}
});
document.body.appendChild(dialog);

dialog.showModal();
}
11 changes: 11 additions & 0 deletions packages/blockly/core/flyout_base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import * as eventUtils from './events/utils.js';
import {FlyoutItem} from './flyout_item.js';
import {FlyoutMetricsManager} from './flyout_metrics_manager.js';
import {FlyoutSeparator, SeparatorAxis} from './flyout_separator.js';
import {getFocusManager} from './focus_manager.js';
import {IAutoHideable} from './interfaces/i_autohideable.js';
import type {IFlyout} from './interfaces/i_flyout.js';
import type {IFlyoutInflater} from './interfaces/i_flyout_inflater.js';
Expand Down Expand Up @@ -583,6 +584,16 @@ export abstract class Flyout
* toolbox definition, or a string with the name of the dynamic category.
*/
show(flyoutDef: toolbox.FlyoutDefinition | string) {
// As part of showing, the existing contents of the flyout will be cleared.
// If the focused element is a flyout item, i.e. a child of the workspace
// and not the workspace itself, move focus to the workspace to prevent
// focus from being lost when the currently focused element is destroyed.
if (
getFocusManager().getFocusedTree() === this.workspace_ &&
getFocusManager().getFocusedNode() !== this.workspace_
) {
getFocusManager().focusNode(this.workspace_);
}
this.workspace_.setResizesEnabled(false);
eventUtils.setRecordUndo(false);
this.hide();
Expand Down
28 changes: 26 additions & 2 deletions packages/blockly/core/variables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
// Former goog.module ID: Blockly.Variables

import type {Block} from './block.js';
import type {BlockSvg} from './block_svg.js';
import {Blocks} from './blocks.js';
import * as dialog from './dialog.js';
import type {BlockCreate} from './events/events.js';
import * as Events from './events/events.js';
import {getFocusManager} from './focus_manager.js';
import {isLegacyProcedureDefBlock} from './interfaces/i_legacy_procedure_blocks.js';
import {isVariableBackedParameterModel} from './interfaces/i_variable_backed_parameter_model.js';
import {IVariableModel, IVariableState} from './interfaces/i_variable_model.js';
Expand Down Expand Up @@ -403,7 +407,7 @@ export function generateUniqueNameFromOptions(
* will default to '', which is a specific type.
*/
export function createVariableButtonHandler(
workspace: Workspace,
workspace: WorkspaceSvg,
opt_callback?: (p1?: string | null) => void,
opt_type?: string,
) {
Expand All @@ -420,8 +424,28 @@ export function createVariableButtonHandler(
const existing = nameUsedWithAnyType(text, workspace);
if (!existing) {
// No conflict
workspace.getVariableMap().createVariable(text, type);
const variable = workspace.getVariableMap().createVariable(text, type);
if (opt_callback) opt_callback(text);
const flyoutWorkspace = workspace.getFlyout()?.getWorkspace();
if (!flyoutWorkspace) return;
const changeListener = (e: Events.Abstract) => {
// Focus the newly created variable_set block.
if (e.type === Events.BLOCK_CREATE) {
const blockId = (e as BlockCreate).blockId;
if (blockId) {
const block = flyoutWorkspace.getBlockById(blockId);
if (
block &&
block.type === 'variables_set' &&
block.getFieldValue('VAR') === variable.getId()
) {
getFocusManager().focusNode(block as BlockSvg);
flyoutWorkspace.removeChangeListener(changeListener);
}
}
}
};
flyoutWorkspace.addChangeListener(changeListener);
return;
}

Expand Down
20 changes: 14 additions & 6 deletions packages/blockly/tests/mocha/contextmenu_items_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -318,30 +318,38 @@ suite('Context Menu Items', function () {

test('Deletes all blocks after confirming', function () {
// Mocks the confirmation dialog and calls the callback with 'true' simulating ok.
const confirmStub = sinon.stub(window, 'confirm').returns(true);
let callCount = 0;
Blockly.dialog.setConfirm((_message, callback) => {
callCount++;
callback(true);
});

this.workspace.newBlock('text');
this.workspace.newBlock('text');
this.deleteOption.callback(this.scope);
this.clock.runAll();
sinon.assert.calledOnce(confirmStub);
assert.equal(callCount, 1);
assert.equal(this.workspace.getTopBlocks(false).length, 0);

confirmStub.restore();
Blockly.dialog.setConfirm();
});

test('Does not delete blocks if not confirmed', function () {
// Mocks the confirmation dialog and calls the callback with 'false' simulating cancel.
const confirmStub = sinon.stub(window, 'confirm').returns(false);
let callCount = 0;
Blockly.dialog.setConfirm((_message, callback) => {
callCount++;
callback(false);
});

this.workspace.newBlock('text');
this.workspace.newBlock('text');
this.deleteOption.callback(this.scope);
this.clock.runAll();
sinon.assert.calledOnce(confirmStub);
assert.equal(callCount, 1);
assert.equal(this.workspace.getTopBlocks(false).length, 2);

confirmStub.restore();
Blockly.dialog.setConfirm();
});

test('No dialog for single block', function () {
Expand Down
Loading
Loading