Remove old realtime files.

This commit is contained in:
Neil Fraser
2015-12-02 22:37:55 -08:00
parent fa279b91ce
commit 96e130d113
7 changed files with 3 additions and 1382 deletions

View File

@@ -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;

View File

@@ -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() {},

View File

@@ -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.<string>} 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);
}
});
};

View File

@@ -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));
}
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

View File

@@ -49,8 +49,6 @@
<script src="../blocks/colour.js"></script>
<script src="../blocks/variables.js"></script>
<script src="../blocks/procedures.js"></script>
<!-- Load the Google Drive SDK Realtime libraries. -->
<script src="https://apis.google.com/js/api.js"></script>
<script>
'use strict';
// Depending on the URL argument, render as LTR or RTL.
@@ -581,9 +579,6 @@ h1 {
<br />
-->
<!-- Text area that will be used for our collaborative chat box. -->
<textarea id="chatbox" style="width: 26%; height: 12em" disabled="true"></textarea>
</div>
</body>
</html>