| /* |
| * Copyright 2011 Google Inc. |
| * |
| * 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. |
| */ |
| |
| /** |
| * @fileoverview The implementation of the "Dev Mode On" and "Compile" |
| * bookmarklets. |
| * |
| * The "Dev Mode On" bookmarklet displays a dialog at the top of the page |
| * allowing the user to choose a GWT app to recompile. This dialog |
| * contains a "Compile" bookmarklet for each available module. |
| * |
| * The "Compile" bookmarklet tells the code server to recompile a GWT app, |
| * and if successful, reloads the current page with Super Dev Mode enabled. |
| * |
| * The bookmarklets themselves are just bootstrap scripts that set some |
| * parameters on window and include this script. That way, users don't have |
| * to delete and replace their bookmarklets when we change this code. |
| */ |
| (function() { |
| var $doc = document; |
| |
| // Set up globals needed for JSONP calls. (These persist between bookmarklet |
| // calls, in case the user clicks the bookmarklet more than once.) |
| var globals = window.__gwt_bookmarklet_globals; |
| if (typeof globals == 'undefined') { |
| globals = { |
| callback_counter: 0, |
| callbacks: {}, |
| dismissModuleDialog: function() {} |
| }; |
| window.__gwt_bookmarklet_globals = globals; |
| } |
| |
| /** |
| * Deletes and returns the parameters passed in by the "Dev Mode On" or |
| * "Compile" bookmarklet. We expect the bookmarklet to pass the following |
| * parameters: |
| * <ul> |
| * <li>server_url The top-level URL of the code server, including the |
| * trailing slash. |
| * <li>module_name (optional) The name of the module to compile. (Skips |
| * the module dialog if the module is available.) |
| * </ul> |
| * @return {Object} The parameters, or null if not found. |
| */ |
| function getBookmarkletParams() { |
| var params = window.__gwt_bookmarklet_params; |
| if (!params) { |
| return null; |
| } |
| try { |
| delete window.__gwt_bookmarklet_params; |
| } catch (e) { |
| // Delete window.x throws and exception in IE8. |
| window['__gwt_bookmarklet_params'] = null; |
| } |
| return params; |
| } |
| |
| /** |
| * Creates an element and populates it with text. |
| * Ensures that a text is black, regardless of style sheet. |
| * @param tagName {string} |
| * @param fontSize {string} |
| * @param text {string} |
| */ |
| function makeTextElt(tagName, fontSize, text) { |
| var elt = $doc.createElement(tagName); |
| elt.style.color = 'black'; |
| elt.style.background = 'white'; |
| elt.style.fontSize = fontSize; |
| elt.appendChild($doc.createTextNode(text)); |
| return elt; |
| } |
| |
| function makeOverlay() { |
| var overlay = $doc.createElement('div'); |
| overlay.style.zIndex = 1000000; |
| overlay.style.position = 'absolute'; |
| overlay.style.top = 0; |
| overlay.style.left = 0; |
| overlay.style.bottom = 0; |
| overlay.style.right = 0; |
| overlay.style.background = 'black'; // darken background |
| overlay.style.opacity = '0.5'; |
| return overlay; |
| } |
| |
| function makeDialog() { |
| var dialog = $doc.createElement('div'); |
| dialog.style.zIndex = 1000001; |
| dialog.style.position = 'fixed'; |
| dialog.style.top = '20pt'; |
| dialog.style.left = '20pt'; |
| dialog.style.right = '20pt'; |
| dialog.style.color = 'black'; |
| dialog.style.background = 'white'; |
| dialog.style.border = '4px solid #ccc'; |
| dialog.style.padding = '1em'; |
| dialog.style.borderRadius = '5px'; |
| dialog.style.wordWrap = 'break-word'; |
| return dialog; |
| } |
| |
| function makeBookmarklet(name, javascript) { |
| var result = makeTextElt('a', '12pt', name); |
| result.style.fontFamily = 'sans'; |
| result.style.textDecoration = 'none'; |
| result.style.background = '#ddd'; |
| result.style.border = '2px outset #ddd'; |
| result.style.padding = '3pt'; |
| result.setAttribute('href', 'javascript:' + encodeURIComponent(javascript)); |
| result.title = 'Tip: drag this button to the bookmark bar'; |
| return result; |
| } |
| |
| function makeCompileBookmarklet(codeserver_url, module_name) { |
| var bookmarklets_js = codeserver_url + 'dev_mode_on.js'; |
| var javascript = '{ window.__gwt_bookmarklet_params = {' |
| + 'server_url:\'' + codeserver_url + '\',' |
| + 'module_name:\'' + module_name + '\'};' |
| + ' var s = document.createElement(\'script\');' |
| + ' s.src = \'' + bookmarklets_js + '\';' |
| + ' void(document.getElementsByTagName(\'head\')[0].appendChild(s));}'; |
| return makeBookmarklet('Compile', javascript); |
| } |
| |
| /** |
| * Determines whether we can recompile a module and see the results. If not, |
| * explains why not. |
| * @param module_name {string} |
| * @return {string} The error message, or null if there is no error and |
| * a recompile will succeed. |
| */ |
| function getCannotCompileError(module_name) { |
| if (!isModuleOnCodeServer(module_name)) { |
| return 'The code server isn\'t configured to compile this module.'; |
| } |
| |
| var modules_on_page = window.__gwt_activeModules; |
| if (!modules_on_page || !(module_name in modules_on_page)) { |
| return 'The current page doesn\'t have this module.'; |
| } |
| |
| var mod = modules_on_page[module_name]; |
| |
| var dev_mode_key = '__gwtDevModeHook:' + module_name; |
| var dev_mode_on = mod['superdevmode'] || |
| window.sessionStorage[dev_mode_key]; |
| |
| if (!dev_mode_on && !mod.canRedirect) { |
| return 'This module doesn\'t have Super Dev Mode enabled.'; |
| } |
| |
| // looks okay |
| return null; |
| } |
| |
| /** |
| * Determines if the code server is configured to run the given module. |
| * @param module_name {string} |
| * @return {boolean} true if the code server supports the given module. |
| */ |
| function isModuleOnCodeServer(module_name) { |
| var modules_on_codeserver = window.__gwt_codeserver_config.moduleNames; |
| // Support browsers without indexOf() (e.g. IE8). |
| for (var i = 0; i < modules_on_codeserver.length; i++) { |
| if (modules_on_codeserver[i] == module_name) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| /** |
| * Displays the "Choose module to compile" dialog. |
| * @param codeserver_url {string} The URL of the code server that will |
| * compile the chosen module. |
| */ |
| function showModuleDialog(codeserver_url) { |
| |
| function makeHeader() { |
| return makeTextElt('div', '20pt', 'Choose a module to recompile:'); |
| } |
| |
| function makeModuleRows(mod) { |
| var module_name = mod.moduleName; |
| var error = getCannotCompileError(module_name); |
| var moduleColor = error ? 'grey' : 'black'; |
| |
| var row = $doc.createElement('tr'); |
| |
| // Bullet and module name |
| var cell = $doc.createElement('td'); |
| var text = makeTextElt('span', '14pt', "\u2022 " + module_name + ": "); |
| text.style.color = moduleColor; |
| cell.appendChild(text); |
| |
| // Status (usually clickable) |
| var status = makeTextElt('span', '14pt', mod.superdevmode ? "on" : "off"); |
| status.style.color = moduleColor; |
| if (!error || status.superdevmode) { |
| status.style.cursor = "pointer"; |
| status.onclick = function() { |
| if (mod.superdevmode) { |
| reloadWithoutDevMode(module_name); |
| } else { |
| reloadInDevMode(module_name, codeserver_url); |
| } |
| return false; |
| }; |
| status.title = "Click to turn " + |
| (mod.superdevmode ? "off." : "on without recompiling.") + |
| " (Reloads the page.)"; |
| } |
| cell.appendChild(status); |
| |
| row.appendChild(cell); |
| |
| if (!error) { |
| // Compile button |
| var button = makeCompileBookmarklet(codeserver_url, module_name); |
| button.className = 'module_' + module_name; |
| cell = $doc.createElement('td'); |
| cell.style.paddingLeft = '1em'; |
| cell.appendChild(button); |
| row.appendChild(cell); |
| } |
| |
| var rows = [row]; |
| if (error) { |
| // Error message |
| cell = makeTextElt('td', '10pt', error); |
| cell.style.color = 'gray'; |
| |
| row = $doc.createElement('tr'); |
| row.appendChild(cell); |
| rows.push(row); |
| } |
| |
| return rows; |
| } |
| |
| function makeCodeServerLink() { |
| var div = $doc.createElement('div'); |
| div.style.float = "right"; |
| |
| var hostPort = codeserver_url.replace(/^http:\/\//, '').replace(/\/$/, ''); |
| var link = makeTextElt('a', '10pt', hostPort); |
| link.style.textDecoration = "none"; |
| link.setAttribute('href', codeserver_url); |
| link.setAttribute('target', '_blank'); |
| link.title = "The address of the server this bookmarklet uses to compile."; |
| div.appendChild(link); |
| return div; |
| } |
| |
| function makeFooter() { |
| var warnings = window.__gwt_codeserver_config.warnings; |
| var div = $doc.createElement("div"); |
| if (warnings.length > 0) { |
| div.style.marginTop = "10px"; |
| } |
| div.appendChild(makeCodeServerLink()); |
| for (var i = 0; i < warnings.length; i++) { |
| var warning = makeTextElt('div', '10pt', warnings[i]); |
| div.appendChild(warning); |
| } |
| return div; |
| } |
| |
| var active_modules = window.__gwt_activeModules; |
| |
| var moduleTable = $doc.createElement('table'); |
| moduleTable.style.marginTop = "10px"; |
| for (var module_name in active_modules) { |
| var rows = makeModuleRows(active_modules[module_name]); |
| for (var i = 0; i < rows.length; i++) { |
| moduleTable.appendChild(rows[i]); |
| } |
| } |
| |
| // Assemble the dialog. |
| var dialog = makeDialog(); |
| if (moduleTable.hasChildNodes()) { |
| dialog.appendChild(makeHeader()); |
| dialog.appendChild(moduleTable); |
| } else { |
| dialog.appendChild(makeTextElt('span', '16pt', |
| 'Can\'t find any GWT Modules on this page.')); |
| } |
| dialog.appendChild(makeFooter()); |
| |
| // Grey out everything under the dialog. |
| var overlay = makeOverlay(); |
| |
| var body = $doc.getElementsByTagName('body')[0]; |
| body.appendChild(overlay); |
| body.appendChild(dialog); |
| |
| globals.dismissModuleDialog = function() { |
| body.removeChild(dialog); |
| body.removeChild(overlay); |
| globals.dismissModuleDialog = function() {}; // uninstall |
| }; |
| |
| // Clicking outside the module dialog dismisses it. |
| overlay.onclick = function() { |
| globals.dismissModuleDialog(); |
| }; |
| } |
| |
| /** |
| * Displays the "Compiling..." dialog. |
| * @param {string} text A line of text to display. |
| * @return {Object} An object representing the dialog. It |
| * has two methods, updateProgress and showError. |
| */ |
| function showCompilingDialog(text) { |
| // Grey out everything under the dialog. |
| var overlay = makeOverlay(); |
| var dialog = makeDialog(); |
| |
| var message = makeTextElt('div', '16pt', text); |
| dialog.appendChild(message); |
| |
| var body = $doc.getElementsByTagName('body')[0]; |
| body.appendChild(overlay); |
| body.appendChild(dialog); |
| |
| var result = {}; |
| |
| /** |
| * Updates the progress while compiling. |
| */ |
| result.updateProgress = function(json) { |
| // Just append a dot for now. |
| message.appendChild($doc.createTextNode(" .")); |
| }; |
| |
| /** |
| * Updates the dialog with an error message. |
| * @param {string} errorText The text to display in red. |
| * @param {string} log_url A URL that the error message should link to. |
| * @param {function()} onClickTryAgain A callback for the |
| * "Try Again" button. |
| */ |
| result.showError = function(errorText, log_url, onClickTryAgain) { |
| var error = makeTextElt('a', '16pt', errorText); |
| error.setAttribute('href', log_url); |
| error.setAttribute('target', 'gwt_dev_mode_log'); |
| error.style.color = 'red'; |
| error.style.textDecoration = 'underline'; |
| message.appendChild(error); |
| |
| var button = makeTextElt('button', '12pt', 'Try Again'); |
| button.onclick = function() { |
| body.removeChild(dialog); |
| body.removeChild(overlay); |
| onClickTryAgain(); |
| }; |
| dialog.appendChild(button); |
| }; |
| |
| return result; |
| } |
| |
| /** |
| * Makes a JSONP call. Assumes that the callback parameter is named |
| * "callback". Handles multiple callbacks in flight at once. |
| * @param {string} url_prefix A URL prefix that ends with '?' or '&'. |
| * @param {function(Object)} callback A function to call with the result |
| * of the JSONP call. |
| */ |
| function callJsonp(url_prefix, callback) { |
| var callback_id = 'c' + globals.callback_counter++; |
| globals.callbacks[callback_id] = function(json) { |
| delete globals.callbacks[callback_id]; |
| callback(json); |
| }; |
| |
| var url = url_prefix + '_callback=__gwt_bookmarklet_globals.callbacks.' + |
| callback_id; |
| |
| var script = $doc.createElement('script'); |
| script.src = url; |
| $doc.getElementsByTagName('head')[0].appendChild(script); |
| } |
| |
| /** |
| * Gets the binding properties to use for this recompile. The properties |
| * come from the GWT application on the current page, with a fallback |
| * to a cached value in session storage. (The cached value will normally |
| * be used if the current page is already using dev mode.) |
| * @param {string} module_name Identifies the GWT app we are recompiling. |
| * @param {function} get_prop_map Returns the app's binding properties. |
| * @return {string} Zero or more encoded url parameters; the string is |
| * either empty or ends with '&'. |
| */ |
| function getBindingParameters(module_name, get_prop_map) { |
| var session_key = '__gwtDevModeSession:' + module_name; |
| |
| var prop_map = get_prop_map(); |
| var props = []; |
| for (var key in prop_map) { |
| props.push(encodePair(key, prop_map[key])); |
| } |
| |
| if (!props.length) { |
| // There is only one permutation, maybe because we're in dev mode already. |
| // Use the cached value if present. |
| var cached = window.sessionStorage[session_key]; |
| return cached || ''; |
| } |
| |
| var encoded = props.join('&') + '&'; |
| // Cache it for the next recompile. |
| window.sessionStorage[session_key] = encoded; |
| return encoded; |
| } |
| |
| function encodePair(key, value) { |
| return encodeURIComponent(key) + '=' + encodeURIComponent(value); |
| } |
| |
| /** |
| * Tells the GWT application to replace itself with a different version that |
| * the code server is serving, then reloads the page. (This only works if |
| * the GWT application was compiled with the new dev mode hook turned on in |
| * the GWT linker.) |
| * |
| * @param {string} module_name The module to replace (after rename). |
| * @param {string} codeserver_url The code server to use |
| * (with trailing slash). |
| */ |
| function reloadInDevMode(module_name, codeserver_url) { |
| var key = '__gwtDevModeHook:' + module_name; |
| sessionStorage[key] = codeserver_url + module_name + '/' + |
| module_name + '.nocache.js'; |
| window.location.reload(); |
| } |
| |
| /** |
| * Turns dev mode off for the given module, then reloads the page. |
| * @param {string} module_name |
| */ |
| function reloadWithoutDevMode(module_name) { |
| var key = '__gwtDevModeHook:' + module_name; |
| sessionStorage.removeItem(key); |
| window.location.reload(); |
| } |
| |
| /** |
| * Displays the "compiling" dialog and starts the recompile. |
| * @param {string} module_name The module to replace (after rename). |
| * @param {string} codeserver_url The code server to use. |
| * @param {function} get_prop_map Returns the app's binding properties. |
| */ |
| function compile(module_name, codeserver_url, get_prop_map) { |
| var dialog = showCompilingDialog('Compiling ' + module_name + '...'); |
| |
| var compiling = false; |
| var lastPollStart; |
| |
| function onPollFinished(event) { |
| if (compiling && event.status == "compiling") { |
| dialog.updateProgress(event); |
| // Date.now() fails in IE8 |
| var waitTime = 1000 - (new Date().getTime() - lastPollStart); |
| if (waitTime > 0) { |
| setTimeout(poll, waitTime); |
| } else { |
| poll(); |
| } |
| } |
| // otherwise it's idle or an unknown event type, so stop |
| } |
| |
| function poll() { |
| if (compiling) { |
| // Date.now() fails in IE8 |
| lastPollStart = new Date().getTime(); |
| callJsonp(codeserver_url + 'progress?', onPollFinished); |
| } |
| } |
| |
| function onClickTryAgain() { |
| // Just start over from the beginning. |
| compile(module_name, codeserver_url, get_prop_map); |
| } |
| |
| function onCompileFinished(json) { |
| globals.compiling = false; |
| compiling = false; |
| if (json.status != 'ok') { |
| var log_url = codeserver_url + 'log/' + module_name; |
| dialog.showError(json.status, log_url, onClickTryAgain); |
| return; |
| } |
| reloadInDevMode(module_name, codeserver_url); |
| } |
| |
| var url_prefix = codeserver_url + 'recompile/' + module_name + '?' + |
| getBindingParameters(module_name, get_prop_map); |
| globals.compiling = true; |
| compiling = true; |
| setTimeout(poll, 1000); |
| callJsonp(url_prefix, onCompileFinished); |
| } |
| |
| /** |
| * Determines whether we should run the "Dev Mode On" or "Compile" |
| * bookmarklet. |
| * @param params {map} Parameters passed in by the bookmarklet. |
| */ |
| function runBookmarklet(params) { |
| if (!!globals.compiling) { |
| // A module is already being compiled. |
| // We ignore the bookmarklet: the page will reload once the compilation |
| // ends. |
| return; |
| } |
| if (!params || !params.server_url) { |
| window.alert('Need to reinstall the bookmarklets.'); |
| return; |
| } |
| |
| var module_name = params.module_name; |
| |
| var error = null; |
| if (module_name) { |
| error = getCannotCompileError(module_name); |
| } |
| |
| if (module_name && !error) { |
| // The user clicked a "Compile" bookmarklet and we believe it will |
| // succeed. |
| |
| // Use the binding properties provided in params if available. |
| // (This happens if we're being called from a stub rather than a bookmarklet.) |
| var getPropMap = params.getPropMap; |
| if (!getPropMap) { |
| // Probably a regular compile, so check in the page. |
| var active_modules = window.__gwt_activeModules; |
| getPropMap = active_modules[module_name].bindings; |
| } |
| compile(module_name, params.server_url, getPropMap); |
| } else { |
| // The user clicked the "Dev Mode On" bookmarklet or something is wrong. |
| showModuleDialog(params.server_url); |
| } |
| } |
| |
| // remove previous module dialog, if present |
| if (globals.dismissModuleDialog) { |
| globals.dismissModuleDialog(); |
| } |
| |
| runBookmarklet(getBookmarkletParams()); |
| })(); |