diff --git a/blockly_uncompressed.js b/blockly_uncompressed.js index 665e8af12..ec454a12c 100644 --- a/blockly_uncompressed.js +++ b/blockly_uncompressed.js @@ -65,8 +65,6 @@ goog.addDependency("../../../" + dir + "/core/msg.js", ['Blockly.Msg'], []); goog.addDependency("../../../" + dir + "/core/mutator.js", ['Blockly.Mutator'], ['Blockly.Bubble', 'Blockly.Icon', 'Blockly.WorkspaceSvg', 'goog.Timer', 'goog.dom']); goog.addDependency("../../../" + dir + "/core/names.js", ['Blockly.Names'], []); goog.addDependency("../../../" + dir + "/core/procedures.js", ['Blockly.Procedures'], ['Blockly.Field', 'Blockly.Names', 'Blockly.Workspace']); -goog.addDependency("../../../" + dir + "/core/realtime-client-utils.js", ['rtclient'], []); -goog.addDependency("../../../" + dir + "/core/realtime.js", ['Blockly.Realtime'], ['goog.array', 'goog.dom', 'goog.style', 'rtclient']); goog.addDependency("../../../" + dir + "/core/scrollbar.js", ['Blockly.Scrollbar', 'Blockly.ScrollbarPair'], ['goog.dom', 'goog.events']); goog.addDependency("../../../" + dir + "/core/toolbox.js", ['Blockly.Toolbox'], ['Blockly.Flyout', 'goog.dom', 'goog.events', 'goog.events.BrowserFeature', 'goog.html.SafeHtml', 'goog.math.Rect', 'goog.style', 'goog.ui.tree.TreeControl', 'goog.ui.tree.TreeNode']); goog.addDependency("../../../" + dir + "/core/tooltip.js", ['Blockly.Tooltip'], ['goog.dom']); @@ -130,7 +128,7 @@ goog.addDependency("crypt/aes.js", ['goog.crypt.Aes'], ['goog.asserts', 'goog.cr goog.addDependency("crypt/aes_test.js", ['goog.crypt.AesTest'], ['goog.crypt', 'goog.crypt.Aes', 'goog.testing.jsunit']); goog.addDependency("crypt/arc4.js", ['goog.crypt.Arc4'], ['goog.asserts']); goog.addDependency("crypt/arc4_test.js", ['goog.crypt.Arc4Test'], ['goog.array', 'goog.crypt.Arc4', 'goog.testing.jsunit']); -goog.addDependency("crypt/base64.js", ['goog.crypt.base64'], ['goog.asserts', 'goog.crypt', 'goog.userAgent']); +goog.addDependency("crypt/base64.js", ['goog.crypt.base64'], ['goog.asserts', 'goog.crypt', 'goog.string', 'goog.userAgent']); goog.addDependency("crypt/base64_test.js", ['goog.crypt.base64Test'], ['goog.crypt', 'goog.crypt.base64', 'goog.testing.jsunit']); goog.addDependency("crypt/basen.js", ['goog.crypt.baseN'], []); goog.addDependency("crypt/basen_test.js", ['goog.crypt.baseNTest'], ['goog.crypt.baseN', 'goog.testing.jsunit']); @@ -1095,7 +1093,7 @@ goog.addDependency("testing/asynctestcase_async_test.js", ['goog.testing.AsyncTe goog.addDependency("testing/asynctestcase_noasync_test.js", ['goog.testing.AsyncTestCaseSyncTest'], ['goog.testing.AsyncTestCase', 'goog.testing.jsunit']); goog.addDependency("testing/asynctestcase_test.js", ['goog.testing.AsyncTestCaseTest'], ['goog.debug.Error', 'goog.testing.AsyncTestCase', 'goog.testing.asserts', 'goog.testing.jsunit']); goog.addDependency("testing/benchmark.js", ['goog.testing.benchmark'], ['goog.dom', 'goog.dom.TagName', 'goog.testing.PerformanceTable', 'goog.testing.PerformanceTimer', 'goog.testing.TestCase']); -goog.addDependency("testing/continuationtestcase.js", ['goog.testing.ContinuationTestCase', 'goog.testing.ContinuationTestCase.Step', 'goog.testing.ContinuationTestCase.Test'], ['goog.array', 'goog.events.EventHandler', 'goog.testing.TestCase', 'goog.testing.asserts']); +goog.addDependency("testing/continuationtestcase.js", ['goog.testing.ContinuationTestCase', 'goog.testing.ContinuationTestCase.ContinuationTest', 'goog.testing.ContinuationTestCase.Step'], ['goog.array', 'goog.events.EventHandler', 'goog.testing.TestCase', 'goog.testing.asserts']); goog.addDependency("testing/continuationtestcase_test.js", ['goog.testing.ContinuationTestCaseTest'], ['goog.events', 'goog.events.EventTarget', 'goog.testing.ContinuationTestCase', 'goog.testing.MockClock', 'goog.testing.PropertyReplacer', 'goog.testing.TestCase', 'goog.testing.jsunit']); goog.addDependency("testing/deferredtestcase.js", ['goog.testing.DeferredTestCase'], ['goog.testing.AsyncTestCase', 'goog.testing.TestCase']); goog.addDependency("testing/deferredtestcase_test.js", ['goog.testing.DeferredTestCaseTest'], ['goog.async.Deferred', 'goog.testing.DeferredTestCase', 'goog.testing.TestCase', 'goog.testing.TestRunner', 'goog.testing.jsunit', 'goog.testing.recordFunction']); @@ -1289,7 +1287,7 @@ goog.addDependency("ui/dimensionpicker_test.js", ['goog.ui.DimensionPickerTest'] goog.addDependency("ui/dimensionpickerrenderer.js", ['goog.ui.DimensionPickerRenderer'], ['goog.a11y.aria.Announcer', 'goog.a11y.aria.LivePriority', 'goog.dom', 'goog.dom.TagName', 'goog.i18n.bidi', 'goog.style', 'goog.ui.ControlRenderer', 'goog.userAgent']); goog.addDependency("ui/dimensionpickerrenderer_test.js", ['goog.ui.DimensionPickerRendererTest'], ['goog.a11y.aria.LivePriority', 'goog.array', 'goog.testing.jsunit', 'goog.testing.recordFunction', 'goog.ui.DimensionPicker', 'goog.ui.DimensionPickerRenderer']); goog.addDependency("ui/dragdropdetector.js", ['goog.ui.DragDropDetector', 'goog.ui.DragDropDetector.EventType', 'goog.ui.DragDropDetector.ImageDropEvent', 'goog.ui.DragDropDetector.LinkDropEvent'], ['goog.dom', 'goog.dom.InputType', 'goog.dom.TagName', 'goog.events.Event', 'goog.events.EventHandler', 'goog.events.EventTarget', 'goog.events.EventType', 'goog.math.Coordinate', 'goog.string', 'goog.style', 'goog.userAgent']); -goog.addDependency("ui/drilldownrow.js", ['goog.ui.DrilldownRow'], ['goog.asserts', 'goog.dom', 'goog.dom.TagName', 'goog.dom.classlist', 'goog.dom.safe', 'goog.html.SafeHtml', 'goog.html.legacyconversions', 'goog.ui.Component']); +goog.addDependency("ui/drilldownrow.js", ['goog.ui.DrilldownRow'], ['goog.asserts', 'goog.dom', 'goog.dom.TagName', 'goog.dom.classlist', 'goog.dom.safe', 'goog.html.SafeHtml', 'goog.html.legacyconversions', 'goog.string.Unicode', 'goog.ui.Component']); goog.addDependency("ui/drilldownrow_test.js", ['goog.ui.DrilldownRowTest'], ['goog.dom', 'goog.dom.TagName', 'goog.html.SafeHtml', 'goog.testing.jsunit', 'goog.ui.DrilldownRow']); goog.addDependency("ui/filteredmenu.js", ['goog.ui.FilteredMenu'], ['goog.a11y.aria', 'goog.a11y.aria.AutoCompleteValues', 'goog.a11y.aria.State', 'goog.dom', 'goog.dom.InputType', 'goog.dom.TagName', 'goog.events', 'goog.events.EventType', 'goog.events.InputHandler', 'goog.events.KeyCodes', 'goog.object', 'goog.string', 'goog.style', 'goog.ui.Component', 'goog.ui.FilterObservingMenuItem', 'goog.ui.Menu', 'goog.ui.MenuItem', 'goog.userAgent']); goog.addDependency("ui/filteredmenu_test.js", ['goog.ui.FilteredMenuTest'], ['goog.a11y.aria', 'goog.a11y.aria.AutoCompleteValues', 'goog.a11y.aria.State', 'goog.dom', 'goog.dom.TagName', 'goog.events', 'goog.events.EventType', 'goog.events.KeyCodes', 'goog.math.Rect', 'goog.style', 'goog.testing.events', 'goog.testing.jsunit', 'goog.ui.FilteredMenu', 'goog.ui.MenuItem']); @@ -1605,7 +1603,6 @@ goog.require('Blockly.Msg'); goog.require('Blockly.Mutator'); goog.require('Blockly.Names'); goog.require('Blockly.Procedures'); -goog.require('Blockly.Realtime'); goog.require('Blockly.Scrollbar'); goog.require('Blockly.ScrollbarPair'); goog.require('Blockly.Toolbox'); @@ -1620,7 +1617,6 @@ goog.require('Blockly.Xml'); goog.require('Blockly.ZoomControls'); goog.require('Blockly.inject'); goog.require('Blockly.utils'); -goog.require('rtclient'); delete this.BLOCKLY_DIR; delete this.BLOCKLY_BOOT; diff --git a/core/blockly.js b/core/blockly.js index a66112d8e..b2961a5ea 100644 --- a/core/blockly.js +++ b/core/blockly.js @@ -42,7 +42,6 @@ goog.require('Blockly.Generator'); goog.require('Blockly.Msg'); goog.require('Blockly.Procedures'); // Realtime is currently badly broken. Stub it out. -//goog.require('Blockly.Realtime'); Blockly.Realtime = { isEnabled: function() {return false;}, blockChanged: function() {}, diff --git a/core/realtime-client-utils.js b/core/realtime-client-utils.js deleted file mode 100644 index 10e6a9ced..000000000 --- a/core/realtime-client-utils.js +++ /dev/null @@ -1,500 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2013 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. - */ - -/** - * @fileoverview Common utility functionality for Google Drive Realtime API, - * including authorization and file loading. This functionality should serve - * mostly as a well-documented example, though is usable in its own right. - * - * You can find this code as part of the Google Drive Realtime API Quickstart at - * https://developers.google.com/drive/realtime/realtime-quickstart and also as - * part of the Google Drive Realtime Playground code at - * https://github.com/googledrive/realtime-playground/blob/master/js/realtime-client-utils.js - */ -'use strict'; - -/** - * Realtime client utilities namespace. - */ -goog.provide('rtclient'); - - -/** - * OAuth 2.0 scope for installing Drive Apps. - * @const - */ -rtclient.INSTALL_SCOPE = 'https://www.googleapis.com/auth/drive.install'; - -/** - * OAuth 2.0 scope for opening and creating files. - * @const - */ -rtclient.FILE_SCOPE = 'https://www.googleapis.com/auth/drive.file'; - -/** - * OAuth 2.0 scope for accessing the appdata folder, a hidden folder private - * to this app. - * @const - */ -rtclient.APPDATA_SCOPE = 'https://www.googleapis.com/auth/drive.appdata'; - -/** - * OAuth 2.0 scope for accessing the user's ID. - * @const - */ -rtclient.OPENID_SCOPE = 'openid'; - -/** - * MIME type for newly created Realtime files. - * @const - */ -rtclient.REALTIME_MIMETYPE = 'application/vnd.google-apps.drive-sdk'; - -/** - * Key used to store the folder id of the Drive folder in which we will store - * Realtime files. - * @type {string} - */ -rtclient.FOLDER_KEY = 'folderId'; - -/** - * Parses the hash parameters to this page and returns them as an object. - * @return {!Object} Parameter object. - */ -rtclient.getParams = function() { - // Be careful with regards to node.js which has no window or location. - var location = goog.global['location'] || {}; - var params = {}; - function parseParams(fragment) { - // Split up the query string and store in an object. - var paramStrs = fragment.slice(1).split('&'); - for (var i = 0; i < paramStrs.length; i++) { - var paramStr = paramStrs[i].split('='); - params[decodeURIComponent(paramStr[0])] = decodeURIComponent(paramStr[1]); - } - } - var hashFragment = location.hash; - if (hashFragment) { - parseParams(hashFragment); - } - // Opening from Drive will encode the state in a query search parameter. - var searchFragment = location.search; - if (searchFragment) { - parseParams(searchFragment); - } - return params; -}; - -/** - * Instance of the query parameters. - */ -rtclient.params = rtclient.getParams(); - -/** - * Fetches an option from options or a default value, logging an error if - * neither is available. - * @param {!Object} options Containing options. - * @param {string} key Option key. - * @param {*=} opt_defaultValue Default option value (optional). - * @return {*} Option value. - */ -rtclient.getOption = function(options, key, opt_defaultValue) { - if (options.hasOwnProperty(key)) { - return options[key]; - } - if (opt_defaultValue === undefined) { - console.error(key + ' should be present in the options.'); - } - return opt_defaultValue; -}; - -/** - * Creates a new Authorizer from the options. - * @constructor - * @param {!Object} options For authorizer. Two keys are required as mandatory, - * these are: - * - * 1. "clientId", the Client ID from the console - * 2. "authButtonElementId", the is of the dom element to use for - * authorizing. - */ -rtclient.Authorizer = function(options) { - this.clientId = rtclient.getOption(options, 'clientId'); - // Get the user ID if it's available in the state query parameter. - this.userId = rtclient.params['userId']; - this.authButton = document.getElementById(rtclient.getOption(options, - 'authButtonElementId')); - this.authDiv = document.getElementById(rtclient.getOption(options, - 'authDivElementId')); -}; - -/** - * Start the authorization process. - * @param {Function} onAuthComplete To call once authorization has completed. - */ -rtclient.Authorizer.prototype.start = function(onAuthComplete) { - var _this = this; - gapi.load('auth:client,drive-realtime,drive-share', function() { - _this.authorize(onAuthComplete); - }); -}; - -/** - * Reauthorize the client with no callback (used for authorization failure). - * @param {Function} onAuthComplete To call once authorization has completed. - */ -rtclient.Authorizer.prototype.authorize = function(onAuthComplete) { - var clientId = this.clientId; - var userId = this.userId; - var _this = this; - var handleAuthResult = function(authResult) { - if (authResult && !authResult.error) { - _this.authButton.disabled = true; - _this.fetchUserId(onAuthComplete); - _this.authDiv.style.display = 'none'; - } else { - _this.authButton.disabled = false; - _this.authButton.onclick = authorizeWithPopup; - _this.authDiv.style.display = 'block'; - } - }; - var authorizeWithPopup = function() { - gapi.auth.authorize({ - 'client_id': clientId, - 'scope': [ - rtclient.INSTALL_SCOPE, - rtclient.FILE_SCOPE, - rtclient.OPENID_SCOPE, - rtclient.APPDATA_SCOPE - ], - 'user_id': userId, - 'immediate': false - }, handleAuthResult); - }; - // Try with no popups first. - gapi.auth.authorize({ - 'client_id': clientId, - 'scope': [ - rtclient.INSTALL_SCOPE, - rtclient.FILE_SCOPE, - rtclient.OPENID_SCOPE, - rtclient.APPDATA_SCOPE - ], - 'user_id': userId, - 'immediate': true - }, handleAuthResult); -}; - -/** - * Fetch the user ID using the UserInfo API and save it locally. - * @param {Function} callback The callback to call after user ID has been - * fetched. - */ -rtclient.Authorizer.prototype.fetchUserId = function(callback) { - var _this = this; - gapi.client.load('oauth2', 'v2', function() { - gapi.client.oauth2.userinfo.get().execute(function(resp) { - if (resp.id) { - _this.userId = resp.id; - } - if (callback) { - callback(); - } - }); - }); -}; - -/** - * Creates a new Realtime file. - * @param {string} title Title of the newly created file. - * @param {string} mimeType The MIME type of the new file. - * @param {string} folderTitle Title of the folder to place the file in. - * @param {Function} callback The callback to call after creation. - */ -rtclient.createRealtimeFile = function(title, mimeType, folderTitle, callback) { - - function insertFile(folderId) { - gapi.client.drive.files.insert({ - 'resource': { - 'mimeType': mimeType, - 'title': title, - 'parents': [{'id': folderId}] - } - }).execute(callback); - } - - function getOrCreateFolder() { - - function storeInAppdataProperty(folderId) { - // Store folder id in a custom property of the appdata folder. The - // 'appdata' folder is a special Google Drive folder that is only - // accessible by a specific app (i.e. identified by the client id). - gapi.client.drive.properties.insert({ - 'fileId': 'appdata', - 'resource': { 'key': rtclient.FOLDER_KEY, 'value': folderId } - }).execute(function(resp) { - insertFile(folderId); - }); - }; - - function createFolder() { - gapi.client.drive.files.insert({ - 'resource': { - 'mimeType': 'application/vnd.google-apps.folder', - 'title': folderTitle - } - }).execute(function(folder) { - storeInAppdataProperty(folder.id); - }); - } - - // Get the folder id from the appdata properties. - gapi.client.drive.properties.get({ - 'fileId': 'appdata', - 'propertyKey': rtclient.FOLDER_KEY - }).execute(function(resp) { - if (resp.error) { - // There's no folder id stored yet so we create a new folder if a - // folderTitle has been supplied. - if (folderTitle) { - createFolder(); - } else { - // There's no folder specified, so we just store the file in the - // user's root folder. - storeInAppdataProperty('root'); - } - } else { - var folderId = resp.result.value; - gapi.client.drive.files.get({ - 'fileId': folderId - }).execute(function(resp) { - if (resp.error || resp.labels.trashed) { - // Folder doesn't exist or was deleted, so create a new one. - createFolder(); - } else { - insertFile(folderId); - } - }); - } - }); - } - - gapi.client.load('drive', 'v2', function() { - getOrCreateFolder(); - }); -}; - -/** - * Fetches the metadata for a Realtime file. - * @param {string} fileId The file to load metadata for. - * @param {Function} callback The callback to be called on completion, - * with signature: - * - * function onGetFileMetadata(file) {} - * - * where the file parameter is a Google Drive API file resource instance. - */ -rtclient.getFileMetadata = function(fileId, callback) { - gapi.client.load('drive', 'v2', function() { - gapi.client.drive.files.get({ - 'fileId': fileId - }).execute(callback); - }); -}; - -/** - * Parses the state parameter passed from the Drive user interface after - * Open With operations. - * @param {string} stateParam The state query parameter as a JSON string. - * @return {Object} The state query parameter as an object or null if - * parsing failed. - */ -rtclient.parseState = function(stateParam) { - try { - var stateObj = JSON.parse(stateParam); - return stateObj; - } catch (e) { - return null; - } -}; - -/** - * Handles authorizing, parsing query parameters, loading and creating Realtime - * documents. - * @constructor - * @param {!Object} options Options for loader. Four keys are required as - * mandatory, these are: - * - * 1. "clientId", the Client ID from the console - * 2. "initializeModel", the callback to call when the file is loaded. - * 3. "onFileLoaded", the callback to call when the model is first created. - * - * and two keys are optional: - * - * 1. "defaultTitle", the title of newly created Realtime files. - * 2. "defaultFolderTitle", the folder to place in which to place newly - * created Realtime files. - */ -rtclient.RealtimeLoader = function(options) { - // Initialize configuration variables. - this.onFileLoaded = rtclient.getOption(options, 'onFileLoaded'); - this.newFileMimeType = rtclient.getOption(options, 'newFileMimeType', - rtclient.REALTIME_MIMETYPE); - this.initializeModel = rtclient.getOption(options, 'initializeModel'); - this.registerTypes = rtclient.getOption(options, 'registerTypes', - function() {}); - this.afterAuth = rtclient.getOption(options, 'afterAuth', function() {}); - // This tells us if need to we automatically create a file after auth. - this.autoCreate = rtclient.getOption(options, 'autoCreate', false); - this.defaultTitle = rtclient.getOption(options, 'defaultTitle', - 'New Realtime File'); - this.defaultFolderTitle = rtclient.getOption(options, 'defaultFolderTitle', - ''); - this.afterCreate = rtclient.getOption(options, 'afterCreate', function() {}); - this.authorizer = new rtclient.Authorizer(options); -}; - -/** - * Redirects the browser back to the current page with an appropriate file ID. - * @param {Array.} fileIds The IDs of the files to open. - * @param {string} userId The ID of the user. - */ -rtclient.RealtimeLoader.prototype.redirectTo = function(fileIds, userId) { - var params = []; - if (fileIds) { - params.push('fileIds=' + fileIds.join(',')); - } - if (userId) { - params.push('userId=' + userId); - } - // Naive URL construction. - var newUrl = params.length == 0 ? - window.location.pathname : - (window.location.pathname + '#' + params.join('&')); - // Using HTML URL re-write if available. - if (window.history && window.history.replaceState) { - window.history.replaceState('Google Drive Realtime API Playground', - 'Google Drive Realtime API Playground', newUrl); - } else { - window.location.href = newUrl; - } - // We are still here that means the page didn't reload. - rtclient.params = rtclient.getParams(); - for (var index in fileIds) { - gapi.drive.realtime.load(fileIds[index], this.onFileLoaded, - this.initializeModel, this.handleErrors); - } -}; - -/** - * Starts the loader by authorizing. - */ -rtclient.RealtimeLoader.prototype.start = function() { - // Bind to local context to make them suitable for callbacks. - var _this = this; - this.authorizer.start(function() { - if (_this.registerTypes) { - _this.registerTypes(); - } - if (_this.afterAuth) { - _this.afterAuth(); - } - _this.load(); - }); -}; - -/** - * Handles errors thrown by the Realtime API. - * @param {!Error} e Error. - */ -rtclient.RealtimeLoader.prototype.handleErrors = function(e) { - if (e.type == gapi.drive.realtime.ErrorType.TOKEN_REFRESH_REQUIRED) { - this.authorizer.authorize(); - } else if (e.type == gapi.drive.realtime.ErrorType.CLIENT_ERROR) { - alert('An Error happened: ' + e.message); - window.location.href = '/'; - } else if (e.type == gapi.drive.realtime.ErrorType.NOT_FOUND) { - alert('The file was not found. It does not exist or you do not have ' + - 'read access to the file.'); - window.location.href = '/'; - } -}; - -/** - * Loads or creates a Realtime file depending on the fileId and state query - * parameters. - */ -rtclient.RealtimeLoader.prototype.load = function() { - var fileIds = rtclient.params['fileIds']; - if (fileIds) { - fileIds = fileIds.split(','); - } - var userId = this.authorizer.userId; - var state = rtclient.params['state']; - // Creating the error callback. - var authorizer = this.authorizer; - // We have file IDs in the query parameters, so we will use them to load a - // file. - if (fileIds) { - for (var index in fileIds) { - gapi.drive.realtime.load(fileIds[index], this.onFileLoaded, - this.initializeModel, this.handleErrors); - } - return; - } - // We have a state parameter being redirected from the Drive UI. - // We will parse it and redirect to the fileId contained. - else if (state) { - var stateObj = rtclient.parseState(state); - // If opening a file from Drive. - if (stateObj.action == 'open') { - fileIds = stateObj.ids; - userId = stateObj.userId; - this.redirectTo(fileIds, userId); - return; - } - } - if (this.autoCreate) { - this.createNewFileAndRedirect(); - } -}; - -/** - * Creates a new file and redirects to the URL to load it. - */ -rtclient.RealtimeLoader.prototype.createNewFileAndRedirect = function() { - // No fileId or state have been passed. We create a new Realtime file and - // redirect to it. - var _this = this; - rtclient.createRealtimeFile(this.defaultTitle, this.newFileMimeType, - this.defaultFolderTitle, - function(file) { - if (file.id) { - if (_this.afterCreate) { - _this.afterCreate(file.id); - } - _this.redirectTo([file.id], _this.authorizer.userId); - } else { - // File failed to be created, log why and do not attempt to redirect. - console.error('Error creating file.'); - console.error(file); - } - }); -}; diff --git a/core/realtime.js b/core/realtime.js deleted file mode 100644 index 4af321896..000000000 --- a/core/realtime.js +++ /dev/null @@ -1,869 +0,0 @@ -/** - * @license - * Visual Blocks Editor - * - * Copyright 2014 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. - */ - -/** - * This file contains functions used by any Blockly app that wants to provide - * realtime collaboration functionality. - */ - -/** - * @fileoverview Common support code for Blockly apps using realtime - * collaboration. - * Note that to use this you must set up a project via the Google Developers - * Console. Instructions on how to do that can be found at - * https://developers.google.com/blockly/realtime-collaboration - * Once you do that you can set the clientId in - * Blockly.Realtime.rtclientOptions_ - * @author markf@google.com (Mark Friedman) - */ -'use strict'; - -goog.provide('Blockly.Realtime'); - -goog.require('goog.array'); -goog.require('goog.dom'); -goog.require('goog.style'); -goog.require('rtclient'); - -/** - * Is realtime collaboration enabled? - * @type {boolean} - * @private - */ -Blockly.Realtime.enabled_ = false; - -/** - * The Realtime document being collaborated on. - * @type {gapi.drive.realtime.Document} - * @private - */ -Blockly.Realtime.document_ = null; - -/** - * The Realtime model of this doc. - * @type {gapi.drive.realtime.Model} - * @private - */ -Blockly.Realtime.model_ = null; - -/** - * The unique id associated with this editing session. - * @type {string} - * @private - */ -Blockly.Realtime.sessionId_ = null; - -/** - * The function used to initialize the UI after realtime is initialized. - * @type {function()} - * @private - */ -Blockly.Realtime.initUi_ = null; - -/** - * A map from block id to blocks. - * @type {gapi.drive.realtime.CollaborativeMap} - * @private - */ -Blockly.Realtime.blocksMap_ = null; - -/** - * Are currently syncing from another instance of this realtime doc. - * @type {boolean} - */ -Blockly.Realtime.withinSync = false; - -/** - * The current instance of the realtime loader client - * @type {rtclient.RealtimeLoader} - * @private - */ -Blockly.Realtime.realtimeLoader_ = null; - -/** - * The id of a text area to be used as a realtime chat box. - * @type {string} - * @private - */ -Blockly.Realtime.chatBoxElementId_ = null; - -/** - * The initial text to be placed in the realtime chat box. - * @type {string} - * @private - */ -Blockly.Realtime.chatBoxInitialText_ = null; - -/** - * Indicator of whether we are in the context of an undo or redo operation. - * @type {boolean} - * @private - */ -Blockly.Realtime.withinUndo_ = false; - -/** - * Returns whether realtime collaboration is enabled. - * @return {boolean} - */ -Blockly.Realtime.isEnabled = function() { - return Blockly.Realtime.enabled_; -}; - -/** - * The id of the button to use for undo. - * @type {string} - * @private - */ -Blockly.Realtime.undoElementId_ = null; - -/** - * The id of the button to use for redo. - * @type {string} - * @private - */ -Blockly.Realtime.redoElementId_ = null; - -/** - * URL of the animated progress indicator. - * @type {string} - * @private - */ -Blockly.Realtime.PROGRESS_URL_ = 'progress.gif'; - -/** - * URL of the anonymous user image. - * @type {string} - * @private - */ -Blockly.Realtime.ANONYMOUS_URL_ = 'anon.jpeg'; - -/** - * This function is called the first time that the Realtime model is created - * for a file. This function should be used to initialize any values of the - * model. - * @param {!gapi.drive.realtime.Model} model The Realtime root model object. - * @private - */ -Blockly.Realtime.initializeModel_ = function(model) { - Blockly.Realtime.model_ = model; - var blocksMap = model.createMap(); - model.getRoot().set('blocks', blocksMap); - var topBlocks = model.createList(); - model.getRoot().set('topBlocks', topBlocks); - if (Blockly.Realtime.chatBoxElementId_) { - model.getRoot().set(Blockly.Realtime.chatBoxElementId_, - model.createString(Blockly.Realtime.chatBoxInitialText_)); - } -}; - -/** - * Delete a block from the realtime blocks map. - * @param {!Blockly.Block} block The block to remove. - */ -Blockly.Realtime.removeBlock = function(block) { - Blockly.Realtime.blocksMap_['delete'](block.id.toString()); -}; - -/** - * Add to the list of top-level blocks. - * @param {!Blockly.Block} block The block to add. - */ -Blockly.Realtime.addTopBlock = function(block) { - if (Blockly.Realtime.topBlocks_.indexOf(block) == -1) { - Blockly.Realtime.topBlocks_.push(block); - } -}; - -/** - * Delete a block from the list of top-level blocks. - * @param {!Blockly.Block} block The block to remove. - */ -Blockly.Realtime.removeTopBlock = function(block) { - Blockly.Realtime.topBlocks_.removeValue(block); -}; - -/** - * Obtain a newly created block known by the Realtime API. - * @param {!Blockly.Workspace} workspace The workspace to put the block in. - * @param {string} prototypeName The name of the prototype for the block. - * @return {!Blockly.Block} - */ -Blockly.Realtime.obtainBlock = function(workspace, prototypeName) { - var newBlock = - Blockly.Realtime.model_.create(Blockly.Block, workspace, prototypeName); - return newBlock; -}; - -/** - * Get an existing block by id. - * @param {string} id The block's id. - * @return {Blockly.Block} The found block. - */ -Blockly.Realtime.getBlockById = function(id) { - return Blockly.Realtime.blocksMap_.get(id); -}; - -/** - * Log the event for debugging purposes. - * @param {gapi.drive.realtime.BaseModelEvent} evt The event that occurred. - * @private - */ -Blockly.Realtime.logEvent_ = function(evt) { - console.log('Object event:'); - console.log(' id: ' + evt.target.id); - console.log(' type: ' + evt.type); - var events = evt.events; - if (events) { - var eventCount = events.length; - for (var i = 0; i < eventCount; i++) { - var event = events[i]; - console.log(' child event:'); - console.log(' id: ' + event.target.id); - console.log(' type: ' + event.type); - } - } -}; - -/** - * Event handler to call when a block is changed. - * @param {!gapi.drive.realtime.ObjectChangedEvent} evt The event that occurred. - * @private - */ -Blockly.Realtime.onObjectChange_ = function(evt) { - var events = evt.events; - var eventCount = evt.events.length; - for (var i = 0; i < eventCount; i++) { - var event = events[i]; - if (!event.isLocal || Blockly.Realtime.withinUndo_) { - var block = event.target; - if (event.type == 'value_changed') { - if (event.property == 'xmlDom') { - Blockly.Realtime.doWithinSync_(function() { - Blockly.Realtime.placeBlockOnWorkspace_(block, false); - Blockly.Realtime.moveBlock_(block); - }); - } else if (event.property == 'relativeX' || - event.property == 'relativeY') { - Blockly.Realtime.doWithinSync_(function() { - if (!block.svg_) { - // If this is a move of a newly disconnected (i.e. newly top - // level) block it will not have any svg (because it has been - // disposed of by its parent), so we need to handle that here. - Blockly.Realtime.placeBlockOnWorkspace_(block, false); - } - Blockly.Realtime.moveBlock_(block); - }); - } - } - } - } -}; - -/** - * Event handler to call when there is a change to the realtime blocks map. - * @param {!gapi.drive.realtime.ValueChangedEvent} evt The event that occurred. - * @private - */ -Blockly.Realtime.onBlocksMapChange_ = function(evt) { - if (!evt.isLocal || Blockly.Realtime.withinUndo_) { - var block = evt.newValue; - if (block) { - Blockly.Realtime.placeBlockOnWorkspace_(block, !(evt.oldValue)); - } else { - block = evt.oldValue; - Blockly.Realtime.deleteBlock(block); - } - } -}; - -/** - * A convenient wrapper around code that synchronizes the local model being - * edited with changes from another non-local model. - * @param {!function()} thunk A thunk of code to call. - * @private - */ -Blockly.Realtime.doWithinSync_ = function(thunk) { - if (Blockly.Realtime.withinSync) { - thunk(); - } else { - try { - Blockly.Realtime.withinSync = true; - thunk(); - } finally { - Blockly.Realtime.withinSync = false; - } - } -}; - -/** - * Places a block to be synced on this docs main workspace. The block might - * already exist on this doc, in which case it is updated and/or moved. - * @param {!Blockly.Block} block The block. - * @param {boolean} addToTop Whether to add the block to the workspace/s list of - * top-level blocks. - * @private - */ -Blockly.Realtime.placeBlockOnWorkspace_ = function(block, addToTop) { - Blockly.Realtime.doWithinSync_(function() { -// if (!Blockly.Realtime.blocksMap_.has(block.id)) { -// Blockly.Realtime.blocksMap_.set(block.id, block); -// } - var blockDom = Blockly.Xml.textToDom(block.xmlDom).firstChild; - var newBlock = - Blockly.Xml.domToBlock(Blockly.mainWorkspace, blockDom, true); - // TODO: The following is for debugging. It should never actually happen. - if (!newBlock) { - return; - } - // Since Blockly.Xml.blockDomToBlock() purposely won't add blocks to - // workspace.topBlocks_ we sometimes need to do it explicitly here. - if (addToTop) { - newBlock.workspace.addTopBlock(newBlock); - } - if (addToTop || - goog.array.contains(Blockly.Realtime.topBlocks_, newBlock)) { - Blockly.Realtime.moveBlock_(newBlock); - } - }); -}; - -/** - * Move a block. - * @param {!Blockly.Block} block The block to move. - * @private - */ -Blockly.Realtime.moveBlock_ = function(block) { - if (!isNaN(block.relativeX) && !isNaN(block.relativeY)) { - var width = Blockly.svgSize().width; - var curPos = block.getRelativeToSurfaceXY(); - var dx = block.relativeX - curPos.x; - var dy = block.relativeY - curPos.y; - block.moveBy(Blockly.RTL ? width - dx : dx, dy); - } -}; - -/** - * Delete a block. - * @param {!Blockly.Block} block The block to delete. - */ -Blockly.Realtime.deleteBlock = function(block) { - Blockly.Realtime.doWithinSync_(function() { - block.dispose(true, true, true); - }); -}; - -/** - * Load all the blocks from the realtime model's blocks map and place them - * appropriately on the main Blockly workspace. - * @private - */ -Blockly.Realtime.loadBlocks_ = function() { - var topBlocks = Blockly.Realtime.topBlocks_; - for (var j = 0; j < topBlocks.length; j++) { - var topBlock = topBlocks.get(j); - Blockly.Realtime.placeBlockOnWorkspace_(topBlock, true); - } -}; - -/** - * Cause a changed block to update the realtime model, and therefore to be - * synced with other apps editing this same doc. - * @param {!Blockly.Block} block The block that changed. - */ -Blockly.Realtime.blockChanged = function(block) { - if (block.workspace == Blockly.mainWorkspace && - Blockly.Realtime.isEnabled() && - !Blockly.Realtime.withinSync) { - var rootBlock = block.getRootBlock(); - var xy = rootBlock.getRelativeToSurfaceXY(); - var changed = false; - var xml = Blockly.Xml.blockToDom_(rootBlock); - xml.setAttribute('id', rootBlock.id); - var topXml = goog.dom.createDom('xml'); - topXml.appendChild(xml); - var newXml = Blockly.Xml.domToText(topXml); - if (newXml != rootBlock.xmlDom) { - changed = true; - rootBlock.xmlDom = newXml; - } - if (rootBlock.relativeX != xy.x || rootBlock.relativeY != xy.y) { - rootBlock.relativeX = xy.x; - rootBlock.relativeY = xy.y; - changed = true; - } - if (changed) { - var blockId = rootBlock.id.toString(); - Blockly.Realtime.blocksMap_.set(blockId, rootBlock); - } - } -}; - -/** - * This function is called when the Realtime file has been loaded. It should - * be used to initialize any user interface components and event handlers - * depending on the Realtime model. In this case, create a text control binder - * and bind it to our string model that we created in initializeModel. - * @param {!gapi.drive.realtime.Document} doc The Realtime document. - * @private - */ -Blockly.Realtime.onFileLoaded_ = function(doc) { - Blockly.Realtime.document_ = doc; - Blockly.Realtime.sessionId_ = Blockly.Realtime.getSessionId_(doc); - Blockly.Realtime.model_ = doc.getModel(); - Blockly.Realtime.blocksMap_ = - Blockly.Realtime.model_.getRoot().get('blocks'); - Blockly.Realtime.topBlocks_ = - Blockly.Realtime.model_.getRoot().get('topBlocks'); - - Blockly.Realtime.model_.getRoot().addEventListener( - gapi.drive.realtime.EventType.OBJECT_CHANGED, - Blockly.Realtime.onObjectChange_); - Blockly.Realtime.blocksMap_.addEventListener( - gapi.drive.realtime.EventType.VALUE_CHANGED, - Blockly.Realtime.onBlocksMapChange_); - - Blockly.Realtime.initUi_(); - - //Adding Listeners for Collaborator events. - doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_JOINED, - Blockly.Realtime.onCollaboratorJoined_); - doc.addEventListener(gapi.drive.realtime.EventType.COLLABORATOR_LEFT, - Blockly.Realtime.onCollaboratorLeft_); - Blockly.Realtime.updateCollabUi_(); - - Blockly.Realtime.loadBlocks_(); - - // Add logic for undo button. - // TODO: Uncomment this when undo/redo are fixed. -// -// var undoButton = document.getElementById(Blockly.Realtime.undoElementId_); -// var redoButton = document.getElementById(Blockly.Realtime.redoElementId_); -// -// if (undoButton) { -// undoButton.onclick = function (e) { -// try { -// Blockly.Realtime.withinUndo_ = true; -// Blockly.Realtime.model_.undo(); -// } finally { -// Blockly.Realtime.withinUndo_ = false; -// } -// }; -// } -// if (redoButton) { -// redoButton.onclick = function (e) { -// try { -// Blockly.Realtime.withinUndo_ = true; -// Blockly.Realtime.model_.redo(); -// } finally { -// Blockly.Realtime.withinUndo_ = false; -// } -// }; -// } -// -// // Add event handler for UndoRedoStateChanged events. -// var onUndoRedoStateChanged = function(e) { -// undoButton.disabled = !e.canUndo; -// redoButton.disabled = !e.canRedo; -// }; -// Blockly.Realtime.model_.addEventListener( -// gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED, -// onUndoRedoStateChanged); - -}; - -/** - * Get the sessionId associated with this editing session. Note that it is - * unique to the current browser window/tab. - * @param {gapi.drive.realtime.Document} doc - * @return {*} - * @private - */ -Blockly.Realtime.getSessionId_ = function(doc) { - var collaborators = doc.getCollaborators(); - for (var i = 0; i < collaborators.length; i++) { - var collaborator = collaborators[i]; - if (collaborator.isMe) { - return collaborator.sessionId; - } - } - return undefined; -}; - -/** - * Register the Blockly types and attributes that are reflected in the realtime - * model. - * @private - */ -Blockly.Realtime.registerTypes_ = function() { - var custom = gapi.drive.realtime.custom; - - custom.registerType(Blockly.Block, 'Block'); - Blockly.Block.prototype.id = custom.collaborativeField('id'); - Blockly.Block.prototype.xmlDom = custom.collaborativeField('xmlDom'); - Blockly.Block.prototype.relativeX = custom.collaborativeField('relativeX'); - Blockly.Block.prototype.relativeY = custom.collaborativeField('relativeY'); - - custom.setInitializer(Blockly.Block, Blockly.Block.prototype.initialize); -}; - -/** - * Time period for realtime re-authorization - * @type {number} - * @private - */ -Blockly.Realtime.REAUTH_INTERVAL_IN_MILLISECONDS_ = 30 * 60 * 1000; - -/** - * What to do after Realtime authorization. - * @private - */ -Blockly.Realtime.afterAuth_ = function() { - // This is a workaround for the fact that the code in realtime-client-utils.js - // doesn't deal with auth timeouts correctly. So we explicitly reauthorize at - // regular intervals. - setTimeout( - function() { - Blockly.Realtime.realtimeLoader_.authorizer.authorize( - Blockly.Realtime.afterAuth_); - }, - Blockly.Realtime.REAUTH_INTERVAL_IN_MILLISECONDS_); -}; - -/** - * Add "Anyone with the link" permissions to the file. - * @param {string} fileId the file id - * @private - */ -Blockly.Realtime.afterCreate_ = function(fileId) { - var resource = { - 'type': 'anyone', - 'role': 'writer', - 'value': 'default', - 'withLink': true - }; - var request = gapi.client.drive.permissions.insert({ - 'fileId': fileId, - 'resource': resource - }); - request.execute(function(resp) { - // If we have an error try to just set the permission for all users - // of the domain. - if (resp.error) { - Blockly.Realtime.getUserDomain(fileId, function(domain) { - var resource = { - 'type': 'domain', - 'role': 'writer', - 'value': domain, - 'withLink': true - }; - request = gapi.client.drive.permissions.insert({ - 'fileId': fileId, - 'resource': resource - }); - request.execute(function(resp) { }); - }); - } - }); -}; - -/** - * Get the domain (if it exists) associated with a realtime file. The callback - * will be called with the domain, if it exists. - * @param {string} fileId the id of the file - * @param {function(string)} callback a function to call back with the domain - */ -Blockly.Realtime.getUserDomain = function(fileId, callback) { - /** - * Note that there may be a more direct way to get the domain by, for example, - * using the Google profile API but this way we don't need any additional - * APIs or scopes. But if it turns out that the permissions API stops - * providing the domain this might have to change. - */ - var request = gapi.client.drive.permissions.list({ - 'fileId': fileId - }); - request.execute(function(resp) { - for (var i = 0; i < resp.items.length; i++) { - var item = resp.items[i]; - if (item.role == 'owner') { - callback(item.domain); - return; - } - } - }); -}; - -/** - * Options for the Realtime loader. - * @private - */ -Blockly.Realtime.rtclientOptions_ = { - /** - * Client ID from the console. - * This will be set from the options passed into Blockly.Realtime.start() - */ - 'clientId': null, - - /** - * The ID of the button to click to authorize. Must be a DOM element ID. - */ - 'authButtonElementId': 'authorizeButton', - - /** - * The ID of the container of the authorize button. - */ - 'authDivElementId': 'authButtonDiv', - - /** - * Function to be called when a Realtime model is first created. - */ - 'initializeModel': Blockly.Realtime.initializeModel_, - - /** - * Autocreate files right after auth automatically. - */ - 'autoCreate': true, - - /** - * The name of newly created Drive files. - */ - 'defaultTitle': 'Realtime Blockly File', - - /** - * The name of the folder to place newly created Drive files in. - */ - 'defaultFolderTitle': 'Realtime Blockly Folder', - - /** - * The MIME type of newly created Drive Files. By default the application - * specific MIME type will be used: - * application/vnd.google-apps.drive-sdk. - */ - 'newFileMimeType': null, // Using default. - - /** - * Function to be called every time a Realtime file is loaded. - */ - 'onFileLoaded': Blockly.Realtime.onFileLoaded_, - - /** - * Function to be called to initialize custom Collaborative Objects types. - */ - 'registerTypes': Blockly.Realtime.registerTypes_, - - /** - * Function to be called after authorization and before loading files. - */ - 'afterAuth': Blockly.Realtime.afterAuth_, - - /** - * Function to be called after file creation, if autoCreate is true. - */ - 'afterCreate': Blockly.Realtime.afterCreate_ -}; - -/** - * Parse options to startRealtime(). - * @param {!Object} options Object containing the options. - * @private - */ -Blockly.Realtime.parseOptions_ = function(options) { - var chatBoxOptions = rtclient.getOption(options, 'chatbox'); - if (chatBoxOptions) { - Blockly.Realtime.chatBoxElementId_ = - rtclient.getOption(chatBoxOptions, 'elementId'); - Blockly.Realtime.chatBoxInitialText_ = - rtclient.getOption(chatBoxOptions, 'initText', Blockly.Msg.CHAT); - } - Blockly.Realtime.rtclientOptions_.clientId = - rtclient.getOption(options, 'clientId'); - Blockly.Realtime.collabElementId = - rtclient.getOption(options, 'collabElementId'); - // TODO: Uncomment this when undo/redo are fixed. -// Blockly.Realtime.undoElementId_ = -// rtclient.getOption(options, 'undoElementId', 'undoButton'); -// Blockly.Realtime.redoElementId_ = -// rtclient.getOption(options, 'redoElementId', 'redoButton'); -}; - -/** - * Setup the Blockly container for realtime authorization and start the - * Realtime loader. - * @param {function()} uiInitialize Function to initialize the Blockly UI. - * @param {!Element} uiContainer Container element for the Blockly UI. - * @param {!Object} options The realtime options. - */ -Blockly.Realtime.startRealtime = function(uiInitialize, uiContainer, options) { - Blockly.Realtime.parseOptions_(options); - Blockly.Realtime.enabled_ = true; - // Note that we need to setup the UI for realtime authorization before - // loading the realtime code (which, in turn, will handle initializing the - // rest of the Blockly UI). - var authDiv = Blockly.Realtime.addAuthUi_(uiContainer); - Blockly.Realtime.initUi_ = function() { - uiInitialize(); - if (Blockly.Realtime.chatBoxElementId_) { - var chatText = Blockly.Realtime.model_.getRoot().get( - Blockly.Realtime.chatBoxElementId_); - var chatBox = document.getElementById(Blockly.Realtime.chatBoxElementId_); - gapi.drive.realtime.databinding.bindString(chatText, chatBox); - chatBox.disabled = false; - } - }; - Blockly.Realtime.realtimeLoader_ = - new rtclient.RealtimeLoader(Blockly.Realtime.rtclientOptions_); - Blockly.Realtime.realtimeLoader_.start(); -}; - -/** - * Setup the Blockly container for realtime authorization. - * @param {!Element} uiContainer A DOM container element for the Blockly UI. - * @return {!Element} The DOM element for the authorization UI. - * @private - */ -Blockly.Realtime.addAuthUi_ = function(uiContainer) { - // Add progress indicator to the UI container. - uiContainer.style.background = 'url(' + Blockly.pathToMedia + - Blockly.Realtime.PROGRESS_URL_ + ') no-repeat center center'; - // Setup authorization button - var blocklyDivBounds = goog.style.getBounds(uiContainer); - var authButtonDiv = goog.dom.createDom('div'); - authButtonDiv.id = Blockly.Realtime.rtclientOptions_['authDivElementId']; - var authText = goog.dom.createDom('p', null, Blockly.Msg.AUTH); - authButtonDiv.appendChild(authText); - var authButton = goog.dom.createDom('button', null, 'Authorize'); - authButton.id = Blockly.Realtime.rtclientOptions_.authButtonElementId; - authButtonDiv.appendChild(authButton); - uiContainer.appendChild(authButtonDiv); - - // TODO: I would have liked to set the style for the authButtonDiv in css.js - // but that CSS doesn't get injected until after this code gets run. - authButtonDiv.style.display = 'none'; - authButtonDiv.style.position = 'relative'; - authButtonDiv.style.textAlign = 'center'; - authButtonDiv.style.border = '1px solid'; - authButtonDiv.style.backgroundColor = '#f6f9ff'; - authButtonDiv.style.borderRadius = '15px'; - authButtonDiv.style.boxShadow = '10px 10px 5px #888'; - authButtonDiv.style.width = (blocklyDivBounds.width / 3) + 'px'; - var authButtonDivBounds = goog.style.getBounds(authButtonDiv); - authButtonDiv.style.left = - (blocklyDivBounds.width - authButtonDivBounds.width) / 3 + 'px'; - authButtonDiv.style.top = - (blocklyDivBounds.height - authButtonDivBounds.height) / 4 + 'px'; - return authButtonDiv; -}; - -/** - * Update the collaborators UI to include the latest set of users. - * @private - */ -Blockly.Realtime.updateCollabUi_ = function() { - if (!Blockly.Realtime.collabElementId) { - return; - } - var collabElement = goog.dom.getElement(Blockly.Realtime.collabElementId); - goog.dom.removeChildren(collabElement); - var collaboratorsList = Blockly.Realtime.document_.getCollaborators(); - for (var i = 0; i < collaboratorsList.length; i++) { - var collaborator = collaboratorsList[i]; - var imgSrc = collaborator.photoUrl || - Blockly.pathToMedia + Blockly.Realtime.ANONYMOUS_URL_; - var img = goog.dom.createDom('img', - { - 'src': imgSrc, - 'alt': collaborator.displayName, - 'title': collaborator.displayName + - (collaborator.isMe ? ' (' + Blockly.Msg.ME + ')' : '')}); - img.style.backgroundColor = collaborator.color; - goog.dom.appendChild(collabElement, img); - } -}; - -/** - * Event handler for when collaborators join. - * @param {gapi.drive.realtime.CollaboratorJoinedEvent} event The event. - * @private - */ -Blockly.Realtime.onCollaboratorJoined_ = function(event) { - Blockly.Realtime.updateCollabUi_(); -}; - -/** - * Event handler for when collaborators leave. - * @param {gapi.drive.realtime.CollaboratorLeftEvent} event The event. - * @private - */ -Blockly.Realtime.onCollaboratorLeft_ = function(event) { - Blockly.Realtime.updateCollabUi_(); -}; - -/** - * Execute a command. Generally, a command is the result of a user action - * e.g. a click, drag or context menu selection. - * @param {function()} cmdThunk A function representing the command execution. - */ -Blockly.Realtime.doCommand = function(cmdThunk) { - // TODO(): We'd like to use the realtime API compound operations as in the - // commented out code below. However, it appears that the realtime API is - // re-ordering events when they're within compound operations in a way which - // breaks us. We might need to implement our own compound operations as a - // workaround. Doing so might give us some other advantages since we could - // then allow compound operations that span synchronous blocks of code (e.g., - // span multiple Blockly events). It would also allow us to deal with the - // fact that the current realtime API puts some operations into the undo stack - // that we would prefer weren't there; namely local changes that occur as a - // result of remote realtime events. -// try { -// Blockly.Realtime.model_.beginCompoundOperation(); -// cmdThunk(); -// } finally { -// Blockly.Realtime.model_.endCompoundOperation(); -// } - cmdThunk(); -}; - -/** - * Generate an id that is unique among the all the sessions that ever - * collaborated on this document. - * @param {string} extra A string id which is unique within this particular - * session. - * @return {string} - */ -Blockly.Realtime.genUid = function(extra) { - /* The idea here is that we use the extra string to ensure uniqueness within - this session and the current sessionId to ensure uniqueness across - all the current sessions. There's still the (remote) chance that the - current sessionId is the same as some old (non-current) one, so we still - need to check that our uid hasn't been previously used. - - Note that you could potentially use a random number to generate the id but - there remains the small chance of regenerating the same number that's been - used before and I'm paranoid. It's not enough to just check that the - random uid hasn't been previously used because other concurrent sessions - might generate the same uid at the same time. Like I said, I'm paranoid. - */ - var potentialUid = Blockly.Realtime.sessionId_ + '-' + extra; - if (!Blockly.Realtime.blocksMap_.has(potentialUid)) { - return potentialUid; - } else { - return (Blockly.Realtime.genUid('-' + extra)); - } -}; diff --git a/media/anon.jpeg b/media/anon.jpeg deleted file mode 100644 index 5ac84edbe..000000000 Binary files a/media/anon.jpeg and /dev/null differ diff --git a/media/progress.gif b/media/progress.gif deleted file mode 100644 index e97adb875..000000000 Binary files a/media/progress.gif and /dev/null differ diff --git a/tests/playground.html b/tests/playground.html index aa53d5a85..9a0888060 100644 --- a/tests/playground.html +++ b/tests/playground.html @@ -49,8 +49,6 @@ - -