From 96e130d1133eae429d989f61ab9eefbaf5a56dae Mon Sep 17 00:00:00 2001 From: Neil Fraser Date: Wed, 2 Dec 2015 22:37:55 -0800 Subject: [PATCH] Remove old realtime files. --- blockly_uncompressed.js | 10 +- core/blockly.js | 1 - core/realtime-client-utils.js | 500 ------------------- core/realtime.js | 869 ---------------------------------- media/anon.jpeg | Bin 2064 -> 0 bytes media/progress.gif | Bin 19602 -> 0 bytes tests/playground.html | 5 - 7 files changed, 3 insertions(+), 1382 deletions(-) delete mode 100644 core/realtime-client-utils.js delete mode 100644 core/realtime.js delete mode 100644 media/anon.jpeg delete mode 100644 media/progress.gif 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 5ac84edbe28c0aafe0b6254f1f22198bdcaff0d2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2064 zcmb7=c~q0x8OFaaVHd=xFam}+sKJgh7G#q^K%wk0MGspj(18(IMfMO9z*9RY0x2LE zc80|iK^T^RN>~R-G02jzM-l>L5E&*31W?M-WZJR%U;Ey3?!D(e=lwnJa}TBu=7A&5 z_D=Qy2m$~oUBCecumzMA6qOVel$8{fR8*7?scEXKsj8~!9M?Fac?zzlcM6U`oH=KF z;SADHAAvBiGB7kTv#_u@ecsy6+T7OovW1xp0;;H}sHv)HtE+39okg5A`@id;8-OVR ze1H!H^#BMAgu=jsK0sSKBovg5`?2KY6+kFNs+k=D0SE|@gD5CN6#p%Q5U3nX{^*xi z45NSLy~) z_PUPmn{ay81_C7BG`MOg^t>6TDmvm&cK=LnGOV-|UQd?j@x+6O&VOvxJLAHL-sgkG zSsqp49PGWDW~(jz6}NuGa}sdA_GOwC#RWBAWO24`(Vo{kxHSYCcOgIK5y<(}U48^Y*?}+8Z?+x)IsIgq^GX|_{U|b6quc2EjPsiY`*^m*>q$@ibuYcbtrfiv2Y*IC`^}DFzv%WxssWi zf;&$mF835yCx&Qj+cQ^6LzCd(x80xcHH&pHe?gYU_+^JT=8!_fnRHX1-#yp7_NSR2 zh9oV2Sd`>3nPrO=P;4YQ@{t@@2FOhrn3 zNgizv=~{qB4efCxyZI9W%iYftLD?>kM?*z|(!7V!oL4Cl1F*! zNiHpZo4CuLm#i|fPYQz}S+b_zN--?w&eD@$G2?b*d)Lyu_gKxdip_@}2@f*)6OnS` zF)PdR)X9DpI&N(*9}6$0`}w6BmIIb?1)bz-=Hreq6mhwE2Ftj#fh!ChNoz=%g$JVW z%tVbxS-3t|p_O2iF#4O-e@+sgNkS8TVoZjy0vE_J4%88{Te?v|>t@+25^JbhL|VMh zxG-eL3L1m&4TcYRPvQMtM7nV{ppI|d6SBI=OD^Gh$=U_U>{Lh7wIuKSg*Id+V>==qgU|R4Xp75s;I|ui8^> zYuJ&d)T$~HJ!+@dB*=h|YPisOtchV0{PCmYhU_9QwVVlQ(a!W>!`?RTCS)}WR9mrU z@+&dY5RaemcS`KphHs;GJ*C1SUw!qS>djg|Vi|mAM3kT96IqQ!{Jqc*L1hyr8cg<4 zn~#2Q=C%WYHZljhi92g(>SW=%|031NMd#v)k$3obDwf_y2u)g9)Q#H9)*Fjd-UuC| z;ndKz_0AE=Qw>g@@HWoFv_;Fa@9?*&3LDr+EGaO|VQ%g)5_yZKYl!}R%PW87nZR;m znFF{@?J06ktXC1XWiG#CY!iZGTPFpDn;Gt9e!LcAb1_>(kFu&!hqpUhI!toFFY;cd zRq-izT>bTFOtDX|<@e@CT6teNbu0XelOX{>F2XNO;i*-1d{@|1Rj(sgoUv7#w#*U+ zZ$0n)is7|!FPK8%>f=Kn(XnYeTCJ5>-C@E~Rs4tE>F~`PO!a=eolvK{ITb*7iFs%o zzkgH6n}ZA1Y4cuzxyXvZXVm;ox7`;b7c*{OqJ2Z{!JxGl7&;z}?{~T7>vh0D>aRn5 z+%C~=T_jcx2B*WlE!YApK0Il_&>nNxg;bYJC-HjIEe3?w=CnE4k1 CPj8O^ diff --git a/media/progress.gif b/media/progress.gif deleted file mode 100644 index e97adb87586cfd3e6e9055bf80f39d03a455c3d1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19602 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 MOa`wDPDBC!2h(fb1poj5 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 @@ - -