Comments realtime sync

This commit is contained in:
benweet 2014-03-23 02:33:41 +00:00
parent 9039cac7f5
commit e310d234cf
14 changed files with 404 additions and 170 deletions

18
.eslintrc Normal file
View File

@ -0,0 +1,18 @@
{
"env": {
"browser": true,
"amd": true
},
"rules": {
"curly": 2,
"no-comma-dangle": 0,
"strict": 0,
"quotes": 0,
"no-underscore-dangle": 0,
"consistent-return": 0,
"no-unused-expressions": 0,
"no-use-before-define": 0,
"camelcase": 0,
"eqeqeq": 0
}
}

View File

@ -16,13 +16,6 @@ module.exports = function(grunt) {
jshint: { jshint: {
options: { options: {
jshintrc: true, jshintrc: true,
ignores: [
'node_modules/**/*.js',
'public/libs/**/*.js',
'public/res/libs/**/*.js',
'public/res/bower-libs/**/*.js',
'public/res-min/**/*.js'
]
}, },
client: ['public/**/*.js'], client: ['public/**/*.js'],
}, },

View File

@ -12,6 +12,7 @@ define([
this._editorEnd = parseInt(storage[fileIndex + ".editorEnd"]) || 0; this._editorEnd = parseInt(storage[fileIndex + ".editorEnd"]) || 0;
this._previewScrollTop = parseInt(storage[fileIndex + ".previewScrollTop"]) || 0; this._previewScrollTop = parseInt(storage[fileIndex + ".previewScrollTop"]) || 0;
this._selectTime = parseInt(storage[fileIndex + ".selectTime"]) || 0; this._selectTime = parseInt(storage[fileIndex + ".selectTime"]) || 0;
this._discussionList = JSON.parse(storage[fileIndex + ".discussionList"] || '{}');
this.syncLocations = syncLocations || {}; this.syncLocations = syncLocations || {};
this.publishLocations = publishLocations || {}; this.publishLocations = publishLocations || {};
Object.defineProperty(this, 'title', { Object.defineProperty(this, 'title', {
@ -76,6 +77,15 @@ define([
storage[this.fileIndex + ".selectTime"] = selectTime; storage[this.fileIndex + ".selectTime"] = selectTime;
} }
}); });
Object.defineProperty(this, 'discussionList', {
get: function() {
return this._discussionList;
},
set: function(discussionList) {
this._discussionList = discussionList;
storage[this.fileIndex + ".discussionList"] = JSON.stringify(discussionList);
}
});
} }
FileDescriptor.prototype.addSyncLocation = function(syncAttributes) { FileDescriptor.prototype.addSyncLocation = function(syncAttributes) {

View File

@ -799,7 +799,7 @@ define([
}); });
$(".action-import-docs-settings-confirm").click(function() { $(".action-import-docs-settings-confirm").click(function() {
storage.clear(); storage.clear();
var allowedKeys = /^file\.|^folder\.|^publish\.|^settings$|^sync\.|^google\.|^themeV3$|^mode$|^version$|^welcomeTour$/; var allowedKeys = /^file\.|^folder\.|^publish\.|^settings$|^sync\.|^google\.|^themeV3$|^version$/;
_.each(newstorage, function(value, key) { _.each(newstorage, function(value, key) {
if(allowedKeys.test(key)) { if(allowedKeys.test(key)) {
storage[key] = value; storage[key] = value;

View File

@ -10,11 +10,11 @@ define([
'libs/prism-markdown' 'libs/prism-markdown'
], function ($, _, settings, eventMgr, Prism, crel) { ], function ($, _, settings, eventMgr, Prism, crel) {
String.prototype.splice = function (i, remove, add) { function strSplice(str, i, remove, add) {
remove = +remove || 0; remove = +remove || 0;
add = add || ''; add = add || '';
return this.slice(0, i) + add + this.slice(i + remove); return str.slice(0, i) + add + str.slice(i + remove);
}; }
var editor = {}; var editor = {};
var selectionStart = 0; var selectionStart = 0;
@ -87,14 +87,14 @@ define([
currentTextContent += '\n'; currentTextContent += '\n';
} }
fileDesc.content = currentTextContent; fileDesc.content = currentTextContent;
eventMgr.onContentChanged(fileDesc); eventMgr.onContentChanged(fileDesc, currentTextContent);
} }
else { else {
if(!/\n$/.test(currentTextContent)) { if(!/\n$/.test(currentTextContent)) {
currentTextContent += '\n'; currentTextContent += '\n';
fileDesc.content = currentTextContent; fileDesc.content = currentTextContent;
} }
eventMgr.onFileOpen(fileDesc); eventMgr.onFileOpen(fileDesc, currentTextContent);
previewElt.scrollTop = fileDesc.previewScrollTop; previewElt.scrollTop = fileDesc.previewScrollTop;
selectionStart = fileDesc.editorStart; selectionStart = fileDesc.editorStart;
selectionEnd = fileDesc.editorEnd; selectionEnd = fileDesc.editorEnd;
@ -105,12 +105,108 @@ define([
previousTextContent = currentTextContent; previousTextContent = currentTextContent;
} }
function findOffset(ss) {
var offset = 0,
element = editor.contentElt,
container;
do {
container = element;
element = element.firstChild;
if (element) {
do {
var len = element.textContent.length;
if (offset <= ss && offset + len > ss) {
break;
}
offset += len;
} while (element = element.nextSibling);
}
if (!element) {
// It's the container's lastChild
break;
}
} while (element && element.hasChildNodes() && element.nodeType != 3);
if (element) {
return {
element: element,
offset: ss - offset
};
} else if (container) {
element = container;
while (element && element.lastChild) {
element = element.lastChild;
}
if (element.nodeType === 3) {
return {
element: element,
offset: element.textContent.length
};
} else {
return {
element: element,
offset: 0
};
}
}
return {
element: editor.contentElt,
offset: 0,
error: true
};
}
function getCoordinates(inputOffset, element, offset) {
var x = 0;
var y = 0;
if(element.textContent == '\n') {
y = element.parentNode.offsetTop + element.parentNode.offsetHeight / 2;
}
else {
var selectedChar = inputElt.textContent[inputOffset];
var selectionRange;
if(selectedChar === undefined || selectedChar == '\n') {
selectionRange = inputElt.createRange(inputOffset - 1, {
element: element,
offset: offset
});
}
else {
selectionRange = inputElt.createRange({
element: element,
offset: offset
}, inputOffset + 1);
}
var selectionRect = selectionRange.getBoundingClientRect();
y = selectionRect.top + selectionRect.height / 2 - inputElt.offsetTop + inputElt.scrollTop;
selectionRange.detach();
}
return {
x: x,
y: y
};
}
var cursorY = 0; var cursorY = 0;
function saveCursorCoordinates() { var isBackwardSelection = false;
function updateCursorCoordinates() {
saveEditorState(); saveEditorState();
$inputElt.toggleClass('has-selection', selectionStart !== selectionEnd); $inputElt.toggleClass('has-selection', selectionStart !== selectionEnd);
var backwards = false; var element;
var offset;
var inputOffset;
if(inputElt.focused) {
isBackwardSelection = false;
var selection = window.getSelection(); var selection = window.getSelection();
if(!selection.rangeCount) { if(!selection.rangeCount) {
return; return;
@ -119,34 +215,28 @@ define([
var range = document.createRange(); var range = document.createRange();
range.setStart(selection.anchorNode, selection.anchorOffset); range.setStart(selection.anchorNode, selection.anchorOffset);
range.setEnd(selection.focusNode, selection.focusOffset); range.setEnd(selection.focusNode, selection.focusOffset);
backwards = range.collapsed; isBackwardSelection = range.collapsed;
range.detach(); range.detach();
} }
var selectionRange = selection.getRangeAt(0); var selectionRange = selection.getRangeAt(0);
var container = backwards ? selectionRange.startContainer : selectionRange.endContainer; element = isBackwardSelection ? selectionRange.startContainer : selectionRange.endContainer;
if(container.textContent == '\n') { offset = isBackwardSelection ? selectionRange.startOffset : selectionRange.endOffset;
cursorY = container.parentNode.offsetTop + container.parentNode.offsetHeight / 2; inputOffset = isBackwardSelection ? selectionStart : selectionEnd;
} }
else { else {
var cursorOffset = backwards ? selectionStart : selectionEnd; inputOffset = isBackwardSelection ? selectionStart : selectionEnd;
var selectedChar = inputElt.textContent[cursorOffset]; var elementOffset = findOffset(inputOffset);
if(selectedChar === undefined || selectedChar == '\n') { element = elementOffset.element;
selectionRange = inputElt.createRange(cursorOffset - 1, cursorOffset); offset = elementOffset.offset;
} }
else { var coordinates = getCoordinates(inputOffset, element, offset);
selectionRange = inputElt.createRange(cursorOffset, cursorOffset + 1); cursorY = coordinates.y;
} eventMgr.onCursorCoordinates(coordinates.x, coordinates.y);
var selectionRect = selectionRange.getBoundingClientRect();
cursorY = selectionRect.top + selectionRect.height / 2 - inputElt.offsetTop + inputElt.scrollTop;
selectionRange.detach();
}
eventMgr.onCursorCoordinates(0, cursorY);
} }
function adjustCursorPosition() { function adjustCursorPosition() {
inputElt && setTimeout(function() { inputElt && setTimeout(function() {
saveCursorCoordinates(); updateCursorCoordinates();
var adjust = inputElt.offsetHeight / 2; var adjust = inputElt.offsetHeight / 2;
if(adjust > 130) { if(adjust > 130) {
@ -254,7 +344,7 @@ define([
offset = range.startOffset; offset = range.startOffset;
if (!(this.compareDocumentPosition(element) & 0x10)) { if (!(this.compareDocumentPosition(element) & 0x10)) {
return 0; return selectionStart;
} }
do { do {
@ -269,7 +359,7 @@ define([
return offset; return offset;
} else { } else {
return 0; return selectionStart;
} }
}, },
set: function (value) { set: function (value) {
@ -287,7 +377,7 @@ define([
if (selection.rangeCount) { if (selection.rangeCount) {
return this.selectionStart + (selection.getRangeAt(0) + '').length; return this.selectionStart + (selection.getRangeAt(0) + '').length;
} else { } else {
return 0; return selectionEnd;
} }
}, },
set: function (value) { set: function (value) {
@ -312,82 +402,29 @@ define([
return { return {
selectionStart: selectionStart, selectionStart: selectionStart,
selectionEnd: selectionEnd selectionEnd: selectionEnd
} };
}; };
inputElt.createRange = function(ss, se) { inputElt.createRange = function(ss, se) {
function findOffset(ss) {
var offset = 0,
element = editor.contentElt,
container;
do {
container = element;
element = element.firstChild;
if (element) {
do {
var len = element.textContent.length;
if (offset <= ss && offset + len > ss) {
break;
}
offset += len;
} while (element = element.nextSibling);
}
if (!element) {
// It's the container's lastChild
break;
}
} while (element && element.hasChildNodes() && element.nodeType != 3);
if (element) {
return {
element: element,
offset: ss - offset
};
} else if (container) {
element = container;
while (element && element.lastChild) {
element = element.lastChild;
}
if (element.nodeType === 3) {
return {
element: element,
offset: element.textContent.length
};
} else {
return {
element: element,
offset: 0
};
}
}
return {
element: editor.contentElt,
offset: 0,
error: true
};
}
var range = document.createRange(), var range = document.createRange(),
offset = findOffset(ss); offset = _.isObject(ss) ? ss : findOffset(ss);
range.setStart(offset.element, offset.offset); range.setStart(offset.element, offset.offset);
if (se && se != ss) { if (se && se != ss) {
offset = findOffset(se); offset = _.isObject(se) ? se : findOffset(se);
} }
range.setEnd(offset.element, offset.offset); range.setEnd(offset.element, offset.offset);
return range; return range;
}; };
inputElt.getOffsetCoordinates = function(ss) {
var offset = findOffset(ss);
return getCoordinates(ss, offset.element, offset.offset);
};
var clearNewline = false; var clearNewline = false;
editor.$contentElt.on('keydown', function (evt) { editor.$contentElt.on('keydown', function (evt) {
if( if(
@ -423,7 +460,7 @@ define([
}) })
.on('mouseup', function() { .on('mouseup', function() {
setTimeout(function() { setTimeout(function() {
saveCursorCoordinates(); updateCursorCoordinates();
}, 0); }, 0);
}) })
.on('paste', function () { .on('paste', function () {
@ -463,7 +500,7 @@ define([
if (options.inverse) { if (options.inverse) {
if (/\s/.test(state.before.charAt(lf))) { if (/\s/.test(state.before.charAt(lf))) {
state.before = state.before.splice(lf, 1); state.before = strSplice(state.before, lf, 1);
state.ss--; state.ss--;
state.se--; state.se--;
@ -471,7 +508,7 @@ define([
state.selection = state.selection.replace(/^[ \t]/gm, ''); state.selection = state.selection.replace(/^[ \t]/gm, '');
} else if (state.selection) { } else if (state.selection) {
state.before = state.before.splice(lf, 0, '\t'); state.before = strSplice(state.before, lf, 0, '\t');
state.selection = state.selection.replace(/\r?\n(?=[\s\S])/g, '\n\t'); state.selection = state.selection.replace(/\r?\n(?=[\s\S])/g, '\n\t');
state.ss++; state.ss++;

View File

@ -12,29 +12,75 @@ define([
var comments = new Extension("comments", 'Comments'); var comments = new Extension("comments", 'Comments');
var offsetMap = {}; var offsetMap = {};
function setCommentEltCoordinates(commentElt, y) {
var lineIndex = Math.round(y/10);
var top = (y - 8) + 'px';
var right = ((offsetMap[lineIndex] || 0) * 25 + 10) + 'px';
commentElt.style.top = top;
commentElt.style.right = right;
return lineIndex;
}
var inputElt; var inputElt;
var marginElt; var marginElt;
var commentEltList = [];
var newCommentElt = crel('a', { var newCommentElt = crel('a', {
class: 'icon-comment new' class: 'icon-comment new'
}); });
comments.onCursorCoordinates = function(cursorX, cursorY) { var cursorY;
var top = (cursorY - 8) + 'px'; comments.onCursorCoordinates = function(x, y) {
var right = 10 + 'px'; cursorY = y;
newCommentElt.style.top = top; setCommentEltCoordinates(newCommentElt, cursorY);
newCommentElt.style.right = right;
}; };
var fileDesc; var fileDesc;
comments.onFileSelected = function(selectedFileDesc) { comments.onFileSelected = function(selectedFileDesc) {
fileDesc = selectedFileDesc; fileDesc = selectedFileDesc;
refreshDiscussions();
}; };
var openedPopover;
comments.onLayoutResize = function() {
openedPopover && openedPopover.popover('toggle').popover('destroy');
refreshDiscussions();
};
var refreshId;
function refreshDiscussions() {
clearTimeout(refreshId);
commentEltList.forEach(function(commentElt) {
marginElt.removeChild(commentElt);
});
commentEltList = [];
offsetMap = {};
var discussionList = _.map(fileDesc.discussionList, _.identity);
function refreshOne() {
if(discussionList.length === 0) {
return;
}
var discussion = discussionList.pop();
var commentElt = crel('a', {
class: 'icon-comment'
});
commentElt.discussion = discussion;
var coordinates = inputElt.getOffsetCoordinates(discussion.selectionEnd);
var lineIndex = setCommentEltCoordinates(commentElt, coordinates.y);
offsetMap[lineIndex] = (offsetMap[lineIndex] || 0) + 1;
marginElt.appendChild(commentElt);
commentEltList.push(commentElt);
// Replace newCommentElt
setCommentEltCoordinates(newCommentElt, cursorY);
refreshId = setTimeout(refreshOne, 0);
}
refreshId = setTimeout(refreshOne, 50);
}
comments.onReady = function() { comments.onReady = function() {
var cssApplier = rangy.createCssClassApplier("comment-highlight", { var cssApplier = rangy.createCssClassApplier("comment-highlight", {
normalize: false normalize: false
}); });
var openedPopover;
var selectionRange; var selectionRange;
var rangyRange; var rangyRange;
var currentDiscussion; var currentDiscussion;
@ -42,7 +88,9 @@ define([
inputElt = document.getElementById('wmd-input'); inputElt = document.getElementById('wmd-input');
marginElt = document.querySelector('#wmd-input > .editor-margin'); marginElt = document.querySelector('#wmd-input > .editor-margin');
marginElt.appendChild(newCommentElt); marginElt.appendChild(newCommentElt);
$(document.body).append($('<div class="comments-popover">')).popover({ $(document.body).append(crel('div', {
class: 'comments-popover'
})).popover({
placement: 'auto top', placement: 'auto top',
container: '.comments-popover', container: '.comments-popover',
html: true, html: true,
@ -51,8 +99,8 @@ define([
return '...'; return '...';
} }
var titleLength = currentDiscussion.selectionEnd - currentDiscussion.selectionStart; var titleLength = currentDiscussion.selectionEnd - currentDiscussion.selectionStart;
var title = fileDesc.content.substr(currentDiscussion.selectionStart, titleLength > 18 ? 18 : titleLength); var title = inputElt.textContent.substr(currentDiscussion.selectionStart, titleLength > 20 ? 20 : titleLength);
if(titleLength > 18) { if(titleLength > 20) {
title += '...'; title += '...';
} }
return title.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' '); return title.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/\u00a0/g, ' ');
@ -65,7 +113,14 @@ define([
selector: '#wmd-input > .editor-margin > .icon-comment' selector: '#wmd-input > .editor-margin > .icon-comment'
}).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('show.bs.popover', '#wmd-input > .editor-margin', function(evt) {
$(evt.target).addClass('active'); $(evt.target).addClass('active');
inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 2/3; inputElt.scrollTop += parseInt(evt.target.style.top) - inputElt.scrollTop - inputElt.offsetHeight * 3 / 4;
// If it's an existing discussion
if(evt.target.discussion) {
currentDiscussion = evt.target.discussion;
selectionRange = inputElt.createRange(currentDiscussion.selectionStart, currentDiscussion.selectionEnd);
return;
}
// Get selected text // Get selected text
var inputSelection = inputElt.getSelectionStartEnd(); var inputSelection = inputElt.getSelectionStartEnd();
@ -88,12 +143,12 @@ define([
currentDiscussion = { currentDiscussion = {
selectionStart: selectionStart, selectionStart: selectionStart,
selectionEnd: selectionEnd, selectionEnd: selectionEnd,
comments: [] commentList: []
}; };
}).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('shown.bs.popover', '#wmd-input > .editor-margin', function(evt) {
// Move the popover in the margin // Move the popover in the margin
var popoverElt = document.querySelector('.comments-popover .popover'); var popoverElt = document.querySelector('.comments-popover .popover:last-child');
var left = -10; var left = -10;
if(popoverElt.offsetWidth < marginElt.offsetWidth) { if(popoverElt.offsetWidth < marginElt.offsetWidth) {
left = marginElt.offsetWidth - popoverElt.offsetWidth - 10; left = marginElt.offsetWidth - popoverElt.offsetWidth - 10;
@ -101,25 +156,47 @@ define([
popoverElt.style.left = left + 'px'; popoverElt.style.left = left + 'px';
popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(evt.target.style.right) - evt.target.offsetWidth / 2 - left) + 'px'; popoverElt.querySelector('.arrow').style.left = (marginElt.offsetWidth - parseInt(evt.target.style.right) - evt.target.offsetWidth / 2 - left) + 'px';
var textarea = popoverElt.querySelector('.input-comment-content'); var $textarea = $(popoverElt.querySelector('.input-comment-content'));
$(popoverElt.querySelector('.action-add-comment')).click(function(evt) { var $addButton = $(popoverElt.querySelector('.action-add-comment'));
$textarea.keydown(function(evt) {
// Enter key
switch(evt.which) {
case 13:
evt.preventDefault();
$addButton.click();
return;
case 27:
evt.preventDefault();
openedPopover && openedPopover.popover('toggle').popover('destroy');
inputElt.focus();
return;
}
});
$addButton.click(function(evt) {
var author = utils.getInputTextValue(popoverElt.querySelector('.input-comment-author')); var author = utils.getInputTextValue(popoverElt.querySelector('.input-comment-author'));
var content = utils.getInputTextValue(textarea, evt); var content = utils.getInputTextValue($textarea, evt);
if(evt.isPropagationStopped()) { if(evt.isPropagationStopped()) {
return; return;
} }
var fileDiscussions = fileDesc.discussions || {}; var discussionList = fileDesc.discussionList || {};
if(!currentDiscussion.discussionIndex) { if(!currentDiscussion.discussionIndex) {
// Create discussion index
var discussionIndex;
do { do {
currentDiscussion.discussionIndex = utils.randomString(); discussionIndex = utils.randomString();
} while(_.has(fileDiscussions, currentDiscussion.discussionIndex)); } while(_.has(discussionList, discussionIndex));
currentDiscussion.discussionIndex = discussionIndex;
discussionList[discussionIndex] = currentDiscussion;
} }
currentDiscussion.push({ currentDiscussion.commentList.push({
author: author, author: author,
content: content content: content
}); });
fileDesc.discussionList = discussionList;
openedPopover.popover('toggle').popover('destroy'); openedPopover.popover('toggle').popover('destroy');
refreshDiscussions();
inputElt.focus();
}); });
// Prevent from closing on click inside the popover // Prevent from closing on click inside the popover
@ -136,7 +213,7 @@ define([
cssApplier.applyToRange(rangyRange); cssApplier.applyToRange(rangyRange);
// Focus on textarea // Focus on textarea
textarea.focus(); $textarea.focus();
}, 10); }, 10);
}).on('hide.bs.popover', '#wmd-input > .editor-margin', function(evt) { }).on('hide.bs.popover', '#wmd-input > .editor-margin', function(evt) {
$(evt.target).removeClass('active'); $(evt.target).removeClass('active');

View File

@ -41,9 +41,9 @@ define([
}; };
var sectionCounter = 0; var sectionCounter = 0;
function parseFileContent(fileDesc) { function parseFileContent(fileDesc, content) {
var frontMatter = (fileDesc.frontMatter || {})._frontMatter || ''; var frontMatter = (fileDesc.frontMatter || {})._frontMatter || '';
var text = fileDesc.content.substring(frontMatter.length); var text = content.substring(frontMatter.length);
var tmpText = text + "\n\n"; var tmpText = text + "\n\n";
function addSection(startOffset, endOffset) { function addSection(startOffset, endOffset) {
var sectionText = tmpText.substring(offset, endOffset); var sectionText = tmpText.substring(offset, endOffset);

View File

@ -18,8 +18,8 @@ define([
var regex = /^(\s*-{3}\s*\n([\w\W]+?)\n\s*-{3}\s*?\n)?([\w\W]*)$/; var regex = /^(\s*-{3}\s*\n([\w\W]+?)\n\s*-{3}\s*?\n)?([\w\W]*)$/;
function parseFrontMatter(fileDesc) { function parseFrontMatter(fileDesc, content) {
var results = regex.exec(fileDesc.content); var results = regex.exec(content);
var yaml = results[2]; var yaml = results[2];
if(!yaml) { if(!yaml) {

View File

@ -135,7 +135,7 @@ define([
storage.removeItem(fileDesc.fileIndex + ".editorEnd"); storage.removeItem(fileDesc.fileIndex + ".editorEnd");
storage.removeItem(fileDesc.fileIndex + ".editorScrollTop"); storage.removeItem(fileDesc.fileIndex + ".editorScrollTop");
storage.removeItem(fileDesc.fileIndex + ".previewScrollTop"); storage.removeItem(fileDesc.fileIndex + ".previewScrollTop");
storage.removeItem(fileDesc.fileIndex + ".editorSelectRange"); storage.removeItem(fileDesc.fileIndex + ".discussionList");
eventMgr.onFileDeleted(fileDesc); eventMgr.onFileDeleted(fileDesc);
}; };

View File

@ -1,6 +1,7 @@
<div class="form-horizontal"> <div class="form-horizontal">
<div class="discussion-history"></div>
<div class="form-group"> <div class="form-group">
<input class="form-control input-comment-author" placeholder="Anonymous"></input> <input class="form-control input-comment-author" placeholder="Your name"></input>
<textarea class="form-control input-comment-content"></textarea> <textarea class="form-control input-comment-content"></textarea>
</div> </div>
<div class="form-group text-right"> <div class="form-group text-right">

View File

@ -191,7 +191,7 @@ define([
// If file content changed // If file content changed
if(fileContentChanged && remoteContentChanged === true) { if(fileContentChanged && remoteContentChanged === true) {
fileDesc.content = file.content; fileDesc.content = file.content;
eventMgr.onContentChanged(fileDesc); eventMgr.onContentChanged(fileDesc, file.content);
eventMgr.onMessage('"' + localTitle + '" has been updated from Dropbox.'); eventMgr.onMessage('"' + localTitle + '" has been updated from Dropbox.');
if(fileMgr.currentFile === fileDesc) { if(fileMgr.currentFile === fileDesc) {
fileMgr.selectFile(); // Refresh editor fileMgr.selectFile(); // Refresh editor

View File

@ -232,7 +232,7 @@ define([
// If file content changed // If file content changed
if(!syncAttributes.isRealtime && fileContentChanged && remoteContentChanged === true) { if(!syncAttributes.isRealtime && fileContentChanged && remoteContentChanged === true) {
fileDesc.content = file.content; fileDesc.content = file.content;
eventMgr.onContentChanged(fileDesc); eventMgr.onContentChanged(fileDesc, file.content);
eventMgr.onMessage('"' + file.title + '" has been updated from ' + providerName + '.'); eventMgr.onMessage('"' + file.title + '" has been updated from ' + providerName + '.');
if(fileMgr.currentFile === fileDesc) { if(fileMgr.currentFile === fileDesc) {
fileMgr.selectFile(); // Refresh editor fileMgr.selectFile(); // Refresh editor
@ -283,10 +283,16 @@ define([
pagedownEditor = pagedownEditorParam; pagedownEditor = pagedownEditorParam;
}); });
var realtimeContext;
eventMgr.addListener('onContentChanged', function(aceEditorParam) {
});
// Start realtime synchronization // Start realtime synchronization
gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) { gdriveProvider.startRealtimeSync = function(fileDesc, syncAttributes) {
var localContext = {}; var context = {
realtimeContext = localContext; fileDesc: fileDesc
};
realtimeContext = context;
googleHelper.loadRealtime(syncAttributes.id, fileDesc.content, accountId, function(err, doc) { googleHelper.loadRealtime(syncAttributes.id, fileDesc.content, accountId, function(err, doc) {
if(err || !doc) { if(err || !doc) {
return; return;
@ -294,15 +300,16 @@ define([
// If user just switched to another document or file has just been // If user just switched to another document or file has just been
// reselected // reselected
if(localContext.isStopped === true) { if(context !== realtimeContext) {
doc.close(); return doc.close();
return;
} }
logger.log("Starting Google Drive realtime synchronization"); logger.log("Starting Google Drive realtime synchronization");
localContext.document = doc; context.document = doc;
var model = doc.getModel(); var model = doc.getModel();
context.model = realtimeString;
var realtimeString = model.getRoot().get('content'); var realtimeString = model.getRoot().get('content');
context.string = realtimeString;
// Saves model content checksum // Saves model content checksum
function updateContentState() { function updateContentState() {
@ -359,6 +366,7 @@ define([
var remoteContentCRC = utils.crc32(remoteContent); var remoteContentCRC = utils.crc32(remoteContent);
var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC; var remoteContentChanged = syncAttributes.contentCRC != remoteContentCRC;
var fileContentChanged = localContent != remoteContent; var fileContentChanged = localContent != remoteContent;
model.beginCompoundOperation('Open and merge');
if(fileContentChanged === true && localContentChanged === true) { if(fileContentChanged === true && localContentChanged === true) {
if(remoteContentChanged === true) { if(remoteContentChanged === true) {
// Conflict detected // Conflict detected
@ -371,11 +379,6 @@ define([
} }
} }
if(aceEditor === undefined) {
// Binds model with textarea
localContext.binding = gapi.drive.realtime.databinding.bindString(realtimeString, document.getElementById("wmd-input"));
}
// Update content state according to collaborators changes // Update content state according to collaborators changes
if(remoteContentChanged === true) { if(remoteContentChanged === true) {
logger.log("Google Drive realtime document updated from server"); logger.log("Google Drive realtime document updated from server");
@ -384,10 +387,15 @@ define([
aceEditor === undefined && debouncedRefreshPreview(); aceEditor === undefined && debouncedRefreshPreview();
} }
if(aceEditor !== undefined) { var realtimeDiscussionList = model.getRoot().get('discussionList');
// Tell ACE to update realtime string on each change context.discussionList = realtimeDiscussionList;
localContext.string = realtimeString;
if(!realtimeDiscussionList) {
realtimeDiscussionList = model.createMap();
model.getRoot().set('discussionList', realtimeDiscussionList);
} }
mergeDiscussionList(context, remoteContentChanged === true);
model.endCompoundOperation();
// Save undo/redo buttons default actions // Save undo/redo buttons default actions
undoExecute = pagedownEditor.uiManager.buttons.undo.execute; undoExecute = pagedownEditor.uiManager.buttons.undo.execute;
@ -425,7 +433,7 @@ define([
}); });
}, function(err) { }, function(err) {
console.error(err); logger.error(err);
if(err.type == "token_refresh_required") { if(err.type == "token_refresh_required") {
googleHelper.refreshGdriveToken(accountId); googleHelper.refreshGdriveToken(accountId);
} }
@ -442,12 +450,89 @@ define([
}); });
}; };
function mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer) {
if(takeServer) {
localDiscussion.selectionStart = realtimeDiscussion.get('selectionStart');
localDiscussion.selectionEnd = realtimeDiscussion.get('selectionEnd');
}
else {
realtimeDiscussion.set('selectionStart', localDiscussion.selectionStart);
realtimeDiscussion.set('selectionEnd', localDiscussion.selectionEnd);
}
function isCommentInDiscussion(comment, commentList) {
return commentList.some(function(commentInDiscussion) {
return comment.author == commentInDiscussion.author && comment.content == commentInDiscussion.content;
});
}
var realtimeCommentList = realtimeDiscussion.get('commentList').asArray();
localDiscussion.commentList.forEach(function(comment) {
if(!isCommentInDiscussion(comment, realtimeCommentList)) {
realtimeDiscussion.get('commentList').push(comment);
}
});
realtimeCommentList.forEach(function(comment) {
if(!isCommentInDiscussion(comment, localDiscussion.commentList)) {
localDiscussion.commentList.push(comment);
}
});
}
function createRealtimeDiscussion(context, discussion) {
var commentList = context.model.createList(discussion.commentList);
var realtimeDiscussion = context.model.createMap({
selectionStart: discussion.selectionStart,
selectionEnd: discussion.selectionEnd,
commentList: commentList
});
context.discussionList.set(discussion.discussionIndex, realtimeDiscussion);
}
function mergeDiscussionList(context, takeServer) {
var localDiscussionList = context.fileDesc.discussionList;
_.each(localDiscussionList, function(localDiscussion) {
var realtimeDiscussion = context.discussionList.get(localDiscussion.discussionIndex);
if(realtimeDiscussion) {
mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer);
}
else {
createRealtimeDiscussion(context, localDiscussion);
}
});
context.discussionList.keys().forEach(function(discussionIndex) {
var localDiscussion = localDiscussionList[discussionIndex];
var realtimeDiscussion = context.discussionList.get(discussionIndex);
if(localDiscussion) {
mergeDiscussion(localDiscussion, realtimeDiscussion, takeServer);
}
else {
var discussion = {
discussionIndex: discussionIndex,
selectionStart: realtimeDiscussion.get('selectionStart'),
selectionEnd: realtimeDiscussion.get('selectionEnd'),
commentList: realtimeDiscussion.get('commentList').asArray()
};
localDiscussionList[discussionIndex] = discussion;
eventMgr.onDiscussionCreated(context.fileDesc, discussion);
}
});
context.fileDesc.discussionList = localDiscussionList; // Write in localStorage
}
eventMgr.addListener('onDiscussionCreated', function(fileDesc, discussion) {
if(realtimeContext === undefined || realtimeContext.fileDesc !== fileDesc) {
return;
}
if(!realtimeContext.discussionList.has(discussion.discussionIndex)) {
createRealtimeDiscussion(realtimeContext, discussion);
}
});
// Stop realtime synchronization // Stop realtime synchronization
gdriveProvider.stopRealtimeSync = function() { gdriveProvider.stopRealtimeSync = function() {
logger.log("Stopping Google Drive realtime synchronization"); logger.log("Stopping Google Drive realtime synchronization");
if(realtimeContext !== undefined) { if(realtimeContext !== undefined) {
realtimeContext.isStopped = true;
realtimeContext.binding && realtimeContext.binding.unbind();
realtimeContext.document && realtimeContext.document.close(); realtimeContext.document && realtimeContext.document.close();
realtimeContext = undefined; realtimeContext = undefined;
} }

View File

@ -127,6 +127,8 @@
@popover-arrow-outer-color: @secondary-border-color; @popover-arrow-outer-color: @secondary-border-color;
@popover-title-bg: @transparent; @popover-title-bg: @transparent;
@alert-border-radius: 0; @alert-border-radius: 0;
@label-warning-bg: #d90;
@label-danger-bg: #d00;
body { body {
@ -1057,23 +1059,33 @@ a {
position: absolute; position: absolute;
top: 0; top: 0;
.icon-comment { .icon-comment {
.opacity(0.4); &.new {
color: fade(@tertiary-color, 12%);
&:hover {
color: fade(@tertiary-color, 35%) !important;
}
}
&.replied {
color: fade(@label-danger-bg, 30%);
&:hover, &.active, &.active:hover {
color: fade(@label-danger-bg, 45%) !important;
}
}
position: absolute; position: absolute;
color: fade(@tertiary-color, 25%); color: fade(@label-warning-bg, 40%);
cursor: pointer; cursor: pointer;
&:hover, &.active { &:hover, &.active, &.active:hover {
.opacity(1); color: fade(@label-warning-bg, 60%) !important;
color: fade(@tertiary-color, 35%);
text-decoration: none; text-decoration: none;
} }
} }
} }
&.has-selection > .editor-margin .icon-comment { &.has-selection > .editor-margin .icon-comment.new {
.opacity(1); color: fade(@tertiary-color, 25%);
} }
.comment-highlight { .comment-highlight {
background-color: fade(@label-danger-bg, 25%); background-color: fade(@label-warning-bg, 25%);
//border-radius: @border-radius-base; //border-radius: @border-radius-base;
} }
@ -1387,7 +1399,7 @@ input[type="file"] {
*********************/ *********************/
.popover { .popover {
max-width: 350px; max-width: 240px;
padding: 10px 20px 0; padding: 10px 20px 0;
//.box-shadow(0 5px 30px rgba(0,0,0,.175)); //.box-shadow(0 5px 30px rgba(0,0,0,.175));
.popover-title { .popover-title {
@ -1395,6 +1407,7 @@ input[type="file"] {
font-size: 24px; font-size: 24px;
padding: 5px 0 15px; padding: 5px 0 15px;
border-bottom: 1px solid @hr-border; border-bottom: 1px solid @hr-border;
line-height: @headings-line-height;
} }
.icon-lock { .icon-lock {
font-size: 38px; font-size: 38px;