From 8f3b4bcb5ebea62e45b42fd3acf69638fe7100ba Mon Sep 17 00:00:00 2001 From: Andrew n marshall Date: Thu, 20 Oct 2016 09:30:45 -0700 Subject: [PATCH] Replaces calls to window.alert(), window.confirm(), and window.prompt() with Blockly.alert(), Blockly.confirm(), and Blockly.prompt(). These are designed to allow app developers to replace the dialogs with versions that match their own open app, possibly avoiding modal browser dialogs. They each take a callback, so the developer has the opportunity to implement non-modal behavior. --- core/blockly.js | 36 +++ core/field_angle.js | 2 +- core/field_textinput.js | 13 +- core/variables.js | 78 +++--- core/workspace.js | 29 ++- core/workspace_svg.js | 12 +- demos/custom-dialogs/custom-dialog.js | 101 ++++++++ demos/custom-dialogs/index.html | 352 ++++++++++++++++++++++++++ 8 files changed, 567 insertions(+), 56 deletions(-) create mode 100644 demos/custom-dialogs/custom-dialog.js create mode 100644 demos/custom-dialogs/index.html 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. +

+ +
+ + + + + + + +