diff --git a/core/blockly.js b/core/blockly.js index 15914fd79..4b85b99bf 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -341,6 +341,42 @@ Blockly.getMainWorkspace = function() { return Blockly.mainWorkspace; }; +/** + * Wrapper to window.alert() that app developers may override to + * provide alternatives to the modal browser window. + * @param {string} message The message to display to the user. + * @param {function()=} opt_callback The callback when the alert is dismissed. + */ +Blockly.alert = function(message, opt_callback) { + window.alert(message); + if (opt_callback) { + opt_callback(); + } +}; + +/** + * Wrapper to window.confirm() that app developers may override to + * provide alternatives to the modal browser window. + * @param {string} message The message to display to the user. + * @param {!function(boolean)} callback The callback for handling user response. + */ +Blockly.confirm = function(message, callback) { + callback(window.confirm(message)); +}; + +/** + * Wrapper to window.prompt() that app developers may override to provide + * alternatives to the modal browser window. Built-in browser prompts are + * often used for better text input experience on mobile device. We strongly + * recommend testing mobile when overriding this. + * @param {string} message The message to display to the user. + * @param {string} defaultValue The value to initialize the prompt with. + * @param {!function(string)} callback The callback for handling user reponse. + */ +Blockly.prompt = function(message, defaultValue, callback) { + callback(window.prompt(message, defaultValue)); +}; + // IE9 does not have a console. Create a stub to stop errors. if (!goog.global['console']) { goog.global['console'] = { diff --git a/core/field_angle.js b/core/field_angle.js index 5bea3cbc1..1a303fcf4 100644 --- a/core/field_angle.js +++ b/core/field_angle.js @@ -130,7 +130,7 @@ Blockly.FieldAngle.prototype.showEditor_ = function() { Blockly.FieldAngle.superClass_.showEditor_.call(this, noFocus); var div = Blockly.WidgetDiv.DIV; if (!div.firstChild) { - // Mobile interface uses window.prompt. + // Mobile interface uses Blockly.prompt. return; } // Build the SVG DOM. diff --git a/core/field_textinput.js b/core/field_textinput.js index 5c057513f..8f3129eeb 100644 --- a/core/field_textinput.js +++ b/core/field_textinput.js @@ -114,11 +114,14 @@ Blockly.FieldTextInput.prototype.showEditor_ = function(opt_quietInput) { if (!quietInput && (goog.userAgent.MOBILE || goog.userAgent.ANDROID || goog.userAgent.IPAD)) { // Mobile browsers have issues with in-line textareas (focus & keyboards). - var newValue = window.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_); - if (this.sourceBlock_) { - newValue = this.callValidator(newValue); - } - this.setValue(newValue); + var fieldText = this; + Blockly.prompt(Blockly.Msg.CHANGE_VALUE_TITLE, this.text_, + function(newValue) { + if (fieldText.sourceBlock_) { + newValue = fieldText.callValidator(newValue); + } + fieldText.setValue(newValue); + }); return; } diff --git a/core/variables.js b/core/variables.js index 00c38ad21..35fb3139b 100644 --- a/core/variables.js +++ b/core/variables.js @@ -227,47 +227,57 @@ Blockly.Variables.generateUniqueName = function(workspace) { * Create a new variable on the given workspace. * @param {!Blockly.Workspace} workspace The workspace on which to create the * variable. - * @return {null|undefined|string} An acceptable new variable name, or null if - * change is to be aborted (cancel button), or undefined if an existing - * variable was chosen. + * @param {function(null|undefined|string)=} opt_callback A callback. It will + * return an acceptable new variable name, or null if change is to be + * aborted (cancel button), or undefined if an existing variable was chosen. */ -Blockly.Variables.createVariable = function(workspace) { - while (true) { - var text = Blockly.Variables.promptName(Blockly.Msg.NEW_VARIABLE_TITLE, ''); - if (text) { - if (workspace.variableIndexOf(text) != -1) { - window.alert(Blockly.Msg.VARIABLE_ALREADY_EXISTS.replace('%1', - text.toLowerCase())); - } else { - workspace.createVariable(text); - break; - } - } else { - text = null; - break; - } - } - return text; +Blockly.Variables.createVariable = function(workspace, opt_callback) { + var promptAndCheckWithAlert = function(defaultName) { + Blockly.Variables.promptName(Blockly.Msg.NEW_VARIABLE_TITLE, defaultName, + function(text) { + if (text) { + if (workspace.variableIndexOf(text) != -1) { + Blockly.alert(Blockly.Msg.VARIABLE_ALREADY_EXISTS.replace('%1', + text.toLowerCase()), + function() { + promptAndCheckWithAlert(text); // Recurse + }); + } else { + workspace.createVariable(text); + if (opt_callback) { + opt_callback(text); + } + } + } else { + // User canceled prompt without a value. + if (opt_callback) { + opt_callback(null); + } + } + }); + }; + promptAndCheckWithAlert(''); }; /** * Prompt the user for a new variable name. * @param {string} promptText The string of the prompt. * @param {string} defaultText The default value to show in the prompt's field. - * @return {?string} The new variable name, or null if the user picked - * something illegal. + * @param {function(?string)} callback A callback. It will return the new + * variable name, or null if the user picked something illegal. */ -Blockly.Variables.promptName = function(promptText, defaultText) { - var newVar = window.prompt(promptText, defaultText); - // Merge runs of whitespace. Strip leading and trailing whitespace. - // Beyond this, all names are legal. - if (newVar) { - newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); - if (newVar == Blockly.Msg.RENAME_VARIABLE || - newVar == Blockly.Msg.NEW_VARIABLE) { - // Ok, not ALL names are legal... - newVar = null; +Blockly.Variables.promptName = function(promptText, defaultText, callback) { + Blockly.prompt(promptText, defaultText, function(newVar) { + // Merge runs of whitespace. Strip leading and trailing whitespace. + // Beyond this, all names are legal. + if (newVar) { + newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, ''); + if (newVar == Blockly.Msg.RENAME_VARIABLE || + newVar == Blockly.Msg.NEW_VARIABLE) { + // Ok, not ALL names are legal... + newVar = null; + } } - } - return newVar; + callback(newVar); + }); }; diff --git a/core/workspace.js b/core/workspace.js index 805790869..5cf916571 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -309,26 +309,29 @@ Blockly.Workspace.prototype.deleteVariable = function(name) { if (block.type == 'procedures_defnoreturn' || block.type == 'procedures_defreturn') { var procedureName = block.getFieldValue('NAME'); - window.alert( + Blockly.alert( Blockly.Msg.CANNOT_DELETE_VARIABLE_PROCEDURE.replace('%1', name). - replace('%2', procedureName)); + replace('%2', procedureName)); return; } } - var ok = window.confirm( + var workspace = this; + Blockly.confirm( Blockly.Msg.DELETE_VARIABLE_CONFIRMATION.replace('%1', uses.length). - replace('%2', name)); - if (!ok) { - return; - } - } + replace('%2', name), + function(ok) { + if (!ok) { + return; + } - Blockly.Events.setGroup(true); - for (var i = 0; i < uses.length; i++) { - uses[i].dispose(true, false); + Blockly.Events.setGroup(true); + for (var i = 0; i < uses.length; i++) { + uses[i].dispose(true, false); + } + Blockly.Events.setGroup(false); + workspace.variableList.splice(variableIndex, 1); + }); } - Blockly.Events.setGroup(false); - this.variableList.splice(variableIndex, 1); } }; diff --git a/core/workspace_svg.js b/core/workspace_svg.js index 24797f93b..fe2ad54bd 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -998,10 +998,16 @@ Blockly.WorkspaceSvg.prototype.showContextMenu_ = function(e) { Blockly.Msg.DELETE_X_BLOCKS.replace('%1', String(deleteList.length)), enabled: deleteList.length > 0, callback: function() { - if (deleteList.length < 2 || - window.confirm(Blockly.Msg.DELETE_ALL_BLOCKS.replace('%1', - String(deleteList.length)))) { + if (deleteList.length < 2 ) { deleteNext(); + } else { + Blockly.confirm(Blockly.Msg.DELETE_ALL_BLOCKS. + replace('%1',String(deleteList.length)), + function(ok) { + if (ok) { + deleteNext(); + } + }); } } }; diff --git a/demos/custom-dialogs/custom-dialog.js b/demos/custom-dialogs/custom-dialog.js new file mode 100644 index 000000000..77a79660a --- /dev/null +++ b/demos/custom-dialogs/custom-dialog.js @@ -0,0 +1,163 @@ +/** + * Blockly Demos: Custom Dialogs + * + * Copyright 2016 Google Inc. + * https://developers.google.com/blockly/ + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +CustomDialog = {} + +/** Override Blockly.alert() with custom implementation. */ +Blockly.alert = function(message, callback) { + console.log('Alert: ' + message); + CustomDialog.show('Alert', message, { + onCancel: callback + }); +}; + +/** Override Blockly.confirm() with custom implementation. */ +Blockly.confirm = function(message, callback) { + console.log('Confirm: ' + message); + CustomDialog.show('Confirm', message, { + showOkay: true, + onOkay: function() { + callback(true) + }, + showCancel: true, + onCancel: function() { + callback(false) + } + }); +}; + +/** Override Blockly.prompt() with custom implementation. */ +Blockly.prompt = function(message, defaultValue, callback) { + console.log('Prompt: ' + message); + CustomDialog.show('Prompt', message, { + showInput: true, + showOkay: true, + onOkay: function() { + callback(CustomDialog.inputField.value) + }, + showCancel: true, + onCancel: function() { + callback(null) + } + }); + CustomDialog.inputField.value = defaultValue; +}; + + +CustomDialog.hide = function() { + if (CustomDialog.backdropDiv_) { + CustomDialog.backdropDiv_.style.display = 'none' + CustomDialog.dialogDiv_.style.display = 'none' + } +} + +/** + * Shows the dialog. + * Allowed options: + * - showOkay: Whether to show the OK button. + * - showCancel: Whether to show the Cancel button. + * - showInput: Whether to show the text input field. + * - onOkay: Callback to handle the okay button. + * - onCancel: Callback to handle the cancel button. + */ +CustomDialog.show = function(title, message, options) { + var backdropDiv = CustomDialog.backdropDiv_; + var dialogDiv = CustomDialog.dialogDiv_; + if (!dialogDiv) { + // Generate HTML + var body = document.getElementsByTagName('body')[0]; + backdropDiv = document.createElement('div'); + backdropDiv.setAttribute('id', 'backdrop'); + backdropDiv.style.cssText = + 'position: absolute;' + + 'top: 0px;' + + 'left: 0px;' + + 'right: 0px;' + + 'bottom: 0px;' + + 'background-color: rgba(0, 0, 0, 0.7);'; + body.appendChild(backdropDiv); + + dialogDiv = document.createElement('div'); + dialogDiv.setAttribute('id', 'dialog'); + dialogDiv.style.cssText = + 'background-color: white;' + + 'width: 400px;' + + 'margin: 20px auto 0;' + + 'padding: 10px;'; + backdropDiv.appendChild(dialogDiv); + + dialogDiv.onclick = function(event) { + event.stopPropagation(); + }; + + CustomDialog.backdropDiv_ = backdropDiv; + CustomDialog.dialogDiv_ = dialogDiv; + } + backdropDiv.style.display = 'block'; + dialogDiv.style.display = 'block'; + + dialogDiv.innerHTML = '
' + title + '
' + + '

' + message + '

' + + (options.showInput? '
' : '') + + '
' + + (options.showCancel ? '': '') + + (options.showOkay ? '': '') + + '
'; + + var onOkay = function(event) { + CustomDialog.hide(); + options.onOkay && options.onOkay(); + event && event.stopPropagation(); + }; + var onCancel = function(event) { + CustomDialog.hide(); + options.onCancel && options.onCancel(); + event && event.stopPropagation(); + }; + + var dialogInput = document.getElementById('dialogInput'); + CustomDialog.inputField = dialogInput; + if (dialogInput) { + dialogInput.focus(); + + dialogInput.onkeyup = function(event) { + if (event.keyCode == 13) { + // Process as OK when user hits enter. + onOkay(); + return false; + } else if (event.keyCode == 27) { + // Process as cancel when user hits esc. + onCancel(); + return false; + } + }; + } else { + var okay = document.getElementById('dialogOkay'); + okay && okay.focus(); + } + + if (options.showOkay) { + document.getElementById('dialogOkay').onclick = onOkay; + } + if (options.showCancel) { + document.getElementById('dialogCancel').onclick = onCancel; + } + + backdropDiv.onclick = onCancel; +} diff --git a/demos/custom-dialogs/index.html b/demos/custom-dialogs/index.html new file mode 100644 index 000000000..42cba5477 --- /dev/null +++ b/demos/custom-dialogs/index.html @@ -0,0 +1,57 @@ + + + + + Blockly Demo: Custom Dialog + + + + + + +

Blockly > + Demos > Custom Dialog

+ +

This is a simple demo of replacing modal browser dialogs with HTML.

+ +

Try creating new variables, creating variables with names already in + use, or deleting multiple blocks on the workspace. +

+ +
+ + + + + + + +