Google Realtime synchronization

This commit is contained in:
benweet 2013-07-20 02:08:17 +01:00
parent d1cb3db557
commit 7eaeb45806
12 changed files with 205 additions and 72 deletions

View File

@ -134,6 +134,7 @@ input::-webkit-input-placeholder,textarea::-webkit-input-placeholder {
} }
.navbar-inner .btn.disabled, .navbar-inner .btn.disabled,
.navbar-inner .btn.blocked,
.navbar-inner .btn[disabled] { .navbar-inner .btn[disabled] {
color: #333333; color: #333333;
background-color: #ddd; background-color: #ddd;

View File

@ -33,7 +33,7 @@
<script src="js/libs/require.js"></script> <script src="js/libs/require.js"></script>
</head> </head>
<body> <body>
<div id="navbar" class="navbar navbar-fixed-top ui-layout-north"> <div class="navbar navbar-fixed-top ui-layout-north">
<div class="navbar-inner"> <div class="navbar-inner">
<ul class="nav"> <ul class="nav">
@ -303,7 +303,8 @@
</div> </div>
<br /> <br /> <br /> <br />
<p> <p>
<label class="checkbox"> <input id="input-sync-export-gdrive-realtime" type="checkbox"> <label class="checkbox"> <input
id="input-sync-export-gdrive-realtime" type="checkbox">
Create a real time collaborative document Create a real time collaborative document
</label> </label>
</p> </p>
@ -314,7 +315,9 @@
your root folder.</li> your root folder.</li>
<li>You can move or rename the file afterwards within Google <li>You can move or rename the file afterwards within Google
Drive.</li> Drive.</li>
<li>Real time collaborative document can not have multiple <li>Real time collaborative documents can't be open outside
StackEdit</li>
<li>Real time collaborative documents can't have multiple
synchronized locations.</li> synchronized locations.</li>
</ul> </ul>
</blockquote> </blockquote>

View File

@ -197,7 +197,7 @@ define([
south__minSize: 200 south__minSize: 200
})); }));
} }
$("#navbar").click(function() { $(".navbar").click(function() {
layout.allowOverflow('north'); layout.allowOverflow('north');
}); });
$(".ui-layout-toggler-north").addClass("btn").append($("<b>").addClass("caret")); $(".ui-layout-toggler-north").addClass("btn").append($("<b>").addClass("caret"));
@ -212,7 +212,6 @@ define([
var editor = undefined; var editor = undefined;
var fileDesc = undefined; var fileDesc = undefined;
var documentContent = undefined; var documentContent = undefined;
var undoManager = undefined;
core.initEditor = function(fileDescParam) { core.initEditor = function(fileDescParam) {
if(fileDesc !== undefined) { if(fileDesc !== undefined) {
extensionMgr.onFileClosed(fileDesc); extensionMgr.onFileClosed(fileDesc);
@ -224,7 +223,7 @@ define([
editorElt.val(initDocumentContent); editorElt.val(initDocumentContent);
if(editor !== undefined) { if(editor !== undefined) {
// If the editor is already created // If the editor is already created
undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop); editor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
editor.refreshPreview(); editor.refreshPreview();
extensionMgr.onFileOpen(fileDesc); extensionMgr.onFileOpen(fileDesc);
return; return;
@ -310,9 +309,8 @@ define([
} }
extensionMgr.onEditorConfigure(editor); extensionMgr.onEditorConfigure(editor);
editor.hooks.chain("onPreviewRefresh", extensionMgr.onAsyncPreview); editor.hooks.chain("onPreviewRefresh", extensionMgr.onAsyncPreview);
undoManager = editor.run(previewWrapper); editor.run(previewWrapper);
undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop); editor.undoManager.reinit(initDocumentContent, fileDesc.editorStart, fileDesc.editorEnd, fileDesc.editorScrollTop);
extensionMgr.onFileOpen(fileDesc);
// Hide default buttons // Hide default buttons
$(".wmd-button-row").addClass("btn-group").find("li:not(.wmd-spacer)").addClass("btn").css("left", 0).find("span").hide(); $(".wmd-button-row").addClass("btn-group").find("li:not(.wmd-spacer)").addClass("btn").css("left", 0).find("span").hide();
@ -328,8 +326,15 @@ define([
$("#wmd-ulist-button").append($("<i>").addClass("icon-list")); $("#wmd-ulist-button").append($("<i>").addClass("icon-list"));
$("#wmd-heading-button").append($("<i>").addClass("icon-text-height")); $("#wmd-heading-button").append($("<i>").addClass("icon-text-height"));
$("#wmd-hr-button").append($("<i>").addClass("icon-hr")); $("#wmd-hr-button").append($("<i>").addClass("icon-hr"));
$("#wmd-undo-button").append($("<i>").addClass("icon-undo")); // Create additional undo/redo button for real time synchronization
$("#wmd-redo-button").append($("<i>").addClass("icon-share-alt")); var realtimeUndoButton = $('<li class="btn hide" id="wmd-undo-button-realtime" title="Undo - Ctrl+Z" style="left: 0px;">');
realtimeUndoButton.append($("<i>").addClass("icon-undo"));
$("#wmd-undo-button").append($("<i>").addClass("icon-undo")).after(realtimeUndoButton);
var realtimeRedoButton = $('<li class="btn hide" id="wmd-redo-button-realtime" title="Redo - Ctrl+Shift+Z" style="left: 0px;">');
realtimeRedoButton.append($("<i>").addClass("icon-share-alt"));
$("#wmd-redo-button").append($("<i>").addClass("icon-share-alt")).after(realtimeRedoButton);
extensionMgr.onFileOpen(fileDesc);
}; };
// Used to lock the editor from the user interaction during asynchronous tasks // Used to lock the editor from the user interaction during asynchronous tasks
@ -337,14 +342,7 @@ define([
core.lockUI = function(param) { core.lockUI = function(param) {
uiLocked = param; uiLocked = param;
$("#wmd-input").prop("disabled", uiLocked); $("#wmd-input").prop("disabled", uiLocked);
$(".btn").each(function() { $(".navbar-inner .btn").toggleClass("blocked", uiLocked);
var classes = $(this).attr("class");
if(uiLocked) {
$(this).attr("class", classes + " disabled");
} else {
$(this).attr("class", classes.replace(" disabled", ""));
}
});
if(uiLocked) { if(uiLocked) {
$(".lock-ui").removeClass("hide"); $(".lock-ui").removeClass("hide");
} }

View File

@ -65,15 +65,17 @@ define([
}); });
}; };
} }
extensionMgr.addHookCallback = function(hookName, callback) {
hookCallbackList[hookName].push(callback);
};
// Add a Hook to the extensionMgr // Add a Hook to the extensionMgr
function addHook(hookName, noLog) { function addHook(hookName, noLog) {
extensionMgr[hookName] = createHook(hookName, noLog); extensionMgr[hookName] = createHook(hookName, noLog);
} }
// Used by external modules to listen to extension events
extensionMgr.addHookCallback = function(hookName, callback) {
hookCallbackList[hookName].push(callback);
};
// Set extension config // Set extension config
extensionSettings = settings.extensionSettings || {}; extensionSettings = settings.extensionSettings || {};
_.each(extensionList, function(extension) { _.each(extensionList, function(extension) {

View File

@ -13,6 +13,11 @@ define([
extensionMgr = extensionMgrParameter; extensionMgr = extensionMgrParameter;
}; };
var synchronizer = undefined;
dialogManageSynchronization.onSynchronizerCreated = function(synchronizerParameter) {
synchronizer = synchronizerParameter;
};
var fileDesc = undefined; var fileDesc = undefined;
var removeButtonTemplate = '<a class="btn" title="Remove this location"><i class="icon-trash"></i></a>'; var removeButtonTemplate = '<a class="btn" title="Remove this location"><i class="icon-trash"></i></a>';
var refreshDialog = function(fileDescParameter) { var refreshDialog = function(fileDescParameter) {
@ -36,6 +41,7 @@ define([
syncDesc: syncDesc syncDesc: syncDesc
})); }));
lineElement.append($(removeButtonTemplate).click(function() { lineElement.append($(removeButtonTemplate).click(function() {
synchronizer.tryStopRealtimeSync();
fileDesc.removeSyncLocation(syncAttributes); fileDesc.removeSyncLocation(syncAttributes);
extensionMgr.onSyncRemoved(fileDesc, syncAttributes); extensionMgr.onSyncRemoved(fileDesc, syncAttributes);
})); }));

View File

@ -413,6 +413,14 @@ define([
task.chain(recursiveDownloadContent); task.chain(recursiveDownloadContent);
return; return;
} }
// if file is a real time document
if(file.mimeType.indexOf("application/vnd.google-apps.drive-sdk") === 0) {
file.content = "";
file.isRealtime = true;
objects.shift();
task.chain(recursiveDownloadContent);
return;
}
var headers = {}; var headers = {};
var token = gapi.auth.getToken(); var token = gapi.auth.getToken();
if(token) { if(token) {
@ -560,7 +568,12 @@ define([
pickerBuilder.setAppId(GOOGLE_DRIVE_APP_ID); pickerBuilder.setAppId(GOOGLE_DRIVE_APP_ID);
if(!isImagePicker) { if(!isImagePicker) {
var view = new google.picker.View(google.picker.ViewId.DOCS); var view = new google.picker.View(google.picker.ViewId.DOCS);
view.setMimeTypes("text/x-markdown,text/plain,application/octet-stream"); view.setMimeTypes([
"text/x-markdown",
"text/plain",
"application/octet-stream",
"application/vnd.google-apps.drive-sdk." + GOOGLE_DRIVE_APP_ID
].join(","));
pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN); pickerBuilder.enableFeature(google.picker.Feature.NAV_HIDDEN);
pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED); pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED);
pickerBuilder.addView(view); pickerBuilder.addView(view);

View File

@ -148,7 +148,8 @@
//Not necessary //Not necessary
//forceRefresh(); //forceRefresh();
return undoManager; that.undoManager = undoManager;
that.uiManager = uiManager;
}; };
} }
@ -663,7 +664,7 @@
} }
}; };
util.addEvent(panels.input, "keydown", handleCtrlYZ); //util.addEvent(panels.input, "keydown", handleCtrlYZ);
util.addEvent(panels.input, "keydown", handleModeChange); util.addEvent(panels.input, "keydown", handleModeChange);
util.addEvent(panels.input, "mousedown", function () { util.addEvent(panels.input, "mousedown", function () {
setMode("moving"); setMode("moving");
@ -694,6 +695,7 @@
inputStateObj.setInputAreaSelection(); inputStateObj.setInputAreaSelection();
saveState(); saveState();
}; };
this.setMode = setMode;
init(); init();
} }
@ -1251,7 +1253,7 @@
util.addEvent(inputBox, keyEvent, function (key) { util.addEvent(inputBox, keyEvent, function (key) {
// Check to see if we have a button key and, if so execute the callback. // Check to see if we have a button key and, if so execute the callback.
if ((key.ctrlKey || key.metaKey) && !key.altKey && !key.shiftKey) { if ((key.ctrlKey || key.metaKey) && !key.altKey) {
var keyCode = key.charCode || key.keyCode; var keyCode = key.charCode || key.keyCode;
var keyCodeStr = String.fromCharCode(keyCode).toLowerCase(); var keyCodeStr = String.fromCharCode(keyCode).toLowerCase();
@ -1298,6 +1300,12 @@
doClick(buttons.undo); doClick(buttons.undo);
} }
break; break;
case "v":
undoManager.setMode("typing");
return;
case "x":
undoManager.setMode("deleting");
return;
default: default:
return; return;
} }
@ -1547,6 +1555,8 @@
}; };
this.setUndoRedoButtonStates = setUndoRedoButtonStates; this.setUndoRedoButtonStates = setUndoRedoButtonStates;
this.buttons = buttons;
this.setButtonState = setupButton;
} }

View File

@ -42,16 +42,18 @@ define([
return; return;
} }
var fileDescList = []; var fileDescList = [];
var fileDesc = undefined;
_.each(result, function(file) { _.each(result, function(file) {
var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title); var syncAttributes = createSyncAttributes(file.id, file.etag, file.content, file.title);
syncAttributes.isRealtime = file.isRealtime;
var syncLocations = {}; var syncLocations = {};
syncLocations[syncAttributes.syncIndex] = syncAttributes; syncLocations[syncAttributes.syncIndex] = syncAttributes;
var fileDesc = fileMgr.createFile(file.title, file.content, syncLocations); fileDesc = fileMgr.createFile(file.title, file.content, syncLocations);
fileMgr.selectFile(fileDesc);
fileDescList.push(fileDesc); fileDescList.push(fileDesc);
}); });
if(fileDescList.length !== 0) { if(fileDesc !== undefined) {
extensionMgr.onSyncImportSuccess(fileDescList, gdriveProvider); extensionMgr.onSyncImportSuccess(fileDescList, gdriveProvider);
fileMgr.selectFile(fileDesc);
} }
}); });
}); });
@ -154,7 +156,9 @@ define([
_.each(changes, function(change) { _.each(changes, function(change) {
var syncIndex = createSyncIndex(change.fileId); var syncIndex = createSyncIndex(change.fileId);
var syncAttributes = fileMgr.getSyncAttributes(syncIndex); var syncAttributes = fileMgr.getSyncAttributes(syncIndex);
if(syncAttributes === undefined) { // If file is not synchronized or it's a real time synchronized location
if(syncAttributes === undefined || syncAttributes.isRealtime === true) {
// Skip it
return; return;
} }
// Store syncAttributes to avoid 2 times searching // Store syncAttributes to avoid 2 times searching
@ -261,31 +265,75 @@ define([
}); });
// Start realtime synchronization // Start realtime synchronization
var binding = undefined; var realtimeDocument = undefined;
gdriveProvider.startSync = function(content, syncAttributes, callback) { var realtimeBinding = undefined;
var undoExecute = undefined;
var redoExecute = undefined;
gdriveProvider.startRealtimeSync = function(content, syncAttributes, callback) {
logger.log("Starting Google Drive realtime synchronization"); logger.log("Starting Google Drive realtime synchronization");
googleHelper.loadRealtime(syncAttributes.id, content, function(err, doc) { googleHelper.loadRealtime(syncAttributes.id, content, function(err, doc) {
if(err || !doc) { if(err || !doc) {
callback(err); callback(err);
return; return;
} }
var string = doc.getModel().getRoot().get('content'); realtimeDocument = doc;
binding = gapi.drive.realtime.databinding.bindString(string, $("#wmd-input")[0]); var model = realtimeDocument.getModel();
// Listen to var string = model.getRoot().get('content');
realtimeBinding = gapi.drive.realtime.databinding.bindString(string, $("#wmd-input")[0]);
// Listen to text changed events
var debouncedRefreshPreview = _.debounce(editor.refreshPreview, 100); var debouncedRefreshPreview = _.debounce(editor.refreshPreview, 100);
string.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, debouncedRefreshPreview); string.addEventListener(gapi.drive.realtime.EventType.TEXT_INSERTED, debouncedRefreshPreview);
string.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, debouncedRefreshPreview); string.addEventListener(gapi.drive.realtime.EventType.TEXT_DELETED, debouncedRefreshPreview);
debouncedRefreshPreview();
// Add event handler for UndoRedoStateChanged events.
undoExecute = editor.uiManager.buttons.undo.execute;
redoExecute = editor.uiManager.buttons.redo.execute;
// var undoButton = $('#wmd-undo-button-realtime').removeClass("hide");
// var redoButton = $('#wmd-redo-button-realtime').removeClass("hide");
// $('#wmd-undo-button').addClass("hide");
// $('#wmd-redo-button').addClass("hide");
editor.uiManager.buttons.undo.execute = function() {
model.canUndo && model.undo();
};
editor.uiManager.buttons.redo.execute = function() {
model.canRedo && model.redo();
};
function setUndoRedoState() {
editor.uiManager.setButtonState(editor.uiManager.buttons.undo, model.canUndo);
editor.uiManager.setButtonState(editor.uiManager.buttons.redo, model.canRedo);
}
model.addEventListener(
gapi.drive.realtime.EventType.UNDO_REDO_STATE_CHANGED,
setUndoRedoState);
setUndoRedoState();
callback(); callback();
}); });
}; };
// Stop realtime synchronization // Stop realtime synchronization
gdriveProvider.stopSync = function(syncAttributes) { gdriveProvider.stopRealtimeSync = function() {
logger.log("Stopping Google Drive realtime synchronization"); logger.log("Stopping Google Drive realtime synchronization");
if(binding !== undefined) { if(realtimeBinding !== undefined) {
binding.unbind(); realtimeBinding.unbind();
binding = undefined; realtimeBinding = undefined;
} }
if(realtimeDocument !== undefined) {
realtimeDocument.close();
realtimeDocument = undefined;
}
editor.uiManager.buttons.undo.execute = undoExecute;
editor.uiManager.buttons.redo.execute = redoExecute;
editor.uiManager.setUndoRedoButtonStates();
// $('#wmd-undo-button-realtime').off('click').addClass("hide");
// $('#wmd-redo-button-realtime').off('click').addClass("hide");
// $('#wmd-undo-button').removeClass("hide");
// $('#wmd-redo-button').removeClass("hide");
}; };
core.onReady(function() { core.onReady(function() {

View File

@ -46,6 +46,10 @@ define([
}); });
}); });
/***************************************************************************
* Standard synchronization
**************************************************************************/
// Recursive function to upload a single file on multiple locations // Recursive function to upload a single file on multiple locations
var uploadSyncAttributesList = []; var uploadSyncAttributesList = [];
var uploadContent = undefined; var uploadContent = undefined;
@ -63,6 +67,12 @@ define([
// Dequeue a synchronized location // Dequeue a synchronized location
var syncAttributes = uploadSyncAttributesList.pop(); var syncAttributes = uploadSyncAttributesList.pop();
// Skip real time synchronized location
if(syncAttributes.isRealtime === true) {
locationUp(callback);
return;
}
// Use the specified provider to perform the upload // Use the specified provider to perform the upload
syncAttributes.provider.syncUp(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, function(error, uploadFlag) { syncAttributes.provider.syncUp(uploadContent, uploadContentCRC, uploadTitle, uploadTitleCRC, syncAttributes, function(error, uploadFlag) {
if(uploadFlag === true) { if(uploadFlag === true) {
@ -188,30 +198,65 @@ define([
return true; return true;
}; };
// Used for realtime synchronization /***************************************************************************
* Realtime synchronization
**************************************************************************/
var realtimeFileDesc = undefined;
var realtimeSyncAttributes = undefined;
var isOnline = true;
// Determines if open file has real time sync location and tries to start
// real time sync
function onFileOpen(fileDesc) { function onFileOpen(fileDesc) {
_.each(fileDesc.syncLocations, function(syncAttributes) { realtimeFileDesc = _.some(fileDesc.syncLocations, function(syncAttributes) {
if(syncAttributes.isRealtime) { realtimeSyncAttributes = syncAttributes;
return syncAttributes.isRealtime;
}) ? fileDesc : undefined;
tryStartRealtimeSync();
}
// Tries to start/stop real time sync on online/offline event
function onOfflineChanged(isOfflineParam) {
if(isOfflineParam === false) {
isOnline = true;
tryStartRealtimeSync();
}
else {
synchronizer.tryStopRealtimeSync();
isOnline = false;
}
}
// Starts real time synchronization if:
// 1. current file has real time sync location
// 2. we are online
function tryStartRealtimeSync() {
if(realtimeFileDesc !== undefined && isOnline === true) {
core.lockUI(true); core.lockUI(true);
syncAttributes.provider.startSync(fileDesc.content, syncAttributes, function() { realtimeSyncAttributes.provider.startRealtimeSync(realtimeFileDesc.content, realtimeSyncAttributes, function() {
core.lockUI(false); core.lockUI(false);
}); });
} }
});
}
function onFileClosed(fileDesc) {
_.each(fileDesc.syncLocations, function(syncAttributes) {
if(syncAttributes.isRealtime) {
syncAttributes.provider.stopSync(syncAttributes);
}
});
}
// Enable realtime synchronization
if(viewerMode === false) {
extensionMgr.addHookCallback("onFileOpen", onFileOpen);
extensionMgr.addHookCallback("onFileClosed", onFileClosed);
} }
// Stops previously started synchronization if any
synchronizer.tryStopRealtimeSync = function() {
if(realtimeFileDesc !== undefined && isOnline === true) {
realtimeSyncAttributes.provider.stopRealtimeSync();
}
};
// Triggers realtime synchronization from extensionMgr events
if(viewerMode === false) {
extensionMgr.addHookCallback("onFileOpen", onFileOpen);
extensionMgr.addHookCallback("onFileClosed", synchronizer.tryStopRealtimeSync);
extensionMgr.addHookCallback("onOfflineChanged", onOfflineChanged);
}
/***************************************************************************
* Initialize module
**************************************************************************/
// Initialize the export dialog // Initialize the export dialog
function initExportDialog(provider) { function initExportDialog(provider) {
@ -259,6 +304,11 @@ define([
syncAttributes.isRealtime = true; syncAttributes.isRealtime = true;
fileDesc.addSyncLocation(syncAttributes); fileDesc.addSyncLocation(syncAttributes);
extensionMgr.onSyncExportSuccess(fileDesc, syncAttributes); extensionMgr.onSyncExportSuccess(fileDesc, syncAttributes);
// Start the real time sync
realtimeFileDesc = fileDesc;
realtimeSyncAttributes = syncAttributes;
tryStartRealtimeSync();
}); });
} }
else { else {

View File

@ -44,6 +44,7 @@ textarea[disabled],
} }
.navbar-inner .btn.disabled, .navbar-inner .btn.disabled,
.navbar-inner .btn.blocked,
.navbar-inner .btn[disabled] { .navbar-inner .btn[disabled] {
background-color: #ced5de; background-color: #ced5de;
} }

View File

@ -111,6 +111,7 @@ blockquote {
} }
.navbar-inner .btn.disabled, .navbar-inner .btn.disabled,
.navbar-inner .btn.blocked,
.navbar-inner .btn[disabled] { .navbar-inner .btn[disabled] {
background-color: #444; background-color: #444;
} }

View File

@ -28,7 +28,7 @@
<script src="js/libs/require.js"></script> <script src="js/libs/require.js"></script>
</head> </head>
<body class="viewer"> <body class="viewer">
<div id="navbar" class="navbar navbar-fixed-top ui-layout-north"> <div class="navbar navbar-fixed-top ui-layout-north">
<div class="navbar-inner"> <div class="navbar-inner">
<ul class="nav pull-right hide" id="menu-bar"> <ul class="nav pull-right hide" id="menu-bar">