diff --git a/core/blockly.js b/core/blockly.js index 106957f52..da6adc834 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -338,6 +338,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 a294948e9..75d779f00 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 5af8bf9a7..6356481d3 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 a8e97c04b..81b1ad3a4 100644 --- a/core/workspace.js +++ b/core/workspace.js @@ -316,26 +316,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 6c72c934e..d7074e1d7 100644 --- a/core/workspace_svg.js +++ b/core/workspace_svg.js @@ -985,10 +985,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..984448160 --- /dev/null +++ b/demos/custom-dialogs/custom-dialog.js @@ -0,0 +1,101 @@ +/** Override Blockly.alert() with custom implementation. */ +Blockly.alert = function(message, callback) { + showDialog('Alert', message, { + okay: callback + }); +} + +/** Override Blockly.confirm() with custom implementation. */ +Blockly.confirm = function(message, callback) { + showDialog('Confirm', message, { + showOkay: true, + okay: function() { + callback(true) + }, + showCancel: true, + cancel: function() { + callback(false) + } + }); +} + +/** Override Blockly.prompt() with custom implementation. */ +Blockly.prompt = function(message, defaultValue, callback) { + showDialog('Prompt', message, { + showInput: true, + showOkay: true, + okay: function() { + callback(dialogInput.value) + }, + showCancel: true, + cancel: function() { + callback(null) + } + }); + dialogInput.value = defaultValue +} + +// Implementation details below... +var backdropDiv, dialogDiv, dialogInput + +function hideDialog() { + backdropDiv.style.display = 'none' + dialogDiv.style.display = 'none' +} + +function showDialog(title, message, options) { + if (!backdropDiv) { + // Generate HTML + var body = document.getElementsByTagName('body')[0] + backdropDiv = document.createElement('div') + backdropDiv.setAttribute('id', 'backdrop') + body.appendChild(backdropDiv) + + dialogDiv = document.createElement('div') + dialogDiv.setAttribute('id', 'dialog') + backdropDiv.appendChild(dialogDiv) + dialogDiv.onclick = function(event) { + event.stopPropagation() + } + + } + backdropDiv.style.display = 'block' + dialogDiv.style.display = 'block' + + dialogDiv.innerHTML = '
' + title + '
' + + '

' + message + '

' + + (options.showInput? '
' : '') + + '
' + + (options.showCancel ? '': '') + + (options.showOkay ? '': '') + + '
' + dialogInput = document.getElementById('dialogInput') + + document.getElementById('dialogOkay').onclick = function(event) { + hideDialog() + if (options.okay) { + options.okay() + } + event.stopPropagation() + } + document.getElementById('dialogCancel').onclick = function(event) { + hideDialog() + if (options.cancel) { + options.cancel() + } + event.stopPropagation() + } + + backdropDiv.onclick = function(event) { + hideDialog() + if (options.cancel) { + options.cancel(); + } else if (options.okay) { + options.okay(); + } + event.stopPropagation() + } +} + + + diff --git a/demos/custom-dialogs/index.html b/demos/custom-dialogs/index.html new file mode 100644 index 000000000..0d203fc03 --- /dev/null +++ b/demos/custom-dialogs/index.html @@ -0,0 +1,352 @@ + + + + + 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. +

+ +
+ + + + + + + +